Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
5dd94c8298 | |||
412b44a981 | |||
aef4dd3fe7 | |||
6a92c6af4e | |||
e010940b61 | |||
2c9a4d276a | |||
4dfba73e5c | |||
c282d662ca | |||
b3d7594813 |
76
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
title: "[Bug] "
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: ensure
|
||||||
|
attributes:
|
||||||
|
label: Verify steps
|
||||||
|
description: "
|
||||||
|
在提交之前,请确认
|
||||||
|
Please verify that you've followed these steps
|
||||||
|
"
|
||||||
|
options:
|
||||||
|
- label: "
|
||||||
|
如果你可以自己 debug 并解决的话,提交 PR 吧
|
||||||
|
Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
|
||||||
|
"
|
||||||
|
required: true
|
||||||
|
- label: "
|
||||||
|
我已经在 [Issue Tracker](……/) 中找过我要提出的问题
|
||||||
|
I have searched on the [issue tracker](……/) for a related issue.
|
||||||
|
"
|
||||||
|
required: true
|
||||||
|
- label: "
|
||||||
|
我已经使用 dev 分支版本测试过,问题依旧存在
|
||||||
|
I have tested using the dev branch, and the issue still exists.
|
||||||
|
"
|
||||||
|
required: true
|
||||||
|
- label: "
|
||||||
|
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
|
||||||
|
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
|
||||||
|
"
|
||||||
|
required: true
|
||||||
|
- label: "
|
||||||
|
这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
|
||||||
|
This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash.
|
||||||
|
"
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Clash version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: What OS are you seeing the problem on?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- OpenBSD/FreeBSD
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
render: yaml
|
||||||
|
label: "Clash config"
|
||||||
|
description: "
|
||||||
|
在下方附上 Clash core 脱敏后配置文件的内容
|
||||||
|
Paste the Clash core configuration below.
|
||||||
|
"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
render: shell
|
||||||
|
label: Clash log
|
||||||
|
description: "
|
||||||
|
在下方附上 Clash Core 的日志,log level 使用 DEBUG
|
||||||
|
Paste the Clash core log below with the log level set to `DEBUG`.
|
||||||
|
"
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
validations:
|
||||||
|
required: true
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
|
||||||
|
contact_links:
|
||||||
|
- name: Get help in GitHub Discussions
|
||||||
|
url: https://github.com/Dreamacro/clash/discussions
|
||||||
|
about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on Clash's GitHub Discussions!
|
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
title: "[Feature] "
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
id: ensure
|
||||||
|
attributes:
|
||||||
|
label: Verify steps
|
||||||
|
description: "
|
||||||
|
在提交之前,请确认
|
||||||
|
Please verify that you've followed these steps
|
||||||
|
"
|
||||||
|
options:
|
||||||
|
- label: "
|
||||||
|
我已经在 [Issue Tracker](……/) 中找过我要提出的请求
|
||||||
|
I have searched on the [issue tracker](……/) for a related feature request.
|
||||||
|
"
|
||||||
|
required: true
|
||||||
|
- label: "
|
||||||
|
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
|
||||||
|
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
|
||||||
|
"
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Possible Solution
|
||||||
|
description: "
|
||||||
|
此项非必须,但是如果你有想法的话欢迎提出。
|
||||||
|
Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change
|
||||||
|
"
|
22
.github/workflows/build.yaml
vendored
22
.github/workflows/build.yaml
vendored
@ -1,22 +0,0 @@
|
|||||||
name: Build All
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out code into the Go module directory
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.19'
|
|
||||||
check-latest: true
|
|
||||||
cache: true
|
|
||||||
- name: Build
|
|
||||||
run: make all
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: bin/*
|
|
||||||
draft: true
|
|
30
.github/workflows/codeql-analysis.yml
vendored
Normal file
30
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: CodeQL
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, dev]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: ['go']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
61
.github/workflows/docker.yaml
vendored
61
.github/workflows/docker.yaml
vendored
@ -1,61 +0,0 @@
|
|||||||
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 }}
|
|
80
.github/workflows/docker.yml
vendored
Normal file
80
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
name: Publish Docker Image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
with:
|
||||||
|
platforms: all
|
||||||
|
|
||||||
|
- name: Set up docker buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to Github Package
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: Dreamacro
|
||||||
|
password: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build dev branch and push
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Get all docker tags
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
id: tags
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const ref = context.payload.ref.replace(/\/?refs\/tags\//, '')
|
||||||
|
const tags = [
|
||||||
|
'dreamacro/clash:latest',
|
||||||
|
`dreamacro/clash:${ref}`,
|
||||||
|
'ghcr.io/dreamacro/clash:latest',
|
||||||
|
`ghcr.io/dreamacro/clash:${ref}`
|
||||||
|
]
|
||||||
|
return tags.join(',')
|
||||||
|
result-encoding: string
|
||||||
|
|
||||||
|
- name: Build release and push
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{steps.tags.outputs.result}}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
22
.github/workflows/linter.yml
vendored
Normal file
22
.github/workflows/linter.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
name: Linter
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- 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: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: latest
|
59
.github/workflows/prerelease.yml
vendored
59
.github/workflows/prerelease.yml
vendored
@ -1,59 +0,0 @@
|
|||||||
name: Prerelease
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- Alpha
|
|
||||||
- Beta
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- Alpha
|
|
||||||
- Beta
|
|
||||||
jobs:
|
|
||||||
Build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out code into the Go module directory
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.19'
|
|
||||||
check-latest: true
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- 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
|
|
36
.github/workflows/release.yaml
vendored
36
.github/workflows/release.yaml
vendored
@ -1,36 +0,0 @@
|
|||||||
name: Release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
jobs:
|
|
||||||
Build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out code into the Go module directory
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.19'
|
|
||||||
check-latest: true
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- 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
|
|
46
.github/workflows/release.yml
vendored
Normal file
46
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
name: Release
|
||||||
|
on: [push]
|
||||||
|
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
|
||||||
|
~/.cache/go-build
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Get dependencies, run test
|
||||||
|
run: |
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
env:
|
||||||
|
NAME: clash
|
||||||
|
BINDIR: bin
|
||||||
|
run: make -j releases
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
files: bin/*
|
||||||
|
draft: true
|
18
.github/workflows/stale.yml
vendored
Normal file
18
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
name: Mark stale issues and pull requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v5
|
||||||
|
with:
|
||||||
|
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||||
|
days-before-stale: 60
|
||||||
|
days-before-close: 5
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -23,5 +23,3 @@ vendor
|
|||||||
|
|
||||||
# test suite
|
# test suite
|
||||||
test/config/cache*
|
test/config/cache*
|
||||||
/output
|
|
||||||
/.vscode
|
|
30
Dockerfile
30
Dockerfile
@ -1,26 +1,18 @@
|
|||||||
FROM golang:alpine as builder
|
FROM golang:alpine as builder
|
||||||
|
|
||||||
RUN apk add --no-cache make git && \
|
RUN apk add --no-cache make git && \
|
||||||
mkdir /clash-config && \
|
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
|
||||||
wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \
|
|
||||||
wget -O /clash-config/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \
|
|
||||||
wget -O /clash-config/geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
|
||||||
|
|
||||||
|
|
||||||
COPY . /clash-src
|
|
||||||
WORKDIR /clash-src
|
WORKDIR /clash-src
|
||||||
RUN go mod download &&\
|
COPY --from=tonistiigi/xx:golang / /
|
||||||
make docker &&\
|
COPY . /clash-src
|
||||||
mv ./bin/Clash.Meta-docker /clash
|
RUN go mod download && \
|
||||||
|
make docker && \
|
||||||
|
mv ./bin/clash-docker /clash
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/Clash.Meta"
|
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates tzdata iptables
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
COPY --from=builder /Country.mmdb /root/.config/clash/
|
||||||
VOLUME ["/root/.config/clash/"]
|
COPY --from=builder /clash /
|
||||||
|
ENTRYPOINT ["/clash"]
|
||||||
COPY --from=builder /clash-config/ /root/.config/clash/
|
|
||||||
COPY --from=builder /clash /clash
|
|
||||||
RUN chmod +x /clash
|
|
||||||
ENTRYPOINT [ "/clash" ]
|
|
||||||
|
85
Makefile
85
Makefile
@ -1,60 +1,50 @@
|
|||||||
NAME=Clash.Meta
|
NAME=clash
|
||||||
BINDIR=bin
|
BINDIR=bin
|
||||||
BRANCH=$(shell git branch --show-current)
|
VERSION=$(shell git describe --tags || echo "unknown version")
|
||||||
ifeq ($(BRANCH),Alpha)
|
|
||||||
VERSION=alpha-$(shell git rev-parse --short HEAD)
|
|
||||||
else ifeq ($(BRANCH),Beta)
|
|
||||||
VERSION=beta-$(shell git rev-parse --short HEAD)
|
|
||||||
else ifeq ($(BRANCH),)
|
|
||||||
VERSION=$(shell git describe --tags)
|
|
||||||
else
|
|
||||||
VERSION=$(shell git rev-parse --short HEAD)
|
|
||||||
endif
|
|
||||||
|
|
||||||
BUILDTIME=$(shell date -u)
|
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)" \
|
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
|
||||||
-w -s -buildid='
|
-w -s -buildid='
|
||||||
|
|
||||||
PLATFORM_LIST = \
|
PLATFORM_LIST = \
|
||||||
darwin-amd64 \
|
darwin-amd64 \
|
||||||
|
darwin-amd64-v3 \
|
||||||
darwin-arm64 \
|
darwin-arm64 \
|
||||||
linux-amd64-compatible \
|
linux-386 \
|
||||||
linux-amd64 \
|
linux-amd64 \
|
||||||
|
linux-amd64-v3 \
|
||||||
linux-armv5 \
|
linux-armv5 \
|
||||||
linux-armv6 \
|
linux-armv6 \
|
||||||
linux-armv7 \
|
linux-armv7 \
|
||||||
linux-arm64 \
|
linux-armv8 \
|
||||||
linux-mips64 \
|
|
||||||
linux-mips64le \
|
|
||||||
linux-mips-softfloat \
|
linux-mips-softfloat \
|
||||||
linux-mips-hardfloat \
|
linux-mips-hardfloat \
|
||||||
linux-mipsle-softfloat \
|
linux-mipsle-softfloat \
|
||||||
linux-mipsle-hardfloat \
|
linux-mipsle-hardfloat \
|
||||||
android-arm64 \
|
linux-mips64 \
|
||||||
|
linux-mips64le \
|
||||||
freebsd-386 \
|
freebsd-386 \
|
||||||
freebsd-amd64 \
|
freebsd-amd64 \
|
||||||
|
freebsd-amd64-v3 \
|
||||||
freebsd-arm64
|
freebsd-arm64
|
||||||
|
|
||||||
WINDOWS_ARCH_LIST = \
|
WINDOWS_ARCH_LIST = \
|
||||||
windows-386 \
|
windows-386 \
|
||||||
windows-amd64-compatible \
|
|
||||||
windows-amd64 \
|
windows-amd64 \
|
||||||
|
windows-amd64-v3 \
|
||||||
windows-arm64 \
|
windows-arm64 \
|
||||||
windows-arm32v7
|
windows-arm32v7
|
||||||
|
|
||||||
all:linux-amd64 linux-arm64\
|
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
||||||
darwin-amd64 darwin-arm64\
|
|
||||||
windows-amd64 windows-arm64\
|
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
darwin-amd64:
|
darwin-amd64:
|
||||||
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
darwin-amd64-compatible:
|
darwin-amd64-v3:
|
||||||
GOARCH=amd64 GOOS=darwin GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
darwin-arm64:
|
darwin-arm64:
|
||||||
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
@ -63,14 +53,11 @@ linux-386:
|
|||||||
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-amd64:
|
linux-amd64:
|
||||||
|
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
linux-amd64-v3:
|
||||||
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-amd64-compatible:
|
|
||||||
GOARCH=amd64 GOOS=linux GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
linux-arm64:
|
|
||||||
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
linux-armv5:
|
linux-armv5:
|
||||||
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
@ -80,6 +67,9 @@ linux-armv6:
|
|||||||
linux-armv7:
|
linux-armv7:
|
||||||
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
linux-armv8:
|
||||||
|
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-mips-softfloat:
|
linux-mips-softfloat:
|
||||||
GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
@ -98,13 +88,13 @@ linux-mips64:
|
|||||||
linux-mips64le:
|
linux-mips64le:
|
||||||
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
android-arm64:
|
|
||||||
GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
freebsd-386:
|
freebsd-386:
|
||||||
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
freebsd-amd64:
|
freebsd-amd64:
|
||||||
|
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
freebsd-amd64-v3:
|
||||||
GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
freebsd-arm64:
|
freebsd-arm64:
|
||||||
@ -114,10 +104,10 @@ windows-386:
|
|||||||
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
windows-amd64:
|
windows-amd64:
|
||||||
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
windows-amd64-compatible:
|
windows-amd64-v3:
|
||||||
GOARCH=amd64 GOOS=windows GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
windows-arm64:
|
windows-arm64:
|
||||||
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
@ -139,19 +129,12 @@ all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
|
|||||||
|
|
||||||
releases: $(gz_releases) $(zip_releases)
|
releases: $(gz_releases) $(zip_releases)
|
||||||
|
|
||||||
vet:
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run ./...
|
GOOS=darwin golangci-lint run ./...
|
||||||
|
GOOS=windows golangci-lint run ./...
|
||||||
|
GOOS=linux golangci-lint run ./...
|
||||||
|
GOOS=freebsd golangci-lint run ./...
|
||||||
|
GOOS=openbsd golangci-lint run ./...
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm $(BINDIR)/*
|
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 ./...
|
|
296
README.md
296
README.md
@ -1,20 +1,23 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="Meta.png" alt="Meta Kennel" width="200">
|
<img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200">
|
||||||
<br>Meta Kernel<br>
|
<br>Clash<br>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h3 align="center">Another Clash Kernel.</h3>
|
<h4 align="center">A rule-based tunnel in Go.</h4>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://goreportcard.com/report/github.com/Clash-Mini/Clash.Meta">
|
<a href="https://github.com/Dreamacro/clash/actions">
|
||||||
<img src="https://goreportcard.com/badge/github.com/Clash-Mini/Clash.Meta?style=flat-square">
|
<img src="https://img.shields.io/github/workflow/status/Dreamacro/clash/Go?style=flat-square" alt="Github Actions">
|
||||||
|
</a>
|
||||||
|
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
|
||||||
|
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
|
<img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
|
||||||
<a href="https://github.com/Clash-Mini/Clash.Meta/releases">
|
<a href="https://github.com/Dreamacro/clash/releases">
|
||||||
<img src="https://img.shields.io/github/release/Clash-Mini/Clash.Meta/all.svg?style=flat-square">
|
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Clash-Mini/Clash.Meta">
|
<a href="https://github.com/Dreamacro/clash/releases/tag/premium">
|
||||||
<img src="https://img.shields.io/badge/release-Meta-00b4f0?style=flat-square">
|
<img src="https://img.shields.io/badge/release-Premium-00b4f0?style=flat-square">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -29,283 +32,26 @@
|
|||||||
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
|
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
|
||||||
- Comprehensive HTTP RESTful API controller
|
- Comprehensive HTTP RESTful API controller
|
||||||
|
|
||||||
|
## Premium Features
|
||||||
|
|
||||||
|
- TUN mode on macOS, Linux and Windows. [Doc](https://github.com/Dreamacro/clash/wiki/premium-core-features#tun-device)
|
||||||
|
- Match your tunnel by [Script](https://github.com/Dreamacro/clash/wiki/premium-core-features#script)
|
||||||
|
- [Rule Provider](https://github.com/Dreamacro/clash/wiki/premium-core-features#rule-providers)
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
||||||
|
|
||||||
## Advanced usage for this branch
|
## Premium Release
|
||||||
|
[Release](https://github.com/Dreamacro/clash/releases/tag/premium)
|
||||||
### DNS configuration
|
|
||||||
|
|
||||||
Support `geosite` with `fallback-filter`.
|
|
||||||
|
|
||||||
Restore `Redir remote resolution`.
|
|
||||||
|
|
||||||
Support resolve ip with a `Proxy Tunnel`.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
proxy-groups:
|
|
||||||
|
|
||||||
- name: DNS
|
|
||||||
type: url-test
|
|
||||||
use:
|
|
||||||
- HK
|
|
||||||
url: http://cp.cloudflare.com
|
|
||||||
interval: 180
|
|
||||||
lazy: true
|
|
||||||
```
|
|
||||||
```yaml
|
|
||||||
dns:
|
|
||||||
enable: true
|
|
||||||
use-hosts: true
|
|
||||||
ipv6: false
|
|
||||||
enhanced-mode: redir-host
|
|
||||||
fake-ip-range: 198.18.0.1/16
|
|
||||||
listen: 127.0.0.1:6868
|
|
||||||
default-nameserver:
|
|
||||||
- 119.29.29.29
|
|
||||||
- 114.114.114.114
|
|
||||||
nameserver:
|
|
||||||
- https://doh.pub/dns-query
|
|
||||||
- tls://223.5.5.5:853
|
|
||||||
fallback:
|
|
||||||
- 'https://1.0.0.1/dns-query#DNS' # append the proxy adapter name or group name to the end of DNS URL with '#' prefix.
|
|
||||||
- 'tls://8.8.4.4:853#DNS'
|
|
||||||
fallback-filter:
|
|
||||||
geoip: false
|
|
||||||
geosite:
|
|
||||||
- gfw # `geosite` filter only use fallback server to resolve ip, prevent DNS leaks to unsafe DNS providers.
|
|
||||||
domain:
|
|
||||||
- +.example.com
|
|
||||||
ipcidr:
|
|
||||||
- 0.0.0.0/32
|
|
||||||
```
|
|
||||||
|
|
||||||
### TUN configuration
|
|
||||||
|
|
||||||
Supports macOS, Linux and Windows.
|
|
||||||
|
|
||||||
Built-in [Wintun](https://www.wintun.net) driver.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Enable the TUN listener
|
|
||||||
tun:
|
|
||||||
enable: true
|
|
||||||
stack: gvisor # only gvisor
|
|
||||||
dns-hijack:
|
|
||||||
- 0.0.0.0:53 # additional dns server listen on TUN
|
|
||||||
auto-route: true # auto set global route
|
|
||||||
```
|
|
||||||
### Rules configuration
|
|
||||||
- Support rule `GEOSITE`.
|
|
||||||
- Support rule-providers `RULE-SET`.
|
|
||||||
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
|
|
||||||
- Support `network` condition for all rules.
|
|
||||||
- Support source IPCIDR condition for all rules, just append to the end.
|
|
||||||
- The `GEOSITE` databases via https://github.com/Loyalsoldier/v2ray-rules-dat.
|
|
||||||
```yaml
|
|
||||||
rules:
|
|
||||||
|
|
||||||
# network(tcp/udp) condition for all rules
|
|
||||||
- DOMAIN-SUFFIX,bilibili.com,DIRECT,tcp
|
|
||||||
- DOMAIN-SUFFIX,bilibili.com,REJECT,udp
|
|
||||||
|
|
||||||
# multiport condition for rules SRC-PORT and DST-PORT
|
|
||||||
- DST-PORT,123/136/137-139,DIRECT,udp
|
|
||||||
|
|
||||||
# rule GEOSITE
|
|
||||||
- GEOSITE,category-ads-all,REJECT
|
|
||||||
- GEOSITE,icloud@cn,DIRECT
|
|
||||||
- GEOSITE,apple@cn,DIRECT
|
|
||||||
- GEOSITE,apple-cn,DIRECT
|
|
||||||
- GEOSITE,microsoft@cn,DIRECT
|
|
||||||
- GEOSITE,facebook,PROXY
|
|
||||||
- GEOSITE,youtube,PROXY
|
|
||||||
- GEOSITE,geolocation-cn,DIRECT
|
|
||||||
- GEOSITE,geolocation-!cn,PROXY
|
|
||||||
|
|
||||||
# source IPCIDR condition for all rules in gateway proxy
|
|
||||||
#- GEOSITE,geolocation-!cn,REJECT,192.168.1.88/32,192.168.1.99/32
|
|
||||||
|
|
||||||
- GEOIP,telegram,PROXY,no-resolve
|
|
||||||
- GEOIP,private,DIRECT,no-resolve
|
|
||||||
- GEOIP,cn,DIRECT
|
|
||||||
|
|
||||||
- MATCH,PROXY
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### 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)
|
|
||||||
|
|
||||||
Support `Policy Group Filter`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
proxy-groups:
|
|
||||||
|
|
||||||
- name: 🚀 HK Group
|
|
||||||
type: select
|
|
||||||
use:
|
|
||||||
- ALL
|
|
||||||
filter: 'HK'
|
|
||||||
|
|
||||||
- name: 🚀 US Group
|
|
||||||
type: select
|
|
||||||
use:
|
|
||||||
- ALL
|
|
||||||
filter: 'US'
|
|
||||||
|
|
||||||
proxy-providers:
|
|
||||||
ALL:
|
|
||||||
type: http
|
|
||||||
url: "xxxxx"
|
|
||||||
interval: 3600
|
|
||||||
path: "xxxxx"
|
|
||||||
health-check:
|
|
||||||
enable: true
|
|
||||||
interval: 600
|
|
||||||
url: http://www.gstatic.com/generate_204
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Support outbound transport protocol `VLESS`.
|
|
||||||
|
|
||||||
The XTLS support (TCP/UDP) transport by the XRAY-CORE.
|
|
||||||
```yaml
|
|
||||||
proxies:
|
|
||||||
- name: "vless"
|
|
||||||
type: vless
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
uuid: uuid
|
|
||||||
servername: example.com # AKA SNI
|
|
||||||
# flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS
|
|
||||||
# skip-cert-verify: true
|
|
||||||
|
|
||||||
- name: "vless-ws"
|
|
||||||
type: vless
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
uuid: uuid
|
|
||||||
tls: true
|
|
||||||
udp: true
|
|
||||||
network: ws
|
|
||||||
servername: example.com # priority over wss host
|
|
||||||
# skip-cert-verify: true
|
|
||||||
ws-opts:
|
|
||||||
path: /path
|
|
||||||
headers: { Host: example.com, Edge: "12a00c4.fm.huawei.com:82897" }
|
|
||||||
|
|
||||||
- name: "vless-grpc"
|
|
||||||
type: vless
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
uuid: uuid
|
|
||||||
tls: true
|
|
||||||
udp: true
|
|
||||||
network: grpc
|
|
||||||
servername: example.com # priority over wss host
|
|
||||||
# skip-cert-verify: true
|
|
||||||
grpc-opts:
|
|
||||||
grpc-service-name: grpcname
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### IPTABLES configuration
|
|
||||||
Work on Linux OS who's supported `iptables`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Enable the TPROXY listener
|
|
||||||
tproxy-port: 9898
|
|
||||||
|
|
||||||
iptables:
|
|
||||||
enable: true # default is false
|
|
||||||
inbound-interface: eth0 # detect the inbound interface, default is 'lo'
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### 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)
|
|
||||||
|
|
||||||
+ Rename executable file to `Clash-Meta` and move to `/usr/local/bin/`
|
|
||||||
|
|
||||||
+ Create folder `/etc/Clash-Meta/` as working directory
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Run Meta Kernel by user `clash-meta` as a daemon.
|
|
||||||
|
|
||||||
Create the systemd configuration file at `/etc/systemd/system/Clash-Meta.service`:
|
|
||||||
|
|
||||||
```
|
|
||||||
[Unit]
|
|
||||||
Description=Clash-Meta Daemon, Another Clash Kernel.
|
|
||||||
After=network.target NetworkManager.service systemd-networkd.service iwd.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=clash-meta
|
|
||||||
Group=clash-meta
|
|
||||||
LimitNPROC=500
|
|
||||||
LimitNOFILE=1000000
|
|
||||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
|
|
||||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
|
|
||||||
Restart=always
|
|
||||||
ExecStartPre=/usr/bin/sleep 1s
|
|
||||||
ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
Launch clashd on system startup with:
|
|
||||||
```shell
|
|
||||||
$ systemctl enable Clash-Meta
|
|
||||||
```
|
|
||||||
Launch clashd immediately with:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ systemctl start Clash-Meta
|
|
||||||
```
|
|
||||||
|
|
||||||
### Display Process name
|
|
||||||
|
|
||||||
Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
|
|
||||||
|
|
||||||
To display process name in GUI please use [Dashboard For Meta](https://github.com/MetaCubeX/clash-dashboard).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Development
|
## 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)
|
||||||
If you want to build an application that uses clash as a library, check out the
|
|
||||||
the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
* [Dreamacro/clash](https://github.com/Dreamacro/clash)
|
|
||||||
* [SagerNet/sing-box](https://github.com/SagerNet/sing-box)
|
|
||||||
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||||
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
||||||
* [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
|
* [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
|
||||||
* [yaling888/clash-plus-pro](https://github.com/yaling888/clash)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -4,23 +4,21 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/queue"
|
"github.com/Dreamacro/clash/common/queue"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
var UnifiedDelay = atomic.NewBool(false)
|
|
||||||
|
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
C.ProxyAdapter
|
C.ProxyAdapter
|
||||||
history *queue.Queue[C.DelayHistory]
|
history *queue.Queue
|
||||||
alive *atomic.Bool
|
alive *atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +37,7 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
|||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
|
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
|
||||||
|
p.alive.Store(err == nil)
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,15 +51,16 @@ func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
|
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
|
||||||
|
p.alive.Store(err == nil)
|
||||||
return pc, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DelayHistory implements C.Proxy
|
// DelayHistory implements C.Proxy
|
||||||
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||||
queueM := p.history.Copy()
|
queue := p.history.Copy()
|
||||||
histories := []C.DelayHistory{}
|
histories := []C.DelayHistory{}
|
||||||
for _, item := range queueM {
|
for _, item := range queue {
|
||||||
histories = append(histories, item)
|
histories = append(histories, item.(C.DelayHistory))
|
||||||
}
|
}
|
||||||
return histories
|
return histories
|
||||||
}
|
}
|
||||||
@ -73,7 +73,11 @@ func (p *Proxy) LastDelay() (delay uint16) {
|
|||||||
return max
|
return max
|
||||||
}
|
}
|
||||||
|
|
||||||
history := p.history.Last()
|
last := p.history.Last()
|
||||||
|
if last == nil {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
history := last.(C.DelayHistory)
|
||||||
if history.Delay == 0 {
|
if history.Delay == 0 {
|
||||||
return max
|
return max
|
||||||
}
|
}
|
||||||
@ -88,7 +92,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mapping := map[string]any{}
|
mapping := map[string]any{}
|
||||||
_ = json.Unmarshal(inner, &mapping)
|
json.Unmarshal(inner, &mapping)
|
||||||
mapping["history"] = p.DelayHistory()
|
mapping["history"] = p.DelayHistory()
|
||||||
mapping["name"] = p.Name()
|
mapping["name"] = p.Name()
|
||||||
mapping["udp"] = p.SupportUDP()
|
mapping["udp"] = p.SupportUDP()
|
||||||
@ -110,8 +114,6 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
unifiedDelay := UnifiedDelay.Load()
|
|
||||||
|
|
||||||
addr, err := urlToMetadata(url)
|
addr, err := urlToMetadata(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -122,9 +124,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer instance.Close()
|
||||||
_ = instance.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodHead, url, nil)
|
req, err := http.NewRequest(http.MethodHead, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -133,7 +133,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
|||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
DialContext: func(context.Context, string, string) (net.Conn, error) {
|
Dial: func(string, string) (net.Conn, error) {
|
||||||
return instance, nil
|
return instance, nil
|
||||||
},
|
},
|
||||||
// from http.DefaultTransport
|
// from http.DefaultTransport
|
||||||
@ -144,38 +144,24 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
defer client.CloseIdleConnections()
|
defer client.CloseIdleConnections()
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
resp.Body.Close()
|
||||||
_ = resp.Body.Close()
|
|
||||||
|
|
||||||
if unifiedDelay {
|
|
||||||
second := time.Now()
|
|
||||||
resp, err = client.Do(req)
|
|
||||||
if err == nil {
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
start = second
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t = uint16(time.Since(start) / time.Millisecond)
|
t = uint16(time.Since(start) / time.Millisecond)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||||
return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)}
|
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
||||||
@ -198,9 +184,10 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addr = C.Metadata{
|
addr = C.Metadata{
|
||||||
Host: u.Hostname(),
|
AddrType: C.AtypDomainName,
|
||||||
DstIP: netip.Addr{},
|
Host: u.Hostname(),
|
||||||
DstPort: port,
|
DstIP: nil,
|
||||||
|
DstPort: port,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,5 @@ func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn) *context.ConnCo
|
|||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
if ip, port, err := parseAddr(conn.LocalAddr().String()); err == nil {
|
|
||||||
metadata.InIP = ip
|
|
||||||
metadata.InPort = port
|
|
||||||
}
|
|
||||||
return context.NewConnContext(conn, metadata)
|
return context.NewConnContext(conn, metadata)
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,10 @@ import (
|
|||||||
// NewHTTPS receive CONNECT request and return ConnContext
|
// NewHTTPS receive CONNECT request and return ConnContext
|
||||||
func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
|
func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
|
||||||
metadata := parseHTTPAddr(request)
|
metadata := parseHTTPAddr(request)
|
||||||
metadata.Type = C.HTTPS
|
metadata.Type = C.HTTPCONNECT
|
||||||
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
if ip, port, err := parseAddr(conn.LocalAddr().String()); err == nil {
|
|
||||||
metadata.InIP = ip
|
|
||||||
metadata.InPort = port
|
|
||||||
}
|
|
||||||
return context.NewConnContext(conn, metadata)
|
return context.NewConnContext(conn, metadata)
|
||||||
}
|
}
|
||||||
|
@ -25,12 +25,6 @@ func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAda
|
|||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
if p, ok := packet.(C.UDPPacketInAddr); ok {
|
|
||||||
if ip, port, err := parseAddr(p.InAddr().String()); err == nil {
|
|
||||||
metadata.InIP = ip
|
|
||||||
metadata.InPort = port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PacketAdapter{
|
return &PacketAdapter{
|
||||||
UDPPacket: packet,
|
UDPPacket: packet,
|
||||||
|
@ -2,7 +2,6 @@ package inbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/context"
|
"github.com/Dreamacro/clash/context"
|
||||||
@ -14,40 +13,9 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnCo
|
|||||||
metadata := parseSocksAddr(target)
|
metadata := parseSocksAddr(target)
|
||||||
metadata.NetWork = C.TCP
|
metadata.NetWork = C.TCP
|
||||||
metadata.Type = source
|
metadata.Type = source
|
||||||
remoteAddr := conn.RemoteAddr()
|
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
||||||
// Filter when net.Addr interface is nil
|
metadata.SrcIP = ip
|
||||||
if remoteAddr != nil {
|
metadata.SrcPort = port
|
||||||
if ip, port, err := parseAddr(remoteAddr.String()); err == nil {
|
|
||||||
metadata.SrcIP = ip
|
|
||||||
metadata.SrcPort = port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
localAddr := conn.LocalAddr()
|
|
||||||
// Filter when net.Addr interface is nil
|
|
||||||
if localAddr != nil {
|
|
||||||
if ip, port, err := parseAddr(localAddr.String()); err == nil {
|
|
||||||
metadata.InIP = ip
|
|
||||||
metadata.InPort = port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.NewConnContext(conn, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInner(conn net.Conn, dst string, host string) *context.ConnContext {
|
|
||||||
metadata := &C.Metadata{}
|
|
||||||
metadata.NetWork = C.TCP
|
|
||||||
metadata.Type = C.INNER
|
|
||||||
metadata.DNSMode = C.DNSMapping
|
|
||||||
metadata.Host = host
|
|
||||||
metadata.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return context.NewConnContext(conn, metadata)
|
return context.NewConnContext(conn, metadata)
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package inbound
|
package inbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Dreamacro/clash/common/nnip"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -13,7 +11,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
||||||
metadata := &C.Metadata{}
|
metadata := &C.Metadata{
|
||||||
|
AddrType: int(target[0]),
|
||||||
|
}
|
||||||
|
|
||||||
switch target[0] {
|
switch target[0] {
|
||||||
case socks5.AtypDomainName:
|
case socks5.AtypDomainName:
|
||||||
@ -21,11 +21,12 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
|||||||
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
|
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
|
||||||
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
|
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
|
||||||
case socks5.AtypIPv4:
|
case socks5.AtypIPv4:
|
||||||
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
|
ip := net.IP(target[1 : 1+net.IPv4len])
|
||||||
|
metadata.DstIP = ip
|
||||||
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
|
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
|
||||||
case socks5.AtypIPv6:
|
case socks5.AtypIPv6:
|
||||||
ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len])
|
ip := net.IP(target[1 : 1+net.IPv6len])
|
||||||
metadata.DstIP = ip6.Unmap()
|
metadata.DstIP = ip
|
||||||
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
|
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,26 +44,33 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
|
|||||||
host = strings.TrimRight(host, ".")
|
host = strings.TrimRight(host, ".")
|
||||||
|
|
||||||
metadata := &C.Metadata{
|
metadata := &C.Metadata{
|
||||||
NetWork: C.TCP,
|
NetWork: C.TCP,
|
||||||
Host: host,
|
AddrType: C.AtypDomainName,
|
||||||
DstIP: netip.Addr{},
|
Host: host,
|
||||||
DstPort: port,
|
DstIP: nil,
|
||||||
|
DstPort: port,
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(host)
|
ip := net.ParseIP(host)
|
||||||
if err == nil {
|
if ip != nil {
|
||||||
|
switch {
|
||||||
|
case ip.To4() == nil:
|
||||||
|
metadata.AddrType = C.AtypIPv6
|
||||||
|
default:
|
||||||
|
metadata.AddrType = C.AtypIPv4
|
||||||
|
}
|
||||||
metadata.DstIP = ip
|
metadata.DstIP = ip
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAddr(addr string) (netip.Addr, string, error) {
|
func parseAddr(addr string) (net.IP, string, error) {
|
||||||
host, port, err := net.SplitHostPort(addr)
|
host, port, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return netip.Addr{}, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(host)
|
ip := net.ParseIP(host)
|
||||||
return ip, port, err
|
return ip, port, nil
|
||||||
}
|
}
|
||||||
|
@ -4,23 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/gofrs/uuid"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Base struct {
|
type Base struct {
|
||||||
name string
|
name string
|
||||||
addr string
|
addr string
|
||||||
iface string
|
iface string
|
||||||
tp C.AdapterType
|
tp C.AdapterType
|
||||||
udp bool
|
udp bool
|
||||||
rmark int
|
rmark int
|
||||||
id string
|
|
||||||
prefer C.DNSPrefer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name implements C.ProxyAdapter
|
// Name implements C.ProxyAdapter
|
||||||
@ -28,20 +24,6 @@ func (b *Base) Name() string {
|
|||||||
return b.name
|
return b.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Id implements C.ProxyAdapter
|
|
||||||
func (b *Base) Id() string {
|
|
||||||
if b.id == "" {
|
|
||||||
id, err := uuid.NewV6()
|
|
||||||
if err != nil {
|
|
||||||
b.id = b.name
|
|
||||||
} else {
|
|
||||||
b.id = id.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type implements C.ProxyAdapter
|
// Type implements C.ProxyAdapter
|
||||||
func (b *Base) Type() C.AdapterType {
|
func (b *Base) Type() C.AdapterType {
|
||||||
return b.tp
|
return b.tp
|
||||||
@ -52,25 +34,11 @@ func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
return c, errors.New("no support")
|
return c, errors.New("no support")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
|
||||||
return nil, errors.New("no support")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
return nil, errors.New("no support")
|
return nil, errors.New("no support")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
|
||||||
func (b *Base) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|
||||||
return nil, errors.New("no support")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportUOT implements C.ProxyAdapter
|
|
||||||
func (b *Base) SupportUOT() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportUDP implements C.ProxyAdapter
|
// SupportUDP implements C.ProxyAdapter
|
||||||
func (b *Base) SupportUDP() bool {
|
func (b *Base) SupportUDP() bool {
|
||||||
return b.udp
|
return b.udp
|
||||||
@ -80,7 +48,6 @@ func (b *Base) SupportUDP() bool {
|
|||||||
func (b *Base) MarshalJSON() ([]byte, error) {
|
func (b *Base) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]string{
|
return json.Marshal(map[string]string{
|
||||||
"type": b.Type().String(),
|
"type": b.Type().String(),
|
||||||
"id": b.Id(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +57,7 @@ func (b *Base) Addr() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap implements C.ProxyAdapter
|
// Unwrap implements C.ProxyAdapter
|
||||||
func (b *Base) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
|
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,25 +71,12 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
|
|||||||
opts = append(opts, dialer.WithRoutingMark(b.rmark))
|
opts = append(opts, dialer.WithRoutingMark(b.rmark))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch b.prefer {
|
|
||||||
case C.IPv4Only:
|
|
||||||
opts = append(opts, dialer.WithOnlySingleStack(true))
|
|
||||||
case C.IPv6Only:
|
|
||||||
opts = append(opts, dialer.WithOnlySingleStack(false))
|
|
||||||
case C.IPv4Prefer:
|
|
||||||
opts = append(opts, dialer.WithPreferIPv4())
|
|
||||||
case C.IPv6Prefer:
|
|
||||||
opts = append(opts, dialer.WithPreferIPv6())
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
type BasicOption struct {
|
type BasicOption struct {
|
||||||
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
|
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
|
||||||
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
|
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
|
||||||
IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseOption struct {
|
type BaseOption struct {
|
||||||
@ -132,29 +86,22 @@ type BaseOption struct {
|
|||||||
UDP bool
|
UDP bool
|
||||||
Interface string
|
Interface string
|
||||||
RoutingMark int
|
RoutingMark int
|
||||||
Prefer C.DNSPrefer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBase(opt BaseOption) *Base {
|
func NewBase(opt BaseOption) *Base {
|
||||||
return &Base{
|
return &Base{
|
||||||
name: opt.Name,
|
name: opt.Name,
|
||||||
addr: opt.Addr,
|
addr: opt.Addr,
|
||||||
tp: opt.Type,
|
tp: opt.Type,
|
||||||
udp: opt.UDP,
|
udp: opt.UDP,
|
||||||
iface: opt.Interface,
|
iface: opt.Interface,
|
||||||
rmark: opt.RoutingMark,
|
rmark: opt.RoutingMark,
|
||||||
prefer: opt.Prefer,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type conn struct {
|
type conn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
chain C.Chain
|
chain C.Chain
|
||||||
actualRemoteDestination string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *conn) RemoteDestination() string {
|
|
||||||
return c.actualRemoteDestination
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chains implements C.Connection
|
// Chains implements C.Connection
|
||||||
@ -168,17 +115,12 @@ func (c *conn) AppendToChains(a C.ProxyAdapter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
||||||
return &conn{c, []string{a.Name()}, parseRemoteDestination(a.Addr())}
|
return &conn{c, []string{a.Name()}}
|
||||||
}
|
}
|
||||||
|
|
||||||
type packetConn struct {
|
type packetConn struct {
|
||||||
net.PacketConn
|
net.PacketConn
|
||||||
chain C.Chain
|
chain C.Chain
|
||||||
actualRemoteDestination string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *packetConn) RemoteDestination() string {
|
|
||||||
return c.actualRemoteDestination
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chains implements C.Connection
|
// Chains implements C.Connection
|
||||||
@ -192,17 +134,5 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
||||||
return &packetConn{pc, []string{a.Name()}, parseRemoteDestination(a.Addr())}
|
return &packetConn{pc, []string{a.Name()}}
|
||||||
}
|
|
||||||
|
|
||||||
func parseRemoteDestination(addr string) string {
|
|
||||||
if dst, _, err := net.SplitHostPort(addr); err == nil {
|
|
||||||
return dst
|
|
||||||
} else {
|
|
||||||
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
|
|
||||||
return dst
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ type Direct struct {
|
|||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
opts = append(opts, dialer.WithDirect())
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
|
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -25,7 +24,6 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
|
|||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
opts = append(opts, dialer.WithDirect())
|
|
||||||
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
|
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -40,21 +38,9 @@ type directPacketConn struct {
|
|||||||
func NewDirect() *Direct {
|
func NewDirect() *Direct {
|
||||||
return &Direct{
|
return &Direct{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: "DIRECT",
|
name: "DIRECT",
|
||||||
tp: C.Direct,
|
tp: C.Direct,
|
||||||
udp: true,
|
udp: true,
|
||||||
prefer: C.DualStack,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCompatible() *Direct {
|
|
||||||
return &Direct{
|
|
||||||
Base: &Base{
|
|
||||||
name: "COMPATIBLE",
|
|
||||||
tp: C.Compatible,
|
|
||||||
udp: true,
|
|
||||||
prefer: C.DualStack,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -23,21 +22,18 @@ type Http struct {
|
|||||||
user string
|
user string
|
||||||
pass string
|
pass string
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
option *HttpOption
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpOption struct {
|
type HttpOption struct {
|
||||||
BasicOption
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
UserName string `proxy:"username,omitempty"`
|
UserName string `proxy:"username,omitempty"`
|
||||||
Password string `proxy:"password,omitempty"`
|
Password string `proxy:"password,omitempty"`
|
||||||
TLS bool `proxy:"tls,omitempty"`
|
TLS bool `proxy:"tls,omitempty"`
|
||||||
SNI string `proxy:"sni,omitempty"`
|
SNI string `proxy:"sni,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
|
||||||
Headers map[string]string `proxy:"headers,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamConn implements C.ProxyAdapter
|
// StreamConn implements C.ProxyAdapter
|
||||||
@ -88,13 +84,6 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
//增加headers
|
|
||||||
if len(h.option.Headers) != 0 {
|
|
||||||
for key, value := range h.option.Headers {
|
|
||||||
req.Header.Add(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.user != "" && h.pass != "" {
|
if h.user != "" && h.pass != "" {
|
||||||
auth := h.user + ":" + h.pass
|
auth := h.user + ":" + h.pass
|
||||||
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
||||||
@ -128,41 +117,29 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
|||||||
return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
|
return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttp(option HttpOption) (*Http, error) {
|
func NewHttp(option HttpOption) *Http {
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
if option.TLS {
|
if option.TLS {
|
||||||
sni := option.Server
|
sni := option.Server
|
||||||
if option.SNI != "" {
|
if option.SNI != "" {
|
||||||
sni = option.SNI
|
sni = option.SNI
|
||||||
}
|
}
|
||||||
if len(option.Fingerprint) == 0 {
|
tlsConfig = &tls.Config{
|
||||||
tlsConfig = tlsC.GetGlobalFingerprintTLCConfig(&tls.Config{
|
InsecureSkipVerify: option.SkipCertVerify,
|
||||||
InsecureSkipVerify: option.SkipCertVerify,
|
ServerName: sni,
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Http{
|
return &Http{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Http,
|
tp: C.Http,
|
||||||
iface: option.Interface,
|
iface: option.Interface,
|
||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
},
|
||||||
user: option.UserName,
|
user: option.UserName,
|
||||||
pass: option.Password,
|
pass: option.Password,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
option: &option,
|
}
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
@ -1,332 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/lucas-clemente/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"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
mbpsToBps = 125000
|
|
||||||
minSpeedBPS = 16384
|
|
||||||
|
|
||||||
DefaultStreamReceiveWindow = 15728640 // 15 MB/s
|
|
||||||
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
|
|
||||||
DefaultMaxIncomingStreams = 1024
|
|
||||||
|
|
||||||
DefaultALPN = "hysteria"
|
|
||||||
DefaultProtocol = "udp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
|
|
||||||
|
|
||||||
type Hysteria struct {
|
|
||||||
*Base
|
|
||||||
|
|
||||||
client *core.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
|
||||||
hdc := hyDialerWithContext{
|
|
||||||
ctx: context.Background(),
|
|
||||||
hyDialer: func() (net.PacketConn, error) {
|
|
||||||
return dialer.ListenPacket(ctx, "udp", "", h.Base.DialOptions(opts...)...)
|
|
||||||
},
|
|
||||||
remoteAddr: func(addr string) (net.Addr, error) {
|
|
||||||
return resolveUDPAddrWithPrefer("udp", addr, h.prefer)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), &hdc)
|
|
||||||
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() (net.PacketConn, error) {
|
|
||||||
return dialer.ListenPacket(ctx, "udp", "", h.Base.DialOptions(opts...)...)
|
|
||||||
},
|
|
||||||
remoteAddr: func(addr string) (net.Addr, error) {
|
|
||||||
return resolveUDPAddrWithPrefer("udp", addr, h.prefer)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
udpConn, err := h.client.DialUDP(&hdc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return newPacketConn(&hyPacketConn{udpConn}, h), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type HysteriaOption struct {
|
|
||||||
BasicOption
|
|
||||||
Name string `proxy:"name"`
|
|
||||||
Server string `proxy:"server"`
|
|
||||||
Port int `proxy:"port"`
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
|
|
||||||
var up, down uint64
|
|
||||||
up = stringToBps(c.Up)
|
|
||||||
if up == 0 {
|
|
||||||
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up)
|
|
||||||
}
|
|
||||||
|
|
||||||
down = stringToBps(c.Down)
|
|
||||||
if down == 0 {
|
|
||||||
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down)
|
|
||||||
}
|
|
||||||
|
|
||||||
return up, down, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHysteria(option HysteriaOption) (*Hysteria, error) {
|
|
||||||
clientTransport := &transport.ClientTransport{
|
|
||||||
Dialer: &net.Dialer{
|
|
||||||
Timeout: 8 * time.Second,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
|
||||||
serverName := option.Server
|
|
||||||
if option.SNI != "" {
|
|
||||||
serverName = option.SNI
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
ServerName: serverName,
|
|
||||||
InsecureSkipVerify: option.SkipCertVerify,
|
|
||||||
MinVersion: tls.VersionTLS13,
|
|
||||||
}
|
|
||||||
|
|
||||||
var bs []byte
|
|
||||||
var err error
|
|
||||||
if len(option.CustomCA) > 0 {
|
|
||||||
bs, err = os.ReadFile(option.CustomCA)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("hysteria %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.GetGlobalFingerprintTLCConfig(tlsConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(option.ALPN) > 0 {
|
|
||||||
tlsConfig.NextProtos = option.ALPN
|
|
||||||
} else {
|
|
||||||
tlsConfig.NextProtos = []string{DefaultALPN}
|
|
||||||
}
|
|
||||||
|
|
||||||
quicConfig := &quic.Config{
|
|
||||||
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
|
||||||
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
|
||||||
InitialConnectionReceiveWindow: uint64(option.ReceiveWindow),
|
|
||||||
MaxConnectionReceiveWindow: uint64(option.ReceiveWindow),
|
|
||||||
KeepAlivePeriod: 10 * time.Second,
|
|
||||||
DisablePathMTUDiscovery: option.DisableMTUDiscovery,
|
|
||||||
EnableDatagrams: true,
|
|
||||||
}
|
|
||||||
if option.ObfsProtocol != "" {
|
|
||||||
option.Protocol = option.ObfsProtocol
|
|
||||||
}
|
|
||||||
if option.Protocol == "" {
|
|
||||||
option.Protocol = DefaultProtocol
|
|
||||||
}
|
|
||||||
if option.ReceiveWindowConn == 0 {
|
|
||||||
quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow / 10
|
|
||||||
quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow
|
|
||||||
}
|
|
||||||
if option.ReceiveWindow == 0 {
|
|
||||||
quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow / 10
|
|
||||||
quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow
|
|
||||||
}
|
|
||||||
if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery {
|
|
||||||
log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
var auth = []byte(option.AuthString)
|
|
||||||
if option.Auth != "" {
|
|
||||||
auth, err = base64.StdEncoding.DecodeString(option.Auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var obfuscator obfs.Obfuscator
|
|
||||||
if len(option.Obfs) > 0 {
|
|
||||||
obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs))
|
|
||||||
}
|
|
||||||
|
|
||||||
up, down, err := option.Speed()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if option.UpSpeed != 0 {
|
|
||||||
up = uint64(option.UpSpeed * mbpsToBps)
|
|
||||||
}
|
|
||||||
if option.DownSpeed != 0 {
|
|
||||||
down = uint64(option.DownSpeed * mbpsToBps)
|
|
||||||
}
|
|
||||||
client, err := core.NewClient(
|
|
||||||
addr, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
|
|
||||||
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
|
|
||||||
}, 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,
|
|
||||||
iface: option.Interface,
|
|
||||||
rmark: option.RoutingMark,
|
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
|
||||||
client: client,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringToBps(s string) uint64 {
|
|
||||||
if s == "" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// when have not unit, use Mbps
|
|
||||||
if v, err := strconv.Atoi(s); err == nil {
|
|
||||||
return stringToBps(fmt.Sprintf("%d Mbps", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
m := rateStringRegexp.FindStringSubmatch(s)
|
|
||||||
if m == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
var n uint64
|
|
||||||
switch m[2] {
|
|
||||||
case "K":
|
|
||||||
n = 1 << 10
|
|
||||||
case "M":
|
|
||||||
n = 1 << 20
|
|
||||||
case "G":
|
|
||||||
n = 1 << 30
|
|
||||||
case "T":
|
|
||||||
n = 1 << 40
|
|
||||||
default:
|
|
||||||
n = 1
|
|
||||||
}
|
|
||||||
v, _ := strconv.ParseUint(m[1], 10, 64)
|
|
||||||
n = v * n
|
|
||||||
if m[3] == "b" {
|
|
||||||
// Bits, need to convert to bytes
|
|
||||||
n = n >> 3
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
type hyPacketConn struct {
|
|
||||||
core.UDPConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *hyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
|
||||||
b, addrStr, err := c.UDPConn.ReadFrom()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
n = copy(p, b)
|
|
||||||
addr = M.ParseSocksaddr(addrStr).UDPAddr()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|
||||||
err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String())
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
n = len(p)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type hyDialerWithContext struct {
|
|
||||||
hyDialer func() (net.PacketConn, error)
|
|
||||||
ctx context.Context
|
|
||||||
remoteAddr func(host string) (net.Addr, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hyDialerWithContext) ListenPacket() (net.PacketConn, error) {
|
|
||||||
return h.hyDialer()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hyDialerWithContext) Context() context.Context {
|
|
||||||
return h.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hyDialerWithContext) RemoteAddr(host string) (net.Addr, error) {
|
|
||||||
return h.remoteAddr(host)
|
|
||||||
}
|
|
@ -27,21 +27,9 @@ func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
|||||||
func NewReject() *Reject {
|
func NewReject() *Reject {
|
||||||
return &Reject{
|
return &Reject{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: "REJECT",
|
name: "REJECT",
|
||||||
tp: C.Reject,
|
tp: C.Reject,
|
||||||
udp: true,
|
udp: true,
|
||||||
prefer: C.DualStack,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPass() *Reject {
|
|
||||||
return &Reject{
|
|
||||||
Base: &Base{
|
|
||||||
name: "PASS",
|
|
||||||
tp: C.Pass,
|
|
||||||
udp: true,
|
|
||||||
prefer: C.DualStack,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,18 +13,14 @@ import (
|
|||||||
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
||||||
"github.com/Dreamacro/clash/transport/socks5"
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
||||||
"github.com/sagernet/sing-shadowsocks"
|
|
||||||
"github.com/sagernet/sing-shadowsocks/shadowimpl"
|
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
"github.com/sagernet/sing/common/uot"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShadowSocks struct {
|
type ShadowSocks struct {
|
||||||
*Base
|
*Base
|
||||||
method shadowsocks.Method
|
cipher core.Cipher
|
||||||
|
|
||||||
option *ShadowSocksOption
|
|
||||||
// obfs
|
// obfs
|
||||||
obfsMode string
|
obfsMode string
|
||||||
obfsOption *simpleObfsOption
|
obfsOption *simpleObfsOption
|
||||||
@ -41,7 +37,6 @@ type ShadowSocksOption struct {
|
|||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Plugin string `proxy:"plugin,omitempty"`
|
Plugin string `proxy:"plugin,omitempty"`
|
||||||
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
|
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
|
||||||
UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type simpleObfsOption struct {
|
type simpleObfsOption struct {
|
||||||
@ -54,7 +49,6 @@ type v2rayObfsOption struct {
|
|||||||
Host string `obfs:"host,omitempty"`
|
Host string `obfs:"host,omitempty"`
|
||||||
Path string `obfs:"path,omitempty"`
|
Path string `obfs:"path,omitempty"`
|
||||||
TLS bool `obfs:"tls,omitempty"`
|
TLS bool `obfs:"tls,omitempty"`
|
||||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
|
||||||
Headers map[string]string `obfs:"headers,omitempty"`
|
Headers map[string]string `obfs:"headers,omitempty"`
|
||||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||||
Mux bool `obfs:"mux,omitempty"`
|
Mux bool `obfs:"mux,omitempty"`
|
||||||
@ -75,10 +69,9 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
|
|||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
|
c = ss.cipher.StreamConn(c)
|
||||||
return ss.method.DialConn(c, M.ParseSocksaddr(uot.UOTMagicAddress+":443"))
|
_, err := c.Write(serializesSocksAddr(metadata))
|
||||||
}
|
return c, err
|
||||||
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
@ -97,43 +90,26 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, op
|
|||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
if ss.option.UDPOverTCP {
|
|
||||||
tcpConn, err := ss.DialContext(ctx, metadata, opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return newPacketConn(uot.NewClientConn(tcpConn), ss), nil
|
|
||||||
}
|
|
||||||
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := resolveUDPAddrWithPrefer("udp", ss.addr, ss.prefer)
|
addr, err := resolveUDPAddr("udp", ss.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pc.Close()
|
pc.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pc = ss.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: pc, Addr: addr})
|
|
||||||
return newPacketConn(pc, ss), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
pc = ss.cipher.PacketConn(pc)
|
||||||
func (ss *ShadowSocks) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
|
||||||
if ss.option.UDPOverTCP {
|
|
||||||
return newPacketConn(uot.NewClientConn(c), 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) {
|
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
method, err := shadowimpl.FetchMethod(option.Cipher, option.Password)
|
cipher := option.Cipher
|
||||||
|
password := option.Password
|
||||||
|
ciph, err := core.PickCipher(cipher, nil, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
|
return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
|
||||||
}
|
}
|
||||||
@ -179,17 +155,15 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
|||||||
|
|
||||||
return &ShadowSocks{
|
return &ShadowSocks{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
tp: C.Shadowsocks,
|
tp: C.Shadowsocks,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
iface: option.Interface,
|
iface: option.Interface,
|
||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
},
|
||||||
method: method,
|
cipher: ciph,
|
||||||
|
|
||||||
option: &option,
|
|
||||||
obfsMode: obfsMode,
|
obfsMode: obfsMode,
|
||||||
v2rayOption: v2rayOption,
|
v2rayOption: v2rayOption,
|
||||||
obfsOption: obfsOption,
|
obfsOption: obfsOption,
|
||||||
|
@ -8,11 +8,12 @@ import (
|
|||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/transport/shadowsocks/core"
|
|
||||||
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
|
|
||||||
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
|
|
||||||
"github.com/Dreamacro/clash/transport/ssr/obfs"
|
"github.com/Dreamacro/clash/transport/ssr/obfs"
|
||||||
"github.com/Dreamacro/clash/transport/ssr/protocol"
|
"github.com/Dreamacro/clash/transport/ssr/protocol"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShadowSocksR struct {
|
type ShadowSocksR struct {
|
||||||
@ -79,7 +80,7 @@ func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Me
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := resolveUDPAddrWithPrefer("udp", ssr.addr, ssr.prefer)
|
addr, err := resolveUDPAddr("udp", ssr.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pc.Close()
|
pc.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -143,13 +144,12 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
|||||||
|
|
||||||
return &ShadowSocksR{
|
return &ShadowSocksR{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
tp: C.ShadowsocksR,
|
tp: C.ShadowsocksR,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
iface: option.Interface,
|
iface: option.Interface,
|
||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
},
|
||||||
cipher: coreCiph,
|
cipher: coreCiph,
|
||||||
obfs: obfs,
|
obfs: obfs,
|
||||||
|
@ -53,10 +53,6 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
|
|||||||
// StreamConn implements C.ProxyAdapter
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
|
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
|
||||||
if metadata.NetWork == C.UDP {
|
|
||||||
err := snell.WriteUDPHeader(c, s.version)
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
||||||
return c, err
|
return c, err
|
||||||
@ -108,17 +104,6 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
|
|||||||
return newPacketConn(pc, s), nil
|
return newPacketConn(pc, s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
func (s *Snell) SupportUOT() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSnell(option SnellOption) (*Snell, error) {
|
func NewSnell(option SnellOption) (*Snell, error) {
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
psk := []byte(option.Psk)
|
psk := []byte(option.Psk)
|
||||||
@ -152,13 +137,12 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
|||||||
|
|
||||||
s := &Snell{
|
s := &Snell{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
tp: C.Snell,
|
tp: C.Snell,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
iface: option.Interface,
|
iface: option.Interface,
|
||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
},
|
||||||
psk: psk,
|
psk: psk,
|
||||||
obfsOption: obfsOption,
|
obfsOption: obfsOption,
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -34,7 +33,6 @@ type Socks5Option struct {
|
|||||||
TLS bool `proxy:"tls,omitempty"`
|
TLS bool `proxy:"tls,omitempty"`
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamConn implements C.ProxyAdapter
|
// StreamConn implements C.ProxyAdapter
|
||||||
@ -140,40 +138,30 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
|||||||
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
|
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
|
var tlsConfig *tls.Config
|
||||||
if option.TLS {
|
if option.TLS {
|
||||||
tlsConfig = &tls.Config{
|
tlsConfig = &tls.Config{
|
||||||
InsecureSkipVerify: option.SkipCertVerify,
|
InsecureSkipVerify: option.SkipCertVerify,
|
||||||
ServerName: option.Server,
|
ServerName: option.Server,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(option.Fingerprint) == 0 {
|
|
||||||
tlsConfig = tlsC.GetGlobalFingerprintTLCConfig(tlsConfig)
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Socks5{
|
return &Socks5{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Socks5,
|
tp: C.Socks5,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
iface: option.Interface,
|
iface: option.Interface,
|
||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
},
|
||||||
user: option.UserName,
|
user: option.UserName,
|
||||||
pass: option.Password,
|
pass: option.Password,
|
||||||
tls: option.TLS,
|
tls: option.TLS,
|
||||||
skipCertVerify: option.SkipCertVerify,
|
skipCertVerify: option.SkipCertVerify,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type socksPacketConn struct {
|
type socksPacketConn struct {
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -13,7 +12,8 @@ import (
|
|||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/transport/gun"
|
"github.com/Dreamacro/clash/transport/gun"
|
||||||
"github.com/Dreamacro/clash/transport/trojan"
|
"github.com/Dreamacro/clash/transport/trojan"
|
||||||
"github.com/Dreamacro/clash/transport/vless"
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Trojan struct {
|
type Trojan struct {
|
||||||
@ -24,7 +24,7 @@ type Trojan struct {
|
|||||||
// for gun mux
|
// for gun mux
|
||||||
gunTLSConfig *tls.Config
|
gunTLSConfig *tls.Config
|
||||||
gunConfig *gun.Config
|
gunConfig *gun.Config
|
||||||
transport *gun.TransportWrap
|
transport *http2.Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrojanOption struct {
|
type TrojanOption struct {
|
||||||
@ -36,13 +36,10 @@ type TrojanOption struct {
|
|||||||
ALPN []string `proxy:"alpn,omitempty"`
|
ALPN []string `proxy:"alpn,omitempty"`
|
||||||
SNI string `proxy:"sni,omitempty"`
|
SNI string `proxy:"sni,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Network string `proxy:"network,omitempty"`
|
Network string `proxy:"network,omitempty"`
|
||||||
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||||
WSOpts WSOptions `proxy:"ws-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) {
|
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
|
||||||
@ -85,15 +82,6 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
|||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = t.instance.PresetXTLSConn(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if metadata.NetWork == C.UDP {
|
|
||||||
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
|
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
@ -107,12 +95,6 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = t.instance.PresetXTLSConn(c)
|
|
||||||
if err != nil {
|
|
||||||
c.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
|
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
|
||||||
c.Close()
|
c.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -170,17 +152,6 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
|||||||
return newPacketConn(pc, t), err
|
return newPacketConn(pc, t), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
|
||||||
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|
||||||
pc := t.instance.PacketConn(c)
|
|
||||||
return newPacketConn(pc, t), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportUOT implements C.ProxyAdapter
|
|
||||||
func (t *Trojan) SupportUOT() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
|
||||||
@ -189,18 +160,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
ALPN: option.ALPN,
|
ALPN: option.ALPN,
|
||||||
ServerName: option.Server,
|
ServerName: option.Server,
|
||||||
SkipCertVerify: option.SkipCertVerify,
|
SkipCertVerify: option.SkipCertVerify,
|
||||||
FlowShow: option.FlowShow,
|
|
||||||
Fingerprint: option.Fingerprint,
|
|
||||||
}
|
|
||||||
|
|
||||||
if option.Network != "ws" && len(option.Flow) >= 16 {
|
|
||||||
option.Flow = option.Flow[:16]
|
|
||||||
switch option.Flow {
|
|
||||||
case vless.XRO, vless.XRD, vless.XRS:
|
|
||||||
tOption.Flow = option.Flow
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if option.SNI != "" {
|
if option.SNI != "" {
|
||||||
@ -209,13 +168,12 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
|
|
||||||
t := &Trojan{
|
t := &Trojan{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
tp: C.Trojan,
|
tp: C.Trojan,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
iface: option.Interface,
|
iface: option.Interface,
|
||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
},
|
||||||
instance: trojan.New(tOption),
|
instance: trojan.New(tOption),
|
||||||
option: &option,
|
option: &option,
|
||||||
@ -238,21 +196,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
ServerName: tOption.ServerName,
|
ServerName: tOption.ServerName,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(option.Fingerprint) == 0 {
|
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||||
tlsConfig = tlsC.GetGlobalFingerprintTLCConfig(tlsConfig)
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.option.Flow != "" {
|
|
||||||
t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
|
|
||||||
} else {
|
|
||||||
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.gunTLSConfig = tlsConfig
|
t.gunTLSConfig = tlsConfig
|
||||||
t.gunConfig = &gun.Config{
|
t.gunConfig = &gun.Config{
|
||||||
ServiceName: option.GrpcOpts.GrpcServiceName,
|
ServiceName: option.GrpcOpts.GrpcServiceName,
|
||||||
|
@ -2,12 +2,8 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
|
||||||
xtls "github.com/xtls/go"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
@ -15,49 +11,28 @@ import (
|
|||||||
"github.com/Dreamacro/clash/transport/socks5"
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
globalClientSessionCache tls.ClientSessionCache
|
|
||||||
globalClientXSessionCache xtls.ClientSessionCache
|
|
||||||
once sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
func tcpKeepAlive(c net.Conn) {
|
func tcpKeepAlive(c net.Conn) {
|
||||||
if tcp, ok := c.(*net.TCPConn); ok {
|
if tcp, ok := c.(*net.TCPConn); ok {
|
||||||
_ = tcp.SetKeepAlive(true)
|
tcp.SetKeepAlive(true)
|
||||||
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
|
tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientSessionCache() tls.ClientSessionCache {
|
|
||||||
once.Do(func() {
|
|
||||||
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
|
|
||||||
})
|
|
||||||
return globalClientSessionCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClientXSessionCache() xtls.ClientSessionCache {
|
|
||||||
once.Do(func() {
|
|
||||||
globalClientXSessionCache = xtls.NewLRUClientSessionCache(128)
|
|
||||||
})
|
|
||||||
return globalClientXSessionCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
||||||
var buf [][]byte
|
var buf [][]byte
|
||||||
addrType := metadata.AddrType()
|
aType := uint8(metadata.AddrType)
|
||||||
aType := uint8(addrType)
|
|
||||||
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
|
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
|
||||||
switch addrType {
|
switch metadata.AddrType {
|
||||||
case socks5.AtypDomainName:
|
case socks5.AtypDomainName:
|
||||||
lenM := uint8(len(metadata.Host))
|
len := uint8(len(metadata.Host))
|
||||||
host := []byte(metadata.Host)
|
host := []byte(metadata.Host)
|
||||||
buf = [][]byte{{aType, lenM}, host, port}
|
buf = [][]byte{{aType, len}, host, port}
|
||||||
case socks5.AtypIPv4:
|
case socks5.AtypIPv4:
|
||||||
host := metadata.DstIP.AsSlice()
|
host := metadata.DstIP.To4()
|
||||||
buf = [][]byte{{aType}, host, port}
|
buf = [][]byte{{aType}, host, port}
|
||||||
case socks5.AtypIPv6:
|
case socks5.AtypIPv6:
|
||||||
host := metadata.DstIP.AsSlice()
|
host := metadata.DstIP.To16()
|
||||||
buf = [][]byte{{aType}, host, port}
|
buf = [][]byte{{aType}, host, port}
|
||||||
}
|
}
|
||||||
return bytes.Join(buf, nil)
|
return bytes.Join(buf, nil)
|
||||||
@ -69,64 +44,7 @@ func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := resolver.ResolveProxyServerHost(host)
|
ip, err := resolver.ResolveIP(host)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveUDPAddrWithPrefer(network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) {
|
|
||||||
host, port, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var ip netip.Addr
|
|
||||||
switch prefer {
|
|
||||||
case C.IPv4Only:
|
|
||||||
ip, err = resolver.ResolveIPv4ProxyServerHost(host)
|
|
||||||
case C.IPv6Only:
|
|
||||||
ip, err = resolver.ResolveIPv6ProxyServerHost(host)
|
|
||||||
case C.IPv6Prefer:
|
|
||||||
var ips []netip.Addr
|
|
||||||
ips, err = resolver.ResolveAllIPProxyServerHost(host)
|
|
||||||
var fallback netip.Addr
|
|
||||||
if err == nil {
|
|
||||||
for _, addr := range ips {
|
|
||||||
if addr.Is6() {
|
|
||||||
ip = addr
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
if !fallback.IsValid() {
|
|
||||||
fallback = addr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ip = fallback
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// C.IPv4Prefer, C.DualStack and other
|
|
||||||
var ips []netip.Addr
|
|
||||||
ips, err = resolver.ResolveAllIPProxyServerHost(host)
|
|
||||||
var fallback netip.Addr
|
|
||||||
if err == nil {
|
|
||||||
for _, addr := range ips {
|
|
||||||
if addr.Is4() {
|
|
||||||
ip = addr
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
if !fallback.IsValid() {
|
|
||||||
fallback = addr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ip.IsValid() && fallback.IsValid() {
|
|
||||||
ip = fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -135,6 +53,6 @@ func resolveUDPAddrWithPrefer(network, address string, prefer C.DNSPrefer) (*net
|
|||||||
|
|
||||||
func safeConnClose(c net.Conn, err error) {
|
func safeConnClose(c net.Conn, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Close()
|
c.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,473 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
"github.com/Dreamacro/clash/transport/socks5"
|
|
||||||
"github.com/Dreamacro/clash/transport/vless"
|
|
||||||
"github.com/Dreamacro/clash/transport/vmess"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// max packet length
|
|
||||||
maxLength = 1024 << 3
|
|
||||||
)
|
|
||||||
|
|
||||||
type Vless struct {
|
|
||||||
*Base
|
|
||||||
client *vless.Client
|
|
||||||
option *VlessOption
|
|
||||||
|
|
||||||
// for gun mux
|
|
||||||
gunTLSConfig *tls.Config
|
|
||||||
gunConfig *gun.Config
|
|
||||||
transport *gun.TransportWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
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"`
|
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
|
||||||
ServerName string `proxy:"servername,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|
||||||
var err error
|
|
||||||
switch v.option.Network {
|
|
||||||
case "ws":
|
|
||||||
|
|
||||||
host, port, _ := net.SplitHostPort(v.addr)
|
|
||||||
wsOpts := &vmess.WebsocketConfig{
|
|
||||||
Host: host,
|
|
||||||
Port: port,
|
|
||||||
Path: v.option.WSOpts.Path,
|
|
||||||
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
|
|
||||||
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
|
|
||||||
Headers: http.Header{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(v.option.WSOpts.Headers) != 0 {
|
|
||||||
for key, value := range v.option.WSOpts.Headers {
|
|
||||||
wsOpts.Headers.Add(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v.option.TLS {
|
|
||||||
wsOpts.TLS = true
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
ServerName: host,
|
|
||||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
|
||||||
NextProtos: []string{"http/1.1"},
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(v.option.Fingerprint) == 0 {
|
|
||||||
wsOpts.TLSConfig = tlsC.GetGlobalFingerprintTLCConfig(tlsConfig)
|
|
||||||
} else {
|
|
||||||
wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.option.ServerName != "" {
|
|
||||||
wsOpts.TLSConfig.ServerName = v.option.ServerName
|
|
||||||
} else if host := wsOpts.Headers.Get("Host"); host != "" {
|
|
||||||
wsOpts.TLSConfig.ServerName = host
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if host := wsOpts.Headers.Get("Host"); host == "" {
|
|
||||||
wsOpts.Headers.Set("Host", convert.RandHost())
|
|
||||||
convert.SetUserAgent(wsOpts.Headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
|
||||||
case "http":
|
|
||||||
// readability first, so just copy default TLS logic
|
|
||||||
c, err = v.streamTLSOrXTLSConn(c, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
|
||||||
httpOpts := &vmess.HTTPConfig{
|
|
||||||
Host: host,
|
|
||||||
Method: v.option.HTTPOpts.Method,
|
|
||||||
Path: v.option.HTTPOpts.Path,
|
|
||||||
Headers: v.option.HTTPOpts.Headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
c = vmess.StreamHTTPConn(c, httpOpts)
|
|
||||||
case "h2":
|
|
||||||
c, err = v.streamTLSOrXTLSConn(c, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h2Opts := &vmess.H2Config{
|
|
||||||
Hosts: v.option.HTTP2Opts.Host,
|
|
||||||
Path: v.option.HTTP2Opts.Path,
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err = vmess.StreamH2Conn(c, h2Opts)
|
|
||||||
case "grpc":
|
|
||||||
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
|
|
||||||
c, err = v.streamTLSOrXTLSConn(c, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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.isXTLSEnabled() {
|
|
||||||
xtlsOpts := vless.XTLSConfig{
|
|
||||||
Host: host,
|
|
||||||
SkipCertVerify: v.option.SkipCertVerify,
|
|
||||||
FingerPrint: v.option.Fingerprint,
|
|
||||||
}
|
|
||||||
|
|
||||||
if isH2 {
|
|
||||||
xtlsOpts.NextProtos = []string{"h2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.option.ServerName != "" {
|
|
||||||
xtlsOpts.Host = v.option.ServerName
|
|
||||||
}
|
|
||||||
|
|
||||||
return vless.StreamXTLSConn(conn, &xtlsOpts)
|
|
||||||
|
|
||||||
} else if v.option.TLS {
|
|
||||||
tlsOpts := vmess.TLSConfig{
|
|
||||||
Host: host,
|
|
||||||
SkipCertVerify: v.option.SkipCertVerify,
|
|
||||||
FingerPrint: v.option.Fingerprint,
|
|
||||||
}
|
|
||||||
|
|
||||||
if isH2 {
|
|
||||||
tlsOpts.NextProtos = []string{"h2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.option.ServerName != "" {
|
|
||||||
tlsOpts.Host = v.option.ServerName
|
|
||||||
}
|
|
||||||
|
|
||||||
return vmess.StreamTLSConn(conn, &tlsOpts)
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vless) isXTLSEnabled() bool {
|
|
||||||
return v.client.Addons != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
|
||||||
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
|
||||||
// gun transport
|
|
||||||
if v.transport != nil && len(opts) == 0 {
|
|
||||||
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer safeConnClose(c, err)
|
|
||||||
|
|
||||||
c, err = v.client.StreamConn(c, parseVlessAddr(metadata))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewConn(c, v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
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 needs a net.UDPAddr
|
|
||||||
if !metadata.Resolved() {
|
|
||||||
ip, err := resolver.ResolveIP(metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
|
||||||
|
|
||||||
var c net.Conn
|
|
||||||
// gun transport
|
|
||||||
if v.transport != nil && len(opts) == 0 {
|
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer safeConnClose(c, err)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("new vless client error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.ListenPacketOnStreamConn(c, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
|
||||||
func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|
||||||
return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportUOT implements C.ProxyAdapter
|
|
||||||
func (v *Vless) SupportUOT() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
|
|
||||||
var addrType byte
|
|
||||||
var addr []byte
|
|
||||||
switch metadata.AddrType() {
|
|
||||||
case socks5.AtypIPv4:
|
|
||||||
addrType = vless.AtypIPv4
|
|
||||||
addr = make([]byte, net.IPv4len)
|
|
||||||
copy(addr[:], metadata.DstIP.AsSlice())
|
|
||||||
case socks5.AtypIPv6:
|
|
||||||
addrType = vless.AtypIPv6
|
|
||||||
addr = make([]byte, net.IPv6len)
|
|
||||||
copy(addr[:], metadata.DstIP.AsSlice())
|
|
||||||
case socks5.AtypDomainName:
|
|
||||||
addrType = vless.AtypDomainName
|
|
||||||
addr = make([]byte, len(metadata.Host)+1)
|
|
||||||
addr[0] = byte(len(metadata.Host))
|
|
||||||
copy(addr[1:], metadata.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
|
||||||
return &vless.DstAddr{
|
|
||||||
UDP: metadata.NetWork == C.UDP,
|
|
||||||
AddrType: addrType,
|
|
||||||
Addr: addr,
|
|
||||||
Port: uint(port),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type vlessPacketConn struct {
|
|
||||||
net.Conn
|
|
||||||
rAddr net.Addr
|
|
||||||
remain int
|
|
||||||
mux sync.Mutex
|
|
||||||
cache [2]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *vlessPacketConn) writePacket(payload []byte) (int, error) {
|
|
||||||
binary.BigEndian.PutUint16(c.cache[:], uint16(len(payload)))
|
|
||||||
|
|
||||||
if _, err := c.Conn.Write(c.cache[:]); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Conn.Write(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
|
||||||
total := len(b)
|
|
||||||
if total == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if total <= maxLength {
|
|
||||||
return c.writePacket(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := 0
|
|
||||||
|
|
||||||
for offset < total {
|
|
||||||
cursor := offset + maxLength
|
|
||||||
if cursor > total {
|
|
||||||
cursor = total
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := c.writePacket(b[offset:cursor])
|
|
||||||
if err != nil {
|
|
||||||
return offset + n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
offset = cursor
|
|
||||||
if offset == total {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
|
||||||
c.mux.Lock()
|
|
||||||
defer c.mux.Unlock()
|
|
||||||
|
|
||||||
if c.remain > 0 {
|
|
||||||
length := len(b)
|
|
||||||
if c.remain < length {
|
|
||||||
length = c.remain
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := c.Conn.Read(b[:length])
|
|
||||||
if err != nil {
|
|
||||||
return 0, c.rAddr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.remain -= n
|
|
||||||
return n, c.rAddr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := c.Conn.Read(b[:2]); err != nil {
|
|
||||||
return 0, c.rAddr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
total := int(binary.BigEndian.Uint16(b[:2]))
|
|
||||||
if total == 0 {
|
|
||||||
return 0, c.rAddr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
length := len(b)
|
|
||||||
if length > total {
|
|
||||||
length = total
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := io.ReadFull(c.Conn, b[:length]); err != nil {
|
|
||||||
return 0, c.rAddr, errors.New("read packet error")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.remain = total - length
|
|
||||||
|
|
||||||
return length, c.rAddr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewVless(option VlessOption) (*Vless, error) {
|
|
||||||
var addons *vless.Addons
|
|
||||||
if option.Network != "ws" && len(option.Flow) >= 16 {
|
|
||||||
option.Flow = option.Flow[:16]
|
|
||||||
switch option.Flow {
|
|
||||||
case vless.XRO, vless.XRD, vless.XRS:
|
|
||||||
addons = &vless.Addons{
|
|
||||||
Flow: option.Flow,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := vless.NewClient(option.UUID, addons, option.FlowShow)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
v := &Vless{
|
|
||||||
Base: &Base{
|
|
||||||
name: option.Name,
|
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
|
||||||
tp: C.Vless,
|
|
||||||
udp: option.UDP,
|
|
||||||
iface: option.Interface,
|
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
|
||||||
client: client,
|
|
||||||
option: &option,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch option.Network {
|
|
||||||
case "h2":
|
|
||||||
if len(option.HTTP2Opts.Host) == 0 {
|
|
||||||
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
|
|
||||||
}
|
|
||||||
case "grpc":
|
|
||||||
dialFn := func(network, addr string) (net.Conn, error) {
|
|
||||||
c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
|
||||||
}
|
|
||||||
tcpKeepAlive(c)
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gunConfig := &gun.Config{
|
|
||||||
ServiceName: v.option.GrpcOpts.GrpcServiceName,
|
|
||||||
Host: v.option.ServerName,
|
|
||||||
}
|
|
||||||
tlsConfig := tlsC.GetGlobalFingerprintTLCConfig(&tls.Config{
|
|
||||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
|
||||||
ServerName: v.option.ServerName,
|
|
||||||
})
|
|
||||||
|
|
||||||
if v.option.ServerName == "" {
|
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
|
||||||
tlsConfig.ServerName = host
|
|
||||||
gunConfig.Host = host
|
|
||||||
}
|
|
||||||
|
|
||||||
v.gunTLSConfig = tlsConfig
|
|
||||||
v.gunConfig = gunConfig
|
|
||||||
if v.isXTLSEnabled() {
|
|
||||||
v.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
|
|
||||||
} else {
|
|
||||||
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
@ -5,21 +5,18 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
|
||||||
vmess "github.com/sagernet/sing-vmess"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/transport/gun"
|
"github.com/Dreamacro/clash/transport/gun"
|
||||||
clashVMess "github.com/Dreamacro/clash/transport/vmess"
|
"github.com/Dreamacro/clash/transport/vmess"
|
||||||
"github.com/sagernet/sing-vmess/packetaddr"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Vmess struct {
|
type Vmess struct {
|
||||||
@ -30,32 +27,26 @@ type Vmess struct {
|
|||||||
// for gun mux
|
// for gun mux
|
||||||
gunTLSConfig *tls.Config
|
gunTLSConfig *tls.Config
|
||||||
gunConfig *gun.Config
|
gunConfig *gun.Config
|
||||||
transport *gun.TransportWrap
|
transport *http2.Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
type VmessOption struct {
|
type VmessOption struct {
|
||||||
BasicOption
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
UUID string `proxy:"uuid"`
|
UUID string `proxy:"uuid"`
|
||||||
AlterID int `proxy:"alterId"`
|
AlterID int `proxy:"alterId"`
|
||||||
Cipher string `proxy:"cipher"`
|
Cipher string `proxy:"cipher"`
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Network string `proxy:"network,omitempty"`
|
Network string `proxy:"network,omitempty"`
|
||||||
TLS bool `proxy:"tls,omitempty"`
|
TLS bool `proxy:"tls,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
ServerName string `proxy:"servername,omitempty"`
|
||||||
ServerName string `proxy:"servername,omitempty"`
|
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
||||||
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
||||||
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||||
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
WSOpts WSOptions `proxy:"ws-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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPOptions struct {
|
type HTTPOptions struct {
|
||||||
@ -85,52 +76,42 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
var err error
|
var err error
|
||||||
switch v.option.Network {
|
switch v.option.Network {
|
||||||
case "ws":
|
case "ws":
|
||||||
|
|
||||||
host, port, _ := net.SplitHostPort(v.addr)
|
host, port, _ := net.SplitHostPort(v.addr)
|
||||||
wsOpts := &clashVMess.WebsocketConfig{
|
wsOpts := &vmess.WebsocketConfig{
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
Path: v.option.WSOpts.Path,
|
Path: v.option.WSOpts.Path,
|
||||||
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
|
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
|
||||||
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
|
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
|
||||||
Headers: http.Header{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(v.option.WSOpts.Headers) != 0 {
|
if len(v.option.WSOpts.Headers) != 0 {
|
||||||
|
header := http.Header{}
|
||||||
for key, value := range v.option.WSOpts.Headers {
|
for key, value := range v.option.WSOpts.Headers {
|
||||||
wsOpts.Headers.Add(key, value)
|
header.Add(key, value)
|
||||||
}
|
}
|
||||||
|
wsOpts.Headers = header
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.option.TLS {
|
if v.option.TLS {
|
||||||
wsOpts.TLS = true
|
wsOpts.TLS = true
|
||||||
tlsConfig := &tls.Config{
|
wsOpts.TLSConfig = &tls.Config{
|
||||||
ServerName: host,
|
ServerName: host,
|
||||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||||
NextProtos: []string{"http/1.1"},
|
NextProtos: []string{"http/1.1"},
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(v.option.Fingerprint) == 0 {
|
|
||||||
wsOpts.TLSConfig = tlsC.GetGlobalFingerprintTLCConfig(tlsConfig)
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
if wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.option.ServerName != "" {
|
if v.option.ServerName != "" {
|
||||||
wsOpts.TLSConfig.ServerName = v.option.ServerName
|
wsOpts.TLSConfig.ServerName = v.option.ServerName
|
||||||
} else if host := wsOpts.Headers.Get("Host"); host != "" {
|
} else if host := wsOpts.Headers.Get("Host"); host != "" {
|
||||||
wsOpts.TLSConfig.ServerName = host
|
wsOpts.TLSConfig.ServerName = host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c, err = clashVMess.StreamWebsocketConn(c, wsOpts)
|
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
||||||
case "http":
|
case "http":
|
||||||
// readability first, so just copy default TLS logic
|
// readability first, so just copy default TLS logic
|
||||||
if v.option.TLS {
|
if v.option.TLS {
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
tlsOpts := &clashVMess.TLSConfig{
|
tlsOpts := &vmess.TLSConfig{
|
||||||
Host: host,
|
Host: host,
|
||||||
SkipCertVerify: v.option.SkipCertVerify,
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
}
|
}
|
||||||
@ -139,24 +120,24 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
tlsOpts.Host = v.option.ServerName
|
tlsOpts.Host = v.option.ServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = clashVMess.StreamTLSConn(c, tlsOpts)
|
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
httpOpts := &clashVMess.HTTPConfig{
|
httpOpts := &vmess.HTTPConfig{
|
||||||
Host: host,
|
Host: host,
|
||||||
Method: v.option.HTTPOpts.Method,
|
Method: v.option.HTTPOpts.Method,
|
||||||
Path: v.option.HTTPOpts.Path,
|
Path: v.option.HTTPOpts.Path,
|
||||||
Headers: v.option.HTTPOpts.Headers,
|
Headers: v.option.HTTPOpts.Headers,
|
||||||
}
|
}
|
||||||
|
|
||||||
c = clashVMess.StreamHTTPConn(c, httpOpts)
|
c = vmess.StreamHTTPConn(c, httpOpts)
|
||||||
case "h2":
|
case "h2":
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
tlsOpts := clashVMess.TLSConfig{
|
tlsOpts := vmess.TLSConfig{
|
||||||
Host: host,
|
Host: host,
|
||||||
SkipCertVerify: v.option.SkipCertVerify,
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
NextProtos: []string{"h2"},
|
NextProtos: []string{"h2"},
|
||||||
@ -166,24 +147,24 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
tlsOpts.Host = v.option.ServerName
|
tlsOpts.Host = v.option.ServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = clashVMess.StreamTLSConn(c, &tlsOpts)
|
c, err = vmess.StreamTLSConn(c, &tlsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
h2Opts := &clashVMess.H2Config{
|
h2Opts := &vmess.H2Config{
|
||||||
Hosts: v.option.HTTP2Opts.Host,
|
Hosts: v.option.HTTP2Opts.Host,
|
||||||
Path: v.option.HTTP2Opts.Path,
|
Path: v.option.HTTP2Opts.Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = clashVMess.StreamH2Conn(c, h2Opts)
|
c, err = vmess.StreamH2Conn(c, h2Opts)
|
||||||
case "grpc":
|
case "grpc":
|
||||||
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
|
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
|
||||||
default:
|
default:
|
||||||
// handle TLS
|
// handle TLS
|
||||||
if v.option.TLS {
|
if v.option.TLS {
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
tlsOpts := &clashVMess.TLSConfig{
|
tlsOpts := &vmess.TLSConfig{
|
||||||
Host: host,
|
Host: host,
|
||||||
SkipCertVerify: v.option.SkipCertVerify,
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
}
|
}
|
||||||
@ -192,22 +173,15 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
tlsOpts.Host = v.option.ServerName
|
tlsOpts.Host = v.option.ServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = clashVMess.StreamTLSConn(c, tlsOpts)
|
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if metadata.NetWork == C.UDP {
|
|
||||||
if v.option.XUDP {
|
return v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
return v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
|
||||||
} else {
|
|
||||||
return v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
@ -220,7 +194,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
}
|
}
|
||||||
defer safeConnClose(c, err)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -250,13 +224,6 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
|
|||||||
metadata.DstIP = 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
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil && len(opts) == 0 {
|
if v.transport != nil && len(opts) == 0 {
|
||||||
@ -266,11 +233,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
|
|||||||
}
|
}
|
||||||
defer safeConnClose(c, err)
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
if v.option.XUDP {
|
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
c, err = v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
|
||||||
} else {
|
|
||||||
c, err = v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
|
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -286,53 +249,23 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
|
|||||||
return nil, fmt.Errorf("new vmess client error: %v", err)
|
return nil, fmt.Errorf("new vmess client error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportUOT implements C.ProxyAdapter
|
|
||||||
func (v *Vmess) SupportUOT() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewVmess(option VmessOption) (*Vmess, error) {
|
func NewVmess(option VmessOption) (*Vmess, error) {
|
||||||
security := strings.ToLower(option.Cipher)
|
security := strings.ToLower(option.Cipher)
|
||||||
var options []vmess.ClientOption
|
client, err := vmess.NewClient(vmess.Config{
|
||||||
if option.GlobalPadding {
|
UUID: option.UUID,
|
||||||
options = append(options, vmess.ClientWithGlobalPadding())
|
AlterID: uint16(option.AlterID),
|
||||||
}
|
Security: security,
|
||||||
if option.AuthenticatedLength {
|
HostName: option.Server,
|
||||||
options = append(options, vmess.ClientWithAuthenticatedLength())
|
Port: strconv.Itoa(option.Port),
|
||||||
}
|
IsAead: option.AlterID == 0,
|
||||||
client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch option.PacketEncoding {
|
|
||||||
case "packetaddr", "packet":
|
|
||||||
option.PacketAddr = true
|
|
||||||
case "xudp":
|
|
||||||
option.XUDP = true
|
|
||||||
}
|
|
||||||
if option.XUDP {
|
|
||||||
option.PacketAddr = false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch option.Network {
|
switch option.Network {
|
||||||
case "h2", "grpc":
|
case "h2", "grpc":
|
||||||
if !option.TLS {
|
if !option.TLS {
|
||||||
@ -342,13 +275,12 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
|
|
||||||
v := &Vmess{
|
v := &Vmess{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Vmess,
|
tp: C.Vmess,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
iface: option.Interface,
|
iface: option.Interface,
|
||||||
rmark: option.RoutingMark,
|
rmark: option.RoutingMark,
|
||||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
|
||||||
},
|
},
|
||||||
client: client,
|
client: client,
|
||||||
option: &option,
|
option: &option,
|
||||||
@ -388,29 +320,44 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
v.gunConfig = gunConfig
|
v.gunConfig = gunConfig
|
||||||
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type threadSafePacketConn struct {
|
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
||||||
net.PacketConn
|
var addrType byte
|
||||||
access sync.Mutex
|
var addr []byte
|
||||||
}
|
switch metadata.AddrType {
|
||||||
|
case C.AtypIPv4:
|
||||||
|
addrType = byte(vmess.AtypIPv4)
|
||||||
|
addr = make([]byte, net.IPv4len)
|
||||||
|
copy(addr[:], metadata.DstIP.To4())
|
||||||
|
case C.AtypIPv6:
|
||||||
|
addrType = byte(vmess.AtypIPv6)
|
||||||
|
addr = make([]byte, net.IPv6len)
|
||||||
|
copy(addr[:], metadata.DstIP.To16())
|
||||||
|
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) {
|
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
c.access.Lock()
|
return &vmess.DstAddr{
|
||||||
defer c.access.Unlock()
|
UDP: metadata.NetWork == C.UDP,
|
||||||
return c.PacketConn.WriteTo(b, addr)
|
AddrType: addrType,
|
||||||
|
Addr: addr,
|
||||||
|
Port: uint(port),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type vmessPacketConn struct {
|
type vmessPacketConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
rAddr net.Addr
|
rAddr net.Addr
|
||||||
access sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
uc.access.Lock()
|
|
||||||
defer uc.access.Unlock()
|
|
||||||
return uc.Conn.Write(b)
|
return uc.Conn.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,328 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
"github.com/Dreamacro/clash/listener/sing"
|
|
||||||
|
|
||||||
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 *wgDialer
|
|
||||||
startOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
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 []int `proxy:"reserved,omitempty"`
|
|
||||||
Workers int `proxy:"workers,omitempty"`
|
|
||||||
MTU int `proxy:"mtu,omitempty"`
|
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type wgDialer struct {
|
|
||||||
options []dialer.Option
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *wgDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
return dialer.DialContext(ctx, network, destination.String(), d.options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *wgDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
|
||||||
return dialer.ListenPacket(ctx, "udp", "", d.options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
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: &wgDialer{},
|
|
||||||
}
|
|
||||||
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.ParseSocksaddr(option.Server)
|
|
||||||
peerAddr.Port = 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"
|
|
||||||
}
|
|
||||||
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{}) {
|
|
||||||
sing.Logger.Debug(fmt.Sprintf(strings.ToLower(format), args...))
|
|
||||||
},
|
|
||||||
Errorf: func(format string, args ...interface{}) {
|
|
||||||
sing.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...))
|
|
||||||
},
|
|
||||||
}, option.Workers)
|
|
||||||
if debug.Enabled {
|
|
||||||
sing.Logger.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) {
|
|
||||||
w.dialer.options = opts
|
|
||||||
var conn net.Conn
|
|
||||||
w.startOnce.Do(func() {
|
|
||||||
err = w.tunDevice.Start()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !metadata.Resolved() {
|
|
||||||
var addrs []netip.Addr
|
|
||||||
addrs, err = resolver.ResolveAllIP(metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
conn, err = N.DialSerial(ctx, w.tunDevice, "tcp", M.ParseSocksaddr(metadata.RemoteAddress()), addrs)
|
|
||||||
} else {
|
|
||||||
conn, err = w.tunDevice.DialContext(ctx, "tcp", M.ParseSocksaddr(metadata.Pure().RemoteAddress()))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewConn(&wgConn{conn, w}, w), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
|
||||||
w.dialer.options = opts
|
|
||||||
var pc net.PacketConn
|
|
||||||
w.startOnce.Do(func() {
|
|
||||||
err = w.tunDevice.Start()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !metadata.Resolved() {
|
|
||||||
ip, err := resolver.ResolveIP(metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
|
||||||
pc, err = w.tunDevice.ListenPacket(ctx, M.ParseSocksaddr(metadata.Pure().RemoteAddress()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return newPacketConn(&wgPacketConn{pc, w}, w), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type wgConn struct {
|
|
||||||
conn net.Conn
|
|
||||||
wg *WireGuard
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wgConn) Read(b []byte) (n int, err error) {
|
|
||||||
defer runtime.KeepAlive(c.wg)
|
|
||||||
return c.conn.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wgConn) Write(b []byte) (n int, err error) {
|
|
||||||
defer runtime.KeepAlive(c.wg)
|
|
||||||
return c.conn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wgConn) Close() error {
|
|
||||||
defer runtime.KeepAlive(c.wg)
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wgConn) LocalAddr() net.Addr {
|
|
||||||
defer runtime.KeepAlive(c.wg)
|
|
||||||
return c.conn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wgConn) RemoteAddr() net.Addr {
|
|
||||||
defer runtime.KeepAlive(c.wg)
|
|
||||||
return c.conn.RemoteAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wgConn) SetDeadline(t time.Time) error {
|
|
||||||
defer runtime.KeepAlive(c.wg)
|
|
||||||
return c.conn.SetDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wgConn) SetReadDeadline(t time.Time) error {
|
|
||||||
defer runtime.KeepAlive(c.wg)
|
|
||||||
return c.conn.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wgConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
defer runtime.KeepAlive(c.wg)
|
|
||||||
return c.conn.SetWriteDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
type wgPacketConn struct {
|
|
||||||
pc net.PacketConn
|
|
||||||
wg *WireGuard
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pc *wgPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
|
||||||
defer runtime.KeepAlive(pc.wg)
|
|
||||||
return pc.pc.ReadFrom(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pc *wgPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|
||||||
defer runtime.KeepAlive(pc.wg)
|
|
||||||
return pc.pc.WriteTo(p, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pc *wgPacketConn) Close() error {
|
|
||||||
defer runtime.KeepAlive(pc.wg)
|
|
||||||
return pc.pc.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pc *wgPacketConn) LocalAddr() net.Addr {
|
|
||||||
defer runtime.KeepAlive(pc.wg)
|
|
||||||
return pc.pc.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pc *wgPacketConn) SetDeadline(t time.Time) error {
|
|
||||||
defer runtime.KeepAlive(pc.wg)
|
|
||||||
return pc.pc.SetDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pc *wgPacketConn) SetReadDeadline(t time.Time) error {
|
|
||||||
defer runtime.KeepAlive(pc.wg)
|
|
||||||
return pc.pc.SetReadDeadline(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pc *wgPacketConn) SetWriteDeadline(t time.Time) error {
|
|
||||||
defer runtime.KeepAlive(pc.wg)
|
|
||||||
return pc.pc.SetWriteDeadline(t)
|
|
||||||
}
|
|
24
adapter/outboundgroup/common.go
Normal file
24
adapter/outboundgroup/common.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package outboundgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultGetProxiesDuration = time.Second * 5
|
||||||
|
)
|
||||||
|
|
||||||
|
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
|
||||||
|
proxies := []C.Proxy{}
|
||||||
|
for _, provider := range providers {
|
||||||
|
if touch {
|
||||||
|
proxies = append(proxies, provider.ProxiesWithTouch()...)
|
||||||
|
} else {
|
||||||
|
proxies = append(proxies, provider.Proxies()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return proxies
|
||||||
|
}
|
@ -3,19 +3,19 @@ package outboundgroup
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Fallback struct {
|
type Fallback struct {
|
||||||
*GroupBase
|
*outbound.Base
|
||||||
disableUDP bool
|
disableUDP bool
|
||||||
testUrl string
|
single *singledo.Single
|
||||||
selected string
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fallback) Now() string {
|
func (f *Fallback) Now() string {
|
||||||
@ -29,11 +29,7 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts .
|
|||||||
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
|
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(f)
|
c.AppendToChains(f)
|
||||||
f.onDialSuccess()
|
|
||||||
} else {
|
|
||||||
f.onDialFailed(proxy.Type(), err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +40,6 @@ func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(f)
|
pc.AppendToChains(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pc, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,8 +55,8 @@ func (f *Fallback) SupportUDP() bool {
|
|||||||
|
|
||||||
// MarshalJSON implements C.ProxyAdapter
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (f *Fallback) MarshalJSON() ([]byte, error) {
|
func (f *Fallback) MarshalJSON() ([]byte, error) {
|
||||||
all := []string{}
|
var all []string
|
||||||
for _, proxy := range f.GetProxies(false) {
|
for _, proxy := range f.proxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
@ -72,69 +67,40 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap implements C.ProxyAdapter
|
// Unwrap implements C.ProxyAdapter
|
||||||
func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
|
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
proxy := f.findAliveProxy(touch)
|
proxy := f.findAliveProxy(true)
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fallback) proxies(touch bool) []C.Proxy {
|
||||||
|
elm, _, _ := f.single.Do(func() (any, error) {
|
||||||
|
return getProvidersProxies(f.providers, touch), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.([]C.Proxy)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
|
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
|
||||||
proxies := f.GetProxies(touch)
|
proxies := f.proxies(touch)
|
||||||
for _, proxy := range proxies {
|
for _, proxy := range proxies {
|
||||||
if len(f.selected) == 0 {
|
if proxy.Alive() {
|
||||||
if proxy.Alive() {
|
return proxy
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if proxy.Name() == f.selected {
|
|
||||||
if proxy.Alive() {
|
|
||||||
return proxy
|
|
||||||
} else {
|
|
||||||
f.selected = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxies[0]
|
return proxies[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fallback) Set(name string) error {
|
|
||||||
var p C.Proxy
|
|
||||||
for _, proxy := range f.GetProxies(false) {
|
|
||||||
if proxy.Name() == name {
|
|
||||||
p = proxy
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p == nil {
|
|
||||||
return errors.New("proxy not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.selected = name
|
|
||||||
if !p.Alive() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
|
|
||||||
defer cancel()
|
|
||||||
_, _ = p.URLTest(ctx, f.testUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
||||||
return &Fallback{
|
return &Fallback{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.Fallback,
|
||||||
Type: C.Fallback,
|
Interface: option.Interface,
|
||||||
Interface: option.Interface,
|
RoutingMark: option.RoutingMark,
|
||||||
RoutingMark: option.RoutingMark,
|
|
||||||
},
|
|
||||||
option.Filter,
|
|
||||||
option.ExcludeFilter,
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
providers: providers,
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
testUrl: option.URL,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,265 +0,0 @@
|
|||||||
package outboundgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"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/log"
|
|
||||||
"github.com/Dreamacro/clash/tunnel"
|
|
||||||
"github.com/dlclark/regexp2"
|
|
||||||
"go.uber.org/atomic"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GroupBase struct {
|
|
||||||
*outbound.Base
|
|
||||||
filterRegs []*regexp2.Regexp
|
|
||||||
excludeFilterReg *regexp2.Regexp
|
|
||||||
providers []provider.ProxyProvider
|
|
||||||
failedTestMux sync.Mutex
|
|
||||||
failedTimes int
|
|
||||||
failedTime time.Time
|
|
||||||
failedTesting *atomic.Bool
|
|
||||||
proxies [][]C.Proxy
|
|
||||||
versions []atomic.Uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
type GroupBaseOption struct {
|
|
||||||
outbound.BaseOption
|
|
||||||
filter string
|
|
||||||
excludeFilter string
|
|
||||||
providers []provider.ProxyProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGroupBase(opt GroupBaseOption) *GroupBase {
|
|
||||||
var excludeFilterReg *regexp2.Regexp
|
|
||||||
if opt.excludeFilter != "" {
|
|
||||||
excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var filterRegs []*regexp2.Regexp
|
|
||||||
if opt.filter != "" {
|
|
||||||
for _, filter := range strings.Split(opt.filter, "`") {
|
|
||||||
filterReg := regexp2.MustCompile(filter, 0)
|
|
||||||
filterRegs = append(filterRegs, filterReg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gb := &GroupBase{
|
|
||||||
Base: outbound.NewBase(opt.BaseOption),
|
|
||||||
filterRegs: filterRegs,
|
|
||||||
excludeFilterReg: excludeFilterReg,
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
|
|
||||||
var proxies []C.Proxy
|
|
||||||
if len(gb.filterRegs) == 0 {
|
|
||||||
for _, pd := range gb.providers {
|
|
||||||
if touch {
|
|
||||||
pd.Touch()
|
|
||||||
}
|
|
||||||
proxies = append(proxies, pd.Proxies()...)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for i, pd := range gb.providers {
|
|
||||||
if touch {
|
|
||||||
pd.Touch()
|
|
||||||
}
|
|
||||||
|
|
||||||
if pd.VehicleType() == types.Compatible {
|
|
||||||
gb.versions[i].Store(pd.Version())
|
|
||||||
gb.proxies[i] = pd.Proxies()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
version := gb.versions[i].Load()
|
|
||||||
if version != pd.Version() && gb.versions[i].CompareAndSwap(version, pd.Version()) {
|
|
||||||
var (
|
|
||||||
proxies []C.Proxy
|
|
||||||
newProxies []C.Proxy
|
|
||||||
)
|
|
||||||
|
|
||||||
proxies = pd.Proxies()
|
|
||||||
proxiesSet := map[string]struct{}{}
|
|
||||||
for _, filterReg := range gb.filterRegs {
|
|
||||||
for _, p := range proxies {
|
|
||||||
name := p.Name()
|
|
||||||
if mat, _ := filterReg.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"])
|
|
||||||
}
|
|
||||||
|
|
||||||
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.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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16, error) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var lock sync.Mutex
|
|
||||||
mp := map[string]uint16{}
|
|
||||||
proxies := gb.GetProxies(false)
|
|
||||||
for _, proxy := range proxies {
|
|
||||||
proxy := proxy
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
delay, err := proxy.URLTest(ctx, url)
|
|
||||||
if err == nil {
|
|
||||||
lock.Lock()
|
|
||||||
mp[proxy.Name()] = delay
|
|
||||||
lock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if len(mp) == 0 {
|
|
||||||
return mp, fmt.Errorf("get delay: all proxies timeout")
|
|
||||||
} else {
|
|
||||||
return mp, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error) {
|
|
||||||
if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(err.Error(), "connection refused") {
|
|
||||||
go gb.healthCheck()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
gb.failedTestMux.Lock()
|
|
||||||
defer gb.failedTestMux.Unlock()
|
|
||||||
|
|
||||||
gb.failedTimes++
|
|
||||||
if gb.failedTimes == 1 {
|
|
||||||
log.Debugln("ProxyGroup: %s first failed", gb.Name())
|
|
||||||
gb.failedTime = time.Now()
|
|
||||||
} else {
|
|
||||||
if time.Since(gb.failedTime) > gb.failedTimeoutInterval() {
|
|
||||||
gb.failedTimes = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
|
|
||||||
if gb.failedTimes >= gb.maxFailedTimes() {
|
|
||||||
log.Warnln("because %s failed multiple times, active health check", gb.Name())
|
|
||||||
gb.healthCheck()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gb *GroupBase) healthCheck() {
|
|
||||||
if gb.failedTesting.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gb.failedTesting.Store(true)
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for _, proxyProvider := range gb.providers {
|
|
||||||
wg.Add(1)
|
|
||||||
proxyProvider := proxyProvider
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
proxyProvider.HealthCheck()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
gb.failedTesting.Store(false)
|
|
||||||
gb.failedTimes = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gb *GroupBase) failedIntervalTime() int64 {
|
|
||||||
return 5 * time.Second.Milliseconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gb *GroupBase) onDialSuccess() {
|
|
||||||
if !gb.failedTesting.Load() {
|
|
||||||
gb.failedTimes = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gb *GroupBase) maxFailedTimes() int {
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gb *GroupBase) failedTimeoutInterval() time.Duration {
|
|
||||||
return 5 * time.Second
|
|
||||||
}
|
|
@ -5,12 +5,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/Dreamacro/clash/common/cache"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/common/murmur3"
|
"github.com/Dreamacro/clash/common/murmur3"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
@ -21,25 +20,25 @@ import (
|
|||||||
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
|
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
|
||||||
|
|
||||||
type LoadBalance struct {
|
type LoadBalance struct {
|
||||||
*GroupBase
|
*outbound.Base
|
||||||
disableUDP bool
|
disableUDP bool
|
||||||
|
single *singledo.Single
|
||||||
|
providers []provider.ProxyProvider
|
||||||
strategyFn strategyFn
|
strategyFn strategyFn
|
||||||
}
|
}
|
||||||
|
|
||||||
var errStrategy = errors.New("unsupported strategy")
|
var errStrategy = errors.New("unsupported strategy")
|
||||||
|
|
||||||
func parseStrategy(config map[string]any) string {
|
func parseStrategy(config map[string]any) string {
|
||||||
if strategy, ok := config["strategy"].(string); ok {
|
if elm, ok := config["strategy"]; ok {
|
||||||
return strategy
|
if strategy, ok := elm.(string); ok {
|
||||||
|
return strategy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "consistent-hashing"
|
return "consistent-hashing"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKey(metadata *C.Metadata) string {
|
func getKey(metadata *C.Metadata) string {
|
||||||
if metadata == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if metadata.Host != "" {
|
if metadata.Host != "" {
|
||||||
// ip host
|
// ip host
|
||||||
if ip := net.ParseIP(metadata.Host); ip != nil {
|
if ip := net.ParseIP(metadata.Host); ip != nil {
|
||||||
@ -51,23 +50,13 @@ func getKey(metadata *C.Metadata) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !metadata.DstIP.IsValid() {
|
if metadata.DstIP == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata.DstIP.String()
|
return metadata.DstIP.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeyWithSrcAndDst(metadata *C.Metadata) string {
|
|
||||||
dst := getKey(metadata)
|
|
||||||
src := ""
|
|
||||||
if metadata != nil {
|
|
||||||
src = metadata.SrcIP.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s", src, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
func jumpHash(key uint64, buckets int32) int32 {
|
func jumpHash(key uint64, buckets int32) int32 {
|
||||||
var b, j int64
|
var b, j int64
|
||||||
|
|
||||||
@ -82,17 +71,14 @@ func jumpHash(key uint64, buckets int32) int32 {
|
|||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
|
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
|
||||||
proxy := lb.Unwrap(metadata, true)
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(lb)
|
c.AppendToChains(lb)
|
||||||
lb.onDialSuccess()
|
|
||||||
} else {
|
|
||||||
lb.onDialFailed(proxy.Type(), err)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
proxy := lb.Unwrap(metadata)
|
||||||
|
|
||||||
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -105,7 +91,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...)...)
|
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,62 +129,28 @@ 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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func strategyStickySessions() strategyFn {
|
|
||||||
ttl := time.Minute * 10
|
|
||||||
maxRetry := 5
|
|
||||||
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) C.Proxy {
|
|
||||||
key := uint64(murmur3.Sum32([]byte(getKeyWithSrcAndDst(metadata))))
|
|
||||||
length := len(proxies)
|
|
||||||
idx, has := lruCache.Get(key)
|
|
||||||
if !has {
|
|
||||||
idx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length)))
|
|
||||||
}
|
|
||||||
|
|
||||||
nowIdx := idx
|
|
||||||
for i := 1; i < maxRetry; i++ {
|
|
||||||
proxy := proxies[nowIdx]
|
|
||||||
if proxy.Alive() {
|
|
||||||
if nowIdx != idx {
|
|
||||||
lruCache.Delete(key)
|
|
||||||
lruCache.Set(key, nowIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxy
|
|
||||||
} else {
|
|
||||||
nowIdx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lruCache.Delete(key)
|
|
||||||
lruCache.Set(key, 0)
|
|
||||||
return proxies[0]
|
return proxies[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap implements C.ProxyAdapter
|
// Unwrap implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
|
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
proxies := lb.GetProxies(touch)
|
proxies := lb.proxies(true)
|
||||||
return lb.strategyFn(proxies, metadata)
|
return lb.strategyFn(proxies, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
|
||||||
|
elm, _, _ := lb.single.Do(func() (any, error) {
|
||||||
|
return getProvidersProxies(lb.providers, touch), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.([]C.Proxy)
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalJSON implements C.ProxyAdapter
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
||||||
var all []string
|
var all []string
|
||||||
for _, proxy := range lb.GetProxies(false) {
|
for _, proxy := range lb.proxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
@ -214,23 +166,18 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
|
|||||||
strategyFn = strategyConsistentHashing()
|
strategyFn = strategyConsistentHashing()
|
||||||
case "round-robin":
|
case "round-robin":
|
||||||
strategyFn = strategyRoundRobin()
|
strategyFn = strategyRoundRobin()
|
||||||
case "sticky-sessions":
|
|
||||||
strategyFn = strategyStickySessions()
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
||||||
}
|
}
|
||||||
return &LoadBalance{
|
return &LoadBalance{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.LoadBalance,
|
||||||
Type: C.LoadBalance,
|
Interface: option.Interface,
|
||||||
Interface: option.Interface,
|
RoutingMark: option.RoutingMark,
|
||||||
RoutingMark: option.RoutingMark,
|
|
||||||
},
|
|
||||||
option.Filter,
|
|
||||||
option.ExcludeFilter,
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
providers: providers,
|
||||||
strategyFn: strategyFn,
|
strategyFn: strategyFn,
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -21,16 +21,14 @@ var (
|
|||||||
|
|
||||||
type GroupCommonOption struct {
|
type GroupCommonOption struct {
|
||||||
outbound.BasicOption
|
outbound.BasicOption
|
||||||
Name string `group:"name"`
|
Name string `group:"name"`
|
||||||
Type string `group:"type"`
|
Type string `group:"type"`
|
||||||
Proxies []string `group:"proxies,omitempty"`
|
Proxies []string `group:"proxies,omitempty"`
|
||||||
Use []string `group:"use,omitempty"`
|
Use []string `group:"use,omitempty"`
|
||||||
URL string `group:"url,omitempty"`
|
URL string `group:"url,omitempty"`
|
||||||
Interval int `group:"interval,omitempty"`
|
Interval int `group:"interval,omitempty"`
|
||||||
Lazy bool `group:"lazy,omitempty"`
|
Lazy bool `group:"lazy,omitempty"`
|
||||||
DisableUDP bool `group:"disable-udp,omitempty"`
|
DisableUDP bool `group:"disable-udp,omitempty"`
|
||||||
Filter string `group:"filter,omitempty"`
|
|
||||||
ExcludeFilter string `group:"exclude-filter,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
|
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
|
||||||
@ -76,12 +74,8 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
|||||||
providers = append(providers, pd)
|
providers = append(providers, pd)
|
||||||
providersMap[groupName] = pd
|
providersMap[groupName] = pd
|
||||||
} else {
|
} else {
|
||||||
if groupOption.URL == "" {
|
if groupOption.URL == "" || groupOption.Interval == 0 {
|
||||||
groupOption.URL = "http://www.gstatic.com/generate_204"
|
return nil, errMissHealthCheck
|
||||||
}
|
|
||||||
|
|
||||||
if groupOption.Interval == 0 {
|
|
||||||
groupOption.Interval = 300
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
|
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
|
||||||
@ -101,8 +95,6 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
providers = append(providers, list...)
|
providers = append(providers, list...)
|
||||||
} else {
|
|
||||||
groupOption.Filter = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var group C.ProxyAdapter
|
var group C.ProxyAdapter
|
||||||
|
@ -6,18 +6,26 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Relay struct {
|
type Relay struct {
|
||||||
*GroupBase
|
*outbound.Base
|
||||||
|
single *singledo.Single
|
||||||
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
proxies, chainProxies := r.proxies(metadata, true)
|
var proxies []C.Proxy
|
||||||
|
for _, proxy := range r.proxies(metadata, true) {
|
||||||
|
if proxy.Type() != C.Direct {
|
||||||
|
proxies = append(proxies, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch len(proxies) {
|
switch len(proxies) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -55,86 +63,13 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
|
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn := outbound.NewConn(c, last)
|
return outbound.NewConn(c, r), nil
|
||||||
|
|
||||||
for i := len(chainProxies) - 2; i >= 0; i-- {
|
|
||||||
conn.AppendToChains(chainProxies[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.AppendToChains(r)
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
|
||||||
func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
|
||||||
proxies, chainProxies := r.proxies(metadata, true)
|
|
||||||
|
|
||||||
switch len(proxies) {
|
|
||||||
case 0:
|
|
||||||
return outbound.NewDirect().ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
|
||||||
case 1:
|
|
||||||
return proxies[0].ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
first := proxies[0]
|
|
||||||
last := proxies[len(proxies)-1]
|
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
|
|
||||||
if err != nil {
|
|
||||||
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-- {
|
|
||||||
pc.AppendToChains(chainProxies[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
pc.AppendToChains(r)
|
|
||||||
|
|
||||||
return pc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportUDP implements C.ProxyAdapter
|
|
||||||
func (r *Relay) SupportUDP() bool {
|
|
||||||
proxies, _ := r.proxies(nil, false)
|
|
||||||
if len(proxies) == 0 { // C.Direct
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
last := proxies[len(proxies)-1]
|
|
||||||
return last.SupportUDP() && last.SupportUOT()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements C.ProxyAdapter
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (r *Relay) MarshalJSON() ([]byte, error) {
|
func (r *Relay) MarshalJSON() ([]byte, error) {
|
||||||
all := []string{}
|
var all []string
|
||||||
for _, proxy := range r.GetProxies(false) {
|
for _, proxy := range r.rawProxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
@ -143,50 +78,37 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy) {
|
func (r *Relay) rawProxies(touch bool) []C.Proxy {
|
||||||
rawProxies := r.GetProxies(touch)
|
elm, _, _ := r.single.Do(func() (any, error) {
|
||||||
|
return getProvidersProxies(r.providers, touch), nil
|
||||||
|
})
|
||||||
|
|
||||||
var proxies []C.Proxy
|
return elm.([]C.Proxy)
|
||||||
var chainProxies []C.Proxy
|
|
||||||
var targetProxies []C.Proxy
|
|
||||||
|
|
||||||
for n, proxy := range rawProxies {
|
|
||||||
proxies = append(proxies, proxy)
|
|
||||||
chainProxies = append(chainProxies, proxy)
|
|
||||||
subproxy := proxy.Unwrap(metadata, touch)
|
|
||||||
for subproxy != nil {
|
|
||||||
chainProxies = append(chainProxies, subproxy)
|
|
||||||
proxies[n] = subproxy
|
|
||||||
subproxy = subproxy.Unwrap(metadata, touch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, proxy := range proxies {
|
|
||||||
if proxy.Type() != C.Direct && proxy.Type() != C.Compatible {
|
|
||||||
targetProxies = append(targetProxies, proxy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetProxies, chainProxies
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) Addr() string {
|
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
|
||||||
proxies, _ := r.proxies(nil, true)
|
proxies := r.rawProxies(touch)
|
||||||
return proxies[len(proxies)-1].Addr()
|
|
||||||
|
for n, proxy := range proxies {
|
||||||
|
subproxy := proxy.Unwrap(metadata)
|
||||||
|
for subproxy != nil {
|
||||||
|
proxies[n] = subproxy
|
||||||
|
subproxy = subproxy.Unwrap(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
||||||
return &Relay{
|
return &Relay{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.Relay,
|
||||||
Type: C.Relay,
|
Interface: option.Interface,
|
||||||
Interface: option.Interface,
|
RoutingMark: option.RoutingMark,
|
||||||
RoutingMark: option.RoutingMark,
|
|
||||||
},
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
providers: providers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,18 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Selector struct {
|
type Selector struct {
|
||||||
*GroupBase
|
*outbound.Base
|
||||||
disableUDP bool
|
disableUDP bool
|
||||||
|
single *singledo.Single
|
||||||
selected string
|
selected string
|
||||||
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
@ -46,8 +49,8 @@ func (s *Selector) SupportUDP() bool {
|
|||||||
|
|
||||||
// MarshalJSON implements C.ProxyAdapter
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (s *Selector) MarshalJSON() ([]byte, error) {
|
func (s *Selector) MarshalJSON() ([]byte, error) {
|
||||||
all := []string{}
|
var all []string
|
||||||
for _, proxy := range s.GetProxies(false) {
|
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,9 +66,10 @@ func (s *Selector) Now() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) Set(name string) error {
|
func (s *Selector) Set(name string) error {
|
||||||
for _, proxy := range s.GetProxies(false) {
|
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||||
if proxy.Name() == name {
|
if proxy.Name() == name {
|
||||||
s.selected = name
|
s.selected = name
|
||||||
|
s.single.Reset()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,35 +78,37 @@ func (s *Selector) Set(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap implements C.ProxyAdapter
|
// Unwrap implements C.ProxyAdapter
|
||||||
func (s *Selector) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
|
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
return s.selectedProxy(touch)
|
return s.selectedProxy(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
||||||
proxies := s.GetProxies(touch)
|
elm, _, _ := s.single.Do(func() (any, error) {
|
||||||
for _, proxy := range proxies {
|
proxies := getProvidersProxies(s.providers, touch)
|
||||||
if proxy.Name() == s.selected {
|
for _, proxy := range proxies {
|
||||||
return proxy
|
if proxy.Name() == s.selected {
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return proxies[0]
|
return proxies[0], nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.(C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
|
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
|
||||||
|
selected := providers[0].Proxies()[0].Name()
|
||||||
return &Selector{
|
return &Selector{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.Selector,
|
||||||
Type: C.Selector,
|
Interface: option.Interface,
|
||||||
Interface: option.Interface,
|
RoutingMark: option.RoutingMark,
|
||||||
RoutingMark: option.RoutingMark,
|
|
||||||
},
|
|
||||||
option.Filter,
|
|
||||||
option.ExcludeFilter,
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
selected: "COMPATIBLE",
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
providers: providers,
|
||||||
|
selected: selected,
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,11 +21,13 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type URLTest struct {
|
type URLTest struct {
|
||||||
*GroupBase
|
*outbound.Base
|
||||||
tolerance uint16
|
tolerance uint16
|
||||||
disableUDP bool
|
disableUDP bool
|
||||||
fastNode C.Proxy
|
fastNode C.Proxy
|
||||||
fastSingle *singledo.Single[C.Proxy]
|
single *singledo.Single
|
||||||
|
fastSingle *singledo.Single
|
||||||
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) Now() string {
|
func (u *URLTest) Now() string {
|
||||||
@ -34,13 +36,9 @@ func (u *URLTest) Now() string {
|
|||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
|
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
|
||||||
proxy := u.fast(true)
|
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
|
||||||
c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(u)
|
c.AppendToChains(u)
|
||||||
u.onDialSuccess()
|
|
||||||
} else {
|
|
||||||
u.onDialFailed(proxy.Type(), err)
|
|
||||||
}
|
}
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
@ -51,18 +49,25 @@ func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(u)
|
pc.AppendToChains(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pc, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap implements C.ProxyAdapter
|
// Unwrap implements C.ProxyAdapter
|
||||||
func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
|
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
return u.fast(touch)
|
return u.fast(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *URLTest) proxies(touch bool) []C.Proxy {
|
||||||
|
elm, _, _ := u.single.Do(func() (any, error) {
|
||||||
|
return getProvidersProxies(u.providers, touch), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.([]C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) fast(touch bool) C.Proxy {
|
func (u *URLTest) fast(touch bool) C.Proxy {
|
||||||
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
|
elm, _, _ := u.fastSingle.Do(func() (any, error) {
|
||||||
proxies := u.GetProxies(touch)
|
proxies := u.proxies(touch)
|
||||||
fast := proxies[0]
|
fast := proxies[0]
|
||||||
min := fast.LastDelay()
|
min := fast.LastDelay()
|
||||||
fastNotExist := true
|
fastNotExist := true
|
||||||
@ -90,11 +95,8 @@ func (u *URLTest) fast(touch bool) C.Proxy {
|
|||||||
|
|
||||||
return u.fastNode, nil
|
return u.fastNode, nil
|
||||||
})
|
})
|
||||||
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
|
|
||||||
u.Touch()
|
|
||||||
}
|
|
||||||
|
|
||||||
return elm
|
return elm.(C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupportUDP implements C.ProxyAdapter
|
// SupportUDP implements C.ProxyAdapter
|
||||||
@ -108,8 +110,8 @@ func (u *URLTest) SupportUDP() bool {
|
|||||||
|
|
||||||
// MarshalJSON implements C.ProxyAdapter
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (u *URLTest) MarshalJSON() ([]byte, error) {
|
func (u *URLTest) MarshalJSON() ([]byte, error) {
|
||||||
all := []string{}
|
var all []string
|
||||||
for _, proxy := range u.GetProxies(false) {
|
for _, proxy := range u.proxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
@ -134,19 +136,15 @@ func parseURLTestOption(config map[string]any) []urlTestOption {
|
|||||||
|
|
||||||
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
||||||
urlTest := &URLTest{
|
urlTest := &URLTest{
|
||||||
GroupBase: NewGroupBase(GroupBaseOption{
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
outbound.BaseOption{
|
Name: option.Name,
|
||||||
Name: option.Name,
|
Type: C.URLTest,
|
||||||
Type: C.URLTest,
|
Interface: option.Interface,
|
||||||
Interface: option.Interface,
|
RoutingMark: option.RoutingMark,
|
||||||
RoutingMark: option.RoutingMark,
|
|
||||||
},
|
|
||||||
|
|
||||||
option.Filter,
|
|
||||||
option.ExcludeFilter,
|
|
||||||
providers,
|
|
||||||
}),
|
}),
|
||||||
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
fastSingle: singledo.NewSingle(time.Second * 10),
|
||||||
|
providers: providers,
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ package outboundgroup
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
@ -16,29 +15,37 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ip, err := netip.ParseAddr(host); err != nil {
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
addr = &C.Metadata{
|
addr = &C.Metadata{
|
||||||
Host: host,
|
AddrType: C.AtypDomainName,
|
||||||
DstPort: port,
|
Host: host,
|
||||||
|
DstIP: nil,
|
||||||
|
DstPort: port,
|
||||||
}
|
}
|
||||||
} else {
|
return
|
||||||
|
} else if ip4 := ip.To4(); ip4 != nil {
|
||||||
addr = &C.Metadata{
|
addr = &C.Metadata{
|
||||||
Host: "",
|
AddrType: C.AtypIPv4,
|
||||||
DstIP: ip,
|
Host: "",
|
||||||
DstPort: port,
|
DstIP: ip4,
|
||||||
|
DstPort: port,
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addr = &C.Metadata{
|
||||||
|
AddrType: C.AtypIPv6,
|
||||||
|
Host: "",
|
||||||
|
DstIP: ip,
|
||||||
|
DstPort: port,
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcpKeepAlive(c net.Conn) {
|
func tcpKeepAlive(c net.Conn) {
|
||||||
if tcp, ok := c.(*net.TCPConn); ok {
|
if tcp, ok := c.(*net.TCPConn); ok {
|
||||||
_ = tcp.SetKeepAlive(true)
|
tcp.SetKeepAlive(true)
|
||||||
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
|
tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectAble interface {
|
|
||||||
Set(string) error
|
|
||||||
}
|
|
||||||
|
@ -40,14 +40,14 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
proxy, err = outbound.NewSocks5(*socksOption)
|
proxy = outbound.NewSocks5(*socksOption)
|
||||||
case "http":
|
case "http":
|
||||||
httpOption := &outbound.HttpOption{}
|
httpOption := &outbound.HttpOption{}
|
||||||
err = decoder.Decode(mapping, httpOption)
|
err = decoder.Decode(mapping, httpOption)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
proxy, err = outbound.NewHttp(*httpOption)
|
proxy = outbound.NewHttp(*httpOption)
|
||||||
case "vmess":
|
case "vmess":
|
||||||
vmessOption := &outbound.VmessOption{
|
vmessOption := &outbound.VmessOption{
|
||||||
HTTPOpts: outbound.HTTPOptions{
|
HTTPOpts: outbound.HTTPOptions{
|
||||||
@ -60,13 +60,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
proxy, err = outbound.NewVmess(*vmessOption)
|
proxy, err = outbound.NewVmess(*vmessOption)
|
||||||
case "vless":
|
|
||||||
vlessOption := &outbound.VlessOption{}
|
|
||||||
err = decoder.Decode(mapping, vlessOption)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
proxy, err = outbound.NewVless(*vlessOption)
|
|
||||||
case "snell":
|
case "snell":
|
||||||
snellOption := &outbound.SnellOption{}
|
snellOption := &outbound.SnellOption{}
|
||||||
err = decoder.Decode(mapping, snellOption)
|
err = decoder.Decode(mapping, snellOption)
|
||||||
@ -81,20 +74,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
proxy, err = outbound.NewTrojan(*trojanOption)
|
proxy, err = outbound.NewTrojan(*trojanOption)
|
||||||
case "hysteria":
|
|
||||||
hyOption := &outbound.HysteriaOption{}
|
|
||||||
err = decoder.Decode(mapping, hyOption)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
proxy, err = outbound.NewHysteria(*hyOption)
|
|
||||||
case "wireguard":
|
|
||||||
hyOption := &outbound.WireGuardOption{}
|
|
||||||
err = decoder.Decode(mapping, hyOption)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
proxy, err = outbound.NewWireGuard(*hyOption)
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||||
}
|
}
|
||||||
|
185
adapter/provider/fetcher.go
Normal file
185
adapter/provider/fetcher.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fileMode os.FileMode = 0o666
|
||||||
|
dirMode os.FileMode = 0o755
|
||||||
|
)
|
||||||
|
|
||||||
|
type parser = func([]byte) (any, error)
|
||||||
|
|
||||||
|
type fetcher struct {
|
||||||
|
name string
|
||||||
|
vehicle types.Vehicle
|
||||||
|
updatedAt *time.Time
|
||||||
|
ticker *time.Ticker
|
||||||
|
done chan struct{}
|
||||||
|
hash [16]byte
|
||||||
|
parser parser
|
||||||
|
onUpdate func(any)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) Name() string {
|
||||||
|
return f.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) VehicleType() types.VehicleType {
|
||||||
|
return f.vehicle.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) Initial() (any, error) {
|
||||||
|
var (
|
||||||
|
buf []byte
|
||||||
|
err error
|
||||||
|
isLocal bool
|
||||||
|
)
|
||||||
|
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
||||||
|
buf, err = os.ReadFile(f.vehicle.Path())
|
||||||
|
modTime := stat.ModTime()
|
||||||
|
f.updatedAt = &modTime
|
||||||
|
isLocal = true
|
||||||
|
} else {
|
||||||
|
buf, err = f.vehicle.Read()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, err := f.parser(buf)
|
||||||
|
if err != nil {
|
||||||
|
if !isLocal {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse local file error, fallback to remote
|
||||||
|
buf, err = f.vehicle.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, err = f.parser(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isLocal = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.vehicle.Type() != types.File && !isLocal {
|
||||||
|
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.hash = md5.Sum(buf)
|
||||||
|
|
||||||
|
// pull proxies automatically
|
||||||
|
if f.ticker != nil {
|
||||||
|
go f.pullLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) Update() (any, bool, error) {
|
||||||
|
buf, err := f.vehicle.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
hash := md5.Sum(buf)
|
||||||
|
if bytes.Equal(f.hash[:], hash[:]) {
|
||||||
|
f.updatedAt = &now
|
||||||
|
os.Chtimes(f.vehicle.Path(), now, now)
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, err := f.parser(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.vehicle.Type() != types.File {
|
||||||
|
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.updatedAt = &now
|
||||||
|
f.hash = hash
|
||||||
|
|
||||||
|
return proxies, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) Destroy() error {
|
||||||
|
if f.ticker != nil {
|
||||||
|
f.done <- struct{}{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) pullLoop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-f.ticker.C:
|
||||||
|
elm, same, err := f.Update()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if same {
|
||||||
|
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infoln("[Provider] %s's proxies update", f.Name())
|
||||||
|
if f.onUpdate != nil {
|
||||||
|
f.onUpdate(elm)
|
||||||
|
}
|
||||||
|
case <-f.done:
|
||||||
|
f.ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeWrite(path string, buf []byte) error {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(dir, dirMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(path, buf, fileMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher {
|
||||||
|
var ticker *time.Ticker
|
||||||
|
if interval != 0 {
|
||||||
|
ticker = time.NewTicker(interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fetcher{
|
||||||
|
name: name,
|
||||||
|
ticker: ticker,
|
||||||
|
vehicle: vehicle,
|
||||||
|
parser: parser,
|
||||||
|
done: make(chan struct{}, 1),
|
||||||
|
onUpdate: onUpdate,
|
||||||
|
}
|
||||||
|
}
|
@ -5,11 +5,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/batch"
|
"github.com/Dreamacro/clash/common/batch"
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/log"
|
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,21 +26,19 @@ type HealthCheck struct {
|
|||||||
lazy bool
|
lazy bool
|
||||||
lastTouch *atomic.Int64
|
lastTouch *atomic.Int64
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
singleDo *singledo.Single[struct{}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) process() {
|
func (hc *HealthCheck) process() {
|
||||||
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
||||||
|
|
||||||
go func() {
|
go hc.check()
|
||||||
time.Sleep(30 * time.Second)
|
|
||||||
hc.lazyCheck()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
hc.lazyCheck()
|
now := time.Now().Unix()
|
||||||
|
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
|
||||||
|
hc.check()
|
||||||
|
}
|
||||||
case <-hc.done:
|
case <-hc.done:
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
return
|
return
|
||||||
@ -51,17 +46,6 @@ func (hc *HealthCheck) process() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) lazyCheck() bool {
|
|
||||||
now := time.Now().Unix()
|
|
||||||
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
|
|
||||||
hc.check()
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
log.Debugln("Skip once health check because we are lazy")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
|
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
|
||||||
hc.proxies = proxies
|
hc.proxies = proxies
|
||||||
}
|
}
|
||||||
@ -75,29 +59,17 @@ func (hc *HealthCheck) touch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) check() {
|
func (hc *HealthCheck) check() {
|
||||||
_, _, _ = hc.singleDo.Do(func() (struct{}, error) {
|
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
|
||||||
id := ""
|
for _, proxy := range hc.proxies {
|
||||||
if uid, err := uuid.NewV4(); err == nil {
|
p := proxy
|
||||||
id = uid.String()
|
b.Go(p.Name(), func() (any, error) {
|
||||||
}
|
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||||
log.Debugln("Start New Health Checking {%s}", id)
|
defer cancel()
|
||||||
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
|
p.URLTest(ctx, hc.url)
|
||||||
for _, proxy := range hc.proxies {
|
return nil, nil
|
||||||
p := proxy
|
})
|
||||||
b.Go(p.Name(), func() (bool, error) {
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
b.Wait()
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) close() {
|
func (hc *HealthCheck) close() {
|
||||||
@ -112,6 +84,5 @@ func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *He
|
|||||||
lazy: lazy,
|
lazy: lazy,
|
||||||
lastTouch: atomic.NewInt64(0),
|
lastTouch: atomic.NewInt64(0),
|
||||||
done: make(chan struct{}, 1),
|
done: make(chan struct{}, 1),
|
||||||
singleDo: singledo.NewSingle[struct{}](time.Second),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/Dreamacro/clash/component/resource"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
@ -21,13 +20,12 @@ type healthCheckSchema struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type proxyProviderSchema struct {
|
type proxyProviderSchema struct {
|
||||||
Type string `provider:"type"`
|
Type string `provider:"type"`
|
||||||
Path string `provider:"path"`
|
Path string `provider:"path"`
|
||||||
URL string `provider:"url,omitempty"`
|
URL string `provider:"url,omitempty"`
|
||||||
Interval int `provider:"interval,omitempty"`
|
Interval int `provider:"interval,omitempty"`
|
||||||
Filter string `provider:"filter,omitempty"`
|
Filter string `provider:"filter,omitempty"`
|
||||||
ExcludeFilter string `provider:"exclude-filter,omitempty"`
|
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
|
||||||
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
|
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
|
||||||
@ -53,15 +51,14 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
|
|||||||
var vehicle types.Vehicle
|
var vehicle types.Vehicle
|
||||||
switch schema.Type {
|
switch schema.Type {
|
||||||
case "file":
|
case "file":
|
||||||
vehicle = resource.NewFileVehicle(path)
|
vehicle = NewFileVehicle(path)
|
||||||
case "http":
|
case "http":
|
||||||
vehicle = resource.NewHTTPVehicle(schema.URL, path)
|
vehicle = NewHTTPVehicle(schema.URL, path)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
|
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
interval := time.Duration(uint(schema.Interval)) * time.Second
|
interval := time.Duration(uint(schema.Interval)) * time.Second
|
||||||
filter := schema.Filter
|
filter := schema.Filter
|
||||||
excludeFilter := schema.ExcludeFilter
|
return NewProxySetProvider(name, interval, filter, vehicle, hc)
|
||||||
return NewProxySetProvider(name, interval, filter, excludeFilter, vehicle, hc)
|
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,18 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/Dreamacro/clash/common/convert"
|
"regexp"
|
||||||
clashHttp "github.com/Dreamacro/clash/component/http"
|
|
||||||
"github.com/Dreamacro/clash/component/resource"
|
|
||||||
"github.com/Dreamacro/clash/log"
|
|
||||||
"github.com/dlclark/regexp2"
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter"
|
"github.com/Dreamacro/clash/adapter"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
types "github.com/Dreamacro/clash/constant/provider"
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -30,36 +23,29 @@ type ProxySchema struct {
|
|||||||
Proxies []map[string]any `yaml:"proxies"`
|
Proxies []map[string]any `yaml:"proxies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxySetProvider for auto gc
|
// for auto gc
|
||||||
type ProxySetProvider struct {
|
type ProxySetProvider struct {
|
||||||
*proxySetProvider
|
*proxySetProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
type proxySetProvider struct {
|
type proxySetProvider struct {
|
||||||
*resource.Fetcher[[]C.Proxy]
|
*fetcher
|
||||||
proxies []C.Proxy
|
proxies []C.Proxy
|
||||||
healthCheck *HealthCheck
|
healthCheck *HealthCheck
|
||||||
version uint32
|
|
||||||
subscriptionInfo *SubscriptionInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
"name": pp.Name(),
|
"name": pp.Name(),
|
||||||
"type": pp.Type().String(),
|
"type": pp.Type().String(),
|
||||||
"vehicleType": pp.VehicleType().String(),
|
"vehicleType": pp.VehicleType().String(),
|
||||||
"proxies": pp.Proxies(),
|
"proxies": pp.Proxies(),
|
||||||
"updatedAt": pp.UpdatedAt,
|
"updatedAt": pp.updatedAt,
|
||||||
"subscriptionInfo": pp.subscriptionInfo,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) Version() uint32 {
|
|
||||||
return pp.version
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *proxySetProvider) Name() string {
|
func (pp *proxySetProvider) Name() string {
|
||||||
return pp.Fetcher.Name()
|
return pp.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) HealthCheck() {
|
func (pp *proxySetProvider) HealthCheck() {
|
||||||
@ -67,19 +53,20 @@ func (pp *proxySetProvider) HealthCheck() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) Update() error {
|
func (pp *proxySetProvider) Update() error {
|
||||||
elm, same, err := pp.Fetcher.Update()
|
elm, same, err := pp.fetcher.Update()
|
||||||
if err == nil && !same {
|
if err == nil && !same {
|
||||||
pp.OnUpdate(elm)
|
pp.onUpdate(elm)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) Initial() error {
|
func (pp *proxySetProvider) Initial() error {
|
||||||
elm, err := pp.Fetcher.Initial()
|
elm, err := pp.fetcher.Initial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pp.OnUpdate(elm)
|
|
||||||
|
pp.onUpdate(elm)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,69 +78,28 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
|
|||||||
return pp.proxies
|
return pp.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) Touch() {
|
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
|
||||||
pp.healthCheck.touch()
|
pp.healthCheck.touch()
|
||||||
|
return pp.Proxies()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
||||||
pp.proxies = proxies
|
pp.proxies = proxies
|
||||||
pp.healthCheck.setProxy(proxies)
|
pp.healthCheck.setProxy(proxies)
|
||||||
if pp.healthCheck.auto() {
|
if pp.healthCheck.auto() {
|
||||||
defer func() { go pp.healthCheck.lazyCheck() }()
|
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) {
|
func stopProxyProvider(pd *ProxySetProvider) {
|
||||||
pd.healthCheck.close()
|
pd.healthCheck.close()
|
||||||
_ = pd.Fetcher.Destroy()
|
pd.fetcher.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxySetProvider(name string, interval time.Duration, filter string, excludeFilter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
|
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
|
||||||
excludeFilterReg, err := regexp2.Compile(excludeFilter, 0)
|
filterReg, err := regexp.Compile(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
|
return nil, fmt.Errorf("invalid filter regex: %w", err)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hc.auto() {
|
if hc.auto() {
|
||||||
@ -165,16 +111,53 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, exc
|
|||||||
healthCheck: hc,
|
healthCheck: hc,
|
||||||
}
|
}
|
||||||
|
|
||||||
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, filterRegs, excludeFilterReg), proxiesOnUpdate(pd))
|
onUpdate := func(elm any) {
|
||||||
pd.Fetcher = fetcher
|
ret := elm.([]C.Proxy)
|
||||||
|
pd.setProxies(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxiesParseAndFilter := func(buf []byte) (any, error) {
|
||||||
|
schema := &ProxySchema{}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(buf, schema); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.Proxies == nil {
|
||||||
|
return nil, errors.New("file must have a `proxies` field")
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies := []C.Proxy{}
|
||||||
|
for idx, mapping := range schema.Proxies {
|
||||||
|
if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) {
|
||||||
|
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 {
|
||||||
|
if len(filter) > 0 {
|
||||||
|
return nil, errors.New("doesn't match any proxy, please check your filter")
|
||||||
|
}
|
||||||
|
return nil, errors.New("file doesn't have any proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
|
||||||
|
pd.fetcher = fetcher
|
||||||
|
|
||||||
pd.getSubscriptionInfo()
|
|
||||||
wrapper := &ProxySetProvider{pd}
|
wrapper := &ProxySetProvider{pd}
|
||||||
runtime.SetFinalizer(wrapper, stopProxyProvider)
|
runtime.SetFinalizer(wrapper, stopProxyProvider)
|
||||||
return wrapper, nil
|
return wrapper, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompatibleProvider for auto gc
|
// for auto gc
|
||||||
type CompatibleProvider struct {
|
type CompatibleProvider struct {
|
||||||
*compatibleProvider
|
*compatibleProvider
|
||||||
}
|
}
|
||||||
@ -183,7 +166,6 @@ type compatibleProvider struct {
|
|||||||
name string
|
name string
|
||||||
healthCheck *HealthCheck
|
healthCheck *HealthCheck
|
||||||
proxies []C.Proxy
|
proxies []C.Proxy
|
||||||
version uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
||||||
@ -195,10 +177,6 @@ func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *compatibleProvider) Version() uint32 {
|
|
||||||
return cp.version
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) Name() string {
|
func (cp *compatibleProvider) Name() string {
|
||||||
return cp.name
|
return cp.name
|
||||||
}
|
}
|
||||||
@ -227,8 +205,9 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
|
|||||||
return cp.proxies
|
return cp.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *compatibleProvider) Touch() {
|
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
|
||||||
cp.healthCheck.touch()
|
cp.healthCheck.touch()
|
||||||
|
return cp.Proxies()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopCompatibleProvider(pd *CompatibleProvider) {
|
func stopCompatibleProvider(pd *CompatibleProvider) {
|
||||||
@ -254,72 +233,3 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
|
|||||||
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
|
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
|
||||||
return wrapper, nil
|
return wrapper, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
|
|
||||||
return func(elm []C.Proxy) {
|
|
||||||
pd.setProxies(elm)
|
|
||||||
pd.version += 1
|
|
||||||
pd.getSubscriptionInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func proxiesParseAndFilter(filter string, excludeFilter string, filterRegs []*regexp2.Regexp, excludeFilterReg *regexp2.Regexp) resource.Parser[[]C.Proxy] {
|
|
||||||
return func(buf []byte) ([]C.Proxy, error) {
|
|
||||||
schema := &ProxySchema{}
|
|
||||||
|
|
||||||
if err := yaml.Unmarshal(buf, schema); err != nil {
|
|
||||||
proxies, err1 := convert.ConvertsV2Ray(buf)
|
|
||||||
if err1 != nil {
|
|
||||||
return nil, fmt.Errorf("%s, %w", err.Error(), err1)
|
|
||||||
}
|
|
||||||
schema.Proxies = proxies
|
|
||||||
}
|
|
||||||
|
|
||||||
if schema.Proxies == nil {
|
|
||||||
return nil, errors.New("file must have a `proxies` field")
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies := []C.Proxy{}
|
|
||||||
proxiesSet := map[string]struct{}{}
|
|
||||||
for _, filterReg := range filterRegs {
|
|
||||||
for idx, mapping := range schema.Proxies {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(proxies) == 0 {
|
|
||||||
if len(filter) > 0 {
|
|
||||||
return nil, errors.New("doesn't match any proxy, please check your filter")
|
|
||||||
}
|
|
||||||
return nil, errors.New("file doesn't have any proxy")
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxies, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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,16 @@
|
|||||||
package resource
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
clashHttp "github.com/Dreamacro/clash/component/http"
|
|
||||||
types "github.com/Dreamacro/clash/constant/provider"
|
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileVehicle struct {
|
type FileVehicle struct {
|
||||||
@ -35,10 +38,6 @@ type HTTPVehicle struct {
|
|||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTTPVehicle) Url() string {
|
|
||||||
return h.url
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HTTPVehicle) Type() types.VehicleType {
|
func (h *HTTPVehicle) Type() types.VehicleType {
|
||||||
return types.HTTP
|
return types.HTTP
|
||||||
}
|
}
|
||||||
@ -50,16 +49,47 @@ func (h *HTTPVehicle) Path() string {
|
|||||||
func (h *HTTPVehicle) Read() ([]byte, error) {
|
func (h *HTTPVehicle) Read() ([]byte, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
resp, err := clashHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil)
|
|
||||||
|
uri, err := url.Parse(h.url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, uri.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user := uri.User; user != nil {
|
||||||
|
password, _ := user.Password()
|
||||||
|
req.SetBasicAuth(user.Username(), password)
|
||||||
|
}
|
||||||
|
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
transport := &http.Transport{
|
||||||
|
// from http.DefaultTransport
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, network, address)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := http.Client{Transport: transport}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
buf, err := io.ReadAll(resp.Body)
|
buf, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf, nil
|
return buf, nil
|
||||||
}
|
}
|
||||||
|
|
@ -1,28 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
flags=$(grep '^flags\b' </proc/cpuinfo | head -n 1)
|
|
||||||
flags=" ${flags#*:} "
|
|
||||||
|
|
||||||
has_flags () {
|
|
||||||
for flag; do
|
|
||||||
case "$flags" in
|
|
||||||
*" $flag "*) :;;
|
|
||||||
*) return 1;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
determine_level () {
|
|
||||||
level=0
|
|
||||||
has_flags lm cmov cx8 fpu fxsr mmx syscall sse2 || return 0
|
|
||||||
level=1
|
|
||||||
has_flags cx16 lahf_lm popcnt sse4_1 sse4_2 ssse3 || return 0
|
|
||||||
level=2
|
|
||||||
has_flags avx avx2 bmi1 bmi2 f16c fma abm movbe xsave || return 0
|
|
||||||
level=3
|
|
||||||
has_flags avx512f avx512bw avx512cd avx512dq avx512vl || return 0
|
|
||||||
level=4
|
|
||||||
}
|
|
||||||
|
|
||||||
determine_level
|
|
||||||
echo "Your CPU supports amd64-v$level"
|
|
||||||
return $level
|
|
@ -5,10 +5,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option[T any] func(b *Batch[T])
|
type Option = func(b *Batch)
|
||||||
|
|
||||||
type Result[T any] struct {
|
type Result struct {
|
||||||
Value T
|
Value any
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,8 +17,8 @@ type Error struct {
|
|||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithConcurrencyNum[T any](n int) Option[T] {
|
func WithConcurrencyNum(n int) Option {
|
||||||
return func(b *Batch[T]) {
|
return func(b *Batch) {
|
||||||
q := make(chan struct{}, n)
|
q := make(chan struct{}, n)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
q <- struct{}{}
|
q <- struct{}{}
|
||||||
@ -28,8 +28,8 @@ func WithConcurrencyNum[T any](n int) Option[T] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Batch similar to errgroup, but can control the maximum number of concurrent
|
// Batch similar to errgroup, but can control the maximum number of concurrent
|
||||||
type Batch[T any] struct {
|
type Batch struct {
|
||||||
result map[string]Result[T]
|
result map[string]Result
|
||||||
queue chan struct{}
|
queue chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
@ -38,7 +38,7 @@ type Batch[T any] struct {
|
|||||||
cancel func()
|
cancel func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Batch[T]) Go(key string, fn func() (T, error)) {
|
func (b *Batch) Go(key string, fn func() (any, error)) {
|
||||||
b.wg.Add(1)
|
b.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer b.wg.Done()
|
defer b.wg.Done()
|
||||||
@ -59,14 +59,14 @@ func (b *Batch[T]) Go(key string, fn func() (T, error)) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := Result[T]{value, err}
|
ret := Result{value, err}
|
||||||
b.mux.Lock()
|
b.mux.Lock()
|
||||||
defer b.mux.Unlock()
|
defer b.mux.Unlock()
|
||||||
b.result[key] = ret
|
b.result[key] = ret
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Batch[T]) Wait() *Error {
|
func (b *Batch) Wait() *Error {
|
||||||
b.wg.Wait()
|
b.wg.Wait()
|
||||||
if b.cancel != nil {
|
if b.cancel != nil {
|
||||||
b.cancel()
|
b.cancel()
|
||||||
@ -74,26 +74,26 @@ func (b *Batch[T]) Wait() *Error {
|
|||||||
return b.err
|
return b.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Batch[T]) WaitAndGetResult() (map[string]Result[T], *Error) {
|
func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) {
|
||||||
err := b.Wait()
|
err := b.Wait()
|
||||||
return b.Result(), err
|
return b.Result(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Batch[T]) Result() map[string]Result[T] {
|
func (b *Batch) Result() map[string]Result {
|
||||||
b.mux.Lock()
|
b.mux.Lock()
|
||||||
defer b.mux.Unlock()
|
defer b.mux.Unlock()
|
||||||
copyM := map[string]Result[T]{}
|
copy := map[string]Result{}
|
||||||
for k, v := range b.result {
|
for k, v := range b.result {
|
||||||
copyM[k] = v
|
copy[k] = v
|
||||||
}
|
}
|
||||||
return copyM
|
return copy
|
||||||
}
|
}
|
||||||
|
|
||||||
func New[T any](ctx context.Context, opts ...Option[T]) (*Batch[T], context.Context) {
|
func New(ctx context.Context, opts ...Option) (*Batch, context.Context) {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
b := &Batch[T]{
|
b := &Batch{
|
||||||
result: map[string]Result[T]{},
|
result: map[string]Result{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
|
@ -11,14 +11,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBatch(t *testing.T) {
|
func TestBatch(t *testing.T) {
|
||||||
b, _ := New[string](context.Background())
|
b, _ := New(context.Background())
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
b.Go("foo", func() (string, error) {
|
b.Go("foo", func() (any, error) {
|
||||||
time.Sleep(time.Millisecond * 100)
|
time.Sleep(time.Millisecond * 100)
|
||||||
return "foo", nil
|
return "foo", nil
|
||||||
})
|
})
|
||||||
b.Go("bar", func() (string, error) {
|
b.Go("bar", func() (any, error) {
|
||||||
time.Sleep(time.Millisecond * 150)
|
time.Sleep(time.Millisecond * 150)
|
||||||
return "bar", nil
|
return "bar", nil
|
||||||
})
|
})
|
||||||
@ -32,20 +32,20 @@ func TestBatch(t *testing.T) {
|
|||||||
|
|
||||||
for k, v := range result {
|
for k, v := range result {
|
||||||
assert.NoError(t, v.Err)
|
assert.NoError(t, v.Err)
|
||||||
assert.Equal(t, k, v.Value)
|
assert.Equal(t, k, v.Value.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBatchWithConcurrencyNum(t *testing.T) {
|
func TestBatchWithConcurrencyNum(t *testing.T) {
|
||||||
b, _ := New[string](
|
b, _ := New(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
WithConcurrencyNum[string](3),
|
WithConcurrencyNum(3),
|
||||||
)
|
)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for i := 0; i < 7; i++ {
|
for i := 0; i < 7; i++ {
|
||||||
idx := i
|
idx := i
|
||||||
b.Go(strconv.Itoa(idx), func() (string, error) {
|
b.Go(strconv.Itoa(idx), func() (any, error) {
|
||||||
time.Sleep(time.Millisecond * 100)
|
time.Sleep(time.Millisecond * 100)
|
||||||
return strconv.Itoa(idx), nil
|
return strconv.Itoa(idx), nil
|
||||||
})
|
})
|
||||||
@ -57,21 +57,21 @@ func TestBatchWithConcurrencyNum(t *testing.T) {
|
|||||||
|
|
||||||
for k, v := range result {
|
for k, v := range result {
|
||||||
assert.NoError(t, v.Err)
|
assert.NoError(t, v.Err)
|
||||||
assert.Equal(t, k, v.Value)
|
assert.Equal(t, k, v.Value.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBatchContext(t *testing.T) {
|
func TestBatchContext(t *testing.T) {
|
||||||
b, ctx := New[string](context.Background())
|
b, ctx := New(context.Background())
|
||||||
|
|
||||||
b.Go("error", func() (string, error) {
|
b.Go("error", func() (any, error) {
|
||||||
time.Sleep(time.Millisecond * 100)
|
time.Sleep(time.Millisecond * 100)
|
||||||
return "", errors.New("test error")
|
return nil, errors.New("test error")
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Go("ctx", func() (string, error) {
|
b.Go("ctx", func() (any, error) {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
return "", ctx.Err()
|
return nil, ctx.Err()
|
||||||
})
|
})
|
||||||
|
|
||||||
result, err := b.WaitAndGetResult()
|
result, err := b.WaitAndGetResult()
|
||||||
|
48
common/cache/cache.go
vendored
48
common/cache/cache.go
vendored
@ -7,50 +7,50 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Cache store element with a expired time
|
// Cache store element with a expired time
|
||||||
type Cache[K comparable, V any] struct {
|
type Cache struct {
|
||||||
*cache[K, V]
|
*cache
|
||||||
}
|
}
|
||||||
|
|
||||||
type cache[K comparable, V any] struct {
|
type cache struct {
|
||||||
mapping sync.Map
|
mapping sync.Map
|
||||||
janitor *janitor[K, V]
|
janitor *janitor
|
||||||
}
|
}
|
||||||
|
|
||||||
type element[V any] struct {
|
type element struct {
|
||||||
Expired time.Time
|
Expired time.Time
|
||||||
Payload V
|
Payload any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put element in Cache with its ttl
|
// Put element in Cache with its ttl
|
||||||
func (c *cache[K, V]) Put(key K, payload V, ttl time.Duration) {
|
func (c *cache) Put(key any, payload any, ttl time.Duration) {
|
||||||
c.mapping.Store(key, &element[V]{
|
c.mapping.Store(key, &element{
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
Expired: time.Now().Add(ttl),
|
Expired: time.Now().Add(ttl),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get element in Cache, and drop when it expired
|
// Get element in Cache, and drop when it expired
|
||||||
func (c *cache[K, V]) Get(key K) V {
|
func (c *cache) Get(key any) any {
|
||||||
item, exist := c.mapping.Load(key)
|
item, exist := c.mapping.Load(key)
|
||||||
if !exist {
|
if !exist {
|
||||||
return getZero[V]()
|
return nil
|
||||||
}
|
}
|
||||||
elm := item.(*element[V])
|
elm := item.(*element)
|
||||||
// expired
|
// expired
|
||||||
if time.Since(elm.Expired) > 0 {
|
if time.Since(elm.Expired) > 0 {
|
||||||
c.mapping.Delete(key)
|
c.mapping.Delete(key)
|
||||||
return getZero[V]()
|
return nil
|
||||||
}
|
}
|
||||||
return elm.Payload
|
return elm.Payload
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWithExpire element in Cache with Expire Time
|
// GetWithExpire element in Cache with Expire Time
|
||||||
func (c *cache[K, V]) GetWithExpire(key K) (payload V, expired time.Time) {
|
func (c *cache) GetWithExpire(key any) (payload any, expired time.Time) {
|
||||||
item, exist := c.mapping.Load(key)
|
item, exist := c.mapping.Load(key)
|
||||||
if !exist {
|
if !exist {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
elm := item.(*element[V])
|
elm := item.(*element)
|
||||||
// expired
|
// expired
|
||||||
if time.Since(elm.Expired) > 0 {
|
if time.Since(elm.Expired) > 0 {
|
||||||
c.mapping.Delete(key)
|
c.mapping.Delete(key)
|
||||||
@ -59,10 +59,10 @@ func (c *cache[K, V]) GetWithExpire(key K) (payload V, expired time.Time) {
|
|||||||
return elm.Payload, elm.Expired
|
return elm.Payload, elm.Expired
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cache[K, V]) cleanup() {
|
func (c *cache) cleanup() {
|
||||||
c.mapping.Range(func(k, v any) bool {
|
c.mapping.Range(func(k, v any) bool {
|
||||||
key := k.(string)
|
key := k.(string)
|
||||||
elm := v.(*element[V])
|
elm := v.(*element)
|
||||||
if time.Since(elm.Expired) > 0 {
|
if time.Since(elm.Expired) > 0 {
|
||||||
c.mapping.Delete(key)
|
c.mapping.Delete(key)
|
||||||
}
|
}
|
||||||
@ -70,12 +70,12 @@ func (c *cache[K, V]) cleanup() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type janitor[K comparable, V any] struct {
|
type janitor struct {
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *janitor[K, V]) process(c *cache[K, V]) {
|
func (j *janitor) process(c *cache) {
|
||||||
ticker := time.NewTicker(j.interval)
|
ticker := time.NewTicker(j.interval)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -88,19 +88,19 @@ func (j *janitor[K, V]) process(c *cache[K, V]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopJanitor[K comparable, V any](c *Cache[K, V]) {
|
func stopJanitor(c *Cache) {
|
||||||
c.janitor.stop <- struct{}{}
|
c.janitor.stop <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New return *Cache
|
// New return *Cache
|
||||||
func New[K comparable, V any](interval time.Duration) *Cache[K, V] {
|
func New(interval time.Duration) *Cache {
|
||||||
j := &janitor[K, V]{
|
j := &janitor{
|
||||||
interval: interval,
|
interval: interval,
|
||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
}
|
}
|
||||||
c := &cache[K, V]{janitor: j}
|
c := &cache{janitor: j}
|
||||||
go j.process(c)
|
go j.process(c)
|
||||||
C := &Cache[K, V]{c}
|
C := &Cache{c}
|
||||||
runtime.SetFinalizer(C, stopJanitor[K, V])
|
runtime.SetFinalizer(C, stopJanitor)
|
||||||
return C
|
return C
|
||||||
}
|
}
|
||||||
|
28
common/cache/cache_test.go
vendored
28
common/cache/cache_test.go
vendored
@ -11,50 +11,48 @@ import (
|
|||||||
func TestCache_Basic(t *testing.T) {
|
func TestCache_Basic(t *testing.T) {
|
||||||
interval := 200 * time.Millisecond
|
interval := 200 * time.Millisecond
|
||||||
ttl := 20 * time.Millisecond
|
ttl := 20 * time.Millisecond
|
||||||
c := New[string, int](interval)
|
c := New(interval)
|
||||||
c.Put("int", 1, ttl)
|
c.Put("int", 1, ttl)
|
||||||
|
c.Put("string", "a", ttl)
|
||||||
d := New[string, string](interval)
|
|
||||||
d.Put("string", "a", ttl)
|
|
||||||
|
|
||||||
i := c.Get("int")
|
i := c.Get("int")
|
||||||
assert.Equal(t, i, 1, "should recv 1")
|
assert.Equal(t, i.(int), 1, "should recv 1")
|
||||||
|
|
||||||
s := d.Get("string")
|
s := c.Get("string")
|
||||||
assert.Equal(t, s, "a", "should recv 'a'")
|
assert.Equal(t, s.(string), "a", "should recv 'a'")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_TTL(t *testing.T) {
|
func TestCache_TTL(t *testing.T) {
|
||||||
interval := 200 * time.Millisecond
|
interval := 200 * time.Millisecond
|
||||||
ttl := 20 * time.Millisecond
|
ttl := 20 * time.Millisecond
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
c := New[string, int](interval)
|
c := New(interval)
|
||||||
c.Put("int", 1, ttl)
|
c.Put("int", 1, ttl)
|
||||||
c.Put("int2", 2, ttl)
|
c.Put("int2", 2, ttl)
|
||||||
|
|
||||||
i := c.Get("int")
|
i := c.Get("int")
|
||||||
_, expired := c.GetWithExpire("int2")
|
_, expired := c.GetWithExpire("int2")
|
||||||
assert.Equal(t, i, 1, "should recv 1")
|
assert.Equal(t, i.(int), 1, "should recv 1")
|
||||||
assert.True(t, now.Before(expired))
|
assert.True(t, now.Before(expired))
|
||||||
|
|
||||||
time.Sleep(ttl * 2)
|
time.Sleep(ttl * 2)
|
||||||
i = c.Get("int")
|
i = c.Get("int")
|
||||||
j, _ := c.GetWithExpire("int2")
|
j, _ := c.GetWithExpire("int2")
|
||||||
assert.True(t, i == 0, "should recv 0")
|
assert.Nil(t, i, "should recv nil")
|
||||||
assert.True(t, j == 0, "should recv 0")
|
assert.Nil(t, j, "should recv nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_AutoCleanup(t *testing.T) {
|
func TestCache_AutoCleanup(t *testing.T) {
|
||||||
interval := 10 * time.Millisecond
|
interval := 10 * time.Millisecond
|
||||||
ttl := 15 * time.Millisecond
|
ttl := 15 * time.Millisecond
|
||||||
c := New[string, int](interval)
|
c := New(interval)
|
||||||
c.Put("int", 1, ttl)
|
c.Put("int", 1, ttl)
|
||||||
|
|
||||||
time.Sleep(ttl * 2)
|
time.Sleep(ttl * 2)
|
||||||
i := c.Get("int")
|
i := c.Get("int")
|
||||||
j, _ := c.GetWithExpire("int")
|
j, _ := c.GetWithExpire("int")
|
||||||
assert.True(t, i == 0, "should recv 0")
|
assert.Nil(t, i, "should recv nil")
|
||||||
assert.True(t, j == 0, "should recv 0")
|
assert.Nil(t, j, "should recv nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_AutoGC(t *testing.T) {
|
func TestCache_AutoGC(t *testing.T) {
|
||||||
@ -62,7 +60,7 @@ func TestCache_AutoGC(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
interval := 10 * time.Millisecond
|
interval := 10 * time.Millisecond
|
||||||
ttl := 15 * time.Millisecond
|
ttl := 15 * time.Millisecond
|
||||||
c := New[string, int](interval)
|
c := New(interval)
|
||||||
c.Put("int", 1, ttl)
|
c.Put("int", 1, ttl)
|
||||||
sign <- struct{}{}
|
sign <- struct{}{}
|
||||||
}()
|
}()
|
||||||
|
123
common/cache/lrucache.go
vendored
123
common/cache/lrucache.go
vendored
@ -3,50 +3,49 @@ package cache
|
|||||||
// Modified by https://github.com/die-net/lrucache
|
// Modified by https://github.com/die-net/lrucache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/list"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/generics/list"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option is part of Functional Options Pattern
|
// Option is part of Functional Options Pattern
|
||||||
type Option[K comparable, V any] func(*LruCache[K, V])
|
type Option func(*LruCache)
|
||||||
|
|
||||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||||
type EvictCallback[K comparable, V any] func(key K, value V)
|
type EvictCallback = func(key any, value any)
|
||||||
|
|
||||||
// WithEvict set the evict callback
|
// WithEvict set the evict callback
|
||||||
func WithEvict[K comparable, V any](cb EvictCallback[K, V]) Option[K, V] {
|
func WithEvict(cb EvictCallback) Option {
|
||||||
return func(l *LruCache[K, V]) {
|
return func(l *LruCache) {
|
||||||
l.onEvict = cb
|
l.onEvict = cb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithUpdateAgeOnGet update expires when Get element
|
// WithUpdateAgeOnGet update expires when Get element
|
||||||
func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] {
|
func WithUpdateAgeOnGet() Option {
|
||||||
return func(l *LruCache[K, V]) {
|
return func(l *LruCache) {
|
||||||
l.updateAgeOnGet = true
|
l.updateAgeOnGet = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAge defined element max age (second)
|
// WithAge defined element max age (second)
|
||||||
func WithAge[K comparable, V any](maxAge int64) Option[K, V] {
|
func WithAge(maxAge int64) Option {
|
||||||
return func(l *LruCache[K, V]) {
|
return func(l *LruCache) {
|
||||||
l.maxAge = maxAge
|
l.maxAge = maxAge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithSize defined max length of LruCache
|
// WithSize defined max length of LruCache
|
||||||
func WithSize[K comparable, V any](maxSize int) Option[K, V] {
|
func WithSize(maxSize int) Option {
|
||||||
return func(l *LruCache[K, V]) {
|
return func(l *LruCache) {
|
||||||
l.maxSize = maxSize
|
l.maxSize = maxSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithStale decide whether Stale return is enabled.
|
// WithStale decide whether Stale return is enabled.
|
||||||
// If this feature is enabled, element will not get Evicted according to `WithAge`.
|
// If this feature is enabled, element will not get Evicted according to `WithAge`.
|
||||||
func WithStale[K comparable, V any](stale bool) Option[K, V] {
|
func WithStale(stale bool) Option {
|
||||||
return func(l *LruCache[K, V]) {
|
return func(l *LruCache) {
|
||||||
l.staleReturn = stale
|
l.staleReturn = stale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,22 +53,22 @@ func WithStale[K comparable, V any](stale bool) Option[K, V] {
|
|||||||
// LruCache is a thread-safe, in-memory lru-cache that evicts the
|
// LruCache is a thread-safe, in-memory lru-cache that evicts the
|
||||||
// least recently used entries from memory when (if set) the entries are
|
// least recently used entries from memory when (if set) the entries are
|
||||||
// older than maxAge (in seconds). Use the New constructor to create one.
|
// older than maxAge (in seconds). Use the New constructor to create one.
|
||||||
type LruCache[K comparable, V any] struct {
|
type LruCache struct {
|
||||||
maxAge int64
|
maxAge int64
|
||||||
maxSize int
|
maxSize int
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
cache map[K]*list.Element[*entry[K, V]]
|
cache map[any]*list.Element
|
||||||
lru *list.List[*entry[K, V]] // Front is least-recent
|
lru *list.List // Front is least-recent
|
||||||
updateAgeOnGet bool
|
updateAgeOnGet bool
|
||||||
staleReturn bool
|
staleReturn bool
|
||||||
onEvict EvictCallback[K, V]
|
onEvict EvictCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLRUCache creates an LruCache
|
// NewLRUCache creates an LruCache
|
||||||
func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
|
func NewLRUCache(options ...Option) *LruCache {
|
||||||
lc := &LruCache[K, V]{
|
lc := &LruCache{
|
||||||
lru: list.New[*entry[K, V]](),
|
lru: list.New(),
|
||||||
cache: make(map[K]*list.Element[*entry[K, V]]),
|
cache: make(map[any]*list.Element),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
@ -81,12 +80,12 @@ func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
|
|||||||
|
|
||||||
// Get returns the any representation of a cached response and a bool
|
// Get returns the any representation of a cached response and a bool
|
||||||
// set to true if the key was found.
|
// set to true if the key was found.
|
||||||
func (c *LruCache[K, V]) Get(key K) (V, bool) {
|
func (c *LruCache) Get(key any) (any, bool) {
|
||||||
el := c.get(key)
|
entry := c.get(key)
|
||||||
if el == nil {
|
if entry == nil {
|
||||||
return getZero[V](), false
|
return nil, false
|
||||||
}
|
}
|
||||||
value := el.value
|
value := entry.value
|
||||||
|
|
||||||
return value, true
|
return value, true
|
||||||
}
|
}
|
||||||
@ -95,17 +94,17 @@ func (c *LruCache[K, V]) Get(key K) (V, bool) {
|
|||||||
// a time.Time Give expected expires,
|
// a time.Time Give expected expires,
|
||||||
// and a bool set to true if the key was found.
|
// and a bool set to true if the key was found.
|
||||||
// This method will NOT check the maxAge of element and will NOT update the expires.
|
// This method will NOT check the maxAge of element and will NOT update the expires.
|
||||||
func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
|
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
|
||||||
el := c.get(key)
|
entry := c.get(key)
|
||||||
if el == nil {
|
if entry == nil {
|
||||||
return getZero[V](), time.Time{}, false
|
return nil, time.Time{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return el.value, time.Unix(el.expires, 0), true
|
return entry.value, time.Unix(entry.expires, 0), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exist returns if key exist in cache but not put item to the head of linked list
|
// Exist returns if key exist in cache but not put item to the head of linked list
|
||||||
func (c *LruCache[K, V]) Exist(key K) bool {
|
func (c *LruCache) Exist(key any) bool {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -114,7 +113,7 @@ func (c *LruCache[K, V]) Exist(key K) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set stores the any representation of a response for a given key.
|
// Set stores the any representation of a response for a given key.
|
||||||
func (c *LruCache[K, V]) Set(key K, value V) {
|
func (c *LruCache) Set(key any, value any) {
|
||||||
expires := int64(0)
|
expires := int64(0)
|
||||||
if c.maxAge > 0 {
|
if c.maxAge > 0 {
|
||||||
expires = time.Now().Unix() + c.maxAge
|
expires = time.Now().Unix() + c.maxAge
|
||||||
@ -124,21 +123,21 @@ func (c *LruCache[K, V]) Set(key K, value V) {
|
|||||||
|
|
||||||
// SetWithExpire stores the any representation of a response for a given key and given expires.
|
// SetWithExpire stores the any representation of a response for a given key and given expires.
|
||||||
// The expires time will round to second.
|
// The expires time will round to second.
|
||||||
func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
|
func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if le, ok := c.cache[key]; ok {
|
if le, ok := c.cache[key]; ok {
|
||||||
c.lru.MoveToBack(le)
|
c.lru.MoveToBack(le)
|
||||||
e := le.Value
|
e := le.Value.(*entry)
|
||||||
e.value = value
|
e.value = value
|
||||||
e.expires = expires.Unix()
|
e.expires = expires.Unix()
|
||||||
} else {
|
} else {
|
||||||
e := &entry[K, V]{key: key, value: value, expires: expires.Unix()}
|
e := &entry{key: key, value: value, expires: expires.Unix()}
|
||||||
c.cache[key] = c.lru.PushBack(e)
|
c.cache[key] = c.lru.PushBack(e)
|
||||||
|
|
||||||
if c.maxSize > 0 {
|
if c.maxSize > 0 {
|
||||||
if elLen := c.lru.Len(); elLen > c.maxSize {
|
if len := c.lru.Len(); len > c.maxSize {
|
||||||
c.deleteElement(c.lru.Front())
|
c.deleteElement(c.lru.Front())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,23 +147,23 @@ func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CloneTo clone and overwrite elements to another LruCache
|
// CloneTo clone and overwrite elements to another LruCache
|
||||||
func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
|
func (c *LruCache) CloneTo(n *LruCache) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
defer n.mu.Unlock()
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
n.lru = list.New[*entry[K, V]]()
|
n.lru = list.New()
|
||||||
n.cache = make(map[K]*list.Element[*entry[K, V]])
|
n.cache = make(map[any]*list.Element)
|
||||||
|
|
||||||
for e := c.lru.Front(); e != nil; e = e.Next() {
|
for e := c.lru.Front(); e != nil; e = e.Next() {
|
||||||
elm := e.Value
|
elm := e.Value.(*entry)
|
||||||
n.cache[elm.key] = n.lru.PushBack(elm)
|
n.cache[elm.key] = n.lru.PushBack(elm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LruCache[K, V]) get(key K) *entry[K, V] {
|
func (c *LruCache) get(key any) *entry {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -173,7 +172,7 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.staleReturn && c.maxAge > 0 && le.Value.expires <= time.Now().Unix() {
|
if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() {
|
||||||
c.deleteElement(le)
|
c.deleteElement(le)
|
||||||
c.maybeDeleteOldest()
|
c.maybeDeleteOldest()
|
||||||
|
|
||||||
@ -181,15 +180,15 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.lru.MoveToBack(le)
|
c.lru.MoveToBack(le)
|
||||||
el := le.Value
|
entry := le.Value.(*entry)
|
||||||
if c.maxAge > 0 && c.updateAgeOnGet {
|
if c.maxAge > 0 && c.updateAgeOnGet {
|
||||||
el.expires = time.Now().Unix() + c.maxAge
|
entry.expires = time.Now().Unix() + c.maxAge
|
||||||
}
|
}
|
||||||
return el
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the value associated with a key.
|
// Delete removes the value associated with a key.
|
||||||
func (c *LruCache[K, V]) Delete(key K) {
|
func (c *LruCache) Delete(key any) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|
||||||
if le, ok := c.cache[key]; ok {
|
if le, ok := c.cache[key]; ok {
|
||||||
@ -199,40 +198,26 @@ func (c *LruCache[K, V]) Delete(key K) {
|
|||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LruCache[K, V]) maybeDeleteOldest() {
|
func (c *LruCache) maybeDeleteOldest() {
|
||||||
if !c.staleReturn && c.maxAge > 0 {
|
if !c.staleReturn && c.maxAge > 0 {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
for le := c.lru.Front(); le != nil && le.Value.expires <= now; le = c.lru.Front() {
|
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() {
|
||||||
c.deleteElement(le)
|
c.deleteElement(le)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
|
func (c *LruCache) deleteElement(le *list.Element) {
|
||||||
c.lru.Remove(le)
|
c.lru.Remove(le)
|
||||||
e := le.Value
|
e := le.Value.(*entry)
|
||||||
delete(c.cache, e.key)
|
delete(c.cache, e.key)
|
||||||
if c.onEvict != nil {
|
if c.onEvict != nil {
|
||||||
c.onEvict(e.key, e.value)
|
c.onEvict(e.key, e.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LruCache[K, V]) Clear() error {
|
type entry struct {
|
||||||
c.mu.Lock()
|
key any
|
||||||
|
value any
|
||||||
c.cache = make(map[K]*list.Element[*entry[K, V]])
|
|
||||||
|
|
||||||
c.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type entry[K comparable, V any] struct {
|
|
||||||
key K
|
|
||||||
value V
|
|
||||||
expires int64
|
expires int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func getZero[T any]() T {
|
|
||||||
var result T
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
45
common/cache/lrucache_test.go
vendored
45
common/cache/lrucache_test.go
vendored
@ -19,7 +19,7 @@ var entries = []struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUCache(t *testing.T) {
|
func TestLRUCache(t *testing.T) {
|
||||||
c := NewLRUCache[string, string]()
|
c := NewLRUCache()
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
c.Set(e.key, e.value)
|
c.Set(e.key, e.value)
|
||||||
@ -32,7 +32,7 @@ func TestLRUCache(t *testing.T) {
|
|||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
value, ok := c.Get(e.key)
|
value, ok := c.Get(e.key)
|
||||||
if assert.True(t, ok) {
|
if assert.True(t, ok) {
|
||||||
assert.Equal(t, e.value, value)
|
assert.Equal(t, e.value, value.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,25 +45,25 @@ func TestLRUCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUMaxAge(t *testing.T) {
|
func TestLRUMaxAge(t *testing.T) {
|
||||||
c := NewLRUCache[string, string](WithAge[string, string](86400))
|
c := NewLRUCache(WithAge(86400))
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expected := now + 86400
|
expected := now + 86400
|
||||||
|
|
||||||
// Add one expired entry
|
// Add one expired entry
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
c.lru.Back().Value.expires = now
|
c.lru.Back().Value.(*entry).expires = now
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
e := c.lru.Back().Value
|
e := c.lru.Back().Value.(*entry)
|
||||||
assert.True(t, e.expires >= now)
|
assert.True(t, e.expires >= now)
|
||||||
c.lru.Back().Value.expires = now
|
c.lru.Back().Value.(*entry).expires = now
|
||||||
|
|
||||||
// Set a few and verify expiration times
|
// Set a few and verify expiration times
|
||||||
for _, s := range entries {
|
for _, s := range entries {
|
||||||
c.Set(s.key, s.value)
|
c.Set(s.key, s.value)
|
||||||
e := c.lru.Back().Value
|
e := c.lru.Back().Value.(*entry)
|
||||||
assert.True(t, e.expires >= expected && e.expires <= expected+10)
|
assert.True(t, e.expires >= expected && e.expires <= expected+10)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ func TestLRUMaxAge(t *testing.T) {
|
|||||||
for _, s := range entries {
|
for _, s := range entries {
|
||||||
le, ok := c.cache[s.key]
|
le, ok := c.cache[s.key]
|
||||||
if assert.True(t, ok) {
|
if assert.True(t, ok) {
|
||||||
le.Value.expires = now
|
le.Value.(*entry).expires = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,22 +88,22 @@ func TestLRUMaxAge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUpdateOnGet(t *testing.T) {
|
func TestLRUpdateOnGet(t *testing.T) {
|
||||||
c := NewLRUCache[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]())
|
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expires := now + 86400/2
|
expires := now + 86400/2
|
||||||
|
|
||||||
// Add one expired entry
|
// Add one expired entry
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
c.lru.Back().Value.expires = expires
|
c.lru.Back().Value.(*entry).expires = expires
|
||||||
|
|
||||||
_, ok := c.Get("foo")
|
_, ok := c.Get("foo")
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.True(t, c.lru.Back().Value.expires > expires)
|
assert.True(t, c.lru.Back().Value.(*entry).expires > expires)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMaxSize(t *testing.T) {
|
func TestMaxSize(t *testing.T) {
|
||||||
c := NewLRUCache[string, string](WithSize[string, string](2))
|
c := NewLRUCache(WithSize(2))
|
||||||
// Add one expired entry
|
// Add one expired entry
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
_, ok := c.Get("foo")
|
_, ok := c.Get("foo")
|
||||||
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExist(t *testing.T) {
|
func TestExist(t *testing.T) {
|
||||||
c := NewLRUCache[int, int](WithSize[int, int](1))
|
c := NewLRUCache(WithSize(1))
|
||||||
c.Set(1, 2)
|
c.Set(1, 2)
|
||||||
assert.True(t, c.Exist(1))
|
assert.True(t, c.Exist(1))
|
||||||
c.Set(2, 3)
|
c.Set(2, 3)
|
||||||
@ -126,11 +126,11 @@ func TestExist(t *testing.T) {
|
|||||||
|
|
||||||
func TestEvict(t *testing.T) {
|
func TestEvict(t *testing.T) {
|
||||||
temp := 0
|
temp := 0
|
||||||
evict := func(key int, value int) {
|
evict := func(key any, value any) {
|
||||||
temp = key + value
|
temp = key.(int) + value.(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
c := NewLRUCache[int, int](WithEvict[int, int](evict), WithSize[int, int](1))
|
c := NewLRUCache(WithEvict(evict), WithSize(1))
|
||||||
c.Set(1, 2)
|
c.Set(1, 2)
|
||||||
c.Set(2, 3)
|
c.Set(2, 3)
|
||||||
|
|
||||||
@ -138,22 +138,21 @@ func TestEvict(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSetWithExpire(t *testing.T) {
|
func TestSetWithExpire(t *testing.T) {
|
||||||
c := NewLRUCache[int, *struct{}](WithAge[int, *struct{}](1))
|
c := NewLRUCache(WithAge(1))
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
tenSecBefore := time.Unix(now-10, 0)
|
tenSecBefore := time.Unix(now-10, 0)
|
||||||
c.SetWithExpire(1, &struct{}{}, tenSecBefore)
|
c.SetWithExpire(1, 2, tenSecBefore)
|
||||||
|
|
||||||
// res is expected not to exist, and expires should be empty time.Time
|
// res is expected not to exist, and expires should be empty time.Time
|
||||||
res, expires, exist := c.GetWithExpire(1)
|
res, expires, exist := c.GetWithExpire(1)
|
||||||
|
assert.Equal(t, nil, res)
|
||||||
assert.True(t, nil == res)
|
|
||||||
assert.Equal(t, time.Time{}, expires)
|
assert.Equal(t, time.Time{}, expires)
|
||||||
assert.Equal(t, false, exist)
|
assert.Equal(t, false, exist)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStale(t *testing.T) {
|
func TestStale(t *testing.T) {
|
||||||
c := NewLRUCache[int, int](WithAge[int, int](1), WithStale[int, int](true))
|
c := NewLRUCache(WithAge(1), WithStale(true))
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
tenSecBefore := time.Unix(now-10, 0)
|
tenSecBefore := time.Unix(now-10, 0)
|
||||||
@ -166,11 +165,11 @@ func TestStale(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCloneTo(t *testing.T) {
|
func TestCloneTo(t *testing.T) {
|
||||||
o := NewLRUCache[string, int](WithSize[string, int](10))
|
o := NewLRUCache(WithSize(10))
|
||||||
o.Set("1", 1)
|
o.Set("1", 1)
|
||||||
o.Set("2", 2)
|
o.Set("2", 2)
|
||||||
|
|
||||||
n := NewLRUCache[string, int](WithSize[string, int](2))
|
n := NewLRUCache(WithSize(2))
|
||||||
n.Set("3", 3)
|
n.Set("3", 3)
|
||||||
n.Set("4", 4)
|
n.Set("4", 4)
|
||||||
|
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExecCmd(cmdStr string) (string, error) {
|
|
||||||
args := splitArgs(cmdStr)
|
|
||||||
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
if len(args) == 1 {
|
|
||||||
cmd = exec.Command(args[0])
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command(args[0], args[1:]...)
|
|
||||||
|
|
||||||
}
|
|
||||||
prepareBackgroundCommand(cmd)
|
|
||||||
out, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%v, %s", err, string(out))
|
|
||||||
}
|
|
||||||
return string(out), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitArgs(cmd string) []string {
|
|
||||||
args := strings.Split(cmd, " ")
|
|
||||||
|
|
||||||
// use in pipeline
|
|
||||||
if len(args) > 2 && strings.ContainsAny(cmd, "|") {
|
|
||||||
suffix := strings.Join(args[2:], " ")
|
|
||||||
args = append(args[:2], suffix)
|
|
||||||
}
|
|
||||||
return args
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func prepareBackgroundCommand(cmd *exec.Cmd) {
|
|
||||||
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSplitArgs(t *testing.T) {
|
|
||||||
args := splitArgs("ls")
|
|
||||||
args1 := splitArgs("ls -la")
|
|
||||||
args2 := splitArgs("bash -c ls")
|
|
||||||
args3 := splitArgs("bash -c ls -lahF | grep 'cmd'")
|
|
||||||
|
|
||||||
assert.Equal(t, 1, len(args))
|
|
||||||
assert.Equal(t, 2, len(args1))
|
|
||||||
assert.Equal(t, 3, len(args2))
|
|
||||||
assert.Equal(t, 3, len(args3))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExecCmd(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
_, err := ExecCmd("dir")
|
|
||||||
assert.Nil(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := ExecCmd("ls")
|
|
||||||
_, err1 := ExecCmd("ls -la")
|
|
||||||
_, err2 := ExecCmd("bash -c ls")
|
|
||||||
_, err3 := ExecCmd("bash -c ls -la")
|
|
||||||
_, err4 := ExecCmd("bash -c ls -la | grep 'cmd'")
|
|
||||||
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Nil(t, err1)
|
|
||||||
assert.Nil(t, err2)
|
|
||||||
assert.Nil(t, err3)
|
|
||||||
assert.Nil(t, err4)
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func prepareBackgroundCommand(cmd *exec.Cmd) {
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package collections
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
type (
|
|
||||||
stack struct {
|
|
||||||
top *node
|
|
||||||
length int
|
|
||||||
lock *sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
node struct {
|
|
||||||
value interface{}
|
|
||||||
prev *node
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewStack Create a new stack
|
|
||||||
func NewStack() *stack {
|
|
||||||
return &stack{nil, 0, &sync.RWMutex{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len Return the number of items in the stack
|
|
||||||
func (this *stack) Len() int {
|
|
||||||
return this.length
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peek View the top item on the stack
|
|
||||||
func (this *stack) Peek() interface{} {
|
|
||||||
if this.length == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return this.top.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop the top item of the stack and return it
|
|
||||||
func (this *stack) Pop() interface{} {
|
|
||||||
this.lock.Lock()
|
|
||||||
defer this.lock.Unlock()
|
|
||||||
if this.length == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
n := this.top
|
|
||||||
this.top = n.prev
|
|
||||||
this.length--
|
|
||||||
return n.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push a value onto the top of the stack
|
|
||||||
func (this *stack) Push(value interface{}) {
|
|
||||||
this.lock.Lock()
|
|
||||||
defer this.lock.Unlock()
|
|
||||||
n := &node{value, this.top}
|
|
||||||
this.top = n
|
|
||||||
this.length++
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -1,390 +0,0 @@
|
|||||||
package convert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config
|
|
||||||
func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
|
||||||
data := DecodeBase64(buf)
|
|
||||||
|
|
||||||
arr := strings.Split(string(data), "\n")
|
|
||||||
|
|
||||||
proxies := make([]map[string]any, 0, len(arr))
|
|
||||||
names := make(map[string]int, 200)
|
|
||||||
|
|
||||||
for _, line := range arr {
|
|
||||||
line = strings.TrimRight(line, " \r")
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
scheme, body, found := strings.Cut(line, "://")
|
|
||||||
if !found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
scheme = strings.ToLower(scheme)
|
|
||||||
switch scheme {
|
|
||||||
case "hysteria":
|
|
||||||
urlHysteria, err := url.Parse(line)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
query := urlHysteria.Query()
|
|
||||||
name := uniqueName(names, urlHysteria.Fragment)
|
|
||||||
hysteria := make(map[string]any, 20)
|
|
||||||
|
|
||||||
hysteria["name"] = name
|
|
||||||
hysteria["type"] = scheme
|
|
||||||
hysteria["server"] = urlHysteria.Hostname()
|
|
||||||
hysteria["port"] = urlHysteria.Port()
|
|
||||||
hysteria["sni"] = query.Get("peer")
|
|
||||||
hysteria["obfs"] = query.Get("obfs")
|
|
||||||
hysteria["alpn"] = []string{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["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
|
|
||||||
|
|
||||||
proxies = append(proxies, hysteria)
|
|
||||||
|
|
||||||
case "trojan":
|
|
||||||
urlTrojan, err := url.Parse(line)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
query := urlTrojan.Query()
|
|
||||||
|
|
||||||
name := uniqueName(names, urlTrojan.Fragment)
|
|
||||||
trojan := make(map[string]any, 20)
|
|
||||||
|
|
||||||
trojan["name"] = name
|
|
||||||
trojan["type"] = scheme
|
|
||||||
trojan["server"] = urlTrojan.Hostname()
|
|
||||||
trojan["port"] = urlTrojan.Port()
|
|
||||||
trojan["password"] = urlTrojan.User.Username()
|
|
||||||
trojan["udp"] = true
|
|
||||||
trojan["skip-cert-verify"] = false
|
|
||||||
|
|
||||||
sni := query.Get("sni")
|
|
||||||
if sni != "" {
|
|
||||||
trojan["sni"] = sni
|
|
||||||
}
|
|
||||||
|
|
||||||
network := strings.ToLower(query.Get("type"))
|
|
||||||
if network != "" {
|
|
||||||
trojan["network"] = network
|
|
||||||
}
|
|
||||||
|
|
||||||
switch network {
|
|
||||||
case "ws":
|
|
||||||
headers := make(map[string]any)
|
|
||||||
wsOpts := make(map[string]any)
|
|
||||||
|
|
||||||
headers["User-Agent"] = RandUserAgent()
|
|
||||||
|
|
||||||
wsOpts["path"] = query.Get("path")
|
|
||||||
wsOpts["headers"] = headers
|
|
||||||
|
|
||||||
trojan["ws-opts"] = wsOpts
|
|
||||||
|
|
||||||
case "grpc":
|
|
||||||
grpcOpts := make(map[string]any)
|
|
||||||
grpcOpts["grpc-service-name"] = query.Get("serviceName")
|
|
||||||
trojan["grpc-opts"] = grpcOpts
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies = append(proxies, trojan)
|
|
||||||
|
|
||||||
case "vless":
|
|
||||||
urlVLess, err := url.Parse(line)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
query := urlVLess.Query()
|
|
||||||
vless := make(map[string]any, 20)
|
|
||||||
handleVShareLink(names, urlVLess, scheme, vless)
|
|
||||||
if flow := query.Get("flow"); flow != "" {
|
|
||||||
vless["flow"] = strings.ToLower(flow)
|
|
||||||
}
|
|
||||||
proxies = append(proxies, vless)
|
|
||||||
|
|
||||||
case "vmess":
|
|
||||||
// V2RayN-styled share link
|
|
||||||
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
|
|
||||||
dcBuf, err := tryDecodeBase64([]byte(body))
|
|
||||||
if err != nil {
|
|
||||||
// Xray VMessAEAD share link
|
|
||||||
urlVMess, err := url.Parse(line)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
query := urlVMess.Query()
|
|
||||||
vmess := make(map[string]any, 20)
|
|
||||||
handleVShareLink(names, urlVMess, scheme, vmess)
|
|
||||||
vmess["alterId"] = 0
|
|
||||||
vmess["cipher"] = "auto"
|
|
||||||
if encryption := query.Get("encryption"); encryption != "" {
|
|
||||||
vmess["cipher"] = encryption
|
|
||||||
}
|
|
||||||
if packetEncoding := query.Get("packetEncoding"); packetEncoding != "" {
|
|
||||||
switch packetEncoding {
|
|
||||||
case "packet":
|
|
||||||
vmess["packet-addr"] = true
|
|
||||||
case "xudp":
|
|
||||||
vmess["xudp"] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
proxies = append(proxies, vmess)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonDc := json.NewDecoder(bytes.NewReader(dcBuf))
|
|
||||||
values := make(map[string]any, 20)
|
|
||||||
|
|
||||||
if jsonDc.Decode(&values) != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
name := uniqueName(names, values["ps"].(string))
|
|
||||||
vmess := make(map[string]any, 20)
|
|
||||||
|
|
||||||
vmess["name"] = name
|
|
||||||
vmess["type"] = scheme
|
|
||||||
vmess["server"] = values["add"]
|
|
||||||
vmess["port"] = values["port"]
|
|
||||||
vmess["uuid"] = values["id"]
|
|
||||||
if alterId, ok := values["aid"]; ok {
|
|
||||||
vmess["alterId"] = alterId
|
|
||||||
} else {
|
|
||||||
vmess["alterId"] = 0
|
|
||||||
}
|
|
||||||
vmess["udp"] = true
|
|
||||||
vmess["tls"] = false
|
|
||||||
vmess["skip-cert-verify"] = false
|
|
||||||
|
|
||||||
vmess["cipher"] = "auto"
|
|
||||||
if cipher, ok := values["scy"]; ok && cipher != "" {
|
|
||||||
vmess["cipher"] = cipher
|
|
||||||
}
|
|
||||||
|
|
||||||
if sni, ok := values["sni"]; ok && sni != "" {
|
|
||||||
vmess["servername"] = sni
|
|
||||||
}
|
|
||||||
|
|
||||||
network := 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") {
|
|
||||||
vmess["tls"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
switch network {
|
|
||||||
case "http":
|
|
||||||
headers := make(map[string]any)
|
|
||||||
httpOpts := make(map[string]any)
|
|
||||||
if host, ok := values["host"]; ok && host != "" {
|
|
||||||
headers["Host"] = []string{host.(string)}
|
|
||||||
}
|
|
||||||
httpOpts["path"] = []string{"/"}
|
|
||||||
if path, ok := values["path"]; ok && path != "" {
|
|
||||||
httpOpts["path"] = []string{path.(string)}
|
|
||||||
}
|
|
||||||
httpOpts["headers"] = headers
|
|
||||||
|
|
||||||
vmess["http-opts"] = httpOpts
|
|
||||||
|
|
||||||
case "h2":
|
|
||||||
headers := make(map[string]any)
|
|
||||||
h2Opts := make(map[string]any)
|
|
||||||
if host, ok := values["host"]; ok && host != "" {
|
|
||||||
headers["Host"] = []string{host.(string)}
|
|
||||||
}
|
|
||||||
|
|
||||||
h2Opts["path"] = values["path"]
|
|
||||||
h2Opts["headers"] = headers
|
|
||||||
|
|
||||||
vmess["h2-opts"] = h2Opts
|
|
||||||
|
|
||||||
case "ws":
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
wsOpts["headers"] = headers
|
|
||||||
vmess["ws-opts"] = wsOpts
|
|
||||||
|
|
||||||
case "grpc":
|
|
||||||
grpcOpts := make(map[string]any)
|
|
||||||
grpcOpts["grpc-service-name"] = values["path"]
|
|
||||||
vmess["grpc-opts"] = grpcOpts
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies = append(proxies, vmess)
|
|
||||||
|
|
||||||
case "ss":
|
|
||||||
urlSS, err := url.Parse(line)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
name := uniqueName(names, urlSS.Fragment)
|
|
||||||
port := urlSS.Port()
|
|
||||||
|
|
||||||
if port == "" {
|
|
||||||
dcBuf, err := encRaw.DecodeString(urlSS.Host)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
urlSS, err = url.Parse("ss://" + string(dcBuf))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
cipher = urlSS.User.Username()
|
|
||||||
password string
|
|
||||||
)
|
|
||||||
|
|
||||||
if password, found = urlSS.User.Password(); !found {
|
|
||||||
dcBuf, _ := enc.DecodeString(cipher)
|
|
||||||
if !strings.Contains(string(dcBuf), "2022-blake3") {
|
|
||||||
dcBuf, _ = encRaw.DecodeString(cipher)
|
|
||||||
}
|
|
||||||
cipher, password, found = strings.Cut(string(dcBuf), ":")
|
|
||||||
if !found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ss := make(map[string]any, 10)
|
|
||||||
|
|
||||||
ss["name"] = name
|
|
||||||
ss["type"] = scheme
|
|
||||||
ss["server"] = urlSS.Hostname()
|
|
||||||
ss["port"] = urlSS.Port()
|
|
||||||
ss["cipher"] = cipher
|
|
||||||
ss["password"] = password
|
|
||||||
query := urlSS.Query()
|
|
||||||
ss["udp"] = true
|
|
||||||
if query.Get("udp-over-tcp") == "true" || query.Get("uot") == "1" {
|
|
||||||
ss["udp-over-tcp"] = true
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1
|
|
||||||
|
|
||||||
before, after, ok := strings.Cut(string(dcBuf), "/?")
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeArr := strings.Split(before, ":")
|
|
||||||
|
|
||||||
if len(beforeArr) != 6 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
host := beforeArr[0]
|
|
||||||
port := beforeArr[1]
|
|
||||||
protocol := beforeArr[2]
|
|
||||||
method := beforeArr[3]
|
|
||||||
obfs := beforeArr[4]
|
|
||||||
password := decodeUrlSafe(urlSafe(beforeArr[5]))
|
|
||||||
|
|
||||||
query, err := url.ParseQuery(urlSafe(after))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
remarks := decodeUrlSafe(query.Get("remarks"))
|
|
||||||
name := uniqueName(names, remarks)
|
|
||||||
|
|
||||||
obfsParam := decodeUrlSafe(query.Get("obfsparam"))
|
|
||||||
protocolParam := query.Get("protoparam")
|
|
||||||
|
|
||||||
ssr := make(map[string]any, 20)
|
|
||||||
|
|
||||||
ssr["name"] = name
|
|
||||||
ssr["type"] = scheme
|
|
||||||
ssr["server"] = host
|
|
||||||
ssr["port"] = port
|
|
||||||
ssr["cipher"] = method
|
|
||||||
ssr["password"] = password
|
|
||||||
ssr["obfs"] = obfs
|
|
||||||
ssr["protocol"] = protocol
|
|
||||||
ssr["udp"] = true
|
|
||||||
|
|
||||||
if obfsParam != "" {
|
|
||||||
ssr["obfs-param"] = obfsParam
|
|
||||||
}
|
|
||||||
|
|
||||||
if protocolParam != "" {
|
|
||||||
ssr["protocol-param"] = protocolParam
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies = append(proxies, ssr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(proxies) == 0 {
|
|
||||||
return nil, fmt.Errorf("convert v2ray subscribe error: format invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxies, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func uniqueName(names map[string]int, name string) string {
|
|
||||||
if index, ok := names[name]; ok {
|
|
||||||
index++
|
|
||||||
names[name] = index
|
|
||||||
name = fmt.Sprintf("%s-%02d", name, index)
|
|
||||||
} else {
|
|
||||||
index = 0
|
|
||||||
names[name] = index
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
@ -1,316 +0,0 @@
|
|||||||
package convert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
var hostsSuffix = []string{
|
|
||||||
"-cdn.aliyuncs.com",
|
|
||||||
".alicdn.com",
|
|
||||||
".pan.baidu.com",
|
|
||||||
".tbcache.com",
|
|
||||||
".aliyuncdn.com",
|
|
||||||
".vod.miguvideo.com",
|
|
||||||
".cibntv.net",
|
|
||||||
".myqcloud.com",
|
|
||||||
".smtcdns.com",
|
|
||||||
".alikunlun.com",
|
|
||||||
".smtcdns.net",
|
|
||||||
".apcdns.net",
|
|
||||||
".cdn-go.cn",
|
|
||||||
".cdntip.com",
|
|
||||||
".cdntips.com",
|
|
||||||
".alidayu.com",
|
|
||||||
".alidns.com",
|
|
||||||
".cdngslb.com",
|
|
||||||
".mxhichina.com",
|
|
||||||
".alibabadns.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
var userAgents = []string{
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
|
|
||||||
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
hostsLen = len(hostsSuffix)
|
|
||||||
uaLen = len(userAgents)
|
|
||||||
)
|
|
||||||
|
|
||||||
func RandHost() string {
|
|
||||||
id, _ := uuid.NewV4()
|
|
||||||
base := strings.ToLower(base64.RawURLEncoding.EncodeToString(id.Bytes()))
|
|
||||||
base = strings.ReplaceAll(base, "-", "")
|
|
||||||
base = strings.ReplaceAll(base, "_", "")
|
|
||||||
buf := []byte(base)
|
|
||||||
prefix := string(buf[:3]) + "---"
|
|
||||||
prefix += string(buf[6:8]) + "-"
|
|
||||||
prefix += string(buf[len(buf)-8:])
|
|
||||||
|
|
||||||
return prefix + hostsSuffix[rand.Intn(hostsLen)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func RandUserAgent() string {
|
|
||||||
return userAgents[rand.Intn(uaLen)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetUserAgent(header http.Header) {
|
|
||||||
if header.Get("User-Agent") != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userAgent := RandUserAgent()
|
|
||||||
header.Set("User-Agent", userAgent)
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
package convert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy map[string]any) {
|
|
||||||
// Xray VMessAEAD / VLESS share link standard
|
|
||||||
// https://github.com/XTLS/Xray-core/discussions/716
|
|
||||||
query := url.Query()
|
|
||||||
proxy["name"] = uniqueName(names, url.Fragment)
|
|
||||||
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") {
|
|
||||||
proxy["tls"] = true
|
|
||||||
}
|
|
||||||
if sni := query.Get("sni"); sni != "" {
|
|
||||||
proxy["servername"] = sni
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
proxy["ws-opts"] = wsOpts
|
|
||||||
|
|
||||||
case "grpc":
|
|
||||||
grpcOpts := make(map[string]any)
|
|
||||||
grpcOpts["grpc-service-name"] = query.Get("serviceName")
|
|
||||||
proxy["grpc-opts"] = grpcOpts
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,235 +0,0 @@
|
|||||||
// Copyright 2009 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package 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.
|
|
||||||
type Element[T any] struct {
|
|
||||||
// Next and previous pointers in the doubly-linked list of elements.
|
|
||||||
// To simplify the implementation, internally a list l is implemented
|
|
||||||
// as a ring, such that &l.root is both the next element of the last
|
|
||||||
// list element (l.Back()) and the previous element of the first list
|
|
||||||
// element (l.Front()).
|
|
||||||
next, prev *Element[T]
|
|
||||||
|
|
||||||
// The list to which this element belongs.
|
|
||||||
list *List[T]
|
|
||||||
|
|
||||||
// The value stored with this element.
|
|
||||||
Value T
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next returns the next list element or nil.
|
|
||||||
func (e *Element[T]) Next() *Element[T] {
|
|
||||||
if p := e.next; e.list != nil && p != &e.list.root {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prev returns the previous list element or nil.
|
|
||||||
func (e *Element[T]) Prev() *Element[T] {
|
|
||||||
if p := e.prev; e.list != nil && p != &e.list.root {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List represents a doubly linked list.
|
|
||||||
// The zero value for List is an empty list ready to use.
|
|
||||||
type List[T any] struct {
|
|
||||||
root Element[T] // sentinel list element, only &root, root.prev, and root.next are used
|
|
||||||
len int // current list length excluding (this) sentinel element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes or clears list l.
|
|
||||||
func (l *List[T]) Init() *List[T] {
|
|
||||||
l.root.next = &l.root
|
|
||||||
l.root.prev = &l.root
|
|
||||||
l.len = 0
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns an initialized list.
|
|
||||||
func New[T any]() *List[T] { return new(List[T]).Init() }
|
|
||||||
|
|
||||||
// Len returns the number of elements of list l.
|
|
||||||
// The complexity is O(1).
|
|
||||||
func (l *List[T]) Len() int { return l.len }
|
|
||||||
|
|
||||||
// Front returns the first element of list l or nil if the list is empty.
|
|
||||||
func (l *List[T]) Front() *Element[T] {
|
|
||||||
if l.len == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.root.next
|
|
||||||
}
|
|
||||||
|
|
||||||
// Back returns the last element of list l or nil if the list is empty.
|
|
||||||
func (l *List[T]) Back() *Element[T] {
|
|
||||||
if l.len == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.root.prev
|
|
||||||
}
|
|
||||||
|
|
||||||
// lazyInit lazily initializes a zero List value.
|
|
||||||
func (l *List[T]) lazyInit() {
|
|
||||||
if l.root.next == nil {
|
|
||||||
l.Init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert inserts e after at, increments l.len, and returns e.
|
|
||||||
func (l *List[T]) insert(e, at *Element[T]) *Element[T] {
|
|
||||||
e.prev = at
|
|
||||||
e.next = at.next
|
|
||||||
e.prev.next = e
|
|
||||||
e.next.prev = e
|
|
||||||
e.list = l
|
|
||||||
l.len++
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
|
|
||||||
func (l *List[T]) insertValue(v T, at *Element[T]) *Element[T] {
|
|
||||||
return l.insert(&Element[T]{Value: v}, at)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove removes e from its list, decrements l.len
|
|
||||||
func (l *List[T]) remove(e *Element[T]) {
|
|
||||||
e.prev.next = e.next
|
|
||||||
e.next.prev = e.prev
|
|
||||||
e.next = nil // avoid memory leaks
|
|
||||||
e.prev = nil // avoid memory leaks
|
|
||||||
e.list = nil
|
|
||||||
l.len--
|
|
||||||
}
|
|
||||||
|
|
||||||
// move moves e to next to at.
|
|
||||||
func (l *List[T]) move(e, at *Element[T]) {
|
|
||||||
if e == at {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.prev.next = e.next
|
|
||||||
e.next.prev = e.prev
|
|
||||||
|
|
||||||
e.prev = at
|
|
||||||
e.next = at.next
|
|
||||||
e.prev.next = e
|
|
||||||
e.next.prev = e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes e from l if e is an element of list l.
|
|
||||||
// It returns the element value e.Value.
|
|
||||||
// The element must not be nil.
|
|
||||||
func (l *List[T]) Remove(e *Element[T]) T {
|
|
||||||
if e.list == l {
|
|
||||||
// if e.list == l, l must have been initialized when e was inserted
|
|
||||||
// in l or l == nil (e is a zero Element) and l.remove will crash
|
|
||||||
l.remove(e)
|
|
||||||
}
|
|
||||||
return e.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushFront inserts a new element e with value v at the front of list l and returns e.
|
|
||||||
func (l *List[T]) PushFront(v T) *Element[T] {
|
|
||||||
l.lazyInit()
|
|
||||||
return l.insertValue(v, &l.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushBack inserts a new element e with value v at the back of list l and returns e.
|
|
||||||
func (l *List[T]) PushBack(v T) *Element[T] {
|
|
||||||
l.lazyInit()
|
|
||||||
return l.insertValue(v, l.root.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertBefore inserts a new element e with value v immediately before mark and returns e.
|
|
||||||
// If mark is not an element of l, the list is not modified.
|
|
||||||
// The mark must not be nil.
|
|
||||||
func (l *List[T]) InsertBefore(v T, mark *Element[T]) *Element[T] {
|
|
||||||
if mark.list != l {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// see comment in List.Remove about initialization of l
|
|
||||||
return l.insertValue(v, mark.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
|
|
||||||
// If mark is not an element of l, the list is not modified.
|
|
||||||
// The mark must not be nil.
|
|
||||||
func (l *List[T]) InsertAfter(v T, mark *Element[T]) *Element[T] {
|
|
||||||
if mark.list != l {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// see comment in List.Remove about initialization of l
|
|
||||||
return l.insertValue(v, mark)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveToFront moves element e to the front of list l.
|
|
||||||
// If e is not an element of l, the list is not modified.
|
|
||||||
// The element must not be nil.
|
|
||||||
func (l *List[T]) MoveToFront(e *Element[T]) {
|
|
||||||
if e.list != l || l.root.next == e {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// see comment in List.Remove about initialization of l
|
|
||||||
l.move(e, &l.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveToBack moves element e to the back of list l.
|
|
||||||
// If e is not an element of l, the list is not modified.
|
|
||||||
// The element must not be nil.
|
|
||||||
func (l *List[T]) MoveToBack(e *Element[T]) {
|
|
||||||
if e.list != l || l.root.prev == e {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// see comment in List.Remove about initialization of l
|
|
||||||
l.move(e, l.root.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveBefore moves element e to its new position before mark.
|
|
||||||
// If e or mark is not an element of l, or e == mark, the list is not modified.
|
|
||||||
// The element and mark must not be nil.
|
|
||||||
func (l *List[T]) MoveBefore(e, mark *Element[T]) {
|
|
||||||
if e.list != l || e == mark || mark.list != l {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l.move(e, mark.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveAfter moves element e to its new position after mark.
|
|
||||||
// If e or mark is not an element of l, or e == mark, the list is not modified.
|
|
||||||
// The element and mark must not be nil.
|
|
||||||
func (l *List[T]) MoveAfter(e, mark *Element[T]) {
|
|
||||||
if e.list != l || e == mark || mark.list != l {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l.move(e, mark)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushBackList inserts a copy of another list at the back of list l.
|
|
||||||
// The lists l and other may be the same. They must not be nil.
|
|
||||||
func (l *List[T]) PushBackList(other *List[T]) {
|
|
||||||
l.lazyInit()
|
|
||||||
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
|
|
||||||
l.insertValue(e.Value, l.root.prev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushFrontList inserts a copy of another list at the front of list l.
|
|
||||||
// The lists l and other may be the same. They must not be nil.
|
|
||||||
func (l *List[T]) PushFrontList(other *List[T]) {
|
|
||||||
l.lazyInit()
|
|
||||||
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
|
|
||||||
l.insertValue(e.Value, &l.root)
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,6 +4,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Relay copies between left and right bidirectionally.
|
// Relay copies between left and right bidirectionally.
|
||||||
@ -11,14 +13,18 @@ func Relay(leftConn, rightConn net.Conn) {
|
|||||||
ch := make(chan error)
|
ch := make(chan error)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
|
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
|
||||||
// See also https://github.com/Dreamacro/clash/pull/1209
|
// See also https://github.com/Dreamacro/clash/pull/1209
|
||||||
_, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn})
|
_, err := io.CopyBuffer(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn}, buf)
|
||||||
|
pool.Put(buf)
|
||||||
leftConn.SetReadDeadline(time.Now())
|
leftConn.SetReadDeadline(time.Now())
|
||||||
ch <- err
|
ch <- err
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_, _ = io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn})
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
|
io.CopyBuffer(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn}, buf)
|
||||||
|
pool.Put(buf)
|
||||||
rightConn.SetReadDeadline(time.Now())
|
rightConn.SetReadDeadline(time.Now())
|
||||||
<-ch
|
<-ch
|
||||||
}
|
}
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
package net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SplitNetworkType(s string) (string, string, error) {
|
|
||||||
var (
|
|
||||||
shecme string
|
|
||||||
hostPort string
|
|
||||||
)
|
|
||||||
result := strings.Split(s, "://")
|
|
||||||
if len(result) == 2 {
|
|
||||||
shecme = result[0]
|
|
||||||
hostPort = result[1]
|
|
||||||
} else if len(result) == 1 {
|
|
||||||
hostPort = result[0]
|
|
||||||
} else {
|
|
||||||
return "", "", fmt.Errorf("tcp/udp style error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(shecme) == 0 {
|
|
||||||
shecme = "udp"
|
|
||||||
}
|
|
||||||
|
|
||||||
if shecme != "tcp" && shecme != "udp" {
|
|
||||||
return "", "", fmt.Errorf("scheme should be tcp:// or udp://")
|
|
||||||
} else {
|
|
||||||
return shecme, hostPort, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SplitHostPort(s string) (host, port string, hasPort bool, err error) {
|
|
||||||
temp := s
|
|
||||||
hasPort = true
|
|
||||||
|
|
||||||
if !strings.Contains(s, ":") && !strings.Contains(s, "]:") {
|
|
||||||
temp += ":0"
|
|
||||||
hasPort = false
|
|
||||||
}
|
|
||||||
|
|
||||||
host, port, err = net.SplitHostPort(temp)
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package nnip
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IpToAddr converts the net.IP to netip.Addr.
|
|
||||||
// If slice's length is not 4 or 16, IpToAddr returns netip.Addr{}
|
|
||||||
func IpToAddr(slice net.IP) netip.Addr {
|
|
||||||
ip := slice
|
|
||||||
if len(ip) != 4 {
|
|
||||||
if ip = slice.To4(); ip == nil {
|
|
||||||
ip = slice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if addr, ok := netip.AddrFromSlice(ip); ok {
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
return netip.Addr{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnMasked returns p's last IP address.
|
|
||||||
// If p is invalid, UnMasked returns netip.Addr{}
|
|
||||||
func UnMasked(p netip.Prefix) netip.Addr {
|
|
||||||
if !p.IsValid() {
|
|
||||||
return netip.Addr{}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := p.Addr().As16()
|
|
||||||
|
|
||||||
hi := binary.BigEndian.Uint64(buf[:8])
|
|
||||||
lo := binary.BigEndian.Uint64(buf[8:])
|
|
||||||
|
|
||||||
bits := p.Bits()
|
|
||||||
if bits <= 32 {
|
|
||||||
bits += 96
|
|
||||||
}
|
|
||||||
|
|
||||||
hi = hi | ^uint64(0)>>bits
|
|
||||||
lo = lo | ^(^uint64(0) << (128 - bits))
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint64(buf[:8], hi)
|
|
||||||
binary.BigEndian.PutUint64(buf[8:], lo)
|
|
||||||
|
|
||||||
addr := netip.AddrFrom16(buf)
|
|
||||||
if p.Addr().Is4() {
|
|
||||||
return addr.Unmap()
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
}
|
|
@ -1,3 +1,3 @@
|
|||||||
package observable
|
package observable
|
||||||
|
|
||||||
type Iterable[T any] <-chan T
|
type Iterable <-chan any
|
||||||
|
@ -5,14 +5,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Observable[T any] struct {
|
type Observable struct {
|
||||||
iterable Iterable[T]
|
iterable Iterable
|
||||||
listener map[Subscription[T]]*Subscriber[T]
|
listener map[Subscription]*Subscriber
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
done bool
|
done bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Observable[T]) process() {
|
func (o *Observable) process() {
|
||||||
for item := range o.iterable {
|
for item := range o.iterable {
|
||||||
o.mux.Lock()
|
o.mux.Lock()
|
||||||
for _, sub := range o.listener {
|
for _, sub := range o.listener {
|
||||||
@ -23,7 +23,7 @@ func (o *Observable[T]) process() {
|
|||||||
o.close()
|
o.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Observable[T]) close() {
|
func (o *Observable) close() {
|
||||||
o.mux.Lock()
|
o.mux.Lock()
|
||||||
defer o.mux.Unlock()
|
defer o.mux.Unlock()
|
||||||
|
|
||||||
@ -33,18 +33,18 @@ func (o *Observable[T]) close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Observable[T]) Subscribe() (Subscription[T], error) {
|
func (o *Observable) Subscribe() (Subscription, error) {
|
||||||
o.mux.Lock()
|
o.mux.Lock()
|
||||||
defer o.mux.Unlock()
|
defer o.mux.Unlock()
|
||||||
if o.done {
|
if o.done {
|
||||||
return nil, errors.New("observable is closed")
|
return nil, errors.New("Observable is closed")
|
||||||
}
|
}
|
||||||
subscriber := newSubscriber[T]()
|
subscriber := newSubscriber()
|
||||||
o.listener[subscriber.Out()] = subscriber
|
o.listener[subscriber.Out()] = subscriber
|
||||||
return subscriber.Out(), nil
|
return subscriber.Out(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Observable[T]) UnSubscribe(sub Subscription[T]) {
|
func (o *Observable) UnSubscribe(sub Subscription) {
|
||||||
o.mux.Lock()
|
o.mux.Lock()
|
||||||
defer o.mux.Unlock()
|
defer o.mux.Unlock()
|
||||||
subscriber, exist := o.listener[sub]
|
subscriber, exist := o.listener[sub]
|
||||||
@ -55,10 +55,10 @@ func (o *Observable[T]) UnSubscribe(sub Subscription[T]) {
|
|||||||
subscriber.Close()
|
subscriber.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewObservable[T any](iter Iterable[T]) *Observable[T] {
|
func NewObservable(any Iterable) *Observable {
|
||||||
observable := &Observable[T]{
|
observable := &Observable{
|
||||||
iterable: iter,
|
iterable: any,
|
||||||
listener: map[Subscription[T]]*Subscriber[T]{},
|
listener: map[Subscription]*Subscriber{},
|
||||||
}
|
}
|
||||||
go observable.process()
|
go observable.process()
|
||||||
return observable
|
return observable
|
||||||
|
@ -9,8 +9,8 @@ import (
|
|||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func iterator[T any](item []T) chan T {
|
func iterator(item []any) chan any {
|
||||||
ch := make(chan T)
|
ch := make(chan any)
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
for _, elm := range item {
|
for _, elm := range item {
|
||||||
@ -22,8 +22,8 @@ func iterator[T any](item []T) chan T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable(t *testing.T) {
|
func TestObservable(t *testing.T) {
|
||||||
iter := iterator[int]([]int{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable[int](iter)
|
src := NewObservable(iter)
|
||||||
data, err := src.Subscribe()
|
data, err := src.Subscribe()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
count := 0
|
count := 0
|
||||||
@ -34,15 +34,15 @@ func TestObservable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_MultiSubscribe(t *testing.T) {
|
func TestObservable_MultiSubscribe(t *testing.T) {
|
||||||
iter := iterator[int]([]int{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable[int](iter)
|
src := NewObservable(iter)
|
||||||
ch1, _ := src.Subscribe()
|
ch1, _ := src.Subscribe()
|
||||||
ch2, _ := src.Subscribe()
|
ch2, _ := src.Subscribe()
|
||||||
count := atomic.NewInt32(0)
|
count := atomic.NewInt32(0)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
waitCh := func(ch <-chan int) {
|
waitCh := func(ch <-chan any) {
|
||||||
for range ch {
|
for range ch {
|
||||||
count.Inc()
|
count.Inc()
|
||||||
}
|
}
|
||||||
@ -55,8 +55,8 @@ func TestObservable_MultiSubscribe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_UnSubscribe(t *testing.T) {
|
func TestObservable_UnSubscribe(t *testing.T) {
|
||||||
iter := iterator[int]([]int{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable[int](iter)
|
src := NewObservable(iter)
|
||||||
data, err := src.Subscribe()
|
data, err := src.Subscribe()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
src.UnSubscribe(data)
|
src.UnSubscribe(data)
|
||||||
@ -65,8 +65,8 @@ func TestObservable_UnSubscribe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_SubscribeClosedSource(t *testing.T) {
|
func TestObservable_SubscribeClosedSource(t *testing.T) {
|
||||||
iter := iterator[int]([]int{1})
|
iter := iterator([]any{1})
|
||||||
src := NewObservable[int](iter)
|
src := NewObservable(iter)
|
||||||
data, _ := src.Subscribe()
|
data, _ := src.Subscribe()
|
||||||
<-data
|
<-data
|
||||||
|
|
||||||
@ -75,18 +75,18 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
|
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
|
||||||
sub := Subscription[int](make(chan int))
|
sub := Subscription(make(chan any))
|
||||||
iter := iterator[int]([]int{1})
|
iter := iterator([]any{1})
|
||||||
src := NewObservable[int](iter)
|
src := NewObservable(iter)
|
||||||
src.UnSubscribe(sub)
|
src.UnSubscribe(sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
||||||
iter := iterator[int]([]int{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable[int](iter)
|
src := NewObservable(iter)
|
||||||
max := 100
|
max := 100
|
||||||
|
|
||||||
var list []Subscription[int]
|
var list []Subscription
|
||||||
for i := 0; i < max; i++ {
|
for i := 0; i < max; i++ {
|
||||||
ch, _ := src.Subscribe()
|
ch, _ := src.Subscribe()
|
||||||
list = append(list, ch)
|
list = append(list, ch)
|
||||||
@ -94,7 +94,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
|||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(max)
|
wg.Add(max)
|
||||||
waitCh := func(ch <-chan int) {
|
waitCh := func(ch <-chan any) {
|
||||||
for range ch {
|
for range ch {
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
@ -115,11 +115,11 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Benchmark_Observable_1000(b *testing.B) {
|
func Benchmark_Observable_1000(b *testing.B) {
|
||||||
ch := make(chan int)
|
ch := make(chan any)
|
||||||
o := NewObservable[int](ch)
|
o := NewObservable(ch)
|
||||||
num := 1000
|
num := 1000
|
||||||
|
|
||||||
subs := []Subscription[int]{}
|
subs := []Subscription{}
|
||||||
for i := 0; i < num; i++ {
|
for i := 0; i < num; i++ {
|
||||||
sub, _ := o.Subscribe()
|
sub, _ := o.Subscribe()
|
||||||
subs = append(subs, sub)
|
subs = append(subs, sub)
|
||||||
@ -130,7 +130,7 @@ func Benchmark_Observable_1000(b *testing.B) {
|
|||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for _, sub := range subs {
|
for _, sub := range subs {
|
||||||
go func(s Subscription[int]) {
|
go func(s Subscription) {
|
||||||
for range s {
|
for range s {
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
|
@ -4,30 +4,30 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Subscription[T any] <-chan T
|
type Subscription <-chan any
|
||||||
|
|
||||||
type Subscriber[T any] struct {
|
type Subscriber struct {
|
||||||
buffer chan T
|
buffer chan any
|
||||||
once sync.Once
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber[T]) Emit(item T) {
|
func (s *Subscriber) Emit(item any) {
|
||||||
s.buffer <- item
|
s.buffer <- item
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber[T]) Out() Subscription[T] {
|
func (s *Subscriber) Out() Subscription {
|
||||||
return s.buffer
|
return s.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber[T]) Close() {
|
func (s *Subscriber) Close() {
|
||||||
s.once.Do(func() {
|
s.once.Do(func() {
|
||||||
close(s.buffer)
|
close(s.buffer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSubscriber[T any]() *Subscriber[T] {
|
func newSubscriber() *Subscriber {
|
||||||
sub := &Subscriber[T]{
|
sub := &Subscriber{
|
||||||
buffer: make(chan T, 200),
|
buffer: make(chan any, 200),
|
||||||
}
|
}
|
||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
// Picker provides synchronization, and Context cancelation
|
// Picker provides synchronization, and Context cancelation
|
||||||
// for groups of goroutines working on subtasks of a common task.
|
// for groups of goroutines working on subtasks of a common task.
|
||||||
// Inspired by errGroup
|
// Inspired by errGroup
|
||||||
type Picker[T any] struct {
|
type Picker struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel func()
|
cancel func()
|
||||||
|
|
||||||
@ -17,12 +17,12 @@ type Picker[T any] struct {
|
|||||||
|
|
||||||
once sync.Once
|
once sync.Once
|
||||||
errOnce sync.Once
|
errOnce sync.Once
|
||||||
result T
|
result any
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPicker[T any](ctx context.Context, cancel func()) *Picker[T] {
|
func newPicker(ctx context.Context, cancel func()) *Picker {
|
||||||
return &Picker[T]{
|
return &Picker{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
@ -30,20 +30,20 @@ func newPicker[T any](ctx context.Context, cancel func()) *Picker[T] {
|
|||||||
|
|
||||||
// WithContext returns a new Picker and an associated Context derived from ctx.
|
// WithContext returns a new Picker and an associated Context derived from ctx.
|
||||||
// and cancel when first element return.
|
// and cancel when first element return.
|
||||||
func WithContext[T any](ctx context.Context) (*Picker[T], context.Context) {
|
func WithContext(ctx context.Context) (*Picker, context.Context) {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
return newPicker[T](ctx, cancel), ctx
|
return newPicker(ctx, cancel), ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithTimeout returns a new Picker and an associated Context derived from ctx with timeout.
|
// WithTimeout returns a new Picker and an associated Context derived from ctx with timeout.
|
||||||
func WithTimeout[T any](ctx context.Context, timeout time.Duration) (*Picker[T], context.Context) {
|
func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.Context) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
return newPicker[T](ctx, cancel), ctx
|
return newPicker(ctx, cancel), ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait blocks until all function calls from the Go method have returned,
|
// Wait blocks until all function calls from the Go method have returned,
|
||||||
// then returns the first nil error result (if any) from them.
|
// then returns the first nil error result (if any) from them.
|
||||||
func (p *Picker[T]) Wait() T {
|
func (p *Picker) Wait() any {
|
||||||
p.wg.Wait()
|
p.wg.Wait()
|
||||||
if p.cancel != nil {
|
if p.cancel != nil {
|
||||||
p.cancel()
|
p.cancel()
|
||||||
@ -52,13 +52,13 @@ func (p *Picker[T]) Wait() T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Error return the first error (if all success return nil)
|
// Error return the first error (if all success return nil)
|
||||||
func (p *Picker[T]) Error() error {
|
func (p *Picker) Error() error {
|
||||||
return p.err
|
return p.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go calls the given function in a new goroutine.
|
// Go calls the given function in a new goroutine.
|
||||||
// The first call to return a nil error cancels the group; its result will be returned by Wait.
|
// The first call to return a nil error cancels the group; its result will be returned by Wait.
|
||||||
func (p *Picker[T]) Go(f func() (T, error)) {
|
func (p *Picker) Go(f func() (any, error)) {
|
||||||
p.wg.Add(1)
|
p.wg.Add(1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -8,38 +8,33 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, error) {
|
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
|
||||||
return func() (T, error) {
|
return func() (any, error) {
|
||||||
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
|
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
return input, nil
|
return input, nil
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return getZero[T](), ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPicker_Basic(t *testing.T) {
|
func TestPicker_Basic(t *testing.T) {
|
||||||
picker, ctx := WithContext[int](context.Background())
|
picker, ctx := WithContext(context.Background())
|
||||||
picker.Go(sleepAndSend(ctx, 30, 2))
|
picker.Go(sleepAndSend(ctx, 30, 2))
|
||||||
picker.Go(sleepAndSend(ctx, 20, 1))
|
picker.Go(sleepAndSend(ctx, 20, 1))
|
||||||
|
|
||||||
number := picker.Wait()
|
number := picker.Wait()
|
||||||
assert.NotNil(t, number)
|
assert.NotNil(t, number)
|
||||||
assert.Equal(t, number, 1)
|
assert.Equal(t, number.(int), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPicker_Timeout(t *testing.T) {
|
func TestPicker_Timeout(t *testing.T) {
|
||||||
picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5)
|
picker, ctx := WithTimeout(context.Background(), time.Millisecond*5)
|
||||||
picker.Go(sleepAndSend(ctx, 20, 1))
|
picker.Go(sleepAndSend(ctx, 20, 1))
|
||||||
|
|
||||||
number := picker.Wait()
|
number := picker.Wait()
|
||||||
assert.Equal(t, number, getZero[int]())
|
assert.Nil(t, number)
|
||||||
assert.NotNil(t, picker.Error())
|
assert.NotNil(t, picker.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getZero[T any]() T {
|
|
||||||
var result T
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
@ -52,8 +52,8 @@ func (alloc *Allocator) Put(buf []byte) error {
|
|||||||
return errors.New("allocator Put() incorrect buffer size")
|
return errors.New("allocator Put() incorrect buffer size")
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint
|
|
||||||
//lint:ignore SA6002 ignore temporarily
|
//lint:ignore SA6002 ignore temporarily
|
||||||
|
//nolint
|
||||||
alloc.buffers[bits].Put(buf)
|
alloc.buffers[bits].Put(buf)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package pool
|
|
||||||
|
|
||||||
import "github.com/sagernet/sing/common/buf"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
buf.DefaultAllocator = defaultAllocator
|
|
||||||
}
|
|
@ -5,13 +5,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Queue is a simple concurrent safe queue
|
// Queue is a simple concurrent safe queue
|
||||||
type Queue[T any] struct {
|
type Queue struct {
|
||||||
items []T
|
items []any
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put add the item to the queue.
|
// Put add the item to the queue.
|
||||||
func (q *Queue[T]) Put(items ...T) {
|
func (q *Queue) Put(items ...any) {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -22,9 +22,9 @@ func (q *Queue[T]) Put(items ...T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pop returns the head of items.
|
// Pop returns the head of items.
|
||||||
func (q *Queue[T]) Pop() T {
|
func (q *Queue) Pop() any {
|
||||||
if len(q.items) == 0 {
|
if len(q.items) == 0 {
|
||||||
return GetZero[T]()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
q.lock.Lock()
|
q.lock.Lock()
|
||||||
@ -35,9 +35,9 @@ func (q *Queue[T]) Pop() T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Last returns the last of item.
|
// Last returns the last of item.
|
||||||
func (q *Queue[T]) Last() T {
|
func (q *Queue) Last() any {
|
||||||
if len(q.items) == 0 {
|
if len(q.items) == 0 {
|
||||||
return GetZero[T]()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
q.lock.RLock()
|
q.lock.RLock()
|
||||||
@ -47,8 +47,8 @@ func (q *Queue[T]) Last() T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy get the copy of queue.
|
// Copy get the copy of queue.
|
||||||
func (q *Queue[T]) Copy() []T {
|
func (q *Queue) Copy() []any {
|
||||||
items := []T{}
|
items := []any{}
|
||||||
q.lock.RLock()
|
q.lock.RLock()
|
||||||
items = append(items, q.items...)
|
items = append(items, q.items...)
|
||||||
q.lock.RUnlock()
|
q.lock.RUnlock()
|
||||||
@ -56,7 +56,7 @@ func (q *Queue[T]) Copy() []T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Len returns the number of items in this queue.
|
// Len returns the number of items in this queue.
|
||||||
func (q *Queue[T]) Len() int64 {
|
func (q *Queue) Len() int64 {
|
||||||
q.lock.Lock()
|
q.lock.Lock()
|
||||||
defer q.lock.Unlock()
|
defer q.lock.Unlock()
|
||||||
|
|
||||||
@ -64,13 +64,8 @@ func (q *Queue[T]) Len() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New is a constructor for a new concurrent safe queue.
|
// New is a constructor for a new concurrent safe queue.
|
||||||
func New[T any](hint int64) *Queue[T] {
|
func New(hint int64) *Queue {
|
||||||
return &Queue[T]{
|
return &Queue{
|
||||||
items: make([]T, 0, hint),
|
items: make([]any, 0, hint),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetZero[T any]() T {
|
|
||||||
var result T
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
@ -5,27 +5,28 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type call[T any] struct {
|
type call struct {
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
val T
|
val any
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Single[T any] struct {
|
type Single struct {
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
last time.Time
|
last time.Time
|
||||||
wait time.Duration
|
wait time.Duration
|
||||||
call *call[T]
|
call *call
|
||||||
result *Result[T]
|
result *Result
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result[T any] struct {
|
type Result struct {
|
||||||
Val T
|
Val any
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do single.Do likes sync.singleFlight
|
// Do single.Do likes sync.singleFlight
|
||||||
func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
|
//lint:ignore ST1008 it likes sync.singleFlight
|
||||||
|
func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) {
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if now.Before(s.last.Add(s.wait)) {
|
if now.Before(s.last.Add(s.wait)) {
|
||||||
@ -33,31 +34,31 @@ func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
|
|||||||
return s.result.Val, s.result.Err, true
|
return s.result.Val, s.result.Err, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if callM := s.call; callM != nil {
|
if call := s.call; call != nil {
|
||||||
s.mux.Unlock()
|
s.mux.Unlock()
|
||||||
callM.wg.Wait()
|
call.wg.Wait()
|
||||||
return callM.val, callM.err, true
|
return call.val, call.err, true
|
||||||
}
|
}
|
||||||
|
|
||||||
callM := &call[T]{}
|
call := &call{}
|
||||||
callM.wg.Add(1)
|
call.wg.Add(1)
|
||||||
s.call = callM
|
s.call = call
|
||||||
s.mux.Unlock()
|
s.mux.Unlock()
|
||||||
callM.val, callM.err = fn()
|
call.val, call.err = fn()
|
||||||
callM.wg.Done()
|
call.wg.Done()
|
||||||
|
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
s.call = nil
|
s.call = nil
|
||||||
s.result = &Result[T]{callM.val, callM.err}
|
s.result = &Result{call.val, call.err}
|
||||||
s.last = now
|
s.last = now
|
||||||
s.mux.Unlock()
|
s.mux.Unlock()
|
||||||
return callM.val, callM.err, false
|
return call.val, call.err, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Single[T]) Reset() {
|
func (s *Single) Reset() {
|
||||||
s.last = time.Time{}
|
s.last = time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSingle[T any](wait time.Duration) *Single[T] {
|
func NewSingle(wait time.Duration) *Single {
|
||||||
return &Single[T]{wait: wait}
|
return &Single{wait: wait}
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBasic(t *testing.T) {
|
func TestBasic(t *testing.T) {
|
||||||
single := NewSingle[int](time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
shardCount := atomic.NewInt32(0)
|
shardCount := atomic.NewInt32(0)
|
||||||
call := func() (int, error) {
|
call := func() (any, error) {
|
||||||
foo++
|
foo++
|
||||||
time.Sleep(time.Millisecond * 5)
|
time.Sleep(time.Millisecond * 5)
|
||||||
return 0, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@ -38,32 +38,32 @@ func TestBasic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTimer(t *testing.T) {
|
func TestTimer(t *testing.T) {
|
||||||
single := NewSingle[int](time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
callM := func() (int, error) {
|
call := func() (any, error) {
|
||||||
foo++
|
foo++
|
||||||
return 0, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, _ = single.Do(callM)
|
single.Do(call)
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
_, _, shard := single.Do(callM)
|
_, _, shard := single.Do(call)
|
||||||
|
|
||||||
assert.Equal(t, 1, foo)
|
assert.Equal(t, 1, foo)
|
||||||
assert.True(t, shard)
|
assert.True(t, shard)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReset(t *testing.T) {
|
func TestReset(t *testing.T) {
|
||||||
single := NewSingle[int](time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
callM := func() (int, error) {
|
call := func() (any, error) {
|
||||||
foo++
|
foo++
|
||||||
return 0, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, _ = single.Do(callM)
|
single.Do(call)
|
||||||
single.Reset()
|
single.Reset()
|
||||||
_, _, _ = single.Do(callM)
|
single.Do(call)
|
||||||
|
|
||||||
assert.Equal(t, 2, foo)
|
assert.Equal(t, 2, foo)
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ func NewDecoder(option Option) *Decoder {
|
|||||||
// Decode transform a map[string]any to a struct
|
// Decode transform a map[string]any to a struct
|
||||||
func (d *Decoder) Decode(src map[string]any, dst any) error {
|
func (d *Decoder) Decode(src map[string]any, dst any) error {
|
||||||
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
||||||
return fmt.Errorf("decode must recive a ptr struct")
|
return fmt.Errorf("Decode must recive a ptr struct")
|
||||||
}
|
}
|
||||||
t := reflect.TypeOf(dst).Elem()
|
t := reflect.TypeOf(dst).Elem()
|
||||||
v := reflect.ValueOf(dst).Elem()
|
v := reflect.ValueOf(dst).Elem()
|
||||||
@ -301,7 +301,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
field reflect.StructField
|
field reflect.StructField
|
||||||
val reflect.Value
|
val reflect.Value
|
||||||
}
|
}
|
||||||
var fields []field
|
fields := []field{}
|
||||||
for len(structs) > 0 {
|
for len(structs) > 0 {
|
||||||
structVal := structs[0]
|
structVal := structs[0]
|
||||||
structs = structs[1:]
|
structs = structs[1:]
|
||||||
|
@ -137,3 +137,45 @@ func TestStructure_Nest(t *testing.T) {
|
|||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, s.BazOptional, goal)
|
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,44 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/exp/constraints"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Range[T constraints.Ordered] struct {
|
|
||||||
start T
|
|
||||||
end T
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRange[T constraints.Ordered](start, end T) *Range[T] {
|
|
||||||
if start > end {
|
|
||||||
return &Range[T]{
|
|
||||||
start: end,
|
|
||||||
end: start,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Range[T]{
|
|
||||||
start: start,
|
|
||||||
end: end,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Range[T]) Contains(t T) bool {
|
|
||||||
return t >= r.start && t <= r.end
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Range[T]) LeftContains(t T) bool {
|
|
||||||
return t >= r.start && t < r.end
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Range[T]) RightContains(t T) bool {
|
|
||||||
return t > r.start && t <= r.end
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Range[T]) Start() T {
|
|
||||||
return r.start
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Range[T]) End() T {
|
|
||||||
return r.end
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofrs/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 uuid.NewV5(uuidNamespace, str), nil
|
|
||||||
}
|
|
||||||
return u, nil
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofrs/uuid"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUUIDMap(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
str string
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want uuid.UUID
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "uuid-test-1",
|
|
||||||
args: args{
|
|
||||||
str: "82410302-039e-41b6-98b0-d964084b4170",
|
|
||||||
},
|
|
||||||
want: uuid.FromStringOrNil("82410302-039e-41b6-98b0-d964084b4170"),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "uuid-test-2",
|
|
||||||
args: args{
|
|
||||||
str: "88c502e6-d7eb-4c8e-8259-94cb13d83c77",
|
|
||||||
},
|
|
||||||
want: uuid.FromStringOrNil("88c502e6-d7eb-4c8e-8259-94cb13d83c77"),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "uuid-map-1",
|
|
||||||
args: args{
|
|
||||||
str: "123456",
|
|
||||||
},
|
|
||||||
want: uuid.FromStringOrNil("f8598425-92f2-5508-a071-4fc67f9040ac"),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
// GENERATED BY 'xray uuid -i'
|
|
||||||
{
|
|
||||||
name: "uuid-map-2",
|
|
||||||
args: args{
|
|
||||||
str: "a9dk23bz0",
|
|
||||||
},
|
|
||||||
want: uuid.FromStringOrNil("c91481b6-fc0f-5d9e-b166-5ddf07b9c3c5"),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "uuid-map-2",
|
|
||||||
args: args{
|
|
||||||
str: "中文123",
|
|
||||||
},
|
|
||||||
want: uuid.FromStringOrNil("145c544c-2229-59e5-8dbb-3f33b7610d26"),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := UUIDMap(tt.args.str)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("UUIDMap() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("UUIDMap() got = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,9 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/nnip"
|
|
||||||
"github.com/Dreamacro/clash/component/iface"
|
"github.com/Dreamacro/clash/component/iface"
|
||||||
|
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
@ -17,16 +15,14 @@ var (
|
|||||||
ErrNotFound = errors.New("DNS option not found")
|
ErrNotFound = errors.New("DNS option not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]netip.Addr, error) {
|
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, error) {
|
||||||
conn, err := ListenDHCPClient(context, ifaceName)
|
conn, err := ListenDHCPClient(context, ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer conn.Close()
|
||||||
_ = conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
result := make(chan []netip.Addr, 1)
|
result := make(chan []net.IP, 1)
|
||||||
|
|
||||||
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -56,7 +52,7 @@ func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]netip.Addr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []netip.Addr) {
|
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []net.IP) {
|
||||||
defer close(result)
|
defer close(result)
|
||||||
|
|
||||||
buf := make([]byte, dhcpv4.MaxMessageSize)
|
buf := make([]byte, dhcpv4.MaxMessageSize)
|
||||||
@ -81,17 +77,11 @@ func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []
|
|||||||
}
|
}
|
||||||
|
|
||||||
dns := pkt.DNS()
|
dns := pkt.DNS()
|
||||||
l := len(dns)
|
if len(dns) == 0 {
|
||||||
if l == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsAddr := make([]netip.Addr, l)
|
result <- dns
|
||||||
for i := 0; i < l; i++ {
|
|
||||||
dnsAddr[i] = nnip.IpToAddr(dns[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
result <- dnsAddr
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package dialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/iface"
|
"github.com/Dreamacro/clash/component/iface"
|
||||||
@ -20,9 +19,12 @@ func bindControl(ifaceIdx int, chain controlFn) controlFn {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
addrPort, err := netip.ParseAddrPort(address)
|
ipStr, _, err := net.SplitHostPort(address)
|
||||||
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
|
if err == nil {
|
||||||
return
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip != nil && !ip.IsGlobalUnicast() {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var innerErr error
|
var innerErr error
|
||||||
@ -43,7 +45,7 @@ func bindControl(ifaceIdx int, chain controlFn) controlFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
|
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
|
||||||
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -2,7 +2,6 @@ package dialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
@ -18,9 +17,12 @@ func bindControl(ifaceName string, chain controlFn) controlFn {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
addrPort, err := netip.ParseAddrPort(address)
|
ipStr, _, err := net.SplitHostPort(address)
|
||||||
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
|
if err == nil {
|
||||||
return
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip != nil && !ip.IsGlobalUnicast() {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var innerErr error
|
var innerErr error
|
||||||
@ -36,7 +38,7 @@ func bindControl(ifaceName string, chain controlFn) controlFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
|
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
|
||||||
dialer.Control = bindControl(ifaceName, dialer.Control)
|
dialer.Control = bindControl(ifaceName, dialer.Control)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -4,28 +4,27 @@ package dialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/iface"
|
"github.com/Dreamacro/clash/component/iface"
|
||||||
)
|
)
|
||||||
|
|
||||||
func lookupLocalAddr(ifaceName string, network string, destination netip.Addr, port int) (net.Addr, error) {
|
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) {
|
||||||
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var addr *netip.Prefix
|
var addr *net.IPNet
|
||||||
switch network {
|
switch network {
|
||||||
case "udp4", "tcp4":
|
case "udp4", "tcp4":
|
||||||
addr, err = ifaceObj.PickIPv4Addr(destination)
|
addr, err = ifaceObj.PickIPv4Addr(destination)
|
||||||
case "tcp6", "udp6":
|
case "tcp6", "udp6":
|
||||||
addr, err = ifaceObj.PickIPv6Addr(destination)
|
addr, err = ifaceObj.PickIPv6Addr(destination)
|
||||||
default:
|
default:
|
||||||
if destination.IsValid() {
|
if destination != nil {
|
||||||
if destination.Is4() {
|
if destination.To4() != nil {
|
||||||
addr, err = ifaceObj.PickIPv4Addr(destination)
|
addr, err = ifaceObj.PickIPv4Addr(destination)
|
||||||
} else {
|
} else {
|
||||||
addr, err = ifaceObj.PickIPv6Addr(destination)
|
addr, err = ifaceObj.PickIPv6Addr(destination)
|
||||||
@ -40,12 +39,12 @@ func lookupLocalAddr(ifaceName string, network string, destination netip.Addr, p
|
|||||||
|
|
||||||
if strings.HasPrefix(network, "tcp") {
|
if strings.HasPrefix(network, "tcp") {
|
||||||
return &net.TCPAddr{
|
return &net.TCPAddr{
|
||||||
IP: addr.Addr().AsSlice(),
|
IP: addr.IP,
|
||||||
Port: port,
|
Port: port,
|
||||||
}, nil
|
}, nil
|
||||||
} else if strings.HasPrefix(network, "udp") {
|
} else if strings.HasPrefix(network, "udp") {
|
||||||
return &net.UDPAddr{
|
return &net.UDPAddr{
|
||||||
IP: addr.Addr().AsSlice(),
|
IP: addr.IP,
|
||||||
Port: port,
|
Port: port,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -53,7 +52,7 @@ func lookupLocalAddr(ifaceName string, network string, destination netip.Addr, p
|
|||||||
return nil, iface.ErrAddrNotFound
|
return nil, iface.ErrAddrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
|
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
|
||||||
if !destination.IsGlobalUnicast() {
|
if !destination.IsGlobalUnicast() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -84,7 +83,7 @@ func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, add
|
|||||||
|
|
||||||
local, _ := strconv.ParseUint(port, 10, 16)
|
local, _ := strconv.ParseUint(port, 10, 16)
|
||||||
|
|
||||||
addr, err := lookupLocalAddr(ifaceName, network, netip.Addr{}, int(local))
|
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -3,56 +3,35 @@ package dialer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
|
||||||
"go.uber.org/atomic"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
dialMux sync.Mutex
|
|
||||||
actualSingleDialContext = singleDialContext
|
|
||||||
actualDualStackDialContext = dualStackDialContext
|
|
||||||
tcpConcurrent = false
|
|
||||||
DisableIPv6 = false
|
|
||||||
ErrorInvalidedNetworkStack = errors.New("invalided network stack")
|
|
||||||
ErrorDisableIPv6 = errors.New("IPv6 is disabled, dialer cancel")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
|
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
|
||||||
opt := &option{
|
|
||||||
interfaceName: DefaultInterface.Load(),
|
|
||||||
routingMark: int(DefaultRoutingMark.Load()),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, o := range DefaultOptions {
|
|
||||||
o(opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, o := range options {
|
|
||||||
o(opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch network {
|
switch network {
|
||||||
case "tcp4", "tcp6", "udp4", "udp6":
|
case "tcp4", "tcp6", "udp4", "udp6":
|
||||||
return actualSingleDialContext(ctx, network, address, opt)
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ip net.IP
|
||||||
|
switch network {
|
||||||
|
case "tcp4", "udp4":
|
||||||
|
ip, err = resolver.ResolveIPv4(host)
|
||||||
|
default:
|
||||||
|
ip, err = resolver.ResolveIPv6(host)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialContext(ctx, network, ip, port, options)
|
||||||
case "tcp", "udp":
|
case "tcp", "udp":
|
||||||
return actualDualStackDialContext(ctx, network, address, opt)
|
return dualStackDialContext(ctx, network, address, options)
|
||||||
default:
|
default:
|
||||||
return nil, ErrorInvalidedNetworkStack
|
return nil, errors.New("network invalid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,25 +67,20 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
|
|||||||
return lc.ListenPacket(ctx, network, address)
|
return lc.ListenPacket(ctx, network, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetDial(concurrent bool) {
|
func dialContext(ctx context.Context, network string, destination net.IP, port string, options []Option) (net.Conn, error) {
|
||||||
dialMux.Lock()
|
opt := &option{
|
||||||
tcpConcurrent = concurrent
|
interfaceName: DefaultInterface.Load(),
|
||||||
if concurrent {
|
routingMark: int(DefaultRoutingMark.Load()),
|
||||||
actualSingleDialContext = concurrentSingleDialContext
|
|
||||||
actualDualStackDialContext = concurrentDualStackDialContext
|
|
||||||
} else {
|
|
||||||
actualSingleDialContext = singleDialContext
|
|
||||||
actualDualStackDialContext = dualStackDialContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialMux.Unlock()
|
for _, o := range DefaultOptions {
|
||||||
}
|
o(opt)
|
||||||
|
}
|
||||||
|
|
||||||
func GetDial() bool {
|
for _, o := range options {
|
||||||
return tcpConcurrent
|
o(opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) {
|
|
||||||
dialer := &net.Dialer{}
|
dialer := &net.Dialer{}
|
||||||
if opt.interfaceName != "" {
|
if opt.interfaceName != "" {
|
||||||
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
||||||
@ -117,14 +91,10 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
|
|||||||
bindMarkToDialer(opt.routingMark, dialer, network, destination)
|
bindMarkToDialer(opt.routingMark, dialer, network, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
if DisableIPv6 && destination.Is6() {
|
|
||||||
return nil, ErrorDisableIPv6
|
|
||||||
}
|
|
||||||
|
|
||||||
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
|
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
|
||||||
}
|
}
|
||||||
|
|
||||||
func dualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
|
func dualStackDialContext(ctx context.Context, network, address string, options []Option) (net.Conn, error) {
|
||||||
host, port, err := net.SplitHostPort(address)
|
host, port, err := net.SplitHostPort(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -143,270 +113,56 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
|
|||||||
results := make(chan dialResult)
|
results := make(chan dialResult)
|
||||||
var primary, fallback dialResult
|
var primary, fallback dialResult
|
||||||
|
|
||||||
startRacer := func(ctx context.Context, network, host string, direct bool, ipv6 bool) {
|
startRacer := func(ctx context.Context, network, host string, ipv6 bool) {
|
||||||
result := dialResult{ipv6: ipv6, done: true}
|
result := dialResult{ipv6: ipv6, done: true}
|
||||||
defer func() {
|
defer func() {
|
||||||
select {
|
select {
|
||||||
case results <- result:
|
case results <- result:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
if result.Conn != nil {
|
if result.Conn != nil {
|
||||||
_ = result.Conn.Close()
|
result.Conn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var ip netip.Addr
|
var ip net.IP
|
||||||
if ipv6 {
|
if ipv6 {
|
||||||
if !direct {
|
ip, result.error = resolver.ResolveIPv6(host)
|
||||||
ip, result.error = resolver.ResolveIPv6ProxyServerHost(host)
|
|
||||||
} else {
|
|
||||||
ip, result.error = resolver.ResolveIPv6(host)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if !direct {
|
ip, result.error = resolver.ResolveIPv4(host)
|
||||||
ip, result.error = resolver.ResolveIPv4ProxyServerHost(host)
|
|
||||||
} else {
|
|
||||||
ip, result.error = resolver.ResolveIPv4(host)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if result.error != nil {
|
if result.error != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.resolved = true
|
result.resolved = true
|
||||||
|
|
||||||
result.Conn, result.error = dialContext(ctx, network, ip, port, opt)
|
result.Conn, result.error = dialContext(ctx, network, ip, port, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
go startRacer(ctx, network+"4", host, opt.direct, false)
|
go startRacer(ctx, network+"4", host, false)
|
||||||
go startRacer(ctx, network+"6", host, opt.direct, true)
|
go startRacer(ctx, network+"6", host, true)
|
||||||
|
|
||||||
count := 2
|
for res := range results {
|
||||||
for i := 0; i < count; i++ {
|
if res.error == nil {
|
||||||
select {
|
return res.Conn, nil
|
||||||
case res := <-results:
|
}
|
||||||
if res.error == nil {
|
|
||||||
return res.Conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !res.ipv6 {
|
if !res.ipv6 {
|
||||||
primary = res
|
primary = res
|
||||||
|
} else {
|
||||||
|
fallback = res
|
||||||
|
}
|
||||||
|
|
||||||
|
if primary.done && fallback.done {
|
||||||
|
if primary.resolved {
|
||||||
|
return nil, primary.error
|
||||||
|
} else if fallback.resolved {
|
||||||
|
return nil, fallback.error
|
||||||
} else {
|
} else {
|
||||||
fallback = res
|
return nil, primary.error
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("dual stack tcp shake hands failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func concurrentDualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
|
|
||||||
host, port, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ips []netip.Addr
|
|
||||||
if opt.direct {
|
|
||||||
ips, err = resolver.ResolveAllIP(host)
|
|
||||||
} else {
|
|
||||||
ips, err = resolver.ResolveAllIPProxyServerHost(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
isPrimary bool
|
|
||||||
done bool
|
|
||||||
}
|
|
||||||
|
|
||||||
preferCount := atomic.NewInt32(0)
|
|
||||||
results := make(chan dialResult)
|
|
||||||
tcpRacer := func(ctx context.Context, ip netip.Addr) {
|
|
||||||
result := dialResult{ip: ip, done: true}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
select {
|
|
||||||
case results <- result:
|
|
||||||
case <-returned:
|
|
||||||
if result.Conn != nil {
|
|
||||||
_ = result.Conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if strings.Contains(network, "tcp") {
|
|
||||||
network = "tcp"
|
|
||||||
} else {
|
|
||||||
network = "udp"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip.Is6() {
|
|
||||||
network += "6"
|
|
||||||
if opt.prefer != 4 {
|
|
||||||
result.isPrimary = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ip.Is4() {
|
|
||||||
network += "4"
|
|
||||||
if opt.prefer != 6 {
|
|
||||||
result.isPrimary = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.isPrimary {
|
|
||||||
preferCount.Add(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Conn, result.error = dialContext(ctx, network, ip, port, opt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ip := range ips {
|
return nil, errors.New("never touched")
|
||||||
go tcpRacer(ctx, ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
connCount := len(ips)
|
|
||||||
var fallback dialResult
|
|
||||||
var primaryError error
|
|
||||||
for i := 0; i < connCount; i++ {
|
|
||||||
select {
|
|
||||||
case res := <-results:
|
|
||||||
if res.error == nil {
|
|
||||||
if res.isPrimary {
|
|
||||||
return res.Conn, nil
|
|
||||||
} else {
|
|
||||||
if !fallback.done || fallback.error != nil {
|
|
||||||
fallback = res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if res.isPrimary {
|
|
||||||
primaryError = res.error
|
|
||||||
preferCount.Add(-1)
|
|
||||||
if preferCount.Load() == 0 && fallback.done && fallback.error == nil {
|
|
||||||
return fallback.Conn, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
if fallback.done && fallback.error == nil {
|
|
||||||
return fallback.Conn, nil
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fallback.done && fallback.error == nil {
|
|
||||||
return fallback.Conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if primaryError != nil {
|
|
||||||
return nil, primaryError
|
|
||||||
}
|
|
||||||
|
|
||||||
if fallback.error != nil {
|
|
||||||
return nil, fallback.error
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
switch network {
|
|
||||||
case "tcp4", "udp4":
|
|
||||||
return concurrentIPv4DialContext(ctx, network, address, opt)
|
|
||||||
default:
|
|
||||||
return concurrentIPv6DialContext(ctx, network, address, opt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func concurrentIPv4DialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
|
|
||||||
host, port, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ips []netip.Addr
|
|
||||||
if !opt.direct {
|
|
||||||
ips, err = resolver.ResolveAllIPv4ProxyServerHost(host)
|
|
||||||
} else {
|
|
||||||
ips, err = resolver.ResolveAllIPv4(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return concurrentDialContext(ctx, network, ips, port, opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func concurrentIPv6DialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
|
|
||||||
host, port, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ips []netip.Addr
|
|
||||||
if !opt.direct {
|
|
||||||
ips, err = resolver.ResolveAllIPv6ProxyServerHost(host)
|
|
||||||
} else {
|
|
||||||
ips, err = resolver.ResolveAllIPv6(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return concurrentDialContext(ctx, network, ips, port, opt)
|
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,14 @@ package dialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) {
|
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
|
||||||
dialer.Control = bindMarkToControl(mark, dialer.Control)
|
dialer.Control = bindMarkToControl(mark, dialer.Control)
|
||||||
}
|
}
|
||||||
|
|
||||||
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
|
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
|
||||||
lc.Control = bindMarkToControl(mark, lc.Control)
|
lc.Control = bindMarkToControl(mark, lc.Control)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,17 +23,20 @@ func bindMarkToControl(mark int, chain controlFn) controlFn {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
addrPort, err := netip.ParseAddrPort(address)
|
ipStr, _, err := net.SplitHostPort(address)
|
||||||
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
|
if err == nil {
|
||||||
return
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip != nil && !ip.IsGlobalUnicast() {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Control(func(fd uintptr) {
|
return c.Control(func(fd uintptr) {
|
||||||
switch network {
|
switch network {
|
||||||
case "tcp4", "udp4":
|
case "tcp4", "udp4":
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
||||||
case "tcp6", "udp6":
|
case "tcp6", "udp6":
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ package dialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
@ -18,10 +17,10 @@ func printMarkWarn() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) {
|
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
|
||||||
printMarkWarn()
|
printMarkWarn()
|
||||||
}
|
}
|
||||||
|
|
||||||
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
|
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
|
||||||
printMarkWarn()
|
printMarkWarn()
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package dialer
|
package dialer
|
||||||
|
|
||||||
import (
|
import "go.uber.org/atomic"
|
||||||
"go.uber.org/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
DefaultOptions []Option
|
DefaultOptions []Option
|
||||||
@ -14,9 +12,6 @@ type option struct {
|
|||||||
interfaceName string
|
interfaceName string
|
||||||
addrReuse bool
|
addrReuse bool
|
||||||
routingMark int
|
routingMark int
|
||||||
direct bool
|
|
||||||
network int
|
|
||||||
prefer int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(opt *option)
|
type Option func(opt *option)
|
||||||
@ -38,31 +33,3 @@ func WithRoutingMark(mark int) Option {
|
|||||||
opt.routingMark = mark
|
opt.routingMark = mark
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithDirect() Option {
|
|
||||||
return func(opt *option) {
|
|
||||||
opt.direct = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// We must use this DialContext to query DNS
|
|
||||||
// when using net default resolver.
|
|
||||||
net.DefaultResolver.PreferGo = true
|
|
||||||
net.DefaultResolver.Dial = resolverDialContext
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolverDialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
d := &net.Dialer{}
|
|
||||||
|
|
||||||
interfaceName := DefaultInterface.Load()
|
|
||||||
|
|
||||||
if interfaceName != "" {
|
|
||||||
dstIP, err := netip.ParseAddr(address)
|
|
||||||
if err == nil {
|
|
||||||
_ = bindIfaceToDialer(interfaceName, d, network, dstIP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.DialContext(ctx, network, address)
|
|
||||||
}
|
|
@ -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