Compare commits

..

77 Commits
test ... plus

Author SHA1 Message Date
160e630f03 Feature: add process_path to script metadata 2022-05-09 06:13:26 +08:00
13d19ff101 Chore: code style 2022-05-09 03:17:09 +08:00
934babca85 Chore: merge branch 'with-tun' into plus-pro 2022-05-09 01:56:32 +08:00
95db646b3b Chore: code style 2022-05-09 01:22:43 +08:00
f23d1d5d7c Chore: make sure script module always initialized 2022-05-08 06:17:09 +08:00
4334b45e82 Chore: merge branch 'with-tun' into plus-pro 2022-05-08 04:12:20 +08:00
ad1e09db55 Chore: update dependencies 2022-05-08 04:08:16 +08:00
2eb7f3ad2f Chore: merge branch 'ogn-dev' into with-tun 2022-05-08 03:12:50 +08:00
5dd94c8298 Chore: update dependencies 2022-05-07 21:08:15 +08:00
412b44a981 Fix: decode nil value in slice decoder (#2102) 2022-05-07 11:00:58 +08:00
9ffcc9e352 Chore: merge branch 'with-tun' into plus-pro 2022-05-07 04:17:03 +08:00
fe69ec7d6c Fix: patch tun configs 2022-05-07 04:14:09 +08:00
045b67524c Chore: delay reject 2022-05-04 19:49:04 +08:00
392572d684 Chore: merge branch 'with-tun' into plus-pro 2022-05-01 21:09:15 +08:00
3c07ba6b56 Chore: use absolute path to execute commands on darwin 2022-05-01 21:01:19 +08:00
8c84c8b193 Feature: patch update support tun config 2022-05-01 17:08:17 +08:00
0321ddbb90 Chore: add build windows actions 2022-04-30 16:07:17 +08:00
d74dd69329 Chore: merge branch 'with-tun' into plus-pro 2022-04-29 22:24:18 +08:00
7e85d5a954 Fix: tls handshake with timeout 2022-04-29 05:15:32 +08:00
da92601902 Fix: mitm proxy should handle none-http(s) protocol over tcp 2022-04-28 06:46:57 +08:00
22458ad0be Chore: mitm proxy with authenticate 2022-04-28 00:46:47 +08:00
c8bc4386dd Fix: actions build 2022-04-27 06:14:49 +08:00
1e7cbd6358 Chore: merge branch 'with-tun' into plus-pro 2022-04-27 05:49:45 +08:00
30025c0241 Fix: mitm proxy should forward websocket 2022-04-27 05:35:31 +08:00
7c50c068f5 Fix: if http proxy Upgrade failure 2022-04-27 05:35:31 +08:00
ca4961a146 Chore: merge branch 'ong-dev' into with-tun 2022-04-27 05:33:49 +08:00
aef4dd3fe7 Fix: make log api unblocked 2022-04-26 22:36:10 +08:00
85f14f1c63 Chore: merge branch 'ogn-dev' into tun-dev 2022-04-26 18:46:42 +08:00
6a92c6af4e Fix: http proxy Upgrade behavior (#2097) 2022-04-25 19:50:20 +08:00
7115f7e61b Fix: wildcard certificates 2022-04-25 10:54:12 +08:00
62bc75af8a Chore: signature wildcard certificates 2022-04-25 05:02:24 +08:00
d763900b14 Chore: update dependencies 2022-04-24 02:23:05 +08:00
6acba9ab8f Chore: increase nattable capacity 2022-04-24 02:19:23 +08:00
ca9f3bf8a9 Chore: use generics as possible 2022-04-24 02:07:57 +08:00
c812363090 Chore: wait for system stack to close 2022-04-22 05:37:44 +08:00
0a2701eef0 Fix: actions build 2022-04-21 04:51:01 +08:00
0d004bf6f3 Chore: merge branch 'with-tun' into plus-pro 2022-04-21 04:33:53 +08:00
450c608c83 Chore: fix typos 2022-04-21 03:54:34 +08:00
053366c3e1 Chore: merge branch 'with-tun' into plus-pro 2022-04-20 02:40:44 +08:00
567fe74f10 Chore: update dependencies 2022-04-20 01:59:57 +08:00
cd62daccb0 Refactor: metadata use netip.Addr 2022-04-20 01:52:51 +08:00
29c775331a Chore: IpToAddr 2022-04-19 17:46:13 +08:00
33d23dad6c Chore: remove TODO 2022-04-19 17:05:12 +08:00
42cf42fd8b Chore: merge branch 'ogn-dev' into tun-dev 2022-04-18 17:21:00 +08:00
e010940b61 Improve: replace bootstrap dns (#2080) 2022-04-16 15:31:26 +08:00
46f7c5e565 Fix: only rule mode need break conn when sni update 2022-04-15 01:00:08 +08:00
6327cf7434 Chore: adjust mitm proxy 2022-04-15 00:29:21 +08:00
2c9a4d276a Chore: add more github action cache 2022-04-14 23:37:41 +08:00
4dfba73e5c Fix: SyscallN should not use nargs 2022-04-14 23:37:19 +08:00
c282d662ca Fix: make golangci lint support multi GOOS 2022-04-13 17:51:21 +08:00
ca76e5cf0e Chore: fix typo 2022-04-13 16:47:47 +08:00
b3d7594813 Chore: add none alias to dummy on ShadowsocksR (#2056) 2022-04-13 10:06:06 +08:00
9d72bf2a36 Chore: merge branch 'with-tun' into plus-pro 2022-04-13 06:01:22 +08:00
a3a50f9c7b Chore: persistence fakeip pool state 2022-04-13 05:55:08 +08:00
abc8ed4df0 Chore: hijack traffic destined for port 80 to mitm proxy server by default 2022-04-13 05:51:24 +08:00
643f1ae970 Chore: update dependencies 2022-04-12 22:35:21 +08:00
21a56ea36b Chore: adjust ipstack 2022-04-12 22:33:10 +08:00
a98749eb16 Fix: fakeip pool cycle used 2022-04-12 21:54:54 +08:00
571c34f140 Chore: merge branch 'with-tun' into plus-pro 2022-04-12 00:47:45 +08:00
008ee613ab Refactor: fakeip pool use netip.Prefix, supports ipv6 range 2022-04-12 00:31:04 +08:00
5999b6262d Chore: fix typos 2022-04-11 06:28:42 +08:00
05b4a326de Chore: merge branch 'with-tun' into plus-pro 2022-04-10 05:57:06 +08:00
f036e06f6f Feature: MITM rewrite 2022-04-10 03:59:27 +08:00
5a27ebd1b3 Refactor: DomainTrie use generics 2022-04-10 00:33:33 +08:00
a8646082a3 Refactor: queue use generics 2022-04-10 00:33:33 +08:00
400be9a905 Refactor: cache use generics 2022-04-10 00:33:33 +08:00
0582c608b3 Refactor: lrucache use generics 2022-04-10 00:33:33 +08:00
92d9d03f99 Chore: move sniffing logic into a single file & code style 2022-04-10 00:05:59 +08:00
b6653dd9b5 fix: trojan fail may panic 2022-04-09 23:17:25 +08:00
d77ef6a525 Chore: merge branch 'with-tun' into plus-pro 2022-03-30 00:18:42 +08:00
4d9d8b28ec Chore: merge branch 'with-tun' into plus-pro 2022-03-29 07:36:14 +08:00
7973491625 Chore: merge branch 'with-tun' into plus-pro 2022-03-28 03:34:49 +08:00
edbc8ed972 Chore: merge branch 'with-tun' into plus-pro 2022-03-27 15:16:57 +08:00
bd123dddc6 Chore: merge branch 'with-tun' into plus-pro 2022-03-25 04:38:04 +08:00
ae493f1084 Chore: merge branch 'with-tun' into plus-pro 2022-03-23 16:54:56 +08:00
711b2bcf87 Chore: merge branch 'with-tun' into plus-pro 2022-03-22 05:56:58 +08:00
a45354fa08 Code: refresh code & rebase branch 'with-tun' 2022-03-21 09:03:47 +08:00
253 changed files with 5921 additions and 8203 deletions

76
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View 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
View 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!

View 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
"

75
.github/workflows/build-windows-amd.yml vendored Normal file
View File

@ -0,0 +1,75 @@
name: Build-Windows
on: [push]
jobs:
build:
runs-on: windows-latest
steps:
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: 1.18.x
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Go cache paths
id: go-cache-paths
run: |
echo "::set-output name=go-build::$(go env GOCACHE)"
echo "::set-output name=go-mod::$(go env GOMODCACHE)"
- name: Cache go module
uses: actions/cache@v2
with:
path: |
${{ steps.go-cache-paths.outputs.go-mod }}
${{ steps.go-cache-paths.outputs.go-build }}
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: '3.9'
architecture: 'x64'
- name: Get dependencies, run test
id: test
run: |
cmd /c mklink /J D:\python-amd64 $env:pythonLocation
echo "::set-output name=file_sha::$(git describe --tags --always)"
echo "::set-output name=file_date::$(Get-Date -Format 'yyyyMMdd')"
((Get-Content -path constant/version.go -Raw) -replace 'unknown version',$(git describe --tags --always)) | Set-Content -Path constant/version.go
((Get-Content -path constant/version.go -Raw) -replace 'unknown time',$(Get-Date)) | Set-Content -Path constant/version.go
# go test
go test -tags build_actions ./...
- name: Build
#if: startsWith(github.ref, 'refs/tags/')
run: |
$env:CGO_ENABLED=1; go build -tags build_actions -trimpath -ldflags '-w -s -buildid=' -o bin/clash-windows-amd64.exe
$env:GOAMD64="v3"; $env:CGO_ENABLED=1; go build -tags build_actions -trimpath -ldflags '-w -s -buildid=' -o bin/clash-windows-amd64-v3.exe
cd bin/
Compress-Archive -Path clash-windows-amd64.exe -DestinationPath clash-plus-windows-amd64-$(git describe --tags --always)-$(Get-Date -Format 'yyyy.MM.dd').zip
Compress-Archive -Path clash-windows-amd64-v3.exe -DestinationPath clash-plus-windows-amd64-v3-$(git describe --tags --always)-$(Get-Date -Format 'yyyy.MM.dd').zip
Remove-Item -Force clash-windows-amd64.exe
Remove-Item -Force clash-windows-amd64-v3.exe
- name: Upload files to Artifacts
uses: actions/upload-artifact@v2
if: startsWith(github.ref, 'refs/tags/') == false
with:
name: clash-windows-amd64-${{ steps.test.outputs.file_sha }}-${{ steps.test.outputs.file_date }}
path: |
bin/*
- name: Delete workflow runs
uses: GitRML/delete-workflow-runs@main
with:
retain_days: 1
keep_minimum_runs: 2

View File

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

29
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: CodeQL
on:
push:
branches: [ rm ]
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

View File

@ -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
View File

@ -0,0 +1,80 @@
name: Publish Docker Image
on:
push:
branches:
- rm
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

23
.github/workflows/linter.yml vendored Normal file
View File

@ -0,0 +1,23 @@
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
args: --build-tags=build_local

View File

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

View File

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

View File

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

120
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,120 @@
name: Release
on:
push:
branches:
- rm
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: Go cache paths
id: go-cache-paths
run: |
echo "::set-output name=go-build::$(go env GOCACHE)"
echo "::set-output name=go-mod::$(go env GOMODCACHE)"
- name: Cache go module
uses: actions/cache@v2
with:
path: |
${{ steps.go-cache-paths.outputs.go-mod }}
${{ steps.go-cache-paths.outputs.go-build }}
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Get dependencies, run test
run: |
# fetch python cross compile source files
mkdir -p bin/python/
cd bin/python/
curl -LO https://raw.githubusercontent.com/yaling888/snack/main/python-3.9.7-darwin-amd64.tar.xz
curl -LO https://raw.githubusercontent.com/yaling888/snack/main/python-3.9.7-darwin-arm64.tar.xz
curl -LO https://raw.githubusercontent.com/yaling888/snack/main/python-3.9.7-windows-amd64.tar.xz
curl -LO https://raw.githubusercontent.com/yaling888/snack/main/python-3.9.7-windows-386.tar.xz
#curl -LO https://raw.githubusercontent.com/yaling888/snack/main/python-3.9.7-linux-amd64.tar.xz
#curl -LO https://raw.githubusercontent.com/yaling888/snack/main/python-3.9.7-linux-arm64.tar.xz
#curl -LO https://raw.githubusercontent.com/yaling888/snack/main/python-3.9.7-linux-386.tar.xz
tar -Jxf python-3.9.7-darwin-amd64.tar.xz
tar -Jxf python-3.9.7-darwin-arm64.tar.xz
tar -Jxf python-3.9.7-windows-amd64.tar.xz
tar -Jxf python-3.9.7-windows-386.tar.xz
#tar -Jxf python-3.9.7-linux-amd64.tar.xz
#tar -Jxf python-3.9.7-linux-arm64.tar.xz
#tar -Jxf python-3.9.7-linux-386.tar.xz
rm python-3.9.7-*.tar.xz
cd ../../
# go test
go test -tags build_local ./...
# init xgo
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest
- name: Build
#if: startsWith(github.ref, 'refs/tags/')
env:
NAME: clash
BINDIR: bin
run: |
make -j releases
#ls -lahF bin/python/
- name: Prepare upload
if: startsWith(github.ref, 'refs/tags/') == false
run: |
rm -rf bin/python/
echo "FILE_DATE=_$(date +"%Y%m%d%H%M")" >> $GITHUB_ENV
echo "FILE_SHA=$(git describe --tags --always 2>/dev/null)" >> $GITHUB_ENV
- name: Upload files to Artifacts
uses: actions/upload-artifact@v2
if: startsWith(github.ref, 'refs/tags/') == false
with:
name: clash_${{ env.FILE_SHA }}${{ env.FILE_DATE }}
path: |
bin/*
- name: Upload Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: bin/*
draft: true
prerelease: true
generate_release_notes: true
- name: Delete workflow runs
uses: GitRML/delete-workflow-runs@main
with:
retain_days: 1
keep_minimum_runs: 2
- name: Remove old Releases
uses: dev-drprasad/delete-older-releases@v0.2.0
if: startsWith(github.ref, 'refs/tags/') && !cancelled()
with:
keep_latest: 1
delete_tags: true
delete_tag_pattern: plus-pro
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

19
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Mark stale issues and pull requests
on:
push:
branches:
- rm
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
View File

@ -23,5 +23,3 @@ vendor
# test suite
test/config/cache*
/output
/.vscode

View File

@ -1,26 +1,18 @@
FROM golang:alpine as builder
RUN apk add --no-cache make git && \
mkdir /clash-config && \
wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \
wget -O /clash-config/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \
wget -O /clash-config/geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
COPY . /clash-src
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
WORKDIR /clash-src
RUN go mod download &&\
make docker &&\
mv ./bin/Clash.Meta-docker /clash
COPY --from=tonistiigi/xx:golang / /
COPY . /clash-src
RUN go mod download && \
make docker && \
mv ./bin/clash-docker /clash
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
VOLUME ["/root/.config/clash/"]
COPY --from=builder /clash-config/ /root/.config/clash/
COPY --from=builder /clash /clash
RUN chmod +x /clash
ENTRYPOINT [ "/clash" ]
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash /
ENTRYPOINT ["/clash"]

177
Makefile
View File

@ -1,142 +1,70 @@
NAME=Clash.Meta
BINDIR=bin
BRANCH=$(shell git branch --show-current)
ifeq ($(BRANCH),Alpha)
VERSION=alpha-$(shell git rev-parse --short HEAD)
else ifeq ($(BRANCH),Beta)
VERSION=beta-$(shell git rev-parse --short HEAD)
else ifeq ($(BRANCH),)
VERSION=$(shell git describe --tags)
else
VERSION=unknown
endif
GOCMD=go
XGOCMD=xgo -go=go-1.18.x
GOBUILD=CGO_ENABLED=1 $(GOCMD) build -trimpath
GOCLEAN=$(GOCMD) clean
NAME=clash
BINDIR=$(shell pwd)/bin
VERSION=$(shell git describe --tags --always 2>/dev/null || date +%F)
BUILDTIME=$(shell date -u)
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-w -s -buildid='
BUILD_PACKAGE=.
RELEASE_LDFLAGS='-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-w -s -buildid='
STATIC_LDFLAGS='-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-extldflags "-static" \
-w -s -buildid='
PLATFORM_LIST = \
darwin-amd64v1 \
darwin-amd64v2 \
darwin-amd64v3 \
darwin-amd64 \
darwin-arm64 \
linux-amd64v1 \
linux-amd64v2 \
linux-amd64v3 \
linux-armv5 \
linux-armv6 \
linux-armv7 \
linux-arm64 \
linux-mips64 \
linux-mips64le \
linux-mips-softfloat \
linux-mips-hardfloat \
linux-mipsle-softfloat \
linux-mipsle-hardfloat \
android-arm64 \
freebsd-386 \
freebsd-amd64 \
freebsd-arm64
linux-amd64
# linux-arm64
# linux-386
WINDOWS_ARCH_LIST = \
windows-386 \
windows-amd64v1 \
windows-amd64v2 \
windows-amd64v3 \
windows-arm64 \
windows-arm32v7
windows-amd64 \
windows-386
# windows-arm64
all:linux-amd64v3 linux-arm64\
darwin-amd64v3 darwin-arm64\
windows-amd64v3 windows-arm64\
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
docker:
GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
local:
$(GOBUILD) -ldflags $(RELEASE_LDFLAGS) -tags build_local -o $(BINDIR)/$(NAME)-$@
darwin-amd64v3:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
local-v3:
GOAMD64=v3 $(GOBUILD) -ldflags $(RELEASE_LDFLAGS) -tags build_local -o $(BINDIR)/$(NAME)-$@
darwin-amd64v2:
GOARCH=amd64 GOOS=darwin GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64v1:
GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64:
$(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=darwin-10.12/amd64 $(BUILD_PACKAGE) && \
mv $(BINDIR)/$(NAME)-darwin-10.12-amd64 $(BINDIR)/$(NAME)-darwin-amd64
darwin-arm64:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
$(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=darwin-11.1/arm64 $(BUILD_PACKAGE) && \
mv $(BINDIR)/$(NAME)-darwin-11.1-arm64 $(BINDIR)/$(NAME)-darwin-arm64
linux-386:
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
$(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(STATIC_LDFLAGS) -targets=linux/386 $(BUILD_PACKAGE)
linux-amd64v3:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64v2:
GOARCH=amd64 GOOS=linux GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64v1:
GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64:
$(GOBUILD) -ldflags $(RELEASE_LDFLAGS) -o $(BINDIR)/$(NAME)-$@
#GOARCH=amd64 GOOS=linux $(GOBUILD) -ldflags $(RELEASE_LDFLAGS) -o $(BINDIR)/$(NAME)-$@
#$(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(STATIC_LDFLAGS) -targets=linux/amd64 $(BUILD_PACKAGE)
linux-arm64:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv5:
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv6:
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv7:
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips-softfloat:
GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips-hardfloat:
GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mipsle-softfloat:
GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mipsle-hardfloat:
GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips64:
GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips64le:
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
android-arm64:
GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-386:
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64:
GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-arm64:
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
$(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(STATIC_LDFLAGS) -targets=linux/arm64 $(BUILD_PACKAGE)
windows-386:
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
$(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=windows-6.0/386 $(BUILD_PACKAGE) && \
mv $(BINDIR)/$(NAME)-windows-6.0-386.exe $(BINDIR)/$(NAME)-windows-386.exe
windows-amd64v3:
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64:
$(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=windows-6.0/amd64 $(BUILD_PACKAGE) && \
mv $(BINDIR)/$(NAME)-windows-6.0-amd64.exe $(BINDIR)/$(NAME)-windows-amd64.exe
windows-amd64v2:
GOARCH=amd64 GOOS=windows GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64v1:
GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm64:
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm32v7:
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
#windows-arm64:
# $(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=windows/arm64 $(BUILD_PACKAGE)
# mv $(NAME)-windows-4.0-arm64.exe $(NAME)-windows-arm64.exe
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
@ -153,10 +81,17 @@ all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
releases: $(gz_releases) $(zip_releases)
vet:
go test ./...
$(GOCMD) test -tags build_local ./...
lint:
golangci-lint run ./...
golangci-lint run --build-tags=build_local ./...
clean:
rm $(BINDIR)/*
rm -rf $(BINDIR)/
mkdir -p $(BINDIR)
cleancache:
# go build cache may need to cleanup if changing C source code
$(GOCLEAN) -cache
rm -rf $(BINDIR)/
mkdir -p $(BINDIR)

BIN
Meta.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

410
README.md
View File

@ -1,20 +1,23 @@
<h1 align="center">
<img src="Meta.png" alt="Meta Kennel" width="200">
<br>Meta Kernel<br>
<img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200">
<br>Clash<br>
</h1>
<h3 align="center">Another Clash Kernel.</h3>
<h4 align="center">A rule-based tunnel in Go.</h4>
<p align="center">
<a href="https://goreportcard.com/report/github.com/Clash-Mini/Clash.Meta">
<img src="https://goreportcard.com/badge/github.com/Clash-Mini/Clash.Meta?style=flat-square">
<a href="https://github.com/Dreamacro/clash/actions">
<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>
<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">
<img src="https://img.shields.io/github/release/Clash-Mini/Clash.Meta/all.svg?style=flat-square">
<a href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
</a>
<a href="https://github.com/Clash-Mini/Clash.Meta">
<img src="https://img.shields.io/badge/release-Meta-00b4f0?style=flat-square">
<a href="https://github.com/Dreamacro/clash/releases/tag/premium">
<img src="https://img.shields.io/badge/release-Premium-00b4f0?style=flat-square">
</a>
</p>
@ -33,85 +36,145 @@
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
## Advanced usage for this branch
### Build
This branch requires cgo and Python3.9, so make sure you set up Python3.9 before building.
For example, build on macOS:
```shell
brew update
brew install python@3.9
export PKG_CONFIG_PATH=$(find /usr/local/Cellar -name 'pkgconfig' -type d | grep lib/pkgconfig | tr '\n' ':' | sed s/.$//)
git clone -b plus-pro https://github.com/yaling888/clash.git
cd clash
# build
make cleancache && make local
# or make local-v3
ls bin/
# run
sudo bin/clash-local
```
### MITM configuration
A root CA certificate is required, the
MITM proxy server will generate a CA certificate file and a CA private key file in your Clash home directory, you can use your own certificate replace it.
Need to install and trust the CA certificate on the client device, open this URL [http://mitm.clash/cert.crt](http://mitm.clash/cert.crt) by the web browser to install the CA certificate, the host name 'mitm.clash' was always been hijacked.
NOTE: this feature cannot work on tls pinning
WARNING: DO NOT USE THIS FEATURE TO BREAK LOCAL LAWS
```yaml
# Port of MITM proxy server on the local end
mitm-port: 7894
# Man-In-The-Middle attack
mitm:
hosts: # use for others proxy type. E.g: TUN, socks
- +.example.com
rules: # rewrite rules
- '^https?://www\.example\.com/1 url reject' # The "reject" returns HTTP status code 404 with no content.
- '^https?://www\.example\.com/2 url reject-200' # The "reject-200" returns HTTP status code 200 with no content.
- '^https?://www\.example\.com/3 url reject-img' # The "reject-img" returns HTTP status code 200 with content of 1px png.
- '^https?://www\.example\.com/4 url reject-dict' # The "reject-dict" returns HTTP status code 200 with content of empty json object.
- '^https?://www\.example\.com/5 url reject-array' # The "reject-array" returns HTTP status code 200 with content of empty json array.
- '^https?://www\.example\.com/(6) url 302 https://www.example.com/new-$1'
- '^https?://www\.(example)\.com/7 url 307 https://www.$1.com/new-7'
- '^https?://www\.example\.com/8 url request-header (\r\n)User-Agent:.+(\r\n) request-header $1User-Agent: haha-wriohoh$2' # The "request-header" works for all the http headers not just one single header, so you can match two or more headers including CRLF in one regular expression.
- '^https?://www\.example\.com/9 url request-body "pos_2":\[.*\],"pos_3" request-body "pos_2":[{"xx": "xx"}],"pos_3"'
- '^https?://www\.example\.com/10 url response-header (\r\n)Tracecode:.+(\r\n) response-header $1Tracecode: 88888888888$2'
- '^https?://www\.example\.com/11 url response-body "errmsg":"ok" response-body "errmsg":"not-ok"'
```
### DNS configuration
Support resolve ip with a proxy tunnel.
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
```
Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fakeip
```yaml
dns:
enable: true
use-hosts: true
ipv6: false
enhanced-mode: fake-ip
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#Proxy' # append the proxy adapter name to the end of DNS URL with '#' prefix.
- 'tls://8.8.4.4:853#Proxy'
fallback-filter:
geoip: false
geosite:
- gfw # `geosite` filter only use fallback server to resolve ip, prevent DNS leaks to untrusted 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.
On Windows, you should download the [Wintun](https://www.wintun.net) driver and copy `wintun.dll` into the system32 directory.
```yaml
# Enable the TUN listener
tun:
enable: true
stack: gvisor # only gvisor
stack: gvisor # System or gVisor
# device: tun://utun8 # or fd://xxx, it's optional
dns-hijack:
- 0.0.0.0:53 # additional dns server listen on TUN
- 0.0.0.0:53 # hijack all public
auto-route: true # auto set global route
```
### Rules configuration
- Support rule `GEOSITE`.
- Support rule-providers `RULE-SET`.
- Support rule `USER-AGENT`.
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
- Support `network` condition for all rules.
- Support `process` 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
The `GEOIP` databases via [https://github.com/Loyalsoldier/geoip](https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb).
The `GEOSITE` databases via [https://github.com/Loyalsoldier/v2ray-rules-dat](https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat).
```yaml
mode: rule
script:
shortcuts:
quic: 'network == "udp" and dst_port == 443'
privacy: '"analytics" in host or "adservice" in host or "firebase" in host or "safebrowsing" in host or "doubleclick" in host'
rules:
# rule SCRIPT
- SCRIPT,quic,REJECT # Disable QUIC, same as rule "DST-PORT,443,REJECT,udp"
- SCRIPT,privacy,REJECT
# network condition for all rules
- DOMAIN-SUFFIX,example.com,DIRECT,tcp
- DOMAIN-SUFFIX,example.com,REJECT,udp
# process condition for all rules (add 'P:' prefix)
- DOMAIN-SUFFIX,example.com,REJECT,P:Google Chrome Helper
# multiport condition for rules SRC-PORT and DST-PORT
- DST-PORT,123/136/137-139,DIRECT,udp
# USER-AGENT payload cannot include the comma character, '*' meaning any character.
- USER-AGENT,*example*,PROXY
# rule GEOSITE
- GEOSITE,category-ads-all,REJECT
- GEOSITE,icloud@cn,DIRECT
@ -122,94 +185,134 @@ rules:
- GEOSITE,youtube,PROXY
- GEOSITE,geolocation-cn,DIRECT
- GEOSITE,geolocation-!cn,PROXY
# source IPCIDR condition for all rules in gateway proxy
#- GEOSITE,geolocation-!cn,REJECT,192.168.1.88/32,192.168.1.99/32
- GEOIP,telegram,PROXY,no-resolve
- GEOIP,private,DIRECT,no-resolve
- GEOIP,cn,DIRECT
- GEOIP,telegram,PROXY,no-resolve
- GEOIP,lan,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`
### Script configuration
Script enables users to programmatically select a policy for the packets with more flexibility.
```yaml
proxy-groups:
mode: script
- name: 🚀 HK Group
type: select
use:
- ALL
filter: 'HK'
rules:
# the rule GEOSITE just as a rule provider in mode script
- GEOSITE,category-ads-all,Whatever
- GEOSITE,youtube,Whatever
- GEOSITE,geolocation-cn,Whatever
script:
code: |
def main(ctx, metadata):
if metadata["process_name"] == 'apsd':
return "DIRECT"
- name: 🚀 US Group
type: select
use:
- ALL
filter: 'US'
if metadata["network"] == 'udp' and metadata["dst_port"] == 443:
return "REJECT"
host = metadata["host"]
for kw in ['analytics', 'adservice', 'firebase', 'bugly', 'safebrowsing', 'doubleclick']:
if kw in host:
return "REJECT"
now = time.now()
if (now.hour < 8 or now.hour > 17) and metadata["src_ip"] == '192.168.1.99':
return "REJECT"
if ctx.rule_providers["geosite:category-ads-all"].match(metadata):
return "REJECT"
if ctx.rule_providers["geosite:youtube"].match(metadata):
ctx.log('[Script] domain %s matched youtube' % host)
return "Proxy"
proxy-providers:
ALL:
type: http
url: "xxxxx"
interval: 3600
path: "xxxxx"
health-check:
enable: true
interval: 600
url: http://www.gstatic.com/generate_204
if ctx.rule_providers["geosite:geolocation-cn"].match(metadata):
ctx.log('[Script] domain %s matched geolocation-cn' % host)
return "DIRECT"
ip = metadata["dst_ip"]
if host != "":
ip = ctx.resolve_ip(host)
if ip == "":
return "Proxy"
code = ctx.geoip(ip)
if code == "LAN" or code == "CN":
return "DIRECT"
return "Proxy" # default policy for requests which are not matched by any other script
```
the context and metadata
```ts
interface Metadata {
type: string // socks5、http
network: string // tcp
host: string
process_name: string
process_path: string
src_ip: string
src_port: int
dst_ip: string
dst_port: int
}
interface Context {
resolve_ip: (host: string) => string // ip string
geoip: (ip: string) => string // country code
log: (log: string) => void
rule_providers: Record<string, { match: (metadata: Metadata) => boolean }>
}
```
### Proxies configuration
Support outbound protocol `VLESS`.
Support `Trojan` with XTLS.
Support outbound transport protocol `VLESS`.
The XTLS support (TCP/UDP) transport by the XRAY-CORE.
Currently XTLS only supports TCP transport.
```yaml
proxies:
- name: "vless"
# VLESS
- name: "vless-tls"
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
network: tcp
servername: example.com
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-xtls"
type: vless
server: server
port: 443
uuid: uuid
network: tcp
servername: example.com
flow: xtls-rprx-direct # or xtls-rprx-origin
# flow-show: true # print the XTLS direction log
# udp: true
# skip-cert-verify: true
- name: "vless-grpc"
type: vless
# Trojan
- name: "trojan-xtls"
type: trojan
server: server
port: 443
uuid: uuid
tls: true
udp: true
network: grpc
servername: example.com # priority over wss host
password: yourpsk
network: tcp
flow: xtls-rprx-direct # or xtls-rprx-origin
# flow-show: true # print the XTLS direction log
# udp: true
# sni: example.com # aka server name
# skip-cert-verify: true
grpc-opts:
grpc-service-name: grpcname
```
### IPTABLES configuration
@ -223,73 +326,60 @@ iptables:
enable: true # default is false
inbound-interface: eth0 # detect the inbound interface, default is 'lo'
```
Run Clash as a daemon.
### 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`:
```
Create the systemd configuration file at /etc/systemd/system/clash.service:
```shell
[Unit]
Description=Clash-Meta Daemon, Another Clash Kernel.
After=network.target NetworkManager.service systemd-networkd.service iwd.service
Description=Clash daemon, A rule-based proxy in Go.
After=network.target
[Service]
Type=simple
User=clash-meta
Group=clash-meta
LimitNPROC=500
LimitNOFILE=1000000
CapabilityBoundingSet=cap_net_admin
AmbientCapabilities=cap_net_admin
Restart=always
ExecStartPre=/usr/bin/sleep 1s
ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
ExecStart=/usr/local/bin/clash -d /etc/clash
[Install]
WantedBy=multi-user.target
```
Launch clashd on system startup with:
```shell
$ systemctl enable Clash-Meta
$ systemctl enable clash
```
Launch clashd immediately with:
```shell
$ systemctl start Clash-Meta
$ systemctl start clash
```
### Display Process name
To display process name online by click [https://yaling888.github.io/yacd/](https://yaling888.github.io/yacd/).
Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
You can download the [Dashboard](https://github.com/yaling888/yacd/archive/gh-pages.zip) into Clash home directory:
```shell
cd ~/.config/clash
curl -LJ https://github.com/yaling888/yacd/archive/gh-pages.zip -o dashboard.zip
unzip dashboard.zip
```
To display process name in GUI please use [Dashboard For Meta](https://github.com/Clash-Mini/Dashboard).
Add to config file:
```yaml
external-controller: 127.0.0.1:9090
external-ui: dashboard
```
Open [http://127.0.0.1:9090/ui/](http://127.0.0.1:9090/ui/) by web browser.
![img.png](https://github.com/Clash-Mini/Dashboard/raw/master/View/Dashboard-Process.png)
## Plus Pro Release
[Release](https://github.com/yaling888/clash/releases/tag/plus)
## 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
* [Dreamacro/clash](https://github.com/Dreamacro/clash)
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
* [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
* [yaling888/clash-plus-pro](https://github.com/yaling888/clash)
## License

View File

@ -3,21 +3,24 @@ package adapter
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
"time"
_ "unsafe"
"github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
)
var UnifiedDelay = atomic.NewBool(false)
//go:linkname errCanceled net.errCanceled
var errCanceled error
type Proxy struct {
C.ProxyAdapter
@ -40,11 +43,7 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
// DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
wasCancel := false
if err != nil {
wasCancel = strings.Contains(err.Error(), "operation was canceled")
}
p.alive.Store(err == nil || wasCancel)
p.alive.Store(err == nil || errors.Is(err, errCanceled))
return conn, err
}
@ -117,8 +116,6 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
}
}()
unifiedDelay := UnifiedDelay.Load()
addr, err := urlToMetadata(url)
if err != nil {
return
@ -157,18 +154,11 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
},
}
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
return
}
if unifiedDelay {
start = time.Now()
resp, err = client.Do(req)
if err != nil {
return
}
}
_ = resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond)
return

View File

@ -11,7 +11,7 @@ import (
// NewHTTPS receive CONNECT request and return ConnContext
func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
metadata := parseHTTPAddr(request)
metadata.Type = C.HTTPS
metadata.Type = C.HTTPCONNECT
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port

22
adapter/inbound/mitm.go Normal file
View File

@ -0,0 +1,22 @@
package inbound
import (
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5"
)
// NewMitm receive mitm request and return MitmContext
func NewMitm(target socks5.Addr, source net.Addr, userAgent string, conn net.Conn) *context.ConnContext {
metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP
metadata.Type = C.MITM
metadata.UserAgent = userAgent
if ip, port, err := parseAddr(source.String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return context.NewConnContext(conn, metadata)
}

View File

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

View File

@ -4,9 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"github.com/gofrs/uuid"
"net"
"strings"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
@ -19,7 +17,6 @@ type Base struct {
tp C.AdapterType
udp bool
rmark int
id string
}
// Name implements C.ProxyAdapter
@ -27,20 +24,6 @@ func (b *Base) Name() string {
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
func (b *Base) Type() C.AdapterType {
return b.tp
@ -51,25 +34,11 @@ func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return nil, errors.New("no support")
}
// ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return nil, errors.New("no support")
}
// 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
func (b *Base) SupportUDP() bool {
return b.udp
@ -79,7 +48,6 @@ func (b *Base) SupportUDP() bool {
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
"id": b.Id(),
})
}
@ -133,12 +101,7 @@ func NewBase(opt BaseOption) *Base {
type conn struct {
net.Conn
chain C.Chain
actualRemoteDestination string
}
func (c *conn) RemoteDestination() string {
return c.actualRemoteDestination
chain C.Chain
}
// Chains implements C.Connection
@ -152,17 +115,12 @@ func (c *conn) AppendToChains(a C.ProxyAdapter) {
}
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 {
net.PacketConn
chain C.Chain
actualRemoteDestination string
}
func (c *packetConn) RemoteDestination() string {
return c.actualRemoteDestination
chain C.Chain
}
// Chains implements C.Connection
@ -176,17 +134,5 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}, parseRemoteDestination(a.Addr())}
}
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 ""
}
}
return &packetConn{pc, []string{a.Name()}}
}

View File

@ -46,13 +46,3 @@ func NewDirect() *Direct {
},
}
}
func NewCompatible() *Direct {
return &Direct{
Base: &Base{
name: "COMPATIBLE",
tp: C.Compatible,
udp: true,
},
}
}

View File

@ -22,20 +22,18 @@ type Http struct {
user string
pass string
tlsConfig *tls.Config
option *HttpOption
}
type HttpOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
}
// StreamConn implements C.ProxyAdapter
@ -86,18 +84,15 @@ 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 != "" {
auth := h.user + ":" + h.pass
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
}
if metadata.Type == C.MITM {
req.Header.Set("Origin-Request-Source-Address", metadata.SourceAddress())
}
if err := req.Write(rw); err != nil {
return err
}
@ -150,6 +145,5 @@ func NewHttp(option HttpOption) *Http {
user: option.UserName,
pass: option.Password,
tlsConfig: tlsConfig,
option: &option,
}
}

49
adapter/outbound/mitm.go Normal file
View File

@ -0,0 +1,49 @@
package outbound
import (
"context"
"net"
"time"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
type Mitm struct {
*Base
serverAddr *net.TCPAddr
httpProxyClient *Http
}
// DialContext implements C.ProxyAdapter
func (m *Mitm) DialContext(_ context.Context, metadata *C.Metadata, _ ...dialer.Option) (C.Conn, error) {
c, err := net.DialTCP("tcp", nil, m.serverAddr)
if err != nil {
return nil, err
}
_ = c.SetKeepAlive(true)
_ = c.SetKeepAlivePeriod(60 * time.Second)
metadata.Type = C.MITM
hc, err := m.httpProxyClient.StreamConn(c, metadata)
if err != nil {
_ = c.Close()
return nil, err
}
return NewConn(hc, m), nil
}
func NewMitm(serverAddr string) *Mitm {
tcpAddr, _ := net.ResolveTCPAddr("tcp", serverAddr)
return &Mitm{
Base: &Base{
name: "Mitm",
tp: C.Mitm,
},
serverAddr: tcpAddr,
httpProxyClient: NewHttp(HttpOption{}),
}
}

View File

@ -6,16 +6,43 @@ import (
"net"
"time"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
const (
rejectCountLimit = 50
rejectDelay = time.Second * 35
)
var rejectCounter = cache.NewLRUCache[string, int](cache.WithAge[string, int](15), cache.WithStale[string, int](false), cache.WithSize[string, int](512))
type Reject struct {
*Base
}
// DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
key := metadata.RemoteAddress()
count, existed := rejectCounter.Get(key)
if !existed {
count = 0
}
count = count + 1
rejectCounter.Set(key, count)
if count > rejectCountLimit {
c, _ := net.Pipe()
_ = c.SetDeadline(time.Now().Add(rejectDelay))
return NewConn(c, r), nil
}
return NewConn(&nopConn{}, r), nil
}
@ -34,16 +61,6 @@ func NewReject() *Reject {
}
}
func NewPass() *Reject {
return &Reject{
Base: &Base{
name: "PASS",
tp: C.Pass,
udp: true,
},
}
}
type nopConn struct{}
func (rw *nopConn) Read(b []byte) (int, error) {

View File

@ -53,10 +53,6 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
// StreamConn implements C.ProxyAdapter
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
if metadata.NetWork == C.UDP {
err := snell.WriteUDPHeader(c, s.version)
return c, err
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err
@ -108,17 +104,6 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
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) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk)

View File

@ -13,6 +13,8 @@ import (
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan"
"github.com/Dreamacro/clash/transport/vless"
"golang.org/x/net/http2"
)
type Trojan struct {
@ -23,7 +25,7 @@ type Trojan struct {
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
transport *http2.Transport
}
type TrojanOption struct {
@ -88,10 +90,6 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
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))
return c, err
}
@ -168,17 +166,6 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
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) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))

View File

@ -2,11 +2,8 @@ package outbound
import (
"bytes"
"crypto/tls"
xtls "github.com/xtls/go"
"net"
"strconv"
"sync"
"time"
"github.com/Dreamacro/clash/component/resolver"
@ -14,12 +11,6 @@ import (
"github.com/Dreamacro/clash/transport/socks5"
)
var (
globalClientSessionCache tls.ClientSessionCache
globalClientXSessionCache xtls.ClientSessionCache
once sync.Once
)
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
@ -27,20 +18,6 @@ func tcpKeepAlive(c net.Conn) {
}
}
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 {
var buf [][]byte
aType := uint8(metadata.AddrType)

View File

@ -18,6 +18,8 @@ import (
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vless"
"github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
)
const (
@ -33,7 +35,7 @@ type Vless struct {
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
transport *http2.Transport
}
type VlessOption struct {
@ -44,7 +46,6 @@ type VlessOption struct {
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"`
@ -61,6 +62,12 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
switch v.option.Network {
case "ws":
if v.option.WSOpts.Path == "" {
v.option.WSOpts.Path = v.option.WSPath
}
if len(v.option.WSOpts.Headers) == 0 {
v.option.WSOpts.Headers = v.option.WSHeaders
}
host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{
@ -91,6 +98,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http":
// readability first, so just copy default TLS logic
@ -158,7 +166,7 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
return vless.StreamXTLSConn(conn, &xtlsOpts)
} else if v.option.TLS {
} else {
tlsOpts := vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
@ -174,8 +182,6 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
return vmess.StreamTLSConn(conn, &tlsOpts)
}
return conn, nil
}
func (v *Vless) isXTLSEnabled() bool {
@ -247,36 +253,26 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
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 C.AtypIPv4:
addrType = vless.AtypIPv4
addrType = byte(vless.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6:
addrType = vless.AtypIPv6
addrType = byte(vless.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName:
addrType = vless.AtypDomainName
addrType = byte(vless.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], metadata.Host)
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
@ -291,40 +287,29 @@ func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
type vlessPacketConn struct {
net.Conn
rAddr net.Addr
cache [2]byte
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) {
func (vc *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)
if total < maxLength {
return vc.writePacket(b)
}
offset := 0
for offset < total {
for {
cursor := offset + maxLength
if cursor > total {
cursor = total
}
n, err := c.writePacket(b[offset:cursor])
n, err := vc.writePacket(b[offset:cursor])
if err != nil {
return offset + n, err
}
@ -338,32 +323,33 @@ func (c *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
return total, nil
}
func (c *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
c.mux.Lock()
defer c.mux.Unlock()
func (vc *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
vc.mux.Lock()
defer vc.mux.Unlock()
if c.remain > 0 {
if vc.remain != 0 {
length := len(b)
if c.remain < length {
length = c.remain
if length > vc.remain {
length = vc.remain
}
n, err := c.Conn.Read(b[:length])
n, err := vc.Conn.Read(b[:length])
if err != nil {
return 0, c.rAddr, err
return 0, vc.rAddr, err
}
c.remain -= n
return n, c.rAddr, nil
vc.remain -= n
return n, vc.rAddr, nil
}
if _, err := c.Conn.Read(b[:2]); err != nil {
return 0, c.rAddr, err
if _, err := vc.Conn.Read(b[:2]); err != nil {
return 0, vc.rAddr, err
}
total := int(binary.BigEndian.Uint16(b[:2]))
if total == 0 {
return 0, c.rAddr, nil
return 0, vc.rAddr, nil
}
length := len(b)
@ -371,13 +357,23 @@ func (c *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
length = total
}
if _, err := io.ReadFull(c.Conn, b[:length]); err != nil {
return 0, c.rAddr, errors.New("read packet error")
if _, err := io.ReadFull(vc.Conn, b[:length]); err != nil {
return 0, vc.rAddr, errors.New("read packet error")
}
c.remain = total - length
vc.remain = total - length
return length, c.rAddr, nil
return length, vc.rAddr, nil
}
func (vc *vlessPacketConn) writePacket(payload []byte) (int, error) {
binary.BigEndian.PutUint16(vc.cache[:], uint16(len(payload)))
if _, err := vc.Conn.Write(vc.cache[:]); err != nil {
return 0, err
}
return vc.Conn.Write(payload)
}
func NewVless(option VlessOption) (*Vless, error) {

View File

@ -15,6 +15,8 @@ import (
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
)
type Vmess struct {
@ -25,7 +27,7 @@ type Vmess struct {
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
transport *http2.Transport
}
type VmessOption struct {
@ -258,19 +260,9 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return v.ListenPacketOnStreamConn(c, metadata)
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
// SupportUOT implements C.ProxyAdapter
func (v *Vmess) SupportUOT() bool {
return true
}
func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{

View 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
}

View File

@ -3,19 +3,19 @@ package outboundgroup
import (
"context"
"encoding/json"
"errors"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
"time"
)
type Fallback struct {
*GroupBase
*outbound.Base
disableUDP bool
testUrl string
selected string
single *singledo.Single[[]C.Proxy]
providers []provider.ProxyProvider
}
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...)...)
if err == nil {
c.AppendToChains(f)
f.onDialSuccess()
} else {
f.onDialFailed()
}
return c, err
}
@ -44,7 +40,6 @@ func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata
if err == nil {
pc.AppendToChains(f)
}
return pc, err
}
@ -60,8 +55,8 @@ func (f *Fallback) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter
func (f *Fallback) MarshalJSON() ([]byte, error) {
all := []string{}
for _, proxy := range f.GetProxies(false) {
var all []string
for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
@ -77,57 +72,35 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
return proxy
}
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.GetProxies(touch)
al := proxies[0]
for i := len(proxies) - 1; i > -1; i-- {
proxy := proxies[i]
if proxy.Name() == f.selected && proxy.Alive() {
return proxy
}
if proxy.Alive() {
al = proxy
}
}
return al
func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() ([]C.Proxy, error) {
return getProvidersProxies(f.providers, touch), nil
})
return elm
}
func (f *Fallback) Set(name string) error {
var p C.Proxy
for _, proxy := range f.GetProxies(false) {
if proxy.Name() == name {
p = proxy
break
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.proxies(touch)
for _, proxy := range proxies {
if proxy.Alive() {
return proxy
}
}
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
return proxies[0]
}
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.Fallback,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Fallback,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration),
providers: providers,
disableUDP: option.DisableUDP,
testUrl: option.URL,
}
}

View File

@ -1,194 +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"
"sync"
"time"
)
type GroupBase struct {
*outbound.Base
filter *regexp2.Regexp
providers []provider.ProxyProvider
versions sync.Map // map[string]uint
proxies sync.Map // map[string][]C.Proxy
failedTestMux sync.Mutex
failedTimes int
failedTime time.Time
failedTesting *atomic.Bool
}
type GroupBaseOption struct {
outbound.BaseOption
filter string
providers []provider.ProxyProvider
}
func NewGroupBase(opt GroupBaseOption) *GroupBase {
var filter *regexp2.Regexp = nil
if opt.filter != "" {
filter = regexp2.MustCompile(opt.filter, 0)
}
return &GroupBase{
Base: outbound.NewBase(opt.BaseOption),
filter: filter,
providers: opt.providers,
failedTesting: atomic.NewBool(false),
}
}
func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
if gb.filter == nil {
var proxies []C.Proxy
for _, pd := range gb.providers {
if touch {
proxies = append(proxies, pd.ProxiesWithTouch()...)
} else {
proxies = append(proxies, pd.Proxies()...)
}
}
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
}
return proxies
}
for _, pd := range gb.providers {
if pd.VehicleType() == types.Compatible {
if touch {
gb.proxies.Store(pd.Name(), pd.ProxiesWithTouch())
} else {
gb.proxies.Store(pd.Name(), pd.Proxies())
}
gb.versions.Store(pd.Name(), pd.Version())
continue
}
if version, ok := gb.versions.Load(pd.Name()); !ok || version != pd.Version() {
var (
proxies []C.Proxy
newProxies []C.Proxy
)
if touch {
proxies = pd.ProxiesWithTouch()
} else {
proxies = pd.Proxies()
}
for _, p := range proxies {
if mat, _ := gb.filter.FindStringMatch(p.Name()); mat != nil {
newProxies = append(newProxies, p)
}
}
gb.proxies.Store(pd.Name(), newProxies)
gb.versions.Store(pd.Name(), pd.Version())
}
}
var proxies []C.Proxy
gb.proxies.Range(func(key, value any) bool {
proxies = append(proxies, value.([]C.Proxy)...)
return true
})
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
}
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)
lock.Lock()
if err == nil {
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() {
if gb.failedTesting.Load() {
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() {
return
}
log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
if gb.failedTimes >= gb.maxFailedTimes() {
gb.failedTesting.Store(true)
log.Warnln("because %s failed multiple times, active health check", gb.Name())
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
}

View File

@ -5,12 +5,11 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/Dreamacro/clash/common/cache"
"net"
"time"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
@ -21,8 +20,10 @@ import (
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
type LoadBalance struct {
*GroupBase
*outbound.Base
disableUDP bool
single *singledo.Single[[]C.Proxy]
providers []provider.ProxyProvider
strategyFn strategyFn
}
@ -38,10 +39,6 @@ func parseStrategy(config map[string]any) string {
}
func getKey(metadata *C.Metadata) string {
if metadata == nil {
return ""
}
if metadata.Host != "" {
// ip host
if ip := net.ParseIP(metadata.Host); ip != nil {
@ -60,16 +57,6 @@ func getKey(metadata *C.Metadata) 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 {
var b, j int64
@ -87,9 +74,6 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, op
defer func() {
if err == nil {
c.AppendToChains(lb)
lb.onDialSuccess()
} else {
lb.onDialFailed()
}
}()
@ -149,51 +133,24 @@ func strategyConsistentHashing() strategyFn {
}
}
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]
}
}
// Unwrap implements C.ProxyAdapter
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
proxies := lb.GetProxies(true)
proxies := lb.proxies(true)
return lb.strategyFn(proxies, metadata)
}
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() ([]C.Proxy, error) {
return getProvidersProxies(lb.providers, touch), nil
})
return elm
}
// MarshalJSON implements C.ProxyAdapter
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range lb.GetProxies(false) {
for _, proxy := range lb.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
@ -209,22 +166,18 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
strategyFn = strategyConsistentHashing()
case "round-robin":
strategyFn = strategyRoundRobin()
case "sticky-sessions":
strategyFn = strategyStickySessions()
default:
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
}
return &LoadBalance{
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.LoadBalance,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.LoadBalance,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration),
providers: providers,
strategyFn: strategyFn,
disableUDP: option.DisableUDP,
}, nil

View File

@ -29,7 +29,6 @@ type GroupCommonOption struct {
Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
}
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
@ -76,9 +75,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
providersMap[groupName] = pd
} else {
if groupOption.URL == "" || groupOption.Interval == 0 {
//return nil, errMissHealthCheck
groupOption.URL = "http://www.gstatic.com/generate_204"
groupOption.Interval = 300
return nil, errMissHealthCheck
}
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
@ -98,8 +95,6 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
return nil, err
}
providers = append(providers, list...)
} else {
groupOption.Filter = ""
}
var group C.ProxyAdapter

View File

@ -6,18 +6,26 @@ import (
"fmt"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Relay struct {
*GroupBase
*outbound.Base
single *singledo.Single[[]C.Proxy]
providers []provider.ProxyProvider
}
// DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
proxies, chainProxies := r.proxies(metadata, true)
var proxies []C.Proxy
for _, proxy := range r.proxies(metadata, true) {
if proxy.Type() != C.Direct {
proxies = append(proxies, proxy)
}
}
switch len(proxies) {
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)
}
conn := outbound.NewConn(c, last)
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()
return outbound.NewConn(c, r), nil
}
// MarshalJSON implements C.ProxyAdapter
func (r *Relay) MarshalJSON() ([]byte, error) {
all := []string{}
for _, proxy := range r.GetProxies(false) {
var all []string
for _, proxy := range r.rawProxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
@ -143,49 +78,37 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
})
}
func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy) {
rawProxies := r.GetProxies(touch)
func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() ([]C.Proxy, error) {
return getProvidersProxies(r.providers, touch), nil
})
var proxies []C.Proxy
var chainProxies []C.Proxy
var targetProxies []C.Proxy
return elm
}
for n, proxy := range rawProxies {
proxies = append(proxies, proxy)
chainProxies = append(chainProxies, proxy)
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
proxies := r.rawProxies(touch)
for n, proxy := range proxies {
subproxy := proxy.Unwrap(metadata)
for subproxy != nil {
chainProxies = append(chainProxies, subproxy)
proxies[n] = subproxy
subproxy = subproxy.Unwrap(metadata)
}
}
for _, proxy := range proxies {
if proxy.Type() != C.Direct && proxy.Type() != C.Compatible {
targetProxies = append(targetProxies, proxy)
}
}
return targetProxies, chainProxies
}
func (r *Relay) Addr() string {
proxies, _ := r.proxies(nil, true)
return proxies[len(proxies)-1].Addr()
return proxies
}
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.Relay,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
"",
providers,
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Relay,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration),
providers: providers,
}
}

View File

@ -6,15 +6,18 @@ import (
"errors"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Selector struct {
*GroupBase
*outbound.Base
disableUDP bool
single *singledo.Single[C.Proxy]
selected string
providers []provider.ProxyProvider
}
// DialContext implements C.ProxyAdapter
@ -46,8 +49,8 @@ func (s *Selector) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter
func (s *Selector) MarshalJSON() ([]byte, error) {
all := []string{}
for _, proxy := range s.GetProxies(false) {
var all []string
for _, proxy := range getProvidersProxies(s.providers, false) {
all = append(all, proxy.Name())
}
@ -63,9 +66,10 @@ func (s *Selector) Now() string {
}
func (s *Selector) Set(name string) error {
for _, proxy := range s.GetProxies(false) {
for _, proxy := range getProvidersProxies(s.providers, false) {
if proxy.Name() == name {
s.selected = name
s.single.Reset()
return nil
}
}
@ -74,34 +78,37 @@ func (s *Selector) Set(name string) error {
}
// Unwrap implements C.ProxyAdapter
func (s *Selector) Unwrap(*C.Metadata) C.Proxy {
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
return s.selectedProxy(true)
}
func (s *Selector) selectedProxy(touch bool) C.Proxy {
proxies := s.GetProxies(touch)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
return proxy
elm, _, _ := s.single.Do(func() (C.Proxy, error) {
proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
return proxy, nil
}
}
}
return proxies[0]
return proxies[0], nil
})
return elm
}
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
selected := providers[0].Proxies()[0].Name()
return &Selector{
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.Selector,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Selector,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
selected: "COMPATIBLE",
single: singledo.NewSingle[C.Proxy](defaultGetProxiesDuration),
providers: providers,
selected: selected,
disableUDP: option.DisableUDP,
}
}

View File

@ -21,11 +21,13 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
}
type URLTest struct {
*GroupBase
*outbound.Base
tolerance uint16
disableUDP bool
fastNode C.Proxy
single *singledo.Single[[]C.Proxy]
fastSingle *singledo.Single[C.Proxy]
providers []provider.ProxyProvider
}
func (u *URLTest) Now() string {
@ -37,9 +39,6 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(u)
u.onDialSuccess()
} else {
u.onDialFailed()
}
return c, err
}
@ -50,18 +49,25 @@ func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if err == nil {
pc.AppendToChains(u)
}
return pc, err
}
// Unwrap implements C.ProxyAdapter
func (u *URLTest) Unwrap(*C.Metadata) C.Proxy {
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
return u.fast(true)
}
func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() ([]C.Proxy, error) {
return getProvidersProxies(u.providers, touch), nil
})
return elm
}
func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (C.Proxy, error) {
proxies := u.GetProxies(touch)
proxies := u.proxies(touch)
fast := proxies[0]
min := fast.LastDelay()
fastNotExist := true
@ -104,8 +110,8 @@ func (u *URLTest) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter
func (u *URLTest) MarshalJSON() ([]byte, error) {
all := []string{}
for _, proxy := range u.GetProxies(false) {
var all []string
for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
@ -130,18 +136,15 @@ func parseURLTestOption(config map[string]any) []urlTestOption {
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.URLTest,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.URLTest,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration),
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
providers: providers,
disableUDP: option.DisableUDP,
}

View File

@ -24,7 +24,6 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
DstIP: netip.Addr{},
DstPort: port,
}
err = nil
return
} else if ip.Is4() {
addr = &C.Metadata{
@ -51,7 +50,3 @@ func tcpKeepAlive(c net.Conn) {
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
type SelectAble interface {
Set(string) error
}

View File

@ -26,7 +26,6 @@ type fetcher struct {
done chan struct{}
hash [16]byte
parser parser
interval time.Duration
onUpdate func(any)
}
@ -44,25 +43,11 @@ func (f *fetcher) Initial() (any, error) {
err error
isLocal bool
)
defer func() {
// pull proxies automatically
if f.ticker != nil {
go f.pullLoop()
}
}()
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
buf, err = os.ReadFile(f.vehicle.Path())
modTime := stat.ModTime()
f.updatedAt = &modTime
isLocal = true
if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) {
defer func() {
log.Infoln("[Provider] %s's proxies not updated for a long time, force refresh", f.Name())
go f.update()
}()
}
} else {
buf, err = f.vehicle.Read()
}
@ -99,6 +84,11 @@ func (f *fetcher) Initial() (any, error) {
f.hash = md5.Sum(buf)
// pull proxies automatically
if f.ticker != nil {
go f.pullLoop()
}
return proxies, nil
}
@ -144,11 +134,21 @@ func (f *fetcher) pullLoop() {
for {
select {
case <-f.ticker.C:
same, err := f.update()
if same || err != nil {
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
@ -156,26 +156,6 @@ func (f *fetcher) pullLoop() {
}
}
func (f *fetcher) update() (same bool, err error) {
elm, same, err := f.Update()
if err != nil {
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
return
}
if same {
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
return
}
if f.onUpdate != nil {
f.onUpdate(elm)
}
log.Infoln("[Provider] %s's proxies update", f.Name())
return
}
func safeWrite(path string, buf []byte) error {
dir := filepath.Dir(path)
@ -201,6 +181,5 @@ func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, pars
parser: parser,
done: make(chan struct{}, 1),
onUpdate: onUpdate,
interval: interval,
}
}

View File

@ -4,8 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/dlclark/regexp2"
"math"
"regexp"
"runtime"
"time"
@ -33,11 +32,6 @@ type proxySetProvider struct {
*fetcher
proxies []C.Proxy
healthCheck *HealthCheck
version uint
}
func (pp *proxySetProvider) Version() uint {
return pp.version
}
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
@ -46,8 +40,7 @@ func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
"type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(),
//TODO maybe error because year value overflow
"updatedAt": pp.updatedAt,
"updatedAt": pp.updatedAt,
})
}
@ -74,10 +67,6 @@ func (pp *proxySetProvider) Initial() error {
}
pp.onUpdate(elm)
if pp.healthCheck.auto() {
defer func() { go pp.healthCheck.process() }()
}
return nil
}
@ -100,7 +89,6 @@ func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
if pp.healthCheck.auto() {
go pp.healthCheck.check()
}
}
func stopProxyProvider(pd *ProxySetProvider) {
@ -109,12 +97,15 @@ func stopProxyProvider(pd *ProxySetProvider) {
}
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
//filterReg, err := regexp.Compile(filter)
filterReg, err := regexp2.Compile(filter, 0)
filterReg, err := regexp.Compile(filter)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{
proxies: []C.Proxy{},
healthCheck: hc,
@ -123,11 +114,6 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
onUpdate := func(elm any) {
ret := elm.([]C.Proxy)
pd.setProxies(ret)
if pd.version == math.MaxUint {
pd.version = 0
} else {
pd.version++
}
}
proxiesParseAndFilter := func(buf []byte) (any, error) {
@ -143,9 +129,7 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
name, ok := mapping["name"]
mat, _ := filterReg.FindStringMatch(name.(string))
if ok && len(filter) > 0 && mat == nil {
if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) {
continue
}
proxy, err := adapter.ParseProxy(mapping)
@ -182,11 +166,6 @@ type compatibleProvider struct {
name string
healthCheck *HealthCheck
proxies []C.Proxy
version uint
}
func (cp *compatibleProvider) Version() uint {
return cp.version
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
@ -211,10 +190,6 @@ func (cp *compatibleProvider) Update() error {
}
func (cp *compatibleProvider) Initial() error {
if cp.healthCheck.auto() {
go cp.healthCheck.process()
}
return nil
}
@ -244,6 +219,10 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
return nil, errors.New("provider need one proxy at least")
}
if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{
name: name,
proxies: proxies,

View File

@ -2,12 +2,15 @@ package provider
import (
"context"
netHttp "github.com/Dreamacro/clash/component/http"
types "github.com/Dreamacro/clash/constant/provider"
"io"
"net"
"net/http"
"net/url"
"os"
"time"
"github.com/Dreamacro/clash/component/dialer"
types "github.com/Dreamacro/clash/constant/provider"
)
type FileVehicle struct {
@ -46,16 +49,47 @@ func (h *HTTPVehicle) Path() string {
func (h *HTTPVehicle) Read() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
resp, err := netHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil)
uri, err := url.Parse(h.url)
if err != nil {
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()
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return buf, nil
}

View File

@ -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

303
common/cert/cert.go Normal file
View File

@ -0,0 +1,303 @@
package cert
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"strings"
"sync/atomic"
"time"
)
var currentSerialNumber = time.Now().Unix()
type Config struct {
ca *x509.Certificate
caPrivateKey *rsa.PrivateKey
roots *x509.CertPool
privateKey *rsa.PrivateKey
validity time.Duration
keyID []byte
organization string
certsStorage CertsStorage
}
type CertsStorage interface {
Get(key string) (*tls.Certificate, bool)
Set(key string, cert *tls.Certificate)
}
func NewAuthority(name, organization string, validity time.Duration) (*x509.Certificate, *rsa.PrivateKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
pub := privateKey.Public()
pkixPub, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, nil, err
}
h := sha1.New()
_, err = h.Write(pkixPub)
if err != nil {
return nil, nil, err
}
keyID := h.Sum(nil)
serial := atomic.AddInt64(&currentSerialNumber, 1)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(serial),
Subject: pkix.Name{
CommonName: name,
Organization: []string{organization},
},
SubjectKeyId: keyID,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
NotBefore: time.Now().Add(-validity),
NotAfter: time.Now().Add(validity),
DNSNames: []string{name},
IsCA: true,
}
raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, privateKey)
if err != nil {
return nil, nil, err
}
x509c, err := x509.ParseCertificate(raw)
if err != nil {
return nil, nil, err
}
return x509c, privateKey, nil
}
func NewConfig(ca *x509.Certificate, caPrivateKey *rsa.PrivateKey) (*Config, error) {
roots := x509.NewCertPool()
roots.AddCert(ca)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
pub := privateKey.Public()
pkixPub, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, err
}
h := sha1.New()
_, err = h.Write(pkixPub)
if err != nil {
return nil, err
}
keyID := h.Sum(nil)
return &Config{
ca: ca,
caPrivateKey: caPrivateKey,
privateKey: privateKey,
keyID: keyID,
validity: time.Hour,
organization: "Clash",
certsStorage: NewDomainTrieCertsStorage(),
roots: roots,
}, nil
}
func (c *Config) GetCA() *x509.Certificate {
return c.ca
}
func (c *Config) SetOrganization(organization string) {
c.organization = organization
}
func (c *Config) SetValidity(validity time.Duration) {
c.validity = validity
}
func (c *Config) NewTLSConfigForHost(hostname string) *tls.Config {
tlsConfig := &tls.Config{
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
host := clientHello.ServerName
if host == "" {
host = hostname
}
return c.GetOrCreateCert(host)
},
NextProtos: []string{"http/1.1"},
}
tlsConfig.InsecureSkipVerify = true
return tlsConfig
}
func (c *Config) GetOrCreateCert(hostname string, ips ...net.IP) (*tls.Certificate, error) {
var leaf *x509.Certificate
tlsCertificate, ok := c.certsStorage.Get(hostname)
if ok {
leaf = tlsCertificate.Leaf
if _, err := leaf.Verify(x509.VerifyOptions{
DNSName: hostname,
Roots: c.roots,
}); err == nil {
return tlsCertificate, nil
}
}
var (
key = hostname
topHost = hostname
wildcardHost = "*." + hostname
dnsNames []string
)
if ip := net.ParseIP(hostname); ip != nil {
ips = append(ips, ip)
} else {
parts := strings.Split(hostname, ".")
l := len(parts)
if leaf != nil {
dnsNames = append(dnsNames, leaf.DNSNames...)
}
if l > 2 {
topIndex := l - 2
topHost = strings.Join(parts[topIndex:], ".")
for i := topIndex; i > 0; i-- {
wildcardHost = "*." + strings.Join(parts[i:], ".")
if i == topIndex && (len(dnsNames) == 0 || dnsNames[0] != topHost) {
dnsNames = append(dnsNames, topHost, wildcardHost)
} else if !hasDnsNames(dnsNames, wildcardHost) {
dnsNames = append(dnsNames, wildcardHost)
}
}
} else {
dnsNames = append(dnsNames, topHost, wildcardHost)
}
key = "+." + topHost
}
serial := atomic.AddInt64(&currentSerialNumber, 1)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(serial),
Subject: pkix.Name{
CommonName: topHost,
Organization: []string{c.organization},
},
SubjectKeyId: c.keyID,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
NotBefore: time.Now().Add(-c.validity),
NotAfter: time.Now().Add(c.validity),
DNSNames: dnsNames,
IPAddresses: ips,
}
raw, err := x509.CreateCertificate(rand.Reader, tmpl, c.ca, c.privateKey.Public(), c.caPrivateKey)
if err != nil {
return nil, err
}
x509c, err := x509.ParseCertificate(raw)
if err != nil {
return nil, err
}
tlsCertificate = &tls.Certificate{
Certificate: [][]byte{raw, c.ca.Raw},
PrivateKey: c.privateKey,
Leaf: x509c,
}
c.certsStorage.Set(key, tlsCertificate)
return tlsCertificate, nil
}
// GenerateAndSave generate CA private key and CA certificate and dump them to file
func GenerateAndSave(caPath string, caKeyPath string) error {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix()),
Subject: pkix.Name{
Country: []string{"US"},
CommonName: "Clash Root CA",
Organization: []string{"Clash Trust Services"},
},
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
NotBefore: time.Now().Add(-(time.Hour * 24 * 60)),
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 25),
BasicConstraintsValid: true,
IsCA: true,
}
caRaw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, privateKey.Public(), privateKey)
if err != nil {
return err
}
caOut, err := os.OpenFile(caPath, os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return err
}
defer func(caOut *os.File) {
_ = caOut.Close()
}(caOut)
if err = pem.Encode(caOut, &pem.Block{Type: "CERTIFICATE", Bytes: caRaw}); err != nil {
return err
}
caKeyOut, err := os.OpenFile(caKeyPath, os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return err
}
defer func(caKeyOut *os.File) {
_ = caKeyOut.Close()
}(caKeyOut)
if err = pem.Encode(caKeyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}); err != nil {
return err
}
return nil
}
func hasDnsNames(dnsNames []string, hostname string) bool {
for _, name := range dnsNames {
if name == hostname {
return true
}
}
return false
}

104
common/cert/cert_test.go Normal file
View File

@ -0,0 +1,104 @@
package cert
import (
"crypto/tls"
"crypto/x509"
"net"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCert(t *testing.T) {
ca, privateKey, err := NewAuthority("Clash ca", "Clash", 24*time.Hour)
assert.Nil(t, err)
assert.NotNil(t, ca)
assert.NotNil(t, privateKey)
c, err := NewConfig(ca, privateKey)
assert.Nil(t, err)
c.SetValidity(20 * time.Hour)
c.SetOrganization("Test Organization")
conf := c.NewTLSConfigForHost("example.org")
assert.Equal(t, []string{"http/1.1"}, conf.NextProtos)
assert.True(t, conf.InsecureSkipVerify)
// Test generating a certificate
clientHello := &tls.ClientHelloInfo{
ServerName: "example.org",
}
tlsCert, err := conf.GetCertificate(clientHello)
assert.Nil(t, err)
assert.NotNil(t, tlsCert)
// Assert certificate details
x509c := tlsCert.Leaf
assert.Equal(t, "example.org", x509c.Subject.CommonName)
assert.Nil(t, x509c.VerifyHostname("example.org"))
assert.Nil(t, x509c.VerifyHostname("abc.example.org"))
assert.Equal(t, []string{"Test Organization"}, x509c.Subject.Organization)
assert.NotNil(t, x509c.SubjectKeyId)
assert.True(t, x509c.BasicConstraintsValid)
assert.True(t, x509c.KeyUsage&x509.KeyUsageKeyEncipherment == x509.KeyUsageKeyEncipherment)
assert.True(t, x509c.KeyUsage&x509.KeyUsageDigitalSignature == x509.KeyUsageDigitalSignature)
assert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, x509c.ExtKeyUsage)
assert.Equal(t, []string{"example.org", "*.example.org"}, x509c.DNSNames)
assert.True(t, x509c.NotBefore.Before(time.Now().Add(-2*time.Hour)))
assert.True(t, x509c.NotAfter.After(time.Now().Add(2*time.Hour)))
// Check that certificate is cached
tlsCert2, err := c.GetOrCreateCert("abc.example.org")
assert.Nil(t, err)
assert.True(t, tlsCert == tlsCert2)
// Check that certificate is new
_, _ = c.GetOrCreateCert("a.b.c.d.e.f.g.h.i.j.example.org")
tlsCert3, err := c.GetOrCreateCert("m.k.l.example.org")
x509c = tlsCert3.Leaf
assert.Nil(t, err)
assert.False(t, tlsCert == tlsCert3)
assert.Equal(t, []string{"example.org", "*.example.org", "*.j.example.org", "*.i.j.example.org", "*.h.i.j.example.org", "*.g.h.i.j.example.org", "*.f.g.h.i.j.example.org", "*.e.f.g.h.i.j.example.org", "*.d.e.f.g.h.i.j.example.org", "*.c.d.e.f.g.h.i.j.example.org", "*.b.c.d.e.f.g.h.i.j.example.org", "*.l.example.org", "*.k.l.example.org"}, x509c.DNSNames)
// Check that certificate is cached
tlsCert4, err := c.GetOrCreateCert("xyz.example.org")
x509c = tlsCert4.Leaf
assert.Nil(t, err)
assert.True(t, tlsCert3 == tlsCert4)
assert.Nil(t, x509c.VerifyHostname("example.org"))
assert.Nil(t, x509c.VerifyHostname("jkf.example.org"))
assert.Nil(t, x509c.VerifyHostname("n.j.example.org"))
assert.Nil(t, x509c.VerifyHostname("c.i.j.example.org"))
assert.Nil(t, x509c.VerifyHostname("m.l.example.org"))
assert.Error(t, x509c.VerifyHostname("m.l.jkf.example.org"))
// Check the certificate for an IP
tlsCertForIP, err := c.GetOrCreateCert("192.168.0.1")
x509c = tlsCertForIP.Leaf
assert.Nil(t, err)
assert.Equal(t, 1, len(x509c.IPAddresses))
assert.True(t, net.ParseIP("192.168.0.1").Equal(x509c.IPAddresses[0]))
// Check that certificate is cached
tlsCertForIP2, err := c.GetOrCreateCert("192.168.0.1")
x509c = tlsCertForIP2.Leaf
assert.Nil(t, err)
assert.True(t, tlsCertForIP == tlsCertForIP2)
assert.Nil(t, x509c.VerifyHostname("192.168.0.1"))
}
func TestGenerateAndSave(t *testing.T) {
caPath := "ca.crt"
caKeyPath := "ca.key"
err := GenerateAndSave(caPath, caKeyPath)
assert.Nil(t, err)
_ = os.Remove(caPath)
_ = os.Remove(caKeyPath)
}

32
common/cert/storage.go Normal file
View File

@ -0,0 +1,32 @@
package cert
import (
"crypto/tls"
"github.com/Dreamacro/clash/component/trie"
)
// DomainTrieCertsStorage cache wildcard certificates
type DomainTrieCertsStorage struct {
certsCache *trie.DomainTrie[*tls.Certificate]
}
// Get gets the certificate from the storage
func (c *DomainTrieCertsStorage) Get(key string) (*tls.Certificate, bool) {
ca := c.certsCache.Search(key)
if ca == nil {
return nil, false
}
return ca.Data, true
}
// Set saves the certificate to the storage
func (c *DomainTrieCertsStorage) Set(key string, cert *tls.Certificate) {
_ = c.certsCache.Insert(key, cert)
}
func NewDomainTrieCertsStorage() *DomainTrieCertsStorage {
return &DomainTrieCertsStorage{
certsCache: trie.New[*tls.Certificate](),
}
}

View File

@ -14,9 +14,8 @@ func ExecCmd(cmdStr string) (string, error) {
cmd = exec.Command(args[0])
} else {
cmd = exec.Command(args[0], args[1:]...)
}
prepareBackgroundCommand(cmd)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v, %s", err, string(out))

View File

@ -1,11 +0,0 @@
//go:build !windows
package cmd
import (
"os/exec"
)
func prepareBackgroundCommand(cmd *exec.Cmd) {
}

View File

@ -1,12 +0,0 @@
//go:build windows
package cmd
import (
"os/exec"
"syscall"
)
func prepareBackgroundCommand(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
}

View File

@ -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++
}

View File

@ -12,19 +12,28 @@ import (
func Relay(leftConn, rightConn net.Conn) {
ch := make(chan error)
tcpKeepAlive(leftConn)
tcpKeepAlive(rightConn)
go func() {
buf := pool.Get(pool.RelayBufferSize)
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
// See also https://github.com/Dreamacro/clash/pull/1209
_, err := io.CopyBuffer(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn}, buf)
pool.Put(buf)
leftConn.SetReadDeadline(time.Now())
_ = pool.Put(buf)
_ = leftConn.SetReadDeadline(time.Now())
ch <- err
}()
buf := pool.Get(pool.RelayBufferSize)
io.CopyBuffer(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn}, buf)
pool.Put(buf)
rightConn.SetReadDeadline(time.Now())
_, _ = io.CopyBuffer(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn}, buf)
_ = pool.Put(buf)
_ = rightConn.SetReadDeadline(time.Now())
<-ch
}
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
}
}

View File

@ -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
}

View File

@ -1,115 +1,107 @@
package sniffer
package tls
import (
"encoding/binary"
"errors"
"strings"
C "github.com/Dreamacro/clash/constant"
)
var ErrNoClue = errors.New("not enough information for making a decision")
type SniffHeader struct {
domain string
}
func (h *SniffHeader) Protocol() string {
return "tls"
}
func (h *SniffHeader) Domain() string {
return h.domain
}
var (
errNotTLS = errors.New("not TLS header")
errNotClientHello = errors.New("not client hello")
)
type TLSSniffer struct {
}
func (tls *TLSSniffer) Protocol() string {
return "tls"
}
func (tls *TLSSniffer) SupportNetwork() C.NetWork {
return C.TCP
}
func (tls *TLSSniffer) SniffTCP(bytes []byte) (string, error) {
domain, err := SniffTLS(bytes)
if err == nil {
return *domain, nil
} else {
return "", err
}
}
func IsValidTLSVersion(major, minor byte) bool {
return major == 3
}
// ReadClientHello returns server name (if any) from TLS client hello message.
// https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go#L300
func ReadClientHello(data []byte) (*string, error) {
func ReadClientHello(data []byte, h *SniffHeader) error {
if len(data) < 42 {
return nil, ErrNoClue
return ErrNoClue
}
sessionIDLen := int(data[38])
if sessionIDLen > 32 || len(data) < 39+sessionIDLen {
return nil, ErrNoClue
return ErrNoClue
}
data = data[39+sessionIDLen:]
if len(data) < 2 {
return nil, ErrNoClue
return ErrNoClue
}
// cipherSuiteLen is the number of bytes of cipher suite numbers. Since
// they are uint16s, the number must be even.
cipherSuiteLen := int(data[0])<<8 | int(data[1])
if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen {
return nil, errNotClientHello
return errNotClientHello
}
data = data[2+cipherSuiteLen:]
if len(data) < 1 {
return nil, ErrNoClue
return ErrNoClue
}
compressionMethodsLen := int(data[0])
if len(data) < 1+compressionMethodsLen {
return nil, ErrNoClue
return ErrNoClue
}
data = data[1+compressionMethodsLen:]
if len(data) == 0 {
return nil, errNotClientHello
return errNotClientHello
}
if len(data) < 2 {
return nil, errNotClientHello
return errNotClientHello
}
extensionsLength := int(data[0])<<8 | int(data[1])
data = data[2:]
if extensionsLength != len(data) {
return nil, errNotClientHello
return errNotClientHello
}
for len(data) != 0 {
if len(data) < 4 {
return nil, errNotClientHello
return errNotClientHello
}
extension := uint16(data[0])<<8 | uint16(data[1])
length := int(data[2])<<8 | int(data[3])
data = data[4:]
if len(data) < length {
return nil, errNotClientHello
return errNotClientHello
}
if extension == 0x00 { /* extensionServerName */
d := data[:length]
if len(d) < 2 {
return nil, errNotClientHello
return errNotClientHello
}
namesLen := int(d[0])<<8 | int(d[1])
d = d[2:]
if len(d) != namesLen {
return nil, errNotClientHello
return errNotClientHello
}
for len(d) > 0 {
if len(d) < 3 {
return nil, errNotClientHello
return errNotClientHello
}
nameType := d[0]
nameLen := int(d[1])<<8 | int(d[2])
d = d[3:]
if len(d) < nameLen {
return nil, errNotClientHello
return errNotClientHello
}
if nameType == 0 {
serverName := string(d[:nameLen])
@ -117,22 +109,21 @@ func ReadClientHello(data []byte) (*string, error) {
// trailing dot. See
// https://tools.ietf.org/html/rfc6066#section-3.
if strings.HasSuffix(serverName, ".") {
return nil, errNotClientHello
return errNotClientHello
}
return &serverName, nil
h.domain = serverName
return nil
}
d = d[nameLen:]
}
}
data = data[length:]
}
return nil, errNotTLS
return errNotTLS
}
func SniffTLS(b []byte) (*string, error) {
func SniffTLS(b []byte) (*SniffHeader, error) {
if len(b) < 5 {
return nil, ErrNoClue
}
@ -148,9 +139,10 @@ func SniffTLS(b []byte) (*string, error) {
return nil, ErrNoClue
}
domain, err := ReadClientHello(b[5 : 5+headerLen])
h := &SniffHeader{}
err := ReadClientHello(b[5:5+headerLen], h)
if err == nil {
return domain, nil
return h, nil
}
return nil, err
}

View File

@ -1,4 +1,4 @@
package sniffer
package tls
import (
"testing"
@ -142,7 +142,7 @@ func TestTLSHeaders(t *testing.T) {
}
for _, test := range cases {
domain, err := SniffTLS(test.input)
header, err := SniffTLS(test.input)
if test.err {
if err == nil {
t.Errorf("Exepct error but nil in test %v", test)
@ -151,8 +151,8 @@ func TestTLSHeaders(t *testing.T) {
if err != nil {
t.Errorf("Expect no error but actually %s in test %v", err.Error(), test)
}
if *domain != test.domain {
t.Error("expect domain ", test.domain, " but got ", domain)
if header.Domain() != test.domain {
t.Error("expect domain ", test.domain, " but got ", header.Domain())
}
}
}

View File

@ -31,7 +31,7 @@ func NewDecoder(option Option) *Decoder {
// Decode transform a map[string]any to a struct
func (d *Decoder) Decode(src map[string]any, dst any) error {
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()
v := reflect.ValueOf(dst).Elem()
@ -301,7 +301,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field reflect.StructField
val reflect.Value
}
var fields []field
fields := []field{}
for len(structs) > 0 {
structVal := structs[0]
structs = structs[1:]

View File

@ -137,3 +137,45 @@ func TestStructure_Nest(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, s.BazOptional, goal)
}
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
"bar": []any{"bar", nil},
}
goal := &BazSlice{
Foo: 1,
Bar: []string{"bar", ""},
}
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal.Bar, s.Bar)
s = &BazSlice{}
err = decoder.Decode(rawMap, s)
assert.NotNil(t, err)
}
func TestStructure_SliceNilValueComplex(t *testing.T) {
rawMap := map[string]any{
"bar": []any{map[string]any{"bar": "foo"}, nil},
}
s := &struct {
Bar []map[string]any `test:"bar"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Nil(t, s.Bar[1])
ss := &struct {
Bar []Baz `test:"bar"`
}{}
err = decoder.Decode(rawMap, ss)
assert.NotNil(t, err)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -3,22 +3,12 @@ package dialer
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"sync"
"github.com/Dreamacro/clash/component/resolver"
)
var (
dialMux sync.Mutex
actualSingleDialContext = singleDialContext
actualDualStackDialContext = dualStackDialContext
tcpConcurrent = false
DisableIPv6 = false
)
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
opt := &option{
interfaceName: DefaultInterface.Load(),
@ -35,9 +25,33 @@ func DialContext(ctx context.Context, network, address string, options ...Option
switch network {
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 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)
case "tcp", "udp":
return actualDualStackDialContext(ctx, network, address, opt)
return dualStackDialContext(ctx, network, address, opt)
default:
return nil, errors.New("network invalid")
}
@ -75,24 +89,6 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
return lc.ListenPacket(ctx, network, address)
}
func SetDial(concurrent bool) {
dialMux.Lock()
tcpConcurrent = concurrent
if concurrent {
actualSingleDialContext = concurrentSingleDialContext
actualDualStackDialContext = concurrentDualStackDialContext
} else {
actualSingleDialContext = singleDialContext
actualDualStackDialContext = dualStackDialContext
}
dialMux.Unlock()
}
func GetDial() bool {
return tcpConcurrent
}
func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) {
dialer := &net.Dialer{}
if opt.interfaceName != "" {
@ -104,10 +100,6 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
bindMarkToDialer(opt.routingMark, dialer, network, destination)
}
if DisableIPv6 && destination.Is6() {
return nil, fmt.Errorf("IPv6 is diabled, dialer cancel")
}
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
}
@ -191,130 +183,3 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
return nil, errors.New("never touched")
}
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)
}
return concurrentDialContext(ctx, network, ips, port, opt)
}
func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
returned := make(chan struct{})
defer close(returned)
type dialResult struct {
ip netip.Addr
net.Conn
error
resolved bool
}
results := make(chan dialResult)
tcpRacer := func(ctx context.Context, ip netip.Addr) {
result := dialResult{ip: ip}
defer func() {
select {
case results <- result:
case <-returned:
if result.Conn != nil {
result.Conn.Close()
}
}
}()
v := "4"
if ip.Is6() {
v = "6"
}
result.Conn, result.error = dialContext(ctx, network+v, ip, port, opt)
}
for _, ip := range ips {
go tcpRacer(ctx, ip)
}
connCount := len(ips)
for res := range results {
connCount--
if res.error == nil {
return res.Conn, nil
}
if connCount == 0 {
break
}
}
return nil, fmt.Errorf("all ips %v tcp shake hands failed", ips)
}
func singleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ip netip.Addr
switch network {
case "tcp4", "udp4":
if !opt.direct {
ip, err = resolver.ResolveIPv4ProxyServerHost(host)
} else {
ip, err = resolver.ResolveIPv4(host)
}
default:
if !opt.direct {
ip, err = resolver.ResolveIPv6ProxyServerHost(host)
} else {
ip, err = resolver.ResolveIPv6(host)
}
}
if err != nil {
return nil, err
}
return dialContext(ctx, network, ip, port, opt)
}
func concurrentSingleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ips []netip.Addr
switch network {
case "tcp4", "udp4":
if !opt.direct {
ips, err = resolver.ResolveAllIPv4ProxyServerHost(host)
} else {
ips, err = resolver.ResolveAllIPv4(host)
}
default:
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)
}

View File

@ -3,7 +3,6 @@ package geodata
import (
"errors"
"fmt"
C "github.com/Dreamacro/clash/constant"
"strings"
"github.com/Dreamacro/clash/component/geodata/router"
@ -15,7 +14,7 @@ type loader struct {
}
func (l *loader) LoadGeoSite(list string) ([]*router.Domain, error) {
return l.LoadGeoSiteWithAttr(C.GeositeName, list)
return l.LoadGeoSiteWithAttr("geosite.dat", list)
}
func (l *loader) LoadGeoSiteWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) {
@ -30,7 +29,7 @@ func (l *loader) LoadGeoSiteWithAttr(file string, siteWithAttr string) ([]*route
return nil, fmt.Errorf("empty listname in rule: %s", siteWithAttr)
}
domains, err := l.LoadSiteByPath(file, list)
domains, err := l.LoadSite(file, list)
if err != nil {
return nil, err
}
@ -59,7 +58,7 @@ func (l *loader) LoadGeoSiteWithAttr(file string, siteWithAttr string) ([]*route
}
func (l *loader) LoadGeoIP(country string) ([]*router.CIDR, error) {
return l.LoadIPByPath(C.GeoipName, country)
return l.LoadIP("geoip.dat", country)
}
var loaders map[string]func() LoaderImplementation

View File

@ -5,10 +5,8 @@ import (
)
type LoaderImplementation interface {
LoadSiteByPath(filename, list string) ([]*router.Domain, error)
LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error)
LoadIPByPath(filename, country string) ([]*router.CIDR, error)
LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error)
LoadSite(filename, list string) ([]*router.Domain, error)
LoadIP(filename, country string) ([]*router.CIDR, error)
}
type Loader interface {

View File

@ -8,6 +8,7 @@ import (
"github.com/Dreamacro/clash/component/geodata/router"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"google.golang.org/protobuf/proto"
)
@ -32,7 +33,7 @@ func (g GeoIPCache) Set(key string, value *router.GeoIP) {
}
func (g GeoIPCache) Unmarshal(filename, code string) (*router.GeoIP, error) {
asset := C.Path.GetAssetLocation(filename)
asset := C.Path.Resolve(filename)
idx := strings.ToLower(asset + ":" + code)
if g.Has(idx) {
return g.Get(idx), nil
@ -97,7 +98,7 @@ func (g GeoSiteCache) Set(key string, value *router.GeoSite) {
}
func (g GeoSiteCache) Unmarshal(filename, code string) (*router.GeoSite, error) {
asset := C.Path.GetAssetLocation(filename)
asset := C.Path.Resolve(filename)
idx := strings.ToLower(asset + ":" + code)
if g.Has(idx) {
return g.Get(idx), nil

View File

@ -1,7 +1,6 @@
package memconservative
import (
"errors"
"fmt"
"runtime"
@ -14,7 +13,7 @@ type memConservativeLoader struct {
geositecache GeoSiteCache
}
func (m *memConservativeLoader) LoadIPByPath(filename, country string) ([]*router.CIDR, error) {
func (m *memConservativeLoader) LoadIP(filename, country string) ([]*router.CIDR, error) {
defer runtime.GC()
geoip, err := m.geoipcache.Unmarshal(filename, country)
if err != nil {
@ -23,11 +22,7 @@ func (m *memConservativeLoader) LoadIPByPath(filename, country string) ([]*route
return geoip.Cidr, nil
}
func (m *memConservativeLoader) LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) {
return nil, errors.New("memConservative do not support LoadIPByBytes")
}
func (m *memConservativeLoader) LoadSiteByPath(filename, list string) ([]*router.Domain, error) {
func (m *memConservativeLoader) LoadSite(filename, list string) ([]*router.Domain, error) {
defer runtime.GC()
geosite, err := m.geositecache.Unmarshal(filename, list)
if err != nil {
@ -36,10 +31,6 @@ func (m *memConservativeLoader) LoadSiteByPath(filename, list string) ([]*router
return geosite.Domain, nil
}
func (m *memConservativeLoader) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) {
return nil, errors.New("memConservative do not support LoadSiteByBytes")
}
func newMemConservativeLoader() geodata.LoaderImplementation {
return &memConservativeLoader{make(map[string]*router.GeoIP), make(map[string]*router.GeoSite)}
}

View File

@ -1,10 +1,7 @@
package router
import (
"encoding/binary"
"fmt"
"net"
"sort"
"strings"
"github.com/Dreamacro/clash/component/geodata/strmatcher"
@ -33,10 +30,9 @@ func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) {
type DomainMatcher struct {
matchers strmatcher.IndexMatcher
not bool
}
func NewMphMatcherGroup(domains []*Domain, not bool) (*DomainMatcher, error) {
func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) {
g := strmatcher.NewMphMatcherGroup()
for _, d := range domains {
matcherType, f := matcherTypeMap[d.Type]
@ -51,12 +47,11 @@ func NewMphMatcherGroup(domains []*Domain, not bool) (*DomainMatcher, error) {
g.Build()
return &DomainMatcher{
matchers: g,
not: not,
}, nil
}
// NewDomainMatcher new domain matcher.
func NewDomainMatcher(domains []*Domain, not bool) (*DomainMatcher, error) {
func NewDomainMatcher(domains []*Domain) (*DomainMatcher, error) {
g := new(strmatcher.MatcherGroup)
for _, d := range domains {
m, err := domainToMatcher(d)
@ -68,290 +63,9 @@ func NewDomainMatcher(domains []*Domain, not bool) (*DomainMatcher, error) {
return &DomainMatcher{
matchers: g,
not: not,
}, nil
}
func (m *DomainMatcher) ApplyDomain(domain string) bool {
isMatched := len(m.matchers.Match(strings.ToLower(domain))) > 0
if m.not {
isMatched = !isMatched
}
return isMatched
}
// CIDRList is an alias of []*CIDR to provide sort.Interface.
type CIDRList []*CIDR
// Len implements sort.Interface.
func (l *CIDRList) Len() int {
return len(*l)
}
// Less implements sort.Interface.
func (l *CIDRList) Less(i int, j int) bool {
ci := (*l)[i]
cj := (*l)[j]
if len(ci.Ip) < len(cj.Ip) {
return true
}
if len(ci.Ip) > len(cj.Ip) {
return false
}
for k := 0; k < len(ci.Ip); k++ {
if ci.Ip[k] < cj.Ip[k] {
return true
}
if ci.Ip[k] > cj.Ip[k] {
return false
}
}
return ci.Prefix < cj.Prefix
}
// Swap implements sort.Interface.
func (l *CIDRList) Swap(i int, j int) {
(*l)[i], (*l)[j] = (*l)[j], (*l)[i]
}
type ipv6 struct {
a uint64
b uint64
}
type GeoIPMatcher struct {
countryCode string
reverseMatch bool
ip4 []uint32
prefix4 []uint8
ip6 []ipv6
prefix6 []uint8
}
func normalize4(ip uint32, prefix uint8) uint32 {
return (ip >> (32 - prefix)) << (32 - prefix)
}
func normalize6(ip ipv6, prefix uint8) ipv6 {
if prefix <= 64 {
ip.a = (ip.a >> (64 - prefix)) << (64 - prefix)
ip.b = 0
} else {
ip.b = (ip.b >> (128 - prefix)) << (128 - prefix)
}
return ip
}
func (m *GeoIPMatcher) Init(cidrs []*CIDR) error {
ip4Count := 0
ip6Count := 0
for _, cidr := range cidrs {
ip := cidr.Ip
switch len(ip) {
case 4:
ip4Count++
case 16:
ip6Count++
default:
return fmt.Errorf("unexpect ip length: %d", len(ip))
}
}
cidrList := CIDRList(cidrs)
sort.Sort(&cidrList)
m.ip4 = make([]uint32, 0, ip4Count)
m.prefix4 = make([]uint8, 0, ip4Count)
m.ip6 = make([]ipv6, 0, ip6Count)
m.prefix6 = make([]uint8, 0, ip6Count)
for _, cidr := range cidrs {
ip := cidr.Ip
prefix := uint8(cidr.Prefix)
switch len(ip) {
case 4:
m.ip4 = append(m.ip4, normalize4(binary.BigEndian.Uint32(ip), prefix))
m.prefix4 = append(m.prefix4, prefix)
case 16:
ip6 := ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
}
ip6 = normalize6(ip6, prefix)
m.ip6 = append(m.ip6, ip6)
m.prefix6 = append(m.prefix6, prefix)
}
}
return nil
}
func (m *GeoIPMatcher) SetReverseMatch(isReverseMatch bool) {
m.reverseMatch = isReverseMatch
}
func (m *GeoIPMatcher) match4(ip uint32) bool {
if len(m.ip4) == 0 {
return false
}
if ip < m.ip4[0] {
return false
}
size := uint32(len(m.ip4))
l := uint32(0)
r := size
for l < r {
x := ((l + r) >> 1)
if ip < m.ip4[x] {
r = x
continue
}
nip := normalize4(ip, m.prefix4[x])
if nip == m.ip4[x] {
return true
}
l = x + 1
}
return l > 0 && normalize4(ip, m.prefix4[l-1]) == m.ip4[l-1]
}
func less6(a ipv6, b ipv6) bool {
return a.a < b.a || (a.a == b.a && a.b < b.b)
}
func (m *GeoIPMatcher) match6(ip ipv6) bool {
if len(m.ip6) == 0 {
return false
}
if less6(ip, m.ip6[0]) {
return false
}
size := uint32(len(m.ip6))
l := uint32(0)
r := size
for l < r {
x := (l + r) / 2
if less6(ip, m.ip6[x]) {
r = x
continue
}
if normalize6(ip, m.prefix6[x]) == m.ip6[x] {
return true
}
l = x + 1
}
return l > 0 && normalize6(ip, m.prefix6[l-1]) == m.ip6[l-1]
}
// Match returns true if the given ip is included by the GeoIP.
func (m *GeoIPMatcher) Match(ip net.IP) bool {
switch len(ip) {
case 4:
if m.reverseMatch {
return !m.match4(binary.BigEndian.Uint32(ip))
}
return m.match4(binary.BigEndian.Uint32(ip))
case 16:
if m.reverseMatch {
return !m.match6(ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
})
}
return m.match6(ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
})
default:
return false
}
}
// GeoIPMatcherContainer is a container for GeoIPMatchers. It keeps unique copies of GeoIPMatcher by country code.
type GeoIPMatcherContainer struct {
matchers []*GeoIPMatcher
}
// Add adds a new GeoIP set into the container.
// If the country code of GeoIP is not empty, GeoIPMatcherContainer will try to find an existing one, instead of adding a new one.
func (c *GeoIPMatcherContainer) Add(geoip *GeoIP) (*GeoIPMatcher, error) {
if len(geoip.CountryCode) > 0 {
for _, m := range c.matchers {
if m.countryCode == geoip.CountryCode && m.reverseMatch == geoip.ReverseMatch {
return m, nil
}
}
}
m := &GeoIPMatcher{
countryCode: geoip.CountryCode,
reverseMatch: geoip.ReverseMatch,
}
if err := m.Init(geoip.Cidr); err != nil {
return nil, err
}
if len(geoip.CountryCode) > 0 {
c.matchers = append(c.matchers, m)
}
return m, nil
}
var globalGeoIPContainer GeoIPMatcherContainer
type MultiGeoIPMatcher struct {
matchers []*GeoIPMatcher
}
func NewGeoIPMatcher(geoip *GeoIP) (*GeoIPMatcher, error) {
matcher, err := globalGeoIPContainer.Add(geoip)
if err != nil {
return nil, err
}
return matcher, nil
}
func (m *MultiGeoIPMatcher) ApplyIp(ip net.IP) bool {
for _, matcher := range m.matchers {
if matcher.Match(ip) {
return true
}
}
return false
}
func NewMultiGeoIPMatcher(geoips []*GeoIP) (*MultiGeoIPMatcher, error) {
var matchers []*GeoIPMatcher
for _, geoip := range geoips {
matcher, err := globalGeoIPContainer.Add(geoip)
if err != nil {
return nil, err
}
matchers = append(matchers, matcher)
}
matcher := &MultiGeoIPMatcher{
matchers: matchers,
}
return matcher, nil
return len(m.matchers.Match(strings.ToLower(domain))) > 0
}

View File

@ -26,10 +26,14 @@ func ReadFile(path string) ([]byte, error) {
}
func ReadAsset(file string) ([]byte, error) {
return ReadFile(C.Path.GetAssetLocation(file))
return ReadFile(C.Path.Resolve(file))
}
func loadIP(geoipBytes []byte, country string) ([]*router.CIDR, error) {
func loadIP(filename, country string) ([]*router.CIDR, error) {
geoipBytes, err := ReadAsset(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error())
}
var geoipList router.GeoIPList
if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil {
return nil, err
@ -41,10 +45,14 @@ func loadIP(geoipBytes []byte, country string) ([]*router.CIDR, error) {
}
}
return nil, fmt.Errorf("country %s not found", country)
return nil, fmt.Errorf("country not found in %s%s%s", filename, ": ", country)
}
func loadSite(geositeBytes []byte, list string) ([]*router.Domain, error) {
func loadSite(filename, list string) ([]*router.Domain, error) {
geositeBytes, err := ReadAsset(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error())
}
var geositeList router.GeoSiteList
if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
return nil, err
@ -56,33 +64,17 @@ func loadSite(geositeBytes []byte, list string) ([]*router.Domain, error) {
}
}
return nil, fmt.Errorf("list %s not found", list)
return nil, fmt.Errorf("list not found in %s%s%s", filename, ": ", list)
}
type standardLoader struct{}
func (d standardLoader) LoadSiteByPath(filename, list string) ([]*router.Domain, error) {
geositeBytes, err := ReadAsset(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error())
}
return loadSite(geositeBytes, list)
func (d standardLoader) LoadSite(filename, list string) ([]*router.Domain, error) {
return loadSite(filename, list)
}
func (d standardLoader) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) {
return loadSite(geositeBytes, list)
}
func (d standardLoader) LoadIPByPath(filename, country string) ([]*router.CIDR, error) {
geoipBytes, err := ReadAsset(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error())
}
return loadIP(geoipBytes, country)
}
func (d standardLoader) LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) {
return loadIP(geoipBytes, country)
func (d standardLoader) LoadIP(filename, country string) ([]*router.CIDR, error) {
return loadIP(filename, country)
}
func init() {

View File

@ -1,50 +1,11 @@
package geodata
import (
"fmt"
"github.com/Dreamacro/clash/component/geodata/router"
C "github.com/Dreamacro/clash/constant"
)
var geoLoaderName = "memconservative"
// geoLoaderName = "standard"
func LoaderName() string {
return geoLoaderName
}
func SetLoader(newLoader string) {
if newLoader == "memc" {
newLoader = "memconservative"
}
geoLoaderName = newLoader
}
func Verify(name string) error {
switch name {
case C.GeositeName:
_, _, err := LoadGeoSiteMatcher("CN")
return err
case C.GeoipName:
_, _, err := LoadGeoIPMatcher("CN")
return err
default:
return fmt.Errorf("not support name")
}
}
func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) {
if len(countryCode) == 0 {
return nil, 0, fmt.Errorf("country code could not be empty")
}
not := false
if countryCode[0] == '!' {
not = true
countryCode = countryCode[1:]
}
geoLoaderName := "standard"
geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil {
return nil, 0, err
@ -60,44 +21,10 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error)
matcher, err := router.NewDomainMatcher(domains)
mphminimal perfect hash algorithm
*/
matcher, err := router.NewMphMatcherGroup(domains, not)
matcher, err := router.NewMphMatcherGroup(domains)
if err != nil {
return nil, 0, err
}
return matcher, len(domains), nil
}
func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) {
if len(country) == 0 {
return nil, 0, fmt.Errorf("country code could not be empty")
}
geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil {
return nil, 0, err
}
not := false
if country[0] == '!' {
not = true
country = country[1:]
}
records, err := geoLoader.LoadGeoIP(country)
if err != nil {
return nil, 0, err
}
geoIP := &router.GeoIP{
CountryCode: country,
Cidr: records,
ReverseMatch: not,
}
matcher, err := router.NewGeoIPMatcher(geoIP)
if err != nil {
return nil, 0, err
}
return matcher, len(records), nil
}

View File

@ -1,64 +0,0 @@
package http
import (
"context"
"github.com/Dreamacro/clash/listener/inner"
"github.com/Dreamacro/clash/log"
"io"
"net"
"net/http"
URL "net/url"
"strings"
"time"
)
const (
UA = "Clash"
)
func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) {
method = strings.ToUpper(method)
urlRes, err := URL.Parse(url)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, urlRes.String(), body)
for k, v := range header {
for _, v := range v {
req.Header.Add(k, v)
}
}
if _, ok := header["User-Agent"]; !ok {
req.Header.Set("User-Agent", UA)
}
if err != nil {
return nil, err
}
if user := urlRes.User; user != nil {
password, _ := user.Password()
req.SetBasicAuth(user.Username(), password)
}
req = req.WithContext(ctx)
transport := &http.Transport{
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
log.Infoln(urlRes.String())
conn := inner.HandleTcp(address, urlRes.Hostname())
return conn, nil
},
}
client := http.Client{Transport: transport}
return client.Do(req)
}

View File

@ -1,69 +0,0 @@
//go:build !no_script
package js
import (
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/require"
"net/netip"
)
type Context struct {
runtime *goja.Runtime
}
func (c *Context) Resolve(host string, dnsType C.DnsType) []string {
var ips []string
var ipAddrs []netip.Addr
var err error
switch dnsType {
case C.IPv4:
ipAddrs, err = resolver.ResolveAllIPv4(host)
case C.IPv6:
ipAddrs, err = resolver.ResolveAllIPv6(host)
case C.All:
ipAddrs, err = resolver.ResolveAllIP(host)
}
if err != nil {
log.Errorln("Script resolve %s failed, error: %v", host, err)
return ips
}
for _, addr := range ipAddrs {
ips = append(ips, addr.String())
}
return ips
}
func newContext() require.ModuleLoader {
return func(runtime *goja.Runtime, object *goja.Object) {
ctx := Context{
runtime: runtime,
}
o := object.Get("exports").(*goja.Object)
o.Set("resolve", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return runtime.ToValue([]string{})
}
host := call.Argument(0).String()
dnsType := C.IPv4
if len(call.Arguments) == 2 {
dnsType = int(call.Argument(1).ToInteger())
}
ips := ctx.Resolve(host, C.DnsType(dnsType))
return runtime.ToValue(ips)
})
}
}
func enable(rt *goja.Runtime) {
rt.Set("context", require.Require(rt, "context"))
}

View File

@ -1,60 +0,0 @@
//go:build !no_script
package js
import (
"github.com/Dreamacro/clash/log"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/eventloop"
"github.com/dop251/goja_nodejs/require"
)
func init() {
logPrinter := console.RequireWithPrinter(&JsLog{})
require.RegisterNativeModule("console", logPrinter)
contextFuncLoader := newContext()
require.RegisterNativeModule("context", contextFuncLoader)
}
func preSetting(rt *goja.Runtime) {
registry := new(require.Registry)
registry.Enable(rt)
console.Enable(rt)
enable(rt)
eventloop.EnableConsole(true)
}
func getLoop() *eventloop.EventLoop {
loop := eventloop.NewEventLoop(func(loop *eventloop.EventLoop) {
loop.Run(func(runtime *goja.Runtime) {
preSetting(runtime)
})
})
return loop
}
func compiler(name, code string) (*goja.Program, error) {
return goja.Compile(name, code, false)
}
func run(loop *eventloop.EventLoop, program *goja.Program, args map[string]any, callback func(any, error)) {
loop.Run(func(runtime *goja.Runtime) {
for k, v := range args {
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
err := runtime.Set(k, v)
if err != nil {
log.Errorln("Args to script failed, %s", err.Error())
}
}
v, err := runtime.RunProgram(program)
if v == nil {
callback(nil, err)
} else {
callback(v.Export(), err)
}
})
}

View File

@ -1,20 +0,0 @@
//go:build !no_script
package js
import "github.com/Dreamacro/clash/log"
type JsLog struct {
}
func (j JsLog) Log(s string) {
log.Infoln("[JS] %s", s)
}
func (j JsLog) Warn(s string) {
log.Warnln("[JS] %s", s)
}
func (j JsLog) Error(s string) {
log.Errorln("[JS] %s", s)
}

View File

@ -1,34 +0,0 @@
//go:build !no_script
package js
import (
"github.com/dop251/goja"
"sync"
)
var JS sync.Map
var mux sync.Mutex
func NewJS(name, code string) error {
program, err := compiler(name, code)
if err != nil {
return err
}
if _, ok := JS.Load(name); !ok {
mux.Lock()
defer mux.Unlock()
if _, ok := JS.Load(name); !ok {
JS.Store(name, program)
}
}
return nil
}
func Run(name string, args map[string]any, callback func(any, error)) {
if value, ok := JS.Load(name); ok {
run(getLoop(), value.(*goja.Program), args, callback)
}
}

View File

@ -1,12 +0,0 @@
//go:build no_script
package js
import "fmt"
func NewJS(name, code string) error {
fmt.Errorf("unsupported script on the build")
}
func Run(name string, args map[string]any, callback func(any, error)) {
}

View File

@ -1,11 +1,12 @@
package mmdb
import (
"github.com/oschwald/geoip2-golang"
"sync"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/oschwald/geoip2-golang"
)
var (

View File

@ -2,18 +2,17 @@ package process
import (
"errors"
"github.com/Dreamacro/clash/common/nnip"
C "github.com/Dreamacro/clash/constant"
"net"
"net/netip"
"github.com/Dreamacro/clash/common/nnip"
C "github.com/Dreamacro/clash/constant"
)
var (
ErrInvalidNetwork = errors.New("invalid network")
ErrPlatformNotSupport = errors.New("not support on this platform")
ErrNotFound = errors.New("process not found")
enableFindProcess = true
)
const (
@ -21,26 +20,12 @@ const (
UDP = "udp"
)
func EnableFindProcess(e bool) {
enableFindProcess = e
}
func FindProcessName(network string, srcIP netip.Addr, srcPort int) (int32, string, error) {
func FindProcessName(network string, srcIP netip.Addr, srcPort int) (string, error) {
return findProcessName(network, srcIP, srcPort)
}
func FindUid(network string, srcIP netip.Addr, srcPort int) (int32, error) {
_, uid, err := resolveSocketByNetlink(network, srcIP, srcPort)
if err != nil {
return -1, err
}
return uid, nil
}
func ShouldFindProcess(metadata *C.Metadata) bool {
if !enableFindProcess ||
metadata.Process != "" ||
metadata.ProcessPath != "" {
if metadata.Process != "" {
return false
}
for _, ip := range localIPs {

View File

@ -17,11 +17,7 @@ const (
proccallnumpidinfo = 0x2
)
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
return 0, 0, ErrPlatformNotSupport
}
func findProcessName(network string, ip netip.Addr, port int) (int32, string, error) {
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
var spath string
switch network {
case TCP:
@ -29,14 +25,14 @@ func findProcessName(network string, ip netip.Addr, port int) (int32, string, er
case UDP:
spath = "net.inet.udp.pcblist_n"
default:
return -1, "", ErrInvalidNetwork
return "", ErrInvalidNetwork
}
isIPv4 := ip.Is4()
value, err := syscall.Sysctl(spath)
if err != nil {
return -1, "", err
return "", err
}
buf := []byte(value)
@ -81,11 +77,10 @@ func findProcessName(network string, ip netip.Addr, port int) (int32, string, er
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
pp, err := getExecPathFromPID(pid)
return -1, pp, err
return getExecPathFromPID(pid)
}
return -1, "", ErrNotFound
return "", ErrNotFound
}
func getExecPathFromPID(pid uint32) (string, error) {

View File

@ -21,11 +21,7 @@ var (
once sync.Once
)
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
return 0, 0, ErrPlatformNotSupport
}
func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, error) {
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
once.Do(func() {
if err := initSearcher(); err != nil {
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
@ -35,7 +31,7 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string,
})
if defaultSearcher == nil {
return -1, "", ErrPlatformNotSupport
return "", ErrPlatformNotSupport
}
var spath string
@ -46,22 +42,21 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string,
case UDP:
spath = "net.inet.udp.pcblist"
default:
return -1, "", ErrInvalidNetwork
return "", ErrInvalidNetwork
}
value, err := syscall.Sysctl(spath)
if err != nil {
return -1, "", err
return "", err
}
buf := []byte(value)
pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP)
if err != nil {
return -1, "", err
return "", err
}
pp, err := getExecPathFromPID(pid)
return -1, pp, err
return getExecPathFromPID(pid)
}
func getExecPathFromPID(pid uint32) (string, error) {

View File

@ -8,8 +8,6 @@ import (
"net/netip"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"syscall"
"unicode"
@ -34,13 +32,13 @@ const (
pathProc = "/proc"
)
func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, error) {
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
inode, uid, err := resolveSocketByNetlink(network, ip, srcPort)
if err != nil {
return -1, "", err
return "", err
}
pp, err := resolveProcessNameByProcSearch(inode, uid)
return uid, pp, err
return resolveProcessNameByProcSearch(inode, uid)
}
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
@ -110,7 +108,7 @@ func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32,
return 0, 0, fmt.Errorf("netlink message: NLMSG_ERROR")
}
inode, uid := unpackSocketDiagResponse(&message)
inode, uid := unpackSocketDiagResponse(&messages[0])
if inode < 0 || uid < 0 {
return 0, 0, fmt.Errorf("invalid inode(%d) or uid(%d)", inode, uid)
}
@ -198,19 +196,8 @@ func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
continue
}
if runtime.GOOS == "android" {
if bytes.Equal(buffer[:n], socket) {
cmdline, err := os.ReadFile(path.Join(processPath, "cmdline"))
if err != nil {
return "", err
}
return splitCmdline(cmdline), nil
}
} else {
if bytes.Equal(buffer[:n], socket) {
return os.Readlink(path.Join(processPath, "exe"))
}
if bytes.Equal(buffer[:n], socket) {
return os.Readlink(path.Join(processPath, "exe"))
}
}
}
@ -218,19 +205,6 @@ func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
}
func splitCmdline(cmdline []byte) string {
cmdline = bytes.Trim(cmdline, " ")
idx := bytes.IndexFunc(cmdline, func(r rune) bool {
return unicode.IsControl(r) || unicode.IsSpace(r)
})
if idx == -1 {
return filepath.Base(string(cmdline))
}
return filepath.Base(string(cmdline[:idx]))
}
func isPid(s string) bool {
return strings.IndexFunc(s, func(r rune) bool {
return !unicode.IsDigit(r)

View File

@ -4,10 +4,6 @@ package process
import "net/netip"
func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, error) {
return -1, "", ErrPlatformNotSupport
}
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
return 0, 0, ErrPlatformNotSupport
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
return "", ErrPlatformNotSupport
}

View File

@ -29,10 +29,6 @@ var (
once sync.Once
)
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
return 0, 0, ErrPlatformNotSupport
}
func initWin32API() error {
h, err := windows.LoadLibrary("iphlpapi.dll")
if err != nil {
@ -62,7 +58,7 @@ func initWin32API() error {
return nil
}
func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string, error) {
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
once.Do(func() {
err := initWin32API()
if err != nil {
@ -86,22 +82,21 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (int32, string,
fn = getExUDPTable
class = udpTablePid
default:
return -1, "", ErrInvalidNetwork
return "", ErrInvalidNetwork
}
buf, err := getTransportTable(fn, family, class)
if err != nil {
return -1, "", err
return "", err
}
s := newSearcher(family == windows.AF_INET, network == TCP)
pid, err := s.Search(buf, ip, uint16(srcPort))
if err != nil {
return -1, "", err
return "", err
}
pp, err := getExecPathFromPID(pid)
return -1, pp, err
return getExecPathFromPID(pid)
}
type searcher struct {
@ -220,7 +215,8 @@ func getExecPathFromPID(pid uint32) (string, error) {
uintptr(h),
uintptr(1),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)))
uintptr(unsafe.Pointer(&size)),
)
if r1 == 0 {
return "", err
}

View File

@ -40,10 +40,6 @@ type Resolver interface {
ResolveIP(host string) (ip netip.Addr, err error)
ResolveIPv4(host string) (ip netip.Addr, err error)
ResolveIPv6(host string) (ip netip.Addr, err error)
ResolveAllIP(host string) (ip []netip.Addr, err error)
ResolveAllIPPrimaryIPv4(host string) (ips []netip.Addr, err error)
ResolveAllIPv4(host string) (ips []netip.Addr, err error)
ResolveAllIPv6(host string) (ips []netip.Addr, err error)
}
// ResolveIPv4 with a host, return ipv4
@ -52,11 +48,43 @@ func ResolveIPv4(host string) (netip.Addr, error) {
}
func ResolveIPv4WithResolver(host string, r Resolver) (netip.Addr, error) {
if ips, err := ResolveAllIPv4WithResolver(host, r); err == nil {
return ips[rand.Intn(len(ips))], nil
} else {
return netip.Addr{}, nil
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data; ip.Is4() {
return ip, nil
}
}
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is4() {
return ip, nil
}
return netip.Addr{}, ErrIPVersion
}
if r != nil {
return r.ResolveIPv4(host)
}
if DefaultResolver == nil {
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
if err != nil {
return netip.Addr{}, err
} else if len(ipAddrs) == 0 {
return netip.Addr{}, ErrIPNotFound
}
ip := ipAddrs[rand.Intn(len(ipAddrs))].To4()
if ip == nil {
return netip.Addr{}, ErrIPVersion
}
return netip.AddrFrom4(*(*[4]byte)(ip)), nil
}
return netip.Addr{}, ErrIPNotFound
}
// ResolveIPv6 with a host, return ipv6
@ -65,20 +93,74 @@ func ResolveIPv6(host string) (netip.Addr, error) {
}
func ResolveIPv6WithResolver(host string, r Resolver) (netip.Addr, error) {
if ips, err := ResolveAllIPv6WithResolver(host, r); err == nil {
return ips[rand.Intn(len(ips))], nil
} else {
return netip.Addr{}, err
if DisableIPv6 {
return netip.Addr{}, ErrIPv6Disabled
}
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data; ip.Is6() {
return ip, nil
}
}
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is6() {
return ip, nil
}
return netip.Addr{}, ErrIPVersion
}
if r != nil {
return r.ResolveIPv6(host)
}
if DefaultResolver == nil {
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host)
if err != nil {
return netip.Addr{}, err
} else if len(ipAddrs) == 0 {
return netip.Addr{}, ErrIPNotFound
}
return netip.AddrFrom16(*(*[16]byte)(ipAddrs[rand.Intn(len(ipAddrs))])), nil
}
return netip.Addr{}, ErrIPNotFound
}
// ResolveIPWithResolver same as ResolveIP, but with a resolver
func ResolveIPWithResolver(host string, r Resolver) (netip.Addr, error) {
if ips, err := ResolveAllIPPrimaryIPv4WithResolver(host, r); err == nil {
return ips[rand.Intn(len(ips))], nil
} else {
return netip.Addr{}, err
if node := DefaultHosts.Search(host); node != nil {
return node.Data, nil
}
if r != nil {
if DisableIPv6 {
return r.ResolveIPv4(host)
}
return r.ResolveIP(host)
} else if DisableIPv6 {
return ResolveIPv4(host)
}
ip, err := netip.ParseAddr(host)
if err == nil {
return ip, nil
}
if DefaultResolver == nil {
ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return netip.Addr{}, err
}
return nnip.IpToAddr(ipAddr.IP), nil
}
return netip.Addr{}, ErrIPNotFound
}
// ResolveIP with a host, return ip
@ -89,217 +171,23 @@ func ResolveIP(host string) (netip.Addr, error) {
// ResolveIPv4ProxyServerHost proxies server host only
func ResolveIPv4ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
if ip, err := ResolveIPv4WithResolver(host, ProxyServerHostResolver); err != nil {
return ResolveIPv4(host)
} else {
return ip, nil
}
return ResolveIPv4WithResolver(host, ProxyServerHostResolver)
}
return ResolveIPv4(host)
}
// ResolveIPv6ProxyServerHost proxies server host only
func ResolveIPv6ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
if ip, err := ResolveIPv6WithResolver(host, ProxyServerHostResolver); err != nil {
return ResolveIPv6(host)
} else {
return ip, nil
}
return ResolveIPv6WithResolver(host, ProxyServerHostResolver)
}
return ResolveIPv6(host)
}
// ResolveProxyServerHost proxies server host only
func ResolveProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
if ip, err := ResolveIPWithResolver(host, ProxyServerHostResolver); err != nil {
return ResolveIP(host)
} else {
return ip, err
}
return ResolveIPWithResolver(host, ProxyServerHostResolver)
}
return ResolveIP(host)
}
func ResolveAllIPv6WithResolver(host string, r Resolver) ([]netip.Addr, error) {
if DisableIPv6 {
return []netip.Addr{}, ErrIPv6Disabled
}
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data; ip.Is6() {
return []netip.Addr{ip}, nil
}
}
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is6() {
return []netip.Addr{ip}, nil
}
return []netip.Addr{}, ErrIPVersion
}
if r != nil {
return r.ResolveAllIPv6(host)
}
if DefaultResolver == nil {
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host)
if err != nil {
return []netip.Addr{}, err
} else if len(ipAddrs) == 0 {
return []netip.Addr{}, ErrIPNotFound
}
return []netip.Addr{netip.AddrFrom16(*(*[16]byte)(ipAddrs[rand.Intn(len(ipAddrs))]))}, nil
}
return []netip.Addr{}, ErrIPNotFound
}
func ResolveAllIPv4WithResolver(host string, r Resolver) ([]netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data; ip.Is4() {
return []netip.Addr{node.Data}, nil
}
}
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is4() {
return []netip.Addr{ip}, nil
}
return []netip.Addr{}, ErrIPVersion
}
if r != nil {
return r.ResolveAllIPv4(host)
}
if DefaultResolver == nil {
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
if err != nil {
return []netip.Addr{}, err
} else if len(ipAddrs) == 0 {
return []netip.Addr{}, ErrIPNotFound
}
ip := ipAddrs[rand.Intn(len(ipAddrs))].To4()
if ip == nil {
return []netip.Addr{}, ErrIPVersion
}
return []netip.Addr{netip.AddrFrom4(*(*[4]byte)(ip))}, nil
}
return []netip.Addr{}, ErrIPNotFound
}
func ResolveAllIPWithResolver(host string, r Resolver) ([]netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil {
return []netip.Addr{node.Data}, nil
}
if r != nil {
if DisableIPv6 {
return r.ResolveAllIPv4(host)
}
return r.ResolveAllIP(host)
} else if DisableIPv6 {
return ResolveAllIPv4(host)
}
ip, err := netip.ParseAddr(host)
if err == nil {
return []netip.Addr{ip}, nil
}
if DefaultResolver == nil {
ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return []netip.Addr{}, err
}
return []netip.Addr{nnip.IpToAddr(ipAddr.IP)}, nil
}
return []netip.Addr{}, ErrIPNotFound
}
func ResolveAllIPPrimaryIPv4WithResolver(host string, r Resolver) ([]netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil {
return []netip.Addr{node.Data}, nil
}
if r != nil {
if DisableIPv6 {
return r.ResolveAllIPv4(host)
}
return r.ResolveAllIPPrimaryIPv4(host)
} else if DisableIPv6 {
return ResolveAllIPv4(host)
}
ip, err := netip.ParseAddr(host)
if err == nil {
return []netip.Addr{ip}, nil
}
if DefaultResolver == nil {
ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return []netip.Addr{}, err
}
return []netip.Addr{nnip.IpToAddr(ipAddr.IP)}, nil
}
return []netip.Addr{}, ErrIPNotFound
}
func ResolveAllIP(host string) ([]netip.Addr, error) {
return ResolveAllIPWithResolver(host, DefaultResolver)
}
func ResolveAllIPv4(host string) ([]netip.Addr, error) {
return ResolveAllIPv4WithResolver(host, DefaultResolver)
}
func ResolveAllIPv6(host string) ([]netip.Addr, error) {
return ResolveAllIPv6WithResolver(host, DefaultResolver)
}
func ResolveAllIPv6ProxyServerHost(host string) ([]netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveAllIPv6WithResolver(host, ProxyServerHostResolver)
}
return ResolveAllIPv6(host)
}
func ResolveAllIPv4ProxyServerHost(host string) ([]netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveAllIPv4WithResolver(host, ProxyServerHostResolver)
}
return ResolveAllIPv4(host)
}
func ResolveAllIPProxyServerHost(host string) ([]netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveAllIPWithResolver(host, ProxyServerHostResolver)
}
return ResolveAllIP(host)
}

View File

@ -0,0 +1,10 @@
//go:build build_actions
package script
/*
#cgo windows,amd64 CFLAGS: -ID:/python-amd64/include -DMS_WIN64
#cgo windows,amd64 LDFLAGS: -LD:/python-amd64/libs -lpython39 -lpthread -lm
*/
import "C"

View File

@ -0,0 +1,8 @@
//go:build build_local
package script
/*
#cgo pkg-config: python3-embed
*/
import "C"

View File

@ -0,0 +1,24 @@
//go:build !build_local
package script
/*
#cgo linux,amd64 pkg-config: python3-embed
#cgo darwin,amd64 CFLAGS: -I/build/python/python-3.9.7-darwin-amd64/include/python3.9
#cgo darwin,arm64 CFLAGS: -I/build/python/python-3.9.7-darwin-arm64/include/python3.9
#cgo windows,amd64 CFLAGS: -I/build/python/python-3.9.7-windows-amd64/include -DMS_WIN64
#cgo windows,386 CFLAGS: -I/build/python/python-3.9.7-windows-386/include
//#cgo linux,amd64 CFLAGS: -I/home/runner/work/clash/clash/bin/python/python-3.9.7-linux-amd64/include/python3.9
//#cgo linux,arm64 CFLAGS: -I/build/python/python-3.9.7-linux-arm64/include/python3.9
//#cgo linux,386 CFLAGS: -I/build/python/python-3.9.7-linux-386/include/python3.9
#cgo darwin,amd64 LDFLAGS: -L/build/python/python-3.9.7-darwin-amd64/lib -lpython3.9 -ldl -framework CoreFoundation
#cgo darwin,arm64 LDFLAGS: -L/build/python/python-3.9.7-darwin-arm64/lib -lpython3.9 -ldl -framework CoreFoundation
#cgo windows,amd64 LDFLAGS: -L/build/python/python-3.9.7-windows-amd64/lib -lpython39 -lpthread -lm
#cgo windows,386 LDFLAGS: -L/build/python/python-3.9.7-windows-386/lib -lpython39 -lpthread -lm
//#cgo linux,amd64 LDFLAGS: -L/home/runner/work/clash/clash/bin/python/python-3.9.7-linux-amd64/lib -lpython3.9 -lpthread -ldl -lutil -lm
//#cgo linux,arm64 LDFLAGS: -L/build/python/python-3.9.7-linux-arm64/lib -lpython3.9 -lpthread -ldl -lutil -lm
//#cgo linux,386 LDFLAGS: -L/build/python/python-3.9.7-linux-386/lib -lpython3.9 -lpthread -ldl -lutil -lm
*/
import "C"

View File

@ -0,0 +1,742 @@
#define PY_SSIZE_T_CLEAN
#include "clash_module.h"
#include <structmember.h>
PyObject *clash_module;
PyObject *main_fn;
PyObject *clash_context;
// init_python
void init_python(const char *program, const char *path) {
// Py_NoSiteFlag = 1;
// Py_FrozenFlag = 1;
// Py_IgnoreEnvironmentFlag = 1;
// Py_IsolatedFlag = 1;
append_inittab();
wchar_t *programName = Py_DecodeLocale(program, NULL);
if (programName != NULL) {
Py_SetProgramName(programName);
PyMem_RawFree(programName);
}
// wchar_t *newPath = Py_DecodeLocale(path, NULL);
// if (newPath != NULL) {
// Py_SetPath(newPath);
// PyMem_RawFree(newPath);
// }
// Py_Initialize();
Py_InitializeEx(0);
char *pathPrefix = "import sys; sys.path.append('";
char *pathSuffix = "')";
char *newPath = (char *) malloc(strlen(pathPrefix) + strlen(path) + strlen(pathSuffix));
sprintf(newPath, "%s%s%s", pathPrefix, path, pathSuffix);
PyRun_SimpleString(newPath);
free(newPath);
/* Optionally import the module; alternatively,
import can be deferred until the embedded script
imports it. */
clash_module = PyImport_ImportModule("clash");
}
// Load function, same as "import module_name.func_name as obj" in Python
// Returns the function object or NULL if not found
PyObject *load_func(const char *module_name, char *func_name) {
// Import the module
PyObject *py_mod_name = PyUnicode_FromString(module_name);
if (py_mod_name == NULL) {
return NULL;
}
PyObject *module = PyImport_Import(py_mod_name);
Py_DECREF(py_mod_name);
if (module == NULL) {
return NULL;
}
// Get function, same as "getattr(module, func_name)" in Python
PyObject *func = PyObject_GetAttrString(module, func_name);
Py_DECREF(module);
return func;
}
// Return last error as char *, NULL if there was no error
const char *py_last_error() {
PyObject *err = PyErr_Occurred();
if (err == NULL) {
return NULL;
}
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
if (value == NULL) {
return NULL;
}
PyObject *str = PyObject_Str(value);
const char *utf8 = PyUnicode_AsUTF8(str);
Py_DECREF(str);
PyErr_Clear();
return utf8;
}
void py_clear(PyObject *obj) {
Py_CLEAR(obj);
}
void load_main_func() {
main_fn = load_func(CLASH_SCRIPT_MODULE_NAME, "main");
}
/** callback function, that call go function by python3 script. **/
resolve_ip_callback resolve_ip_callback_fn;
geoip_callback geoip_callback_fn;
rule_provider_callback rule_provider_callback_fn;
log_callback log_callback_fn;
void
set_resolve_ip_callback(resolve_ip_callback cb)
{
resolve_ip_callback_fn = cb;
}
void
set_geoip_callback(geoip_callback cb)
{
geoip_callback_fn = cb;
}
void
set_rule_provider_callback(rule_provider_callback cb)
{
rule_provider_callback_fn = cb;
}
void
set_log_callback(log_callback cb)
{
log_callback_fn = cb;
}
/** end callback function **/
/* --------------------------------------------------------------------- */
/* RuleProvider objects */
typedef struct {
PyObject_HEAD
PyObject *name; /* rule provider name */
} RuleProviderObject;
static int
RuleProvider_traverse(RuleProviderObject *self, visitproc visit, void *arg)
{
Py_VISIT(self->name);
return 0;
}
static int
RuleProvider_clear(RuleProviderObject *self)
{
Py_CLEAR(self->name);
return 0;
}
static void
RuleProvider_dealloc(RuleProviderObject *self)
{
PyObject_GC_UnTrack(self);
RuleProvider_clear(self);
Py_TYPE(self)->tp_free((PyObject *) self);
}
static PyObject *
RuleProvider_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
RuleProviderObject *self;
self = (RuleProviderObject *) type->tp_alloc(type, 0);
if (self != NULL) {
self->name = PyUnicode_FromString("");
if (self->name == NULL) {
Py_DECREF(self);
return NULL;
}
}
return (PyObject *) self;
}
static int
RuleProvider_init(RuleProviderObject *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"name", NULL};
PyObject *name = NULL, *tmp;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Us", kwlist, &name))
return -1;
if (name) {
tmp = self->name;
Py_INCREF(name);
self->name = name;
Py_DECREF(tmp);
}
return 0;
}
//static PyMemberDef RuleProvider_members[] = {
// {"adapter_type", T_STRING, offsetof(RuleProviderObject, adapter_type), 0,
// "adapter type"},
// {NULL} /* Sentinel */
//};
static PyObject *
RuleProvider_getname(RuleProviderObject *self, void *closure)
{
Py_INCREF(self->name);
return self->name;
}
static int
RuleProvider_setname(RuleProviderObject *self, PyObject *value, void *closure)
{
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "Cannot delete the name attribute");
return -1;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"The name attribute value must be a string");
return -1;
}
Py_INCREF(value);
Py_CLEAR(self->name);
self->name = value;
return 0;
}
static PyGetSetDef RuleProvider_getsetters[] = {
{"name", (getter) RuleProvider_getname, (setter) RuleProvider_setname,
"name", NULL},
{NULL} /* Sentinel */
};
static PyObject *
RuleProvider_name(RuleProviderObject *self, PyObject *Py_UNUSED(ignored))
{
Py_INCREF(self->name);
return self->name;
}
static PyObject *
RuleProvider_match(RuleProviderObject *self, PyObject *args)
{
PyObject *result;
PyObject *tmp;
const char *provider_name;
if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &tmp)) //Format "O","O!","O&": Borrowed reference.
return NULL;
if (tmp == NULL)
Py_RETURN_FALSE;
Py_INCREF(tmp);
// PyObject *py_src_port = PyDict_GetItemString(tmp, "src_port"); //Return value: Borrowed reference.
// PyObject *py_dst_port = PyDict_GetItemString(tmp, "dst_port"); //Return value: Borrowed reference.
// Py_INCREF(py_src_port);
// Py_INCREF(py_dst_port);
// char *c_src_port = (char *) malloc(PyLong_AsSize_t(py_src_port));
// char *c_dst_port = (char *) malloc(PyLong_AsSize_t(py_dst_port));
// sprintf(c_src_port, "%ld", PyLong_AsLong(py_src_port));
// sprintf(c_dst_port, "%ld", PyLong_AsLong(py_dst_port));
struct Metadata metadata = {
.type = PyUnicode_AsUTF8(PyDict_GetItemString(tmp, "type")), // PyDict_GetItemString() Return value: Borrowed reference.
.network = PyUnicode_AsUTF8(PyDict_GetItemString(tmp, "network")),
.process_name = PyUnicode_AsUTF8(PyDict_GetItemString(tmp, "process_name")),
.process_path = PyUnicode_AsUTF8(PyDict_GetItemString(tmp, "process_path")),
.host = PyUnicode_AsUTF8(PyDict_GetItemString(tmp, "host")),
.src_ip = PyUnicode_AsUTF8(PyDict_GetItemString(tmp, "src_ip")),
.src_port = (unsigned short)PyLong_AsUnsignedLong(PyDict_GetItemString(tmp, "src_port")),
.dst_ip = PyUnicode_AsUTF8(PyDict_GetItemString(tmp, "dst_ip")),
.dst_port = (unsigned short)PyLong_AsUnsignedLong(PyDict_GetItemString(tmp, "dst_port"))
};
// Py_DECREF(py_src_port);
// Py_DECREF(py_dst_port);
Py_INCREF(self->name);
provider_name = PyUnicode_AsUTF8(self->name);
Py_DECREF(self->name);
Py_DECREF(tmp);
int rs = rule_provider_callback_fn(provider_name, &metadata);
result = (rs == 1) ? Py_True : Py_False;
Py_INCREF(result);
return result;
}
static PyMethodDef RuleProvider_methods[] = {
{"name", (PyCFunction) RuleProvider_name, METH_NOARGS,
"Return the RuleProvider name"
},
{"match", (PyCFunction) RuleProvider_match, METH_VARARGS,
"Match the rule by the RuleProvider, match(metadata) -> boolean"
},
{NULL} /* Sentinel */
};
static PyTypeObject RuleProviderType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "clash.RuleProvider",
.tp_doc = "Clash RuleProvider objects",
.tp_basicsize = sizeof(RuleProviderObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
.tp_new = RuleProvider_new,
.tp_init = (initproc) RuleProvider_init,
.tp_dealloc = (destructor) RuleProvider_dealloc,
.tp_traverse = (traverseproc) RuleProvider_traverse,
.tp_clear = (inquiry) RuleProvider_clear,
// .tp_members = RuleProvider_members,
.tp_methods = RuleProvider_methods,
.tp_getset = RuleProvider_getsetters,
};
/* end RuleProvider objects */
/* --------------------------------------------------------------------- */
/* Context objects */
typedef struct {
PyObject_HEAD
PyObject *rule_providers; /* Dict<String, RuleProvider> */
} ContextObject;
static int
Context_traverse(ContextObject *self, visitproc visit, void *arg)
{
Py_VISIT(self->rule_providers);
return 0;
}
static int
Context_clear(ContextObject *self)
{
Py_CLEAR(self->rule_providers);
return 0;
}
static void
Context_dealloc(ContextObject *self)
{
PyObject_GC_UnTrack(self);
Context_clear(self);
Py_TYPE(self)->tp_free((PyObject *) self);
}
static PyObject *
Context_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
ContextObject *self;
self = (ContextObject *) type->tp_alloc(type, 0);
if (self != NULL) {
self->rule_providers = PyDict_New();
if (self->rule_providers == NULL) {
Py_DECREF(self);
return NULL;
}
}
return (PyObject *) self;
}
static int
Context_init(ContextObject *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"rule_providers", NULL};
PyObject *rule_providers = NULL, *tmp;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist,
&rule_providers))
return -1;
if (rule_providers) {
tmp = self->rule_providers;
Py_INCREF(rule_providers);
self->rule_providers = rule_providers;
Py_DECREF(tmp);
}
return 0;
}
static PyObject *
Context_getrule_providers(ContextObject *self, void *closure)
{
Py_INCREF(self->rule_providers);
return self->rule_providers;
}
static int
Context_setrule_providers(ContextObject *self, PyObject *value, void *closure)
{
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "Cannot delete the rule_providers attribute");
return -1;
}
if (!PyDict_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"The rule_providers attribute value must be a dict");
return -1;
}
Py_INCREF(value);
Py_CLEAR(self->rule_providers);
self->rule_providers = value;
return 0;
}
static PyGetSetDef Context_getsetters[] = {
{"rule_providers", (getter) Context_getrule_providers, (setter) Context_setrule_providers,
"rule_providers", NULL},
{NULL} /* Sentinel */
};
static PyObject *
Context_resolve_ip(PyObject *self, PyObject *args)
{
const char *host;
const char *ip;
if (!PyArg_ParseTuple(args, "s", &host))
return NULL;
if (host == NULL)
return PyUnicode_FromString("");
ip = resolve_ip_callback_fn(host);
return PyUnicode_FromString(ip);
}
static PyObject *
Context_geoip(PyObject *self, PyObject *args)
{
const char *ip;
const char *countryCode;
if (!PyArg_ParseTuple(args, "s", &ip))
return NULL;
if (ip == NULL)
return PyUnicode_FromString("");
countryCode = geoip_callback_fn(ip);
return PyUnicode_FromString(countryCode);
}
static PyObject *
Context_log(PyObject *self, PyObject *args)
{
const char *msg;
if (!PyArg_ParseTuple(args, "s", &msg))
return NULL;
log_callback_fn(msg);
Py_RETURN_NONE;
}
static PyMethodDef Context_methods[] = {
{"resolve_ip", (PyCFunction) Context_resolve_ip, METH_VARARGS,
"resolve_ip(host) -> string"
},
{"geoip", (PyCFunction) Context_geoip, METH_VARARGS,
"geoip(ip) -> string"
},
{"log", (PyCFunction) Context_log, METH_VARARGS,
"log(msg) -> void"
},
{NULL} /* Sentinel */
};
static PyTypeObject ContextType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "clash.Context",
.tp_doc = "Clash Context objects",
.tp_basicsize = sizeof(ContextObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
.tp_new = Context_new,
.tp_init = (initproc) Context_init,
.tp_dealloc = (destructor) Context_dealloc,
.tp_traverse = (traverseproc) Context_traverse,
.tp_clear = (inquiry) Context_clear,
.tp_methods = Context_methods,
.tp_getset = Context_getsetters,
};
static PyModuleDef clashmodule = {
PyModuleDef_HEAD_INIT,
.m_name = "clash",
.m_doc = "Clash module that creates an extension module for python3.",
.m_size = -1,
};
PyMODINIT_FUNC
PyInit_clash(void)
{
PyObject *m;
m = PyModule_Create(&clashmodule);
if (m == NULL)
return NULL;
if (PyType_Ready(&RuleProviderType) < 0)
return NULL;
Py_INCREF(&RuleProviderType);
if (PyModule_AddObject(m, "RuleProvider", (PyObject *) &RuleProviderType) < 0) {
Py_DECREF(&RuleProviderType);
Py_DECREF(m);
return NULL;
}
if (PyType_Ready(&ContextType) < 0)
return NULL;
Py_INCREF(&ContextType);
if (PyModule_AddObject(m, "Context", (PyObject *) &ContextType) < 0) {
Py_DECREF(&ContextType);
Py_DECREF(m);
return NULL;
}
return m;
}
/* end Context objects */
/* --------------------------------------------------------------------- */
void
append_inittab()
{
/* Add a built-in module, before Py_Initialize */
PyImport_AppendInittab("clash", PyInit_clash);
}
int new_clash_py_context(const char *provider_name_arr[], int size) {
PyObject *dict = PyDict_New(); //Return value: New reference.
if (dict == NULL) {
PyErr_SetString(PyExc_TypeError,
"PyDict_New failure");
return 0;
}
for (int i = 0; i < size; i++) {
PyObject *rule_provider = RuleProvider_new(&RuleProviderType, NULL, NULL);
if (rule_provider == NULL) {
Py_DECREF(dict);
PyErr_SetString(PyExc_TypeError,
"RuleProvider_new failure");
return 0;
}
RuleProviderObject *providerObj = (RuleProviderObject *) rule_provider;
PyObject *py_name = PyUnicode_FromString(provider_name_arr[i]); //Return value: New reference.
RuleProvider_setname(providerObj, py_name, NULL);
Py_DECREF(py_name);
PyDict_SetItemString(dict, provider_name_arr[i], rule_provider); //Parameter value: New reference.
Py_DECREF(rule_provider);
}
clash_context = Context_new(&ContextType, NULL, NULL);
if (clash_context == NULL) {
Py_DECREF(dict);
PyErr_SetString(PyExc_TypeError,
"Context_new failure");
return 0;
}
Context_setrule_providers((ContextObject *) clash_context, dict, NULL);
Py_DECREF(dict);
return 1;
}
const char *call_main(
const char *type,
const char *network,
const char *process_name,
const char *process_path,
const char *host,
const char *src_ip,
unsigned short src_port,
const char *dst_ip,
unsigned short dst_port) {
PyObject *metadataDict;
PyObject *tupleArgs;
PyObject *result;
metadataDict = PyDict_New(); //Return value: New reference.
if (metadataDict == NULL) {
PyErr_SetString(PyExc_TypeError,
"PyDict_New failure");
return "-1";
}
PyObject *p_type = PyUnicode_FromString(type); //Return value: New reference.
PyObject *p_network = PyUnicode_FromString(network); //Return value: New reference.
PyObject *p_process_name = PyUnicode_FromString(process_name); //Return value: New reference.
PyObject *p_process_path = PyUnicode_FromString(process_path); //Return value: New reference.
PyObject *p_host = PyUnicode_FromString(host); //Return value: New reference.
PyObject *p_src_ip = PyUnicode_FromString(src_ip); //Return value: New reference.
PyObject *p_src_port = PyLong_FromUnsignedLong((unsigned long)src_port); //Return value: New reference.
PyObject *p_dst_ip = PyUnicode_FromString(dst_ip); //Return value: New reference.
PyObject *p_dst_port = PyLong_FromUnsignedLong((unsigned long)dst_port); //Return value: New reference.
PyDict_SetItemString(metadataDict, "type", p_type); //Parameter value: New reference.
PyDict_SetItemString(metadataDict, "network", p_network); //Parameter value: New reference.
PyDict_SetItemString(metadataDict, "process_name", p_process_name); //Parameter value: New reference.
PyDict_SetItemString(metadataDict, "process_path", p_process_path); //Parameter value: New reference.
PyDict_SetItemString(metadataDict, "host", p_host); //Parameter value: New reference.
PyDict_SetItemString(metadataDict, "src_ip", p_src_ip); //Parameter value: New reference.
PyDict_SetItemString(metadataDict, "src_port", p_src_port); //Parameter value: New reference.
PyDict_SetItemString(metadataDict, "dst_ip", p_dst_ip); //Parameter value: New reference.
PyDict_SetItemString(metadataDict, "dst_port", p_dst_port); //Parameter value: New reference.
Py_DECREF(p_type);
Py_DECREF(p_network);
Py_DECREF(p_process_name);
Py_DECREF(p_process_path);
Py_DECREF(p_host);
Py_DECREF(p_src_ip);
Py_DECREF(p_src_port);
Py_DECREF(p_dst_ip);
Py_DECREF(p_dst_port);
tupleArgs = PyTuple_New(2); //Return value: New reference.
if (tupleArgs == NULL) {
Py_DECREF(metadataDict);
PyErr_SetString(PyExc_TypeError,
"PyTuple_New failure");
return "-1";
}
Py_INCREF(clash_context);
PyTuple_SetItem(tupleArgs, 0, clash_context); //clash_context Parameter value: Stolen reference.
PyTuple_SetItem(tupleArgs, 1, metadataDict); //metadataDict Parameter value: Stolen reference.
Py_INCREF(main_fn);
result = PyObject_CallObject(main_fn, tupleArgs); //Return value: New reference.
Py_DECREF(main_fn);
Py_DECREF(tupleArgs);
if (result == NULL) {
return "-1";
}
if (!PyUnicode_Check(result)) {
Py_DECREF(result);
PyErr_SetString(PyExc_TypeError,
"script main function return value must be a string");
return "-1";
}
const char *adapter = PyUnicode_AsUTF8(result);
Py_DECREF(result);
return adapter;
}
int call_shortcut(PyObject *shortcut_fn,
const char *type,
const char *network,
const char *process_name,
const char *process_path,
const char *host,
const char *src_ip,
unsigned short src_port,
const char *dst_ip,
unsigned short dst_port) {
PyObject *args;
PyObject *result;
args = Py_BuildValue("{s:O, s:s, s:s, s:s, s:s, s:s, s:H, s:s, s:H}",
"ctx", clash_context,
"network", network,
"process_name", process_name,
"process_path", process_path,
"host", host,
"src_ip", src_ip,
"src_port", src_port,
"dst_ip", dst_ip,
"dst_port", dst_port); //Return value: New reference.
if (args == NULL) {
PyErr_SetString(PyExc_TypeError,
"Py_BuildValue failure");
return -1;
}
PyObject *tupleArgs = PyTuple_New(0); //Return value: New reference.
Py_INCREF(clash_context);
Py_INCREF(shortcut_fn);
result = PyObject_Call(shortcut_fn, tupleArgs, args); //Return value: New reference.
Py_DECREF(shortcut_fn);
Py_DECREF(clash_context);
Py_DECREF(tupleArgs);
Py_DECREF(args);
if (result == NULL) {
return -1;
}
if (!PyBool_Check(result)) {
Py_DECREF(result);
PyErr_SetString(PyExc_TypeError,
"script shortcut return value must be as boolean");
return -1;
}
int rs = (result == Py_True) ? 1 : 0;
Py_DECREF(result);
return rs;
}
void finalize_Python() {
Py_CLEAR(main_fn);
Py_CLEAR(clash_context);
Py_CLEAR(clash_module);
Py_FinalizeEx();
clash_module = NULL;
main_fn = NULL;
clash_context = NULL;
}
/* --------------------------------------------------------------------- */

View File

@ -0,0 +1,338 @@
package script
/*
#include "clash_module.h"
extern const char *resolveIPCallbackFn(const char *host);
void
go_set_resolve_ip_callback() {
set_resolve_ip_callback(resolveIPCallbackFn);
}
extern const char *geoipCallbackFn(const char *ip);
void
go_set_geoip_callback() {
set_geoip_callback(geoipCallbackFn);
}
extern const int ruleProviderCallbackFn(const char *provider_name, struct Metadata *metadata);
void
go_set_rule_provider_callback() {
set_rule_provider_callback(ruleProviderCallbackFn);
}
extern void logCallbackFn(const char *msg);
void
go_set_log_callback() {
set_log_callback(logCallbackFn);
}
*/
import "C"
import (
"errors"
"fmt"
"os"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
)
const ClashScriptModuleName = C.CLASH_SCRIPT_MODULE_NAME
var lock sync.Mutex
type PyObject C.PyObject
func togo(cobject *C.PyObject) *PyObject {
return (*PyObject)(cobject)
}
func toc(object *PyObject) *C.PyObject {
return (*C.PyObject)(object)
}
func (pyObject *PyObject) IncRef() {
C.Py_IncRef(toc(pyObject))
}
func (pyObject *PyObject) DecRef() {
C.Py_DecRef(toc(pyObject))
}
func (pyObject *PyObject) Clear() {
C.py_clear(toc(pyObject))
}
// Py_Initialize initialize Python3
func Py_Initialize(program string, path string) error {
lock.Lock()
defer lock.Unlock()
if C.Py_IsInitialized() != 0 {
if pyThreadState != nil {
PyEval_RestoreThread(pyThreadState)
}
C.finalize_Python()
}
path = strings.ReplaceAll(path, "\\", "/")
cPath := C.CString(path)
C.init_python(C.CString(program), cPath)
err := PyLastError()
if err != nil {
if C.Py_IsInitialized() != 0 {
C.finalize_Python()
_ = os.RemoveAll(constant.Path.ScriptDir())
}
return err
} else if C.Py_IsInitialized() == 0 {
err = errors.New("initialized script module failure")
return err
}
initPython3Callback()
return nil
}
func Py_IsInitialized() bool {
lock.Lock()
defer lock.Unlock()
return C.Py_IsInitialized() != 0
}
func Py_Finalize() {
lock.Lock()
defer lock.Unlock()
if C.Py_IsInitialized() != 0 {
if pyThreadState != nil {
PyEval_RestoreThread(pyThreadState)
}
C.finalize_Python()
_ = os.RemoveAll(constant.Path.ScriptDir())
log.Warnln("Clash clean up script mode.")
}
}
//Py_GetVersion get
func Py_GetVersion() string {
cversion := C.Py_GetVersion()
return strings.Split(C.GoString(cversion), "\n")[0]
}
// loadPyFunc loads a Python function by module and function name
func loadPyFunc(moduleName, funcName string) (*C.PyObject, error) {
// Convert names to C char*
cMod := C.CString(moduleName)
cFunc := C.CString(funcName)
// Free memory allocated by C.CString
defer func() {
C.free(unsafe.Pointer(cMod))
C.free(unsafe.Pointer(cFunc))
}()
fnc := C.load_func(cMod, cFunc)
if fnc == nil {
return nil, PyLastError()
}
return fnc, nil
}
//PyLastError python last error
func PyLastError() error {
cp := C.py_last_error()
if cp == nil {
return nil
}
return errors.New(C.GoString(cp))
}
func LoadShortcutFunction(shortcut string) (*PyObject, error) {
fnc, err := loadPyFunc(ClashScriptModuleName, shortcut)
if err != nil {
return nil, err
}
return togo(fnc), nil
}
func LoadMainFunction() error {
C.load_main_func()
err := PyLastError()
if err != nil {
return err
}
return nil
}
//CallPyMainFunction call python script main function
//return the proxy adapter name.
func CallPyMainFunction(mtd *constant.Metadata) (string, error) {
_type := C.CString(mtd.Type.String())
network := C.CString(mtd.NetWork.String())
processName := C.CString(mtd.Process)
processPath := C.CString(mtd.ProcessPath)
host := C.CString(mtd.Host)
srcPortGo, _ := strconv.ParseUint(mtd.SrcPort, 10, 16)
dstPortGo, _ := strconv.ParseUint(mtd.DstPort, 10, 16)
srcPort := C.ushort(srcPortGo)
dstPort := C.ushort(dstPortGo)
dstIpGo := ""
srcIpGo := ""
if mtd.SrcIP.IsValid() {
srcIpGo = mtd.SrcIP.String()
}
if mtd.DstIP.IsValid() {
dstIpGo = mtd.DstIP.String()
}
srcIp := C.CString(srcIpGo)
dstIp := C.CString(dstIpGo)
defer func() {
C.free(unsafe.Pointer(_type))
C.free(unsafe.Pointer(network))
C.free(unsafe.Pointer(processName))
C.free(unsafe.Pointer(processPath))
C.free(unsafe.Pointer(host))
C.free(unsafe.Pointer(srcIp))
C.free(unsafe.Pointer(dstIp))
}()
runtime.LockOSThread()
gilState := PyGILState_Ensure()
defer PyGILState_Release(gilState)
cRs := C.call_main(_type, network, processName, processPath, host, srcIp, srcPort, dstIp, dstPort)
rs := C.GoString(cRs)
if rs == "-1" {
err := PyLastError()
if err != nil {
log.Errorln("[Script] script code error: %v", err)
killSelf()
return "", fmt.Errorf("script code error: %w", err)
} else {
return "", fmt.Errorf("script code error, result: %v", rs)
}
}
return rs, nil
}
//CallPyShortcut call python script shortcuts function
//param: shortcut name
//return the match result.
func CallPyShortcut(fn *PyObject, mtd *constant.Metadata) (bool, error) {
_type := C.CString(mtd.Type.String())
network := C.CString(mtd.NetWork.String())
processName := C.CString(mtd.Process)
processPath := C.CString(mtd.ProcessPath)
host := C.CString(mtd.Host)
srcPortGo, _ := strconv.ParseUint(mtd.SrcPort, 10, 16)
dstPortGo, _ := strconv.ParseUint(mtd.DstPort, 10, 16)
srcPort := C.ushort(srcPortGo)
dstPort := C.ushort(dstPortGo)
dstIpGo := ""
srcIpGo := ""
if mtd.SrcIP.IsValid() {
srcIpGo = mtd.SrcIP.String()
}
if mtd.DstIP.IsValid() {
dstIpGo = mtd.DstIP.String()
}
srcIp := C.CString(srcIpGo)
dstIp := C.CString(dstIpGo)
defer func() {
C.free(unsafe.Pointer(_type))
C.free(unsafe.Pointer(network))
C.free(unsafe.Pointer(processName))
C.free(unsafe.Pointer(processPath))
C.free(unsafe.Pointer(host))
C.free(unsafe.Pointer(srcIp))
C.free(unsafe.Pointer(dstIp))
}()
runtime.LockOSThread()
gilState := PyGILState_Ensure()
defer PyGILState_Release(gilState)
cRs := C.call_shortcut(toc(fn), _type, network, processName, processPath, host, srcIp, srcPort, dstIp, dstPort)
rs := int(cRs)
if rs == -1 {
err := PyLastError()
if err != nil {
log.Errorln("[Script] script shortcut code error: %v", err)
killSelf()
return false, fmt.Errorf("script shortcut code error: %w", err)
} else {
return false, fmt.Errorf("script shortcut code error: result: %d", rs)
}
}
if rs == 1 {
return true, nil
} else {
return false, nil
}
}
func initPython3Callback() {
C.go_set_resolve_ip_callback()
C.go_set_geoip_callback()
C.go_set_rule_provider_callback()
C.go_set_log_callback()
}
//NewClashPyContext new clash context for python
func NewClashPyContext(ruleProvidersName []string) error {
length := len(ruleProvidersName)
cStringArr := make([]*C.char, length)
for i, v := range ruleProvidersName {
cStringArr[i] = C.CString(v)
defer C.free(unsafe.Pointer(cStringArr[i]))
}
cArrPointer := unsafe.Pointer(nil)
if length > 0 {
cArrPointer = unsafe.Pointer(&cStringArr[0])
}
rs := C.new_clash_py_context((**C.char)(cArrPointer), C.int(length))
if int(rs) == 0 {
err := PyLastError()
return fmt.Errorf("new script module context failure: %w", err)
}
return nil
}
func killSelf() {
p, err := os.FindProcess(os.Getpid())
if err != nil {
os.Exit(int(syscall.SIGINT))
return
}
_ = p.Signal(syscall.SIGINT)
}

View File

@ -0,0 +1,65 @@
#ifndef CLASH_CALLBACK_MODULE_H__
#define CLASH_CALLBACK_MODULE_H__
#include <Python.h>
#define CLASH_SCRIPT_MODULE_NAME "clash_script"
struct Metadata {
const char *type; /* type socks5/http */
const char *network; /* network tcp/udp */
const char *process_name;
const char *process_path;
const char *host;
const char *src_ip;
unsigned short src_port;
const char *dst_ip;
unsigned short dst_port;
};
/** callback function, that call go function by python3 script. **/
typedef const char *(*resolve_ip_callback)(const char *host);
typedef const char *(*geoip_callback)(const char *ip);
typedef const int (*rule_provider_callback)(const char *provider_name, struct Metadata *metadata);
typedef void (*log_callback)(const char *msg);
void set_resolve_ip_callback(resolve_ip_callback cb);
void set_geoip_callback(geoip_callback cb);
void set_rule_provider_callback(rule_provider_callback cb);
void set_log_callback(log_callback cb);
/*---------------------------------------------------------------*/
void append_inittab();
void init_python(const char *program, const char *path);
void load_main_func();
void finalize_Python();
void py_clear(PyObject *obj);
const char *py_last_error();
PyObject *load_func(const char *module_name, char *func_name);
int new_clash_py_context(const char *provider_name_arr[], int size);
const char *call_main(
const char *type,
const char *network,
const char *process_name,
const char *process_path,
const char *host,
const char *src_ip,
unsigned short src_port,
const char *dst_ip,
unsigned short dst_port);
int call_shortcut(PyObject *shortcut_fn,
const char *type,
const char *network,
const char *process_name,
const char *process_path,
const char *host,
const char *src_ip,
unsigned short src_port,
const char *dst_ip,
unsigned short dst_port);
#endif // CLASH_CALLBACK_MODULE_H__

View File

@ -0,0 +1,145 @@
package script
/*
#include "clash_module.h"
*/
import "C"
import (
"net/netip"
"strconv"
"strings"
"unsafe"
"github.com/Dreamacro/clash/component/mmdb"
"github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
)
var (
ruleProviders = map[string]constant.Rule{}
pyThreadState *PyThreadState
)
func UpdateRuleProviders(rpd map[string]constant.Rule) {
ruleProviders = rpd
if Py_IsInitialized() {
pyThreadState = PyEval_SaveThread()
}
}
//export resolveIPCallbackFn
func resolveIPCallbackFn(cHost *C.char) *C.char {
host := C.GoString(cHost)
if len(host) == 0 {
cip := C.CString("")
defer C.free(unsafe.Pointer(cip))
return cip
}
if ip, err := resolver.ResolveIP(host); err == nil {
cip := C.CString(ip.String())
defer C.free(unsafe.Pointer(cip))
return cip
} else {
log.Errorln("[Script] resolve ip error: %s", err.Error())
cip := C.CString("")
defer C.free(unsafe.Pointer(cip))
return cip
}
}
//export geoipCallbackFn
func geoipCallbackFn(cIP *C.char) *C.char {
dstIP, err := netip.ParseAddr(C.GoString(cIP))
if err != nil {
emptyC := C.CString("")
defer C.free(unsafe.Pointer(emptyC))
return emptyC
}
if dstIP.IsPrivate() ||
dstIP.IsUnspecified() ||
dstIP.IsLoopback() ||
dstIP.IsMulticast() ||
dstIP.IsLinkLocalUnicast() ||
resolver.IsFakeBroadcastIP(dstIP) {
lanC := C.CString("LAN")
defer C.free(unsafe.Pointer(lanC))
return lanC
}
record, _ := mmdb.Instance().Country(dstIP.AsSlice())
rc := C.CString(strings.ToUpper(record.Country.IsoCode))
defer C.free(unsafe.Pointer(rc))
return rc
}
//export ruleProviderCallbackFn
func ruleProviderCallbackFn(cProviderName *C.char, cMetadata *C.struct_Metadata) C.int {
//_type := C.GoString(cMetadata._type)
//network := C.GoString(cMetadata.network)
processName := C.GoString(cMetadata.process_name)
processPath := C.GoString(cMetadata.process_path)
host := C.GoString(cMetadata.host)
srcIp := C.GoString(cMetadata.src_ip)
srcPort := strconv.Itoa(int(cMetadata.src_port))
dstIp := C.GoString(cMetadata.dst_ip)
dstPort := strconv.Itoa(int(cMetadata.dst_port))
addrType := constant.AtypDomainName
if h, err := netip.ParseAddr(host); err == nil {
if h.Is4() {
addrType = constant.AtypIPv4
} else {
addrType = constant.AtypIPv6
}
}
src, _ := netip.ParseAddr(srcIp)
dst, _ := netip.ParseAddr(dstIp)
metadata := &constant.Metadata{
AddrType: addrType,
SrcIP: src,
DstIP: dst,
SrcPort: srcPort,
DstPort: dstPort,
Host: host,
Process: processName,
ProcessPath: processPath,
}
providerName := C.GoString(cProviderName)
rule, ok := ruleProviders[providerName]
if !ok {
log.Warnln("[Script] rule provider [%s] not found", providerName)
return C.int(0)
}
if strings.HasPrefix(providerName, "geosite:") {
if len(host) == 0 {
return C.int(0)
}
metadata.AddrType = constant.AtypDomainName
}
rs := rule.Match(metadata)
if rs {
return C.int(1)
}
return C.int(0)
}
//export logCallbackFn
func logCallbackFn(msg *C.char) {
log.Infoln(C.GoString(msg))
}

View File

@ -0,0 +1,52 @@
package script
/*
#include "Python.h"
*/
import "C"
//PyThreadState : https://docs.python.org/3/c-api/init.html#c.PyThreadState
type PyThreadState C.PyThreadState
//PyGILState is an opaque “handle” to the thread state when PyGILState_Ensure() was called, and must be passed to PyGILState_Release() to ensure Python is left in the same state
type PyGILState C.PyGILState_STATE
//PyEval_SaveThread : https://docs.python.org/3/c-api/init.html#c.PyEval_SaveThread
func PyEval_SaveThread() *PyThreadState {
return (*PyThreadState)(C.PyEval_SaveThread())
}
//PyEval_RestoreThread : https://docs.python.org/3/c-api/init.html#c.PyEval_RestoreThread
func PyEval_RestoreThread(tstate *PyThreadState) {
C.PyEval_RestoreThread((*C.PyThreadState)(tstate))
}
//PyThreadState_Get : https://docs.python.org/3/c-api/init.html#c.PyThreadState_Get
func PyThreadState_Get() *PyThreadState {
return (*PyThreadState)(C.PyThreadState_Get())
}
//PyThreadState_Swap : https://docs.python.org/3/c-api/init.html#c.PyThreadState_Swap
func PyThreadState_Swap(tstate *PyThreadState) *PyThreadState {
return (*PyThreadState)(C.PyThreadState_Swap((*C.PyThreadState)(tstate)))
}
//PyGILState_Ensure : https://docs.python.org/3/c-api/init.html#c.PyGILState_Ensure
func PyGILState_Ensure() PyGILState {
return PyGILState(C.PyGILState_Ensure())
}
//PyGILState_Release : https://docs.python.org/3/c-api/init.html#c.PyGILState_Release
func PyGILState_Release(state PyGILState) {
C.PyGILState_Release(C.PyGILState_STATE(state))
}
//PyGILState_GetThisThreadState : https://docs.python.org/3/c-api/init.html#c.PyGILState_GetThisThreadState
func PyGILState_GetThisThreadState() *PyThreadState {
return (*PyThreadState)(C.PyGILState_GetThisThreadState())
}
//PyGILState_Check : https://docs.python.org/3/c-api/init.html#c.PyGILState_Check
func PyGILState_Check() bool {
return C.PyGILState_Check() == 1
}

View File

@ -1,176 +0,0 @@
package sniffer
import (
"errors"
"github.com/Dreamacro/clash/constant/sniffer"
"net"
"net/netip"
"strconv"
"time"
"github.com/Dreamacro/clash/component/trie"
CN "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
)
var (
ErrorUnsupportedSniffer = errors.New("unsupported sniffer")
ErrorSniffFailed = errors.New("all sniffer failed")
ErrNoClue = errors.New("not enough information for making a decision")
)
var Dispatcher SnifferDispatcher
type (
SnifferDispatcher struct {
enable bool
sniffers []sniffer.Sniffer
foreDomain *trie.DomainTrie[bool]
skipSNI *trie.DomainTrie[bool]
portRanges *[]utils.Range[uint16]
}
)
func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) {
bufConn, ok := conn.(*CN.BufferedConn)
if !ok {
return
}
if metadata.Host == "" || sd.foreDomain.Search(metadata.Host) != nil {
port, err := strconv.ParseUint(metadata.DstPort, 10, 16)
if err != nil {
log.Debugln("[Sniffer] Dst port is error")
return
}
inWhitelist := false
for _, portRange := range *sd.portRanges {
if portRange.Contains(uint16(port)) {
inWhitelist = true
break
}
}
if !inWhitelist {
return
}
if host, err := sd.sniffDomain(bufConn, metadata); err != nil {
log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%s] to [%s:%s]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort)
return
} else {
if sd.skipSNI.Search(host) != nil {
log.Debugln("[Sniffer] Skip sni[%s]", host)
return
}
sd.replaceDomain(metadata, host)
}
}
}
func (sd *SnifferDispatcher) replaceDomain(metadata *C.Metadata, host string) {
log.Debugln("[Sniffer] Sniff TCP [%s:%s]-->[%s:%s] success, replace domain [%s]-->[%s]",
metadata.SrcIP, metadata.SrcPort,
metadata.DstIP, metadata.DstPort,
metadata.Host, host)
metadata.AddrType = C.AtypDomainName
metadata.Host = host
metadata.DNSMode = C.DNSMapping
resolver.InsertHostByIP(metadata.DstIP, host)
}
func (sd *SnifferDispatcher) Enable() bool {
return sd.enable
}
func (sd *SnifferDispatcher) sniffDomain(conn *CN.BufferedConn, metadata *C.Metadata) (string, error) {
for _, sniffer := range sd.sniffers {
if sniffer.SupportNetwork() == C.TCP {
_ = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
_, err := conn.Peek(1)
_ = conn.SetReadDeadline(time.Time{})
if err != nil {
_, ok := err.(*net.OpError)
if ok {
log.Errorln("[Sniffer] [%s] may not have any sent data, Consider adding skip", metadata.DstIP.String())
_ = conn.Close()
}
return "", err
}
bufferedLen := conn.Buffered()
bytes, err := conn.Peek(bufferedLen)
if err != nil {
log.Debugln("[Sniffer] the data length not enough")
continue
}
host, err := sniffer.SniffTCP(bytes)
if err != nil {
//log.Debugln("[Sniffer] [%s] Sniff data failed %s", sniffer.Protocol(), metadata.DstIP)
continue
}
_, err = netip.ParseAddr(host)
if err == nil {
//log.Debugln("[Sniffer] [%s] Sniff data failed %s", sniffer.Protocol(), metadata.DstIP)
continue
}
return host, nil
}
}
return "", ErrorSniffFailed
}
func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) {
dispatcher := SnifferDispatcher{
enable: false,
}
return &dispatcher, nil
}
func NewSnifferDispatcher(needSniffer []sniffer.Type, forceDomain *trie.DomainTrie[bool],
skipSNI *trie.DomainTrie[bool], ports *[]utils.Range[uint16]) (*SnifferDispatcher, error) {
dispatcher := SnifferDispatcher{
enable: true,
foreDomain: forceDomain,
skipSNI: skipSNI,
portRanges: ports,
}
for _, snifferName := range needSniffer {
sniffer, err := NewSniffer(snifferName)
if err != nil {
log.Errorln("Sniffer name[%s] is error", snifferName)
return &SnifferDispatcher{enable: false}, err
}
dispatcher.sniffers = append(dispatcher.sniffers, sniffer)
}
return &dispatcher, nil
}
func NewSniffer(name sniffer.Type) (sniffer.Sniffer, error) {
switch name {
case sniffer.TLS:
return &TLSSniffer{}, nil
case sniffer.HTTP:
return &HTTPSniffer{}, nil
default:
return nil, ErrorUnsupportedSniffer
}
}

View File

@ -1,100 +0,0 @@
package sniffer
import (
"bytes"
"errors"
C "github.com/Dreamacro/clash/constant"
"net"
"strings"
)
var (
// refer to https://pkg.go.dev/net/http@master#pkg-constants
methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect", "patch", "trace"}
errNotHTTPMethod = errors.New("not an HTTP method")
)
type version byte
const (
HTTP1 version = iota
HTTP2
)
type HTTPSniffer struct {
version version
host string
}
func (http *HTTPSniffer) Protocol() string {
switch http.version {
case HTTP1:
return "http1"
case HTTP2:
return "http2"
default:
return "unknown"
}
}
func (http *HTTPSniffer) SupportNetwork() C.NetWork {
return C.TCP
}
func (http *HTTPSniffer) SniffTCP(bytes []byte) (string, error) {
domain, err := SniffHTTP(bytes)
if err == nil {
return *domain, nil
} else {
return "", err
}
}
func beginWithHTTPMethod(b []byte) error {
for _, m := range &methods {
if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) {
return nil
}
if len(b) < len(m) {
return ErrNoClue
}
}
return errNotHTTPMethod
}
func SniffHTTP(b []byte) (*string, error) {
if err := beginWithHTTPMethod(b); err != nil {
return nil, err
}
_ = &HTTPSniffer{
version: HTTP1,
}
headers := bytes.Split(b, []byte{'\n'})
for i := 1; i < len(headers); i++ {
header := headers[i]
if len(header) == 0 {
break
}
parts := bytes.SplitN(header, []byte{':'}, 2)
if len(parts) != 2 {
continue
}
key := strings.ToLower(string(parts[0]))
if key == "host" {
rawHost := strings.ToLower(string(bytes.TrimSpace(parts[1])))
host, _, err := net.SplitHostPort(rawHost)
if err != nil {
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
host = rawHost
} else {
return nil, err
}
}
return &host, nil
}
}
return nil, ErrNoClue
}

View File

@ -1,3 +0,0 @@
package sniffer
//TODO

View File

@ -1,44 +0,0 @@
package trie
import "errors"
var (
ErrorOverMaxValue = errors.New("the value don't over max value")
)
type IpCidrNode struct {
Mark bool
child map[uint32]*IpCidrNode
maxValue uint32
}
func NewIpCidrNode(mark bool, maxValue uint32) *IpCidrNode {
ipCidrNode := &IpCidrNode{
Mark: mark,
child: map[uint32]*IpCidrNode{},
maxValue: maxValue,
}
return ipCidrNode
}
func (n *IpCidrNode) addChild(value uint32) error {
if value > n.maxValue {
return ErrorOverMaxValue
}
n.child[value] = NewIpCidrNode(false, n.maxValue)
return nil
}
func (n *IpCidrNode) hasChild(value uint32) bool {
return n.getChild(value) != nil
}
func (n *IpCidrNode) getChild(value uint32) *IpCidrNode {
if value <= n.maxValue {
return n.child[value]
}
return nil
}

View File

@ -1,255 +0,0 @@
package trie
import (
"github.com/Dreamacro/clash/log"
"net"
)
type IPV6 bool
const (
ipv4GroupMaxValue = 0xFF
ipv6GroupMaxValue = 0xFFFF
)
type IpCidrTrie struct {
ipv4Trie *IpCidrNode
ipv6Trie *IpCidrNode
}
func NewIpCidrTrie() *IpCidrTrie {
return &IpCidrTrie{
ipv4Trie: NewIpCidrNode(false, ipv4GroupMaxValue),
ipv6Trie: NewIpCidrNode(false, ipv6GroupMaxValue),
}
}
func (trie *IpCidrTrie) AddIpCidr(ipCidr *net.IPNet) error {
subIpCidr, subCidr, isIpv4, err := ipCidrToSubIpCidr(ipCidr)
if err != nil {
return err
}
for _, sub := range subIpCidr {
addIpCidr(trie, isIpv4, sub, subCidr/8)
}
return nil
}
func (trie *IpCidrTrie) AddIpCidrForString(ipCidr string) error {
_, ipNet, err := net.ParseCIDR(ipCidr)
if err != nil {
return err
}
return trie.AddIpCidr(ipNet)
}
func (trie *IpCidrTrie) IsContain(ip net.IP) bool {
ip, isIpv4 := checkAndConverterIp(ip)
if ip == nil {
return false
}
var groupValues []uint32
var ipCidrNode *IpCidrNode
if isIpv4 {
ipCidrNode = trie.ipv4Trie
for _, group := range ip {
groupValues = append(groupValues, uint32(group))
}
} else {
ipCidrNode = trie.ipv6Trie
for i := 0; i < len(ip); i += 2 {
groupValues = append(groupValues, getIpv6GroupValue(ip[i], ip[i+1]))
}
}
return search(ipCidrNode, groupValues) != nil
}
func (trie *IpCidrTrie) IsContainForString(ipString string) bool {
return trie.IsContain(net.ParseIP(ipString))
}
func ipCidrToSubIpCidr(ipNet *net.IPNet) ([]net.IP, int, bool, error) {
maskSize, _ := ipNet.Mask.Size()
var (
ipList []net.IP
newMaskSize int
isIpv4 bool
err error
)
ip, isIpv4 := checkAndConverterIp(ipNet.IP)
ipList, newMaskSize, err = subIpCidr(ip, maskSize, isIpv4)
return ipList, newMaskSize, isIpv4, err
}
func subIpCidr(ip net.IP, maskSize int, isIpv4 bool) ([]net.IP, int, error) {
var subIpCidrList []net.IP
groupSize := 8
if !isIpv4 {
groupSize = 16
}
if maskSize%groupSize == 0 {
return append(subIpCidrList, ip), maskSize, nil
}
lastByteMaskSize := maskSize % 8
lastByteMaskIndex := maskSize / 8
subIpCidrNum := 0xFF >> lastByteMaskSize
for i := 0; i <= subIpCidrNum; i++ {
subIpCidr := make([]byte, len(ip))
copy(subIpCidr, ip)
subIpCidr[lastByteMaskIndex] += byte(i)
subIpCidrList = append(subIpCidrList, subIpCidr)
}
newMaskSize := (lastByteMaskIndex + 1) * 8
if !isIpv4 {
newMaskSize = (lastByteMaskIndex/2 + 1) * 16
}
return subIpCidrList, newMaskSize, nil
}
func addIpCidr(trie *IpCidrTrie, isIpv4 bool, ip net.IP, groupSize int) {
if isIpv4 {
addIpv4Cidr(trie, ip, groupSize)
} else {
addIpv6Cidr(trie, ip, groupSize)
}
}
func addIpv4Cidr(trie *IpCidrTrie, ip net.IP, groupSize int) {
preNode := trie.ipv4Trie
node := preNode.getChild(uint32(ip[0]))
if node == nil {
err := preNode.addChild(uint32(ip[0]))
if err != nil {
return
}
node = preNode.getChild(uint32(ip[0]))
}
for i := 1; i < groupSize; i++ {
if node.Mark {
return
}
groupValue := uint32(ip[i])
if !node.hasChild(groupValue) {
err := node.addChild(groupValue)
if err != nil {
log.Errorln(err.Error())
}
}
preNode = node
node = node.getChild(groupValue)
if node == nil {
err := preNode.addChild(uint32(ip[i-1]))
if err != nil {
return
}
node = preNode.getChild(uint32(ip[i-1]))
}
}
node.Mark = true
cleanChild(node)
}
func addIpv6Cidr(trie *IpCidrTrie, ip net.IP, groupSize int) {
preNode := trie.ipv6Trie
node := preNode.getChild(getIpv6GroupValue(ip[0], ip[1]))
if node == nil {
err := preNode.addChild(getIpv6GroupValue(ip[0], ip[1]))
if err != nil {
return
}
node = preNode.getChild(getIpv6GroupValue(ip[0], ip[1]))
}
for i := 2; i < groupSize; i += 2 {
if node.Mark {
return
}
groupValue := getIpv6GroupValue(ip[i], ip[i+1])
if !node.hasChild(groupValue) {
err := node.addChild(groupValue)
if err != nil {
log.Errorln(err.Error())
}
}
preNode = node
node = node.getChild(groupValue)
if node == nil {
err := preNode.addChild(getIpv6GroupValue(ip[i-2], ip[i-1]))
if err != nil {
return
}
node = preNode.getChild(getIpv6GroupValue(ip[i-2], ip[i-1]))
}
}
node.Mark = true
cleanChild(node)
}
func getIpv6GroupValue(high, low byte) uint32 {
return (uint32(high) << 8) | uint32(low)
}
func cleanChild(node *IpCidrNode) {
for i := uint32(0); i < uint32(len(node.child)); i++ {
delete(node.child, i)
}
}
func search(root *IpCidrNode, groupValues []uint32) *IpCidrNode {
node := root.getChild(groupValues[0])
if node == nil || node.Mark {
return node
}
for _, value := range groupValues[1:] {
if !node.hasChild(value) {
return nil
}
node = node.getChild(value)
if node == nil || node.Mark {
return node
}
}
return nil
}
// return net.IP To4 or To16 and is ipv4
func checkAndConverterIp(ip net.IP) (net.IP, bool) {
ipResult := ip.To4()
if ipResult == nil {
ipResult = ip.To16()
if ipResult == nil {
return nil, false
}
return ipResult, false
}
return ipResult, true
}

View File

@ -1,100 +0,0 @@
package trie
import (
"net"
"testing"
)
import "github.com/stretchr/testify/assert"
func TestIpv4AddSuccess(t *testing.T) {
trie := NewIpCidrTrie()
err := trie.AddIpCidrForString("10.0.0.2/16")
assert.Equal(t, nil, err)
}
func TestIpv4AddFail(t *testing.T) {
trie := NewIpCidrTrie()
err := trie.AddIpCidrForString("333.00.23.2/23")
assert.IsType(t, new(net.ParseError), err)
err = trie.AddIpCidrForString("22.3.34.2/222")
assert.IsType(t, new(net.ParseError), err)
err = trie.AddIpCidrForString("2.2.2.2")
assert.IsType(t, new(net.ParseError), err)
}
func TestIpv4Search(t *testing.T) {
trie := NewIpCidrTrie()
// Boundary testing
assert.NoError(t, trie.AddIpCidrForString("149.154.160.0/20"))
assert.Equal(t, true, trie.IsContainForString("149.154.160.0"))
assert.Equal(t, true, trie.IsContainForString("149.154.175.255"))
assert.Equal(t, false, trie.IsContainForString("149.154.176.0"))
assert.Equal(t, false, trie.IsContainForString("149.154.159.255"))
assert.NoError(t, trie.AddIpCidrForString("129.2.36.0/16"))
assert.NoError(t, trie.AddIpCidrForString("10.2.36.0/18"))
assert.NoError(t, trie.AddIpCidrForString("16.2.23.0/24"))
assert.NoError(t, trie.AddIpCidrForString("11.2.13.2/26"))
assert.NoError(t, trie.AddIpCidrForString("55.5.6.3/8"))
assert.NoError(t, trie.AddIpCidrForString("66.23.25.4/6"))
assert.Equal(t, true, trie.IsContainForString("129.2.3.65"))
assert.Equal(t, false, trie.IsContainForString("15.2.3.1"))
assert.Equal(t, true, trie.IsContainForString("11.2.13.1"))
assert.Equal(t, true, trie.IsContainForString("55.0.0.0"))
assert.Equal(t, true, trie.IsContainForString("64.0.0.0"))
assert.Equal(t, false, trie.IsContainForString("128.0.0.0"))
assert.Equal(t, false, trie.IsContain(net.ParseIP("22")))
assert.Equal(t, false, trie.IsContain(net.ParseIP("")))
}
func TestIpv6AddSuccess(t *testing.T) {
trie := NewIpCidrTrie()
err := trie.AddIpCidrForString("2001:0db8:02de:0000:0000:0000:0000:0e13/32")
assert.Equal(t, nil, err)
err = trie.AddIpCidrForString("2001:1db8:f2de::0e13/18")
assert.Equal(t, nil, err)
}
func TestIpv6AddFail(t *testing.T) {
trie := NewIpCidrTrie()
err := trie.AddIpCidrForString("2001::25de::cade/23")
assert.IsType(t, new(net.ParseError), err)
err = trie.AddIpCidrForString("2001:0fa3:25de::cade/222")
assert.IsType(t, new(net.ParseError), err)
err = trie.AddIpCidrForString("2001:0fa3:25de::cade")
assert.IsType(t, new(net.ParseError), err)
}
func TestIpv6Search(t *testing.T) {
trie := NewIpCidrTrie()
// Boundary testing
assert.NoError(t, trie.AddIpCidrForString("2a0a:f280::/32"))
assert.Equal(t, true, trie.IsContainForString("2a0a:f280:0000:0000:0000:0000:0000:0000"))
assert.Equal(t, true, trie.IsContainForString("2a0a:f280:ffff:ffff:ffff:ffff:ffff:ffff"))
assert.Equal(t, false, trie.IsContainForString("2a0a:f279:ffff:ffff:ffff:ffff:ffff:ffff"))
assert.Equal(t, false, trie.IsContainForString("2a0a:f281:0000:0000:0000:0000:0000:0000"))
assert.NoError(t, trie.AddIpCidrForString("2001:b28:f23d:f001::e/128"))
assert.NoError(t, trie.AddIpCidrForString("2001:67c:4e8:f002::e/12"))
assert.NoError(t, trie.AddIpCidrForString("2001:b28:f23d:f003::e/96"))
assert.NoError(t, trie.AddIpCidrForString("2001:67c:4e8:f002::a/32"))
assert.NoError(t, trie.AddIpCidrForString("2001:67c:4e8:f004::a/60"))
assert.NoError(t, trie.AddIpCidrForString("2001:b28:f23f:f005::a/64"))
assert.Equal(t, true, trie.IsContainForString("2001:b28:f23d:f001::e"))
assert.Equal(t, false, trie.IsContainForString("2222::fff2"))
assert.Equal(t, true, trie.IsContainForString("2000::ffa0"))
assert.Equal(t, true, trie.IsContainForString("2001:b28:f23f:f005:5662::"))
assert.Equal(t, true, trie.IsContainForString("2001:67c:4e8:9666::1213"))
assert.Equal(t, false, trie.IsContain(net.ParseIP("22233:22")))
}

View File

@ -1,23 +1,15 @@
package config
import (
"container/list"
"errors"
"fmt"
"github.com/Dreamacro/clash/constant/sniffer"
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"net"
"net/netip"
"net/url"
"os"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/Dreamacro/clash/common/utils"
R "github.com/Dreamacro/clash/rule"
RP "github.com/Dreamacro/clash/rule/provider"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound"
@ -28,12 +20,15 @@ import (
"github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/component/geodata"
"github.com/Dreamacro/clash/component/geodata/router"
S "github.com/Dreamacro/clash/component/script"
"github.com/Dreamacro/clash/component/trie"
C "github.com/Dreamacro/clash/constant"
providerTypes "github.com/Dreamacro/clash/constant/provider"
snifferTypes "github.com/Dreamacro/clash/constant/sniffer"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"github.com/Dreamacro/clash/log"
rewrites "github.com/Dreamacro/clash/rewrite"
R "github.com/Dreamacro/clash/rule"
T "github.com/Dreamacro/clash/tunnel"
"gopkg.in/yaml.v2"
@ -43,18 +38,12 @@ import (
type General struct {
Inbound
Controller
Mode T.TunnelMode `json:"mode"`
UnifiedDelay bool
LogLevel log.LogLevel `json:"log-level"`
IPv6 bool `json:"ipv6"`
Interface string `json:"interface-name"`
RoutingMark int `json:"-"`
GeodataMode bool `json:"geodata-mode"`
GeodataLoader string `json:"geodata-loader"`
TCPConcurrent bool `json:"tcp-concurrent"`
EnableProcess bool `json:"enable-process"`
Tun Tun `json:"tun"`
Sniffing bool `json:"sniffing"`
Mode T.TunnelMode `json:"mode"`
LogLevel log.LogLevel `json:"log-level"`
IPv6 bool `json:"ipv6"`
Interface string `json:"-"`
RoutingMark int `json:"-"`
Tun Tun `json:"tun"`
}
// Inbound config
@ -64,6 +53,7 @@ type Inbound struct {
RedirPort int `json:"redir-port"`
TProxyPort int `json:"tproxy-port"`
MixedPort int `json:"mixed-port"`
MitmPort int `json:"mitm-port"`
Authentication []string `json:"authentication"`
AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"`
@ -109,29 +99,29 @@ type Profile struct {
// Tun config
type Tun struct {
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"`
TunAddressPrefix netip.Prefix `yaml:"-" json:"-"`
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
}
// Script config
type Script struct {
MainCode string `yaml:"code" json:"code"`
ShortcutsCode map[string]string `yaml:"shortcuts" json:"shortcuts"`
}
// IPTables config
type IPTables struct {
Enable bool `yaml:"enable" json:"enable"`
InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"`
Bypass []string `yaml:"bypass" json:"bypass"`
Enable bool `yaml:"enable" json:"enable"`
InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"`
}
type Sniffer struct {
Enable bool
Sniffers []sniffer.Type
Reverses *trie.DomainTrie[bool]
ForceDomain *trie.DomainTrie[bool]
SkipDomain *trie.DomainTrie[bool]
Ports *[]utils.Range[uint16]
// Mitm config
type Mitm struct {
Hosts *trie.DomainTrie[bool] `yaml:"hosts" json:"hosts"`
Rules C.RewriteRule `yaml:"rules" json:"rules"`
}
// Experimental config
@ -142,16 +132,16 @@ type Config struct {
General *General
Tun *Tun
IPTables *IPTables
Mitm *Mitm
DNS *DNS
Experimental *Experimental
Hosts *trie.DomainTrie[netip.Addr]
Profile *Profile
Rules []C.Rule
RuleProviders map[string]C.Rule
Users []auth.AuthUser
Proxies map[string]C.Proxy
Providers map[string]providerTypes.ProxyProvider
RuleProviders map[string]providerTypes.RuleProvider
Sniffer *Sniffer
}
type RawDNS struct {
@ -179,12 +169,16 @@ type RawFallbackFilter struct {
}
type RawTun struct {
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
AutoDetectInterface bool `yaml:"auto-detect-interface"`
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
}
type RawMitm struct {
Hosts []string `yaml:"hosts" json:"hosts"`
Rules []string `yaml:"rules" json:"rules"`
}
type RawConfig struct {
@ -193,11 +187,11 @@ type RawConfig struct {
RedirPort int `yaml:"redir-port"`
TProxyPort int `yaml:"tproxy-port"`
MixedPort int `yaml:"mixed-port"`
MitmPort int `yaml:"mitm-port"`
Authentication []string `yaml:"authentication"`
AllowLan bool `yaml:"allow-lan"`
BindAddress string `yaml:"bind-address"`
Mode T.TunnelMode `yaml:"mode"`
UnifiedDelay bool `yaml:"unified-delay"`
LogLevel log.LogLevel `yaml:"log-level"`
IPv6 bool `yaml:"ipv6"`
ExternalController string `yaml:"external-controller"`
@ -205,38 +199,19 @@ type RawConfig struct {
Secret string `yaml:"secret"`
Interface string `yaml:"interface-name"`
RoutingMark int `yaml:"routing-mark"`
GeodataMode bool `yaml:"geodata-mode"`
GeodataLoader string `yaml:"geodata-loader"`
TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"`
EnableProcess bool `yaml:"enable-process" json:"enable-process"`
Sniffer RawSniffer `yaml:"sniffer"`
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
RuleProvider map[string]map[string]any `yaml:"rule-providers"`
Hosts map[string]string `yaml:"hosts"`
DNS RawDNS `yaml:"dns"`
Tun RawTun `yaml:"tun"`
IPTables IPTables `yaml:"iptables"`
MITM RawMitm `yaml:"mitm"`
Experimental Experimental `yaml:"experimental"`
Profile Profile `yaml:"profile"`
GeoXUrl RawGeoXUrl `yaml:"geox-url"`
Proxy []map[string]any `yaml:"proxies"`
ProxyGroup []map[string]any `yaml:"proxy-groups"`
Rule []string `yaml:"rules"`
}
type RawGeoXUrl struct {
GeoIp string `yaml:"geoip" json:"geoip"`
Mmdb string `yaml:"mmdb" json:"mmdb"`
GeoSite string `yaml:"geosite" json:"geosite"`
}
type RawSniffer struct {
Enable bool `yaml:"enable" json:"enable"`
Sniffing []string `yaml:"sniffing" json:"sniffing"`
ForceDomain []string `yaml:"force-domain" json:"force-domain"`
SkipDomain []string `yaml:"skip-domain" json:"skip-domain"`
Ports []string `yaml:"port-whitelist" json:"port-whitelist"`
Script Script `yaml:"script"`
}
// Parse config
@ -255,29 +230,22 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
AllowLan: false,
BindAddress: "*",
Mode: T.Rule,
GeodataMode: C.GeodataMode,
GeodataLoader: "memconservative",
UnifiedDelay: false,
Authentication: []string{},
LogLevel: log.INFO,
Hosts: map[string]string{},
Rule: []string{},
Proxy: []map[string]any{},
ProxyGroup: []map[string]any{},
TCPConcurrent: false,
EnableProcess: true,
Tun: RawTun{
Enable: false,
Device: "",
Stack: C.TunGvisor,
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
AutoRoute: false,
AutoDetectInterface: false,
Enable: false,
Device: "",
Stack: C.TunGvisor,
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
AutoRoute: true,
},
IPTables: IPTables{
Enable: false,
InboundInterface: "lo",
Bypass: []string{},
},
DNS: RawDNS{
Enable: false,
@ -293,34 +261,19 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
DefaultNameserver: []string{
"114.114.114.114",
"223.5.5.5",
"8.8.8.8",
"1.0.0.1",
},
NameServer: []string{
NameServer: []string{ // default if user not set
"https://doh.pub/dns-query",
"tls://223.5.5.5:853",
},
FakeIPFilter: []string{
"dns.msftnsci.com",
"www.msftnsci.com",
"www.msftconnecttest.com",
},
},
Sniffer: RawSniffer{
Enable: false,
Sniffing: []string{},
ForceDomain: []string{},
SkipDomain: []string{},
Ports: []string{},
MITM: RawMitm{
Hosts: []string{},
Rules: []string{},
},
Profile: Profile{
StoreSelected: true,
},
GeoXUrl: RawGeoXUrl{
GeoIp: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat",
Mmdb: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb",
GeoSite: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat",
},
}
if err := yaml.Unmarshal(buf, rawCfg); err != nil {
@ -332,8 +285,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config := &Config{}
log.Infoln("Start initial configuration in progress") //Segment finished in xxm
startTime := time.Now()
config.Experimental = &rawCfg.Experimental
config.Profile = &rawCfg.Profile
config.IPTables = &rawCfg.IPTables
@ -344,6 +296,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
}
config.General = general
tunCfg, err := parseTun(rawCfg.Tun, config.General)
if err != nil {
return nil, err
}
config.Tun = tunCfg
dialer.DefaultInterface.Store(config.General.Interface)
proxies, providers, err := parseProxies(rawCfg)
@ -353,6 +311,10 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.Proxies = proxies
config.Providers = providers
if err = parseScript(rawCfg.Script); err != nil {
return nil, err
}
rules, ruleProviders, err := parseRules(rawCfg, proxies)
if err != nil {
return nil, err
@ -372,27 +334,20 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
}
config.DNS = dnsCfg
tunCfg, err := parseTun(rawCfg.Tun, config.General, dnsCfg)
mitm, err := parseMitm(rawCfg.MITM)
if err != nil {
return nil, err
}
config.Tun = tunCfg
config.Mitm = mitm
config.Users = parseAuthentication(rawCfg.Authentication)
config.Sniffer, err = parseSniffer(rawCfg.Sniffer)
if err != nil {
return nil, err
}
elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms
log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm
return config, nil
}
func parseGeneral(cfg *RawConfig) (*General, error) {
externalUI := cfg.ExternalUI
geodata.SetLoader(cfg.GeodataLoader)
// checkout externalUI exist
if externalUI != "" {
externalUI = C.Path.Resolve(externalUI)
@ -409,6 +364,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
RedirPort: cfg.RedirPort,
TProxyPort: cfg.TProxyPort,
MixedPort: cfg.MixedPort,
MitmPort: cfg.MitmPort,
AllowLan: cfg.AllowLan,
BindAddress: cfg.BindAddress,
},
@ -417,16 +373,11 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
ExternalUI: cfg.ExternalUI,
Secret: cfg.Secret,
},
UnifiedDelay: cfg.UnifiedDelay,
Mode: cfg.Mode,
LogLevel: cfg.LogLevel,
IPv6: cfg.IPv6,
Interface: cfg.Interface,
RoutingMark: cfg.RoutingMark,
GeodataMode: cfg.GeodataMode,
GeodataLoader: cfg.GeodataLoader,
TCPConcurrent: cfg.TCPConcurrent,
EnableProcess: cfg.EnableProcess,
Mode: cfg.Mode,
LogLevel: cfg.LogLevel,
IPv6: cfg.IPv6,
Interface: cfg.Interface,
RoutingMark: cfg.RoutingMark,
}, nil
}
@ -438,13 +389,9 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
providersConfig := cfg.ProxyProvider
var proxyList []string
proxiesList := list.New()
groupsList := list.New()
proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect())
proxies["REJECT"] = adapter.NewProxy(outbound.NewReject())
proxies["COMPATIBLE"] = adapter.NewProxy(outbound.NewCompatible())
proxies["PASS"] = adapter.NewProxy(outbound.NewPass())
proxyList = append(proxyList, "DIRECT", "REJECT")
// parse proxy
@ -459,7 +406,6 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
}
proxies[proxy.Name()] = proxy
proxyList = append(proxyList, proxy.Name())
proxiesList.PushBack(mapping)
}
// keep the original order of ProxyGroups in config file
@ -469,7 +415,6 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
return nil, nil, fmt.Errorf("proxy group %d: missing name", idx)
}
proxyList = append(proxyList, groupName)
groupsList.PushBack(mapping)
}
// check if any loop exists and sort the ProxyGroups
@ -491,6 +436,13 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
providersMap[name] = pd
}
for _, proxyProvider := range providersMap {
log.Infoln("Start initial provider %s", proxyProvider.Name())
if err := proxyProvider.Initial(); err != nil {
return nil, nil, fmt.Errorf("initial proxy provider %s error: %w", proxyProvider.Name(), err)
}
}
// parse proxy group
for idx, mapping := range groupsConfig {
group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap)
@ -506,11 +458,20 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
proxies[groupName] = adapter.NewProxy(group)
}
var ps []C.Proxy
for _, v := range proxyList {
if proxies[v].Type() == C.Pass {
// initial compatible provider
for _, pd := range providersMap {
if pd.VehicleType() != providerTypes.Compatible {
continue
}
log.Infoln("Start initial compatible provider %s", pd.Name())
if err := pd.Initial(); err != nil {
return nil, nil, err
}
}
var ps []C.Proxy
for _, v := range proxyList {
ps = append(ps, proxies[v])
}
hc := provider.NewHealthCheck(ps, "", 0, true)
@ -524,26 +485,18 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
[]providerTypes.ProxyProvider{pd},
)
proxies["GLOBAL"] = adapter.NewProxy(global)
return proxies, providersMap, nil
}
func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[string]providerTypes.RuleProvider, error) {
ruleProviders := map[string]providerTypes.RuleProvider{}
log.Infoln("Geodata Loader mode: %s", geodata.LoaderName())
// parse rule provider
for name, mapping := range cfg.RuleProvider {
rp, err := RP.ParseRuleProvider(name, mapping)
if err != nil {
return nil, nil, err
}
func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[string]C.Rule, error) {
var (
rules []C.Rule
providerNames []string
ruleProviders[name] = rp
RP.SetRuleProvider(rp)
}
var rules []C.Rule
rulesConfig := cfg.Rule
ruleProviders = map[string]C.Rule{}
rulesConfig = cfg.Rule
mode = cfg.Mode
)
// parse rules
for idx, line := range rulesConfig {
@ -557,47 +510,52 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
l := len(rule)
if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" {
target = rule[l-1]
payload = strings.Join(rule[1:l-1], ",")
} else {
if l < 2 {
return nil, nil, fmt.Errorf("rules[%d] [%s] error: format invalid", idx, line)
}
if l < 4 {
rule = append(rule, make([]string, 4-l)...)
}
if ruleName == "MATCH" {
l = 2
}
if l >= 3 {
l = 3
payload = rule[1]
}
target = rule[l-1]
params = rule[l:]
if l < 2 {
return nil, nil, fmt.Errorf("rules[%d] [%s] error: format invalid", idx, line)
}
if _, ok := proxies[target]; !ok {
if l < 4 {
rule = append(rule, make([]string, 4-l)...)
}
if ruleName == "MATCH" {
l = 2
}
if l >= 3 {
l = 3
payload = rule[1]
}
target = rule[l-1]
params = rule[l:]
if _, ok := proxies[target]; !ok && (mode != T.Script || ruleName != "GEOSITE") {
return nil, nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target)
}
params = trimArr(params)
if ruleName == "GEOSITE" {
if err := initGeoSite(); err != nil {
return nil, nil, fmt.Errorf("can't initial GeoSite: %s", err)
}
initMode = false
}
parsed, parseErr := R.ParseRule(ruleName, payload, target, params)
if parseErr != nil {
return nil, nil, fmt.Errorf("rules[%d] [%s] error: %s", idx, line, parseErr.Error())
}
if ruleName == "GEOSITE" {
pvName := "geosite:" + strings.ToLower(payload)
providerNames = append(providerNames, pvName)
ruleProviders[pvName] = parsed
}
rules = append(rules, parsed)
}
if err := S.NewClashPyContext(providerNames); err != nil {
return nil, nil, err
} else {
log.Infoln("Start initial script context successful, provider records: %v", len(providerNames))
}
runtime.GC()
return rules, ruleProviders, nil
@ -621,6 +579,11 @@ func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) {
}
}
// add mitm.clash hosts
if err := tree.Insert("mitm.clash", netip.AddrFrom4([4]byte{1, 2, 3, 4})); err != nil {
log.Errorln("insert mitm.clash to host error: %s", err.Error())
}
return tree, nil
}
@ -672,9 +635,6 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
case "dhcp":
addr = u.Host
dnsNetType = "dhcp" // UDP from DHCP
case "quic":
addr, err = hostWithDefaultPort(u.Host, "784")
dnsNetType = "quic" // DNS over QUIC
default:
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
}
@ -689,7 +649,7 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
Net: dnsNetType,
Addr: addr,
ProxyAdapter: u.Fragment,
Interface: dialer.DefaultInterface,
Interface: dialer.DefaultInterface.Load(),
},
)
}
@ -729,11 +689,6 @@ func parseFallbackIPCIDR(ips []string) ([]*netip.Prefix, error) {
func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainMatcher, error) {
var sites []*router.DomainMatcher
if len(countries) > 0 {
if err := initGeoSite(); err != nil {
return nil, fmt.Errorf("can't initial GeoSite: %s", err)
}
}
for _, country := range countries {
found := false
@ -805,10 +760,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R
for _, ns := range dnsCfg.DefaultNameserver {
host, _, err := net.SplitHostPort(ns.Addr)
if err != nil || net.ParseIP(host) == nil {
u, err := url.Parse(ns.Addr)
if err != nil || net.ParseIP(u.Host) == nil {
return nil, errors.New("default nameserver should be pure IP")
}
return nil, errors.New("default nameserver should be pure IP")
}
}
@ -874,7 +826,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R
}
func parseAuthentication(rawRecords []string) []auth.AuthUser {
var users []auth.AuthUser
users := []auth.AuthUser{}
for _, line := range rawRecords {
if user, pass, found := strings.Cut(line, ":"); found {
users = append(users, auth.AuthUser{User: user, Pass: pass})
@ -883,13 +835,11 @@ func parseAuthentication(rawRecords []string) []auth.AuthUser {
return users
}
func parseTun(rawTun RawTun, general *General, dnsCfg *DNS) (*Tun, error) {
if rawTun.Enable && rawTun.AutoDetectInterface {
func parseTun(rawTun RawTun, general *General) (*Tun, error) {
if (rawTun.Enable || general.TProxyPort != 0) && general.Interface == "" {
autoDetectInterfaceName, err := commons.GetAutoDetectInterface()
if err != nil {
log.Warnln("Can not find auto detect interface.[%s]", err)
} else {
log.Warnln("Auto detect interface: %s", autoDetectInterfaceName)
if err != nil || autoDetectInterfaceName == "" {
return nil, fmt.Errorf("can not find auto detect interface: %w. you must be detect `interface-name` if tun set to enable or `tproxy-port` isn't zore", err)
}
general.Interface = autoDetectInterfaceName
@ -901,7 +851,7 @@ func parseTun(rawTun RawTun, general *General, dnsCfg *DNS) (*Tun, error) {
if _, after, ok := strings.Cut(d, "://"); ok {
d = after
}
d = strings.Replace(d, "any", "0.0.0.0", 1)
addrPort, err := netip.ParseAddrPort(d)
if err != nil {
return nil, fmt.Errorf("parse dns-hijack url error: %w", err)
@ -910,91 +860,117 @@ func parseTun(rawTun RawTun, general *General, dnsCfg *DNS) (*Tun, error) {
dnsHijack = append(dnsHijack, addrPort)
}
var tunAddressPrefix netip.Prefix
if dnsCfg.FakeIPRange != nil {
tunAddressPrefix = *dnsCfg.FakeIPRange.IPNet()
} else {
tunAddressPrefix = netip.MustParsePrefix("198.18.0.1/16")
}
return &Tun{
Enable: rawTun.Enable,
Device: rawTun.Device,
Stack: rawTun.Stack,
DNSHijack: dnsHijack,
AutoRoute: rawTun.AutoRoute,
AutoDetectInterface: rawTun.AutoDetectInterface,
TunAddressPrefix: tunAddressPrefix,
Enable: rawTun.Enable,
Device: rawTun.Device,
Stack: rawTun.Stack,
DNSHijack: dnsHijack,
AutoRoute: rawTun.AutoRoute,
}, nil
}
func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
sniffer := &Sniffer{
Enable: snifferRaw.Enable,
}
func parseScript(script Script) error {
mainCode := script.MainCode
shortcutsCode := script.ShortcutsCode
var ports []utils.Range[uint16]
if len(snifferRaw.Ports) == 0 {
ports = append(ports, *utils.NewRange[uint16](0, 65535))
if strings.TrimSpace(mainCode) == "" {
mainCode = `
def main(ctx, metadata):
return "DIRECT"
`
} else {
for _, portRange := range snifferRaw.Ports {
portRaws := strings.Split(portRange, "-")
p, err := strconv.ParseUint(portRaws[0], 10, 16)
if err != nil {
return nil, fmt.Errorf("%s format error", portRange)
}
start := uint16(p)
if len(portRaws) > 1 {
p, err = strconv.ParseUint(portRaws[1], 10, 16)
if err != nil {
return nil, fmt.Errorf("%s format error", portRange)
}
end := uint16(p)
ports = append(ports, *utils.NewRange(start, end))
} else {
ports = append(ports, *utils.NewRange(start, start))
}
}
mainCode = cleanPyKeywords(mainCode)
}
sniffer.Ports = &ports
content := `# -*- coding: UTF-8 -*-
loadSniffer := make(map[snifferTypes.Type]struct{})
from datetime import datetime as whatever
for _, snifferName := range snifferRaw.Sniffing {
find := false
for _, snifferType := range snifferTypes.List {
if snifferType.String() == strings.ToUpper(snifferName) {
find = true
loadSniffer[snifferType] = struct{}{}
}
class ClashTime:
def now(self):
return whatever.now()
def unix(self):
return int(whatever.now().timestamp())
def unix_nano(self):
return int(round(whatever.now().timestamp() * 1000))
time = ClashTime()
`
content += mainCode + "\n\n"
for k, v := range shortcutsCode {
v = cleanPyKeywords(v)
v = strings.TrimSpace(v)
if v == "" {
return fmt.Errorf("initialized rule SCRIPT failure, shortcut [%s] code invalid syntax", k)
}
if !find {
return nil, fmt.Errorf("not find the sniffer[%s]", snifferName)
}
content += "def " + strings.ToLower(k) + "(ctx, network, process_name, process_path, host, src_ip, src_port, dst_ip, dst_port):\n return " + v + "\n\n"
}
for st := range loadSniffer {
sniffer.Sniffers = append(sniffer.Sniffers, st)
}
sniffer.ForceDomain = trie.New[bool]()
for _, domain := range snifferRaw.ForceDomain {
err := sniffer.ForceDomain.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err)
}
err := os.WriteFile(C.Path.Script(), []byte(content), 0o644)
if err != nil {
return fmt.Errorf("initialized script module failure, %w", err)
}
sniffer.SkipDomain = trie.New[bool]()
for _, domain := range snifferRaw.SkipDomain {
err := sniffer.SkipDomain.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err)
}
if err = S.Py_Initialize(C.Path.GetExecutableFullPath(), C.Path.ScriptDir()); err != nil {
return fmt.Errorf("initialized script module failure, %w", err)
}
return sniffer, nil
if err = S.LoadMainFunction(); err != nil {
return fmt.Errorf("initialized script module failure, %w", err)
}
log.Infoln("Start initial script module successful, version: %s", S.Py_GetVersion())
return nil
}
func cleanPyKeywords(code string) string {
keywords := []string{"import", "print"}
for _, kw := range keywords {
reg := regexp.MustCompile("(?m)[\r\n]+^.*" + kw + ".*$")
code = reg.ReplaceAllString(code, "")
}
return code
}
func parseMitm(rawMitm RawMitm) (*Mitm, error) {
var (
req []C.Rewrite
res []C.Rewrite
)
for _, line := range rawMitm.Rules {
rule, err := rewrites.ParseRewrite(line)
if err != nil {
return nil, fmt.Errorf("parse rewrite rule failure: %w", err)
}
if rule.RuleType() == C.MitmResponseHeader || rule.RuleType() == C.MitmResponseBody {
res = append(res, rule)
} else {
req = append(req, rule)
}
}
hosts := trie.New[bool]()
if len(rawMitm.Hosts) != 0 {
for _, domain := range rawMitm.Hosts {
_ = hosts.Insert(domain, true)
}
}
_ = hosts.Insert("mitm.clash", true)
return &Mitm{
Hosts: hosts,
Rules: rewrites.NewRewriteRules(req, res),
}, nil
}

View File

@ -2,20 +2,17 @@ package config
import (
"fmt"
"github.com/Dreamacro/clash/component/geodata"
"github.com/Dreamacro/clash/component/mmdb"
"io"
"net/http"
"os"
"github.com/Dreamacro/clash/component/mmdb"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
)
var initMode = true
func downloadMMDB(path string) (err error) {
resp, err := http.Get(C.MmdbUrl)
resp, err := http.Get("https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/Country.mmdb")
if err != nil {
return
}
@ -31,25 +28,30 @@ func downloadMMDB(path string) (err error) {
return err
}
func downloadGeoIP(path string) (err error) {
resp, err := http.Get(C.GeoIpUrl)
if err != nil {
return
func initMMDB() error {
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
log.Infoln("Can't find MMDB, start download")
if err := downloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
}
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
if !mmdb.Verify() {
log.Warnln("MMDB invalid, remove and download")
if err := os.Remove(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't remove invalid MMDB: %s", err.Error())
}
if err := downloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
return nil
}
func downloadGeoSite(path string) (err error) {
resp, err := http.Get(C.GeoSiteUrl)
resp, err := http.Get("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat")
if err != nil {
return
}
@ -73,59 +75,6 @@ func initGeoSite() error {
}
log.Infoln("Download GeoSite.dat finish")
}
if initMode {
if err := geodata.Verify(C.GeositeName); err != nil {
log.Warnln("GeoSite.dat invalid, remove and download: %s", err)
if err := os.Remove(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error())
}
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
}
}
return nil
}
func initGeoIP() error {
if C.GeodataMode {
if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) {
log.Infoln("Can't find GeoIP.dat, start download")
if err := downloadGeoIP(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
}
log.Infoln("Download GeoIP.dat finish")
}
if err := geodata.Verify(C.GeoipName); err != nil {
log.Warnln("GeoIP.dat invalid, remove and download: %s", err)
if err := os.Remove(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error())
}
if err := downloadGeoIP(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
}
}
return nil
}
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
log.Infoln("Can't find MMDB, start download")
if err := downloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
}
if !mmdb.Verify() {
log.Warnln("MMDB invalid, remove and download")
if err := os.Remove(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't remove invalid MMDB: %s", err.Error())
}
if err := downloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
}
return nil
}
@ -149,23 +98,15 @@ func Init(dir string) error {
f.Write([]byte(`mixed-port: 7890`))
f.Close()
}
buf, _ := os.ReadFile(C.Path.Config())
rawCfg, err := UnmarshalRawConfig(buf)
if err != nil {
log.Errorln(err.Error())
fmt.Printf("configuration file %s test failed\n", C.Path.Config())
os.Exit(1)
}
if !C.GeodataMode {
C.GeodataMode = rawCfg.GeodataMode
}
C.GeoIpUrl = rawCfg.GeoXUrl.GeoIp
C.GeoSiteUrl = rawCfg.GeoXUrl.GeoSite
C.MmdbUrl = rawCfg.GeoXUrl.Mmdb
// initial GeoIP
if err := initGeoIP(); err != nil {
return fmt.Errorf("can't initial GeoIP: %w", err)
// initial mmdb
if err := initMMDB(); err != nil {
return fmt.Errorf("can't initial MMDB: %w", err)
}
// initial GeoSite
if err := initGeoSite(); err != nil {
return fmt.Errorf("can't initial GeoSite: %w", err)
}
return nil
}

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