Compare commits

...

132 Commits

Author SHA1 Message Date
3610d3dd84 Feature: add path to script config
script:
    path: ./script.star
2022-06-06 05:20:59 +08:00
663017a775 Feature: add match_provider to script shortcuts 2022-06-06 04:35:52 +08:00
05f0c1060b Refactor: script auto load geosite as a rule provider 2022-06-06 04:28:54 +08:00
e03f1d0565 Chore: merge branch 'with-tun' into plus-pro 2022-06-06 03:22:48 +08:00
c1821e28d3 Refactor: load geo domain matcher 2022-06-06 03:13:10 +08:00
763929997b Chore: code style 2022-06-06 02:37:10 +08:00
c8e2b30540 Chore: update build 2022-06-04 02:24:15 +08:00
dd95d335d9 Chore: merge branch 'with-tun' into plus-pro 2022-06-04 01:35:18 +08:00
bf9eb000d2 Chore: update dependencies 2022-06-03 23:53:58 +08:00
0563abae13 Chore: update build 2022-06-03 23:50:30 +08:00
3dbba5d8d2 Chore: mix the proxy adapter and interface to dns client 2022-06-03 11:27:41 +08:00
a4d135ed21 Feature: add regexp filter to use proxy provider in proxy group 2022-06-03 05:09:43 +08:00
af5bd0f65e Feature: add custom request header to proxy provider
`header` value is type of string array
header:
  Accept:
    - 'application/vnd.github.v3.raw'
  Authorization:
    - ' token xxxxxxxxxxx'
  User-Agent:
    - 'Clash/v1.10.6'

`prefix-name` add a prefix to proxy name
prefix-name: 'XXX-'
2022-06-03 05:09:43 +08:00
8ed868b0f5 Feature: add V2Ray subscription support to proxy provider 2022-06-03 05:09:42 +08:00
e7b8c9b9db Chore: make hadowsocks2 lib embed 2022-06-02 22:17:14 +08:00
ea8a5409ad Chore: merge branch 'with-tun' into plus-pro 2022-05-29 00:57:07 +08:00
39d524dc18 Chore: update dependencies 2022-05-29 00:45:29 +08:00
0be8fc387a Chore: change GEO databases source 2022-05-29 00:45:13 +08:00
985dc99b5d Refactor: use native Win32 API to detect interface changed on Windows 2022-05-28 09:50:09 +08:00
67905bcf7e Feature: make wintun driver embed 2022-05-27 09:20:46 +08:00
b37e1fb2b9 Chore: yaml bump version from v2 to v3 2022-05-27 09:08:30 +08:00
22449da5d3 Fix: cache cleanup panic 2022-05-25 02:00:24 +08:00
6ad2cde909 Feature: support relay Socks5 UDP
supports relaying of all UDP traffic except the HTTP outbound.
2022-05-25 01:39:58 +08:00
68cf94a866 Chore: test cases 2022-05-25 01:36:27 +08:00
7f41f94fff Fix: benchmark read bytes 2022-05-23 12:58:18 +08:00
d1f0dac302 Fix: test broken on opensource repo 2022-05-23 12:30:54 +08:00
afb3e00067 Chore: add benchmark r/w 2022-05-23 12:27:52 +08:00
5b49414b49 Chore: merge branch 'with-tun' into plus-pro 2022-05-22 05:50:29 +08:00
fe44a762c2 Chore: update dependencies 2022-05-22 05:32:36 +08:00
ce1014eae3 Feature: support relay UDP traffic 2022-05-22 05:32:15 +08:00
9a31ad6151 Chore: cleanup test go.mod 2022-05-21 17:46:34 +08:00
09cc6b69e3 Chore: cleanup test code 2022-05-21 17:38:17 +08:00
622b10d34d Chore: adjust iptables 2022-05-21 09:35:02 +08:00
88b5741ad8 Fix: addrToMetadata err should be nil 2022-05-21 08:19:33 +08:00
d11d28c358 Feature: add force-cert-verify to general config
force verify TLS Certificate, prevent machine-in-the-middle attacks.
2022-05-19 04:27:22 +08:00
03499fcea6 Refactor: fetcher by generics 2022-05-19 04:27:22 +08:00
f788411154 Refactor: use raw proxy adapter to get proxy connection by dns client 2022-05-18 20:35:59 +08:00
3d2b4b1f3a Refactor: get default route interface by syscall on darwin 2022-05-18 05:58:58 +08:00
5642d9c98e Fix: should flush interface cache by switch network 2022-05-18 04:45:19 +08:00
7a406b991e Fix: module clash-test 2022-05-18 04:08:35 +08:00
8603ac40a1 Chore: make linter happy 2022-05-17 19:58:33 +08:00
178c70a320 Chore: merge branch 'with-tun' into plus-pro 2022-05-16 03:01:05 +08:00
34eeb58bfa Chore: update dependencies 2022-05-16 02:24:05 +08:00
3d25f16b3b Feature: make tls sni sniffing switch config 2022-05-16 01:43:24 +08:00
891a56fd99 Feature: apply destination IP to tracker by Direct outbound for fake-ip mode 2022-05-16 01:43:24 +08:00
ffbdcfcbfd Feature: add update GEO databases to rest api 2022-05-16 01:43:23 +08:00
72b9b829e9 Fix: set mitm outbound 2022-05-16 01:43:23 +08:00
8b3e42bf19 Refactor: tun config 2022-05-16 01:43:23 +08:00
e92bea8401 Chore: merge branch 'ogn-dev' into with-tun 2022-05-16 01:41:02 +08:00
b384449717 Fix: fix upgrade header detect (#2134) 2022-05-15 09:12:53 +08:00
53c83118bc Chore: merge branch 'ogn-dev' into with-tun 2022-05-14 02:29:50 +08:00
da7ffc0da9 Fix: add length check for ssr auth_aes128_sha1 (#2129) 2022-05-13 11:21:39 +08:00
6fe19944ad Chore: code style 2022-05-09 09:12:30 +08:00
9f00907647 Chore: merge branch 'with-tun' into plus-pro 2022-05-09 08:11:07 +08:00
ace84ff548 Chore: code style 2022-05-09 08:10:20 +08:00
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
222 changed files with 10541 additions and 3870 deletions

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

@ -0,0 +1,142 @@
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',$(Get-Date -Format 'yyyy.MM.dd')) | 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-plus-pro-windows-amd64.exe
$env:GOAMD64="v3"; $env:CGO_ENABLED=1; go build -tags build_actions -trimpath -ldflags '-w -s -buildid=' -o bin/clash-plus-pro-windows-amd64-v3.exe
$version = Get-Date -Format 'yyyy.MM.dd'
cd bin/
Compress-Archive -Path clash-plus-pro-windows-amd64.exe -DestinationPath clash-plus-pro-windows-amd64-$version.zip
Compress-Archive -Path clash-plus-pro-windows-amd64-v3.exe -DestinationPath clash-plus-pro-windows-amd64-v3-$version.zip
Remove-Item -Force clash-plus-pro-windows-amd64.exe
Remove-Item -Force clash-plus-pro-windows-amd64-v3.exe
"$version" | Out-File version.txt -NoNewLine
- name: Upload files to tag
if: startsWith(github.ref, 'refs/tags/') == false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$version = Get-Date -Format 'yyyy.MM.dd'
$plus_pro = curl `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
https://api.github.com/repos/yaling888/clash/releases/tags/plus_pro | ConvertFrom-Json
$plus_pro_url = $plus_pro.url
$upload_url = $plus_pro.upload_url
$plus_pro_upload_url = $upload_url.Substring(0,$upload_url.Length-13)
curl `
-X PATCH `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$plus_pro_url" `
-d "{`"name`":`"Plus Pro $version`",`"draft`":true}" | Out-Null
foreach ($asset in $plus_pro.assets)
{
curl `
-X DELETE `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$($asset.url)" | Out-Null
}
curl `
-X POST `
-H "Content-Type: application/zip" `
-T "bin/clash-plus-pro-windows-amd64-$version.zip" `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$plus_pro_upload_url?name=clash-plus-pro-windows-amd64-$version.zip" | Out-Null
curl `
-X POST `
-H "Content-Type: application/zip" `
-T "bin/clash-plus-pro-windows-amd64-v3-$version.zip" `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$plus_pro_upload_url?name=clash-plus-pro-windows-amd64-v3-$version.zip" | Out-Null
curl `
-X POST `
-H "Content-Type: text/plain" `
-T "bin/version.txt" `
-H "Accept: application/vnd.github.v3+json" `
-H "Authorization: token $env:GITHUB_TOKEN" `
"$plus_pro_upload_url?name=version.txt" | Out-Null
#- name: Upload files to Artifacts
# uses: actions/upload-artifact@v2
# with:
# name: clash-windows-amd64-${{ steps.test.outputs.file_sha }}-${{ steps.test.outputs.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: false
generate_release_notes: false
#- name: Delete workflow runs
# uses: GitRML/delete-workflow-runs@main
# with:
# retain_days: 1
# keep_minimum_runs: 2

View File

@ -3,8 +3,7 @@ on:
push:
branches:
- rm
tags:
- '*'
jobs:
build:
@ -49,6 +48,8 @@ jobs:
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/')
@ -74,3 +75,5 @@ jobs:
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

View File

@ -20,3 +20,4 @@ jobs:
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --build-tags=build_local

View File

@ -1,5 +1,8 @@
name: Release
on: [push]
on:
push:
branches:
- rm
jobs:
build:
runs-on: ubuntu-latest
@ -17,36 +20,75 @@ jobs:
- 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: ~/go/pkg/mod
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: |
go test ./...
# 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 ../../
- name: SSH connection to Actions
uses: P3TERX/ssh2actions@v1.0.0
if: github.actor == github.repository_owner && contains(github.event.head_commit.message, '[ssh]')
# 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
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: |
@ -58,12 +100,14 @@ jobs:
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: 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
@ -71,6 +115,6 @@ jobs:
with:
keep_latest: 1
delete_tags: true
delete_tag_pattern: tun
delete_tag_pattern: plus-pro
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

140
Makefile
View File

@ -1,119 +1,70 @@
GOCMD=go
XGOCMD=xgo -go=go-1.18.x
GOBUILD=CGO_ENABLED=1 $(GOCMD) build -trimpath
GOCLEAN=$(GOCMD) clean
NAME=clash
BINDIR=bin
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-amd64 \
darwin-amd64-v3 \
darwin-arm64 \
linux-386 \
linux-amd64 \
linux-amd64-v3 \
linux-armv5 \
linux-armv6 \
linux-armv7 \
linux-arm64 \
linux-mips-softfloat \
linux-mips-hardfloat \
linux-mipsle-softfloat \
linux-mipsle-hardfloat \
linux-mips64 \
linux-mips64le \
freebsd-386 \
freebsd-amd64 \
freebsd-amd64-v3 \
freebsd-arm64
linux-amd64
# linux-arm64
# linux-386
WINDOWS_ARCH_LIST = \
windows-386 \
windows-amd64 \
windows-amd64-v3 \
windows-arm64 \
windows-arm32v7
windows-386
# windows-arm64
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
docker:
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
local:
$(GOBUILD) -ldflags $(RELEASE_LDFLAGS) -tags build_local -o $(BINDIR)/$(NAME)-$@
local-v3:
GOAMD64=v3 $(GOBUILD) -ldflags $(RELEASE_LDFLAGS) -tags build_local -o $(BINDIR)/$(NAME)-$@
darwin-amd64:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-v3:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
$(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-amd64:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-v3:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(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)-$@
$(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-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)-$@
freebsd-386:
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64:
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64-v3:
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-amd64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
$(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-amd64-v3:
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(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))
@ -130,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 -rf $(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)

291
README.md
View File

@ -36,12 +36,74 @@
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:
```sh
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 local
# or make local-v3
ls bin/
# run
sudo bin/clash-local
```
### General configuration
```yaml
sniffing: true # Sniff TLS SNI
force-cert-verify: true # force verify TLS Certificate, prevent machine-in-the-middle attacks
```
### 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 resolve ip with a proxy tunnel or interface.
Support `geosite` with `fallback-filter`.
Use curl -X POST controllerip:port/cache/fakeip/flush to flush persistence fakeip
Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fakeip
```yaml
dns:
enable: true
@ -57,8 +119,8 @@ Use curl -X POST controllerip:port/cache/fakeip/flush to flush persistence fakei
- https://doh.pub/dns-query
- tls://223.5.5.5:853
fallback:
- 'tls://8.8.4.4:853#proxy or interface'
- '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:
@ -70,21 +132,62 @@ Use curl -X POST controllerip:port/cache/fakeip/flush to flush persistence fakei
```
### TUN configuration
Supports macOS, Linux and Windows.
Simply add the following to the main configuration:
#### NOTE:
> auto-route and auto-detect-interface only available on macOS, Windows and Linux, receive IPv4 traffic
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 # System or gVisor
stack: system # or gvisor
# device: tun://utun8 # or fd://xxx, it's optional
dns-hijack:
- 0.0.0.0:53 # hijack all public
# dns-hijack:
# - 8.8.8.8:53
# - tcp://8.8.8.8:53
# - any:53
# - tcp://any:53
auto-route: true # auto set global route
auto-detect-interface: true # conflict with interface-name
```
or
```yaml
interface-name: en0
tun:
enable: true
stack: system # or gvisor
# dns-hijack:
# - 8.8.8.8:53
# - tcp://8.8.8.8:53
auto-route: true # auto set global route
```
It's recommended to use fake-ip mode for the DNS server.
Clash needs elevated permission to create TUN device:
```sh
$ sudo ./clash
```
Then manually create the default route and DNS server. If your device already has some TUN device, Clash TUN might not work. In this case, fake-ip-filter may helpful.
Enjoy! :)
#### For Windows:
```yaml
tun:
enable: true
stack: gvisor # or system
dns-hijack:
- 198.18.0.2:53 # when `fake-ip-range` is 198.18.0.1/16, should hijack 198.18.0.2:53
auto-route: true # auto set global route for Windows
# It is recommended to use `interface-name`
auto-detect-interface: true # auto detect interface, conflict with `interface-name`
```
Finally, open the Clash
### Rules configuration
- Support rule `GEOSITE`.
- 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.
@ -94,7 +197,20 @@ The `GEOIP` databases via [https://github.com/Loyalsoldier/geoip](https://raw.gi
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'
BilibiliUdp: |
network == "udp" and match_provider("geosite:bilibili")
rules:
# rule SCRIPT shortcuts
- SCRIPT,quic,REJECT # Disable QUIC, same as rule "DST-PORT,443,REJECT,udp"
- SCRIPT,privacy,REJECT
- SCRIPT,BilibiliUdp,REJECT # same as rule "GEOSITE,bilibili,REJECT,udp"
# network condition for all rules
- DOMAIN-SUFFIX,example.com,DIRECT,tcp
- DOMAIN-SUFFIX,example.com,REJECT,udp
@ -104,7 +220,10 @@ rules:
# 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
@ -126,12 +245,86 @@ rules:
- MATCH,PROXY
```
### Script configuration
Script enables users to programmatically select a policy for the packets with more flexibility.
```yaml
mode: script
script:
# path: ./script.star
code: |
def main(ctx, metadata):
if metadata["process_name"] == 'apsd':
return "DIRECT"
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"
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.
Currently XTLS only supports TCP transport.
Support relay `UDP` traffic.
Support filtering proxy providers in proxy groups.
Support custom http request header, prefix name and V2Ray subscription URL in proxy providers.
```yaml
proxies:
# VLESS
@ -168,6 +361,56 @@ proxies:
# udp: true
# sni: example.com # aka server name
# skip-cert-verify: true
proxy-groups:
# Relay chains the proxies. proxies shall not contain a relay.
# Support relay UDP traffic.
# Traffic: clash <-> ss1 <-> trojan <-> vmess <-> ss2 <-> Internet
- name: "relay-udp-over-tcp"
type: relay
proxies:
- ss1
- trojan
- vmess
- ss2
- name: "relay-raw-udp"
type: relay
proxies:
- ss1
- ss2
- ss3
- name: "filtering-proxy-providers"
type: url-test
url: "http://www.gstatic.com/generate_204"
interval: 300
tolerance: 200
# lazy: true
filter: "XXX" # a regular expression
use:
- provider1
proxy-providers:
provider1:
type: http
url: "url" # support V2Ray subscription URL
interval: 3600
path: ./providers/provider1.yaml
# filter: "xxx"
# prefix-name: "XXX-"
header: # custom http request header
User-Agent:
- "Clash/v1.10.6"
# Accept:
# - 'application/vnd.github.v3.raw'
# Authorization:
# - ' token xxxxxxxxxxx'
health-check:
enable: false
interval: 1200
# lazy: false # default value is true
url: http://www.gstatic.com/generate_204
```
### IPTABLES configuration
@ -184,7 +427,7 @@ iptables:
Run Clash as a daemon.
Create the systemd configuration file at /etc/systemd/system/clash.service:
```shell
```sh
[Unit]
Description=Clash daemon, A rule-based proxy in Go.
After=network.target
@ -199,18 +442,34 @@ ExecStart=/usr/local/bin/clash -d /etc/clash
WantedBy=multi-user.target
```
Launch clashd on system startup with:
```shell
```sh
$ systemctl enable clash
```
Launch clashd immediately with:
```shell
```sh
$ systemctl start clash
```
### Display Process name
Add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
To display process name online by click [http://yacd.clash-plus.cf](http://yacd.clash-plus.cf) for local API by Safari or [https://yacd.clash-plus.cf](https://yacd.clash-plus.cf) for local API by Chrome.
To display process name in GUI please use https://yaling888.github.io/yacd/.
You can download the [Dashboard](https://github.com/yaling888/yacd/archive/gh-pages.zip) into Clash home directory:
```sh
$ cd ~/.config/clash
$ curl -LJ https://github.com/yaling888/yacd/archive/gh-pages.zip -o yacd-gh-pages.zip
$ unzip yacd-gh-pages.zip
$ mv yacd-gh-pages 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.
## Plus Pro Release
[Release](https://github.com/yaling888/clash/releases/tag/plus_pro)
## 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)

View File

@ -6,8 +6,8 @@ import (
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
"time"
"github.com/Dreamacro/clash/common/queue"
@ -19,7 +19,7 @@ import (
type Proxy struct {
C.ProxyAdapter
history *queue.Queue
history *queue.Queue[C.DelayHistory]
alive *atomic.Bool
}
@ -38,11 +38,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)
return conn, err
}
@ -62,10 +58,10 @@ func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
// DelayHistory implements C.Proxy
func (p *Proxy) DelayHistory() []C.DelayHistory {
queue := p.history.Copy()
queueM := p.history.Copy()
histories := []C.DelayHistory{}
for _, item := range queue {
histories = append(histories, item.(C.DelayHistory))
for _, item := range queueM {
histories = append(histories, item)
}
return histories
}
@ -78,11 +74,7 @@ func (p *Proxy) LastDelay() (delay uint16) {
return max
}
last := p.history.Last()
if last == nil {
return max
}
history := last.(C.DelayHistory)
history := p.history.Last()
if history.Delay == 0 {
return max
}
@ -97,7 +89,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
}
mapping := map[string]any{}
json.Unmarshal(inner, &mapping)
_ = json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory()
mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP()
@ -129,7 +121,9 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
if err != nil {
return
}
defer instance.Close()
defer func() {
_ = instance.Close()
}()
req, err := http.NewRequest(http.MethodHead, url, nil)
if err != nil {
@ -138,7 +132,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
req = req.WithContext(ctx)
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
DialContext: func(context.Context, string, string) (net.Conn, error) {
return instance, nil
},
// from http.DefaultTransport
@ -160,13 +154,13 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
if err != nil {
return
}
resp.Body.Close()
_ = resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond)
return
}
func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)}
}
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@ -191,7 +185,7 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
DstIP: nil,
DstIP: netip.Addr{},
DstPort: port,
}
return

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

@ -3,9 +3,11 @@ package inbound
import (
"net"
"net/http"
"net/netip"
"strconv"
"strings"
"github.com/Dreamacro/clash/common/nnip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
@ -21,12 +23,10 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
case socks5.AtypIPv4:
ip := net.IP(target[1 : 1+net.IPv4len])
metadata.DstIP = ip
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6:
ip := net.IP(target[1 : 1+net.IPv6len])
metadata.DstIP = ip
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv6len]))
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
}
@ -47,14 +47,14 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
NetWork: C.TCP,
AddrType: C.AtypDomainName,
Host: host,
DstIP: nil,
DstIP: netip.Addr{},
DstPort: port,
}
ip := net.ParseIP(host)
if ip != nil {
ip, err := netip.ParseAddr(host)
if err == nil {
switch {
case ip.To4() == nil:
case ip.Is6():
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
@ -65,12 +65,12 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
return metadata
}
func parseAddr(addr string) (net.IP, string, error) {
func parseAddr(addr string) (netip.Addr, string, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, "", err
return netip.Addr{}, "", err
}
ip := net.ParseIP(host)
return ip, port, nil
ip, err := netip.ParseAddr(host)
return ip, port, err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"io"
"net"
"github.com/Dreamacro/clash/component/dialer"
@ -30,12 +31,17 @@ func (b *Base) Type() C.AdapterType {
}
// StreamConn implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
func (b *Base) StreamConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
// StreamPacketConn implements C.ProxyAdapter
func (b *Base) StreamPacketConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
// ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
func (b *Base) ListenPacketContext(_ context.Context, _ *C.Metadata, _ ...dialer.Option) (C.PacketConn, error) {
return nil, errors.New("no support")
}
@ -57,7 +63,7 @@ func (b *Base) Addr() string {
}
// Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
func (b *Base) Unwrap(_ *C.Metadata) C.Proxy {
return nil
}
@ -133,6 +139,40 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
func NewPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}}
}
type wrapConn struct {
net.PacketConn
}
func (*wrapConn) Read([]byte) (int, error) {
return 0, io.EOF
}
func (*wrapConn) Write([]byte) (int, error) {
return 0, io.EOF
}
func (*wrapConn) RemoteAddr() net.Addr {
return nil
}
func WrapConn(packetConn net.PacketConn) net.Conn {
return &wrapConn{
PacketConn: packetConn,
}
}
func IsPacketConn(c net.Conn) bool {
if _, ok := c.(net.PacketConn); !ok {
return false
}
if ua, ok := c.LocalAddr().(*net.UnixAddr); ok {
return ua.Net == "unixgram"
}
return true
}

View File

@ -3,6 +3,7 @@ package outbound
import (
"context"
"net"
"net/netip"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
@ -19,18 +20,26 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
if err != nil {
return nil, err
}
tcpKeepAlive(c)
if !metadata.DstIP.IsValid() && c.RemoteAddr() != nil {
if h, _, err := net.SplitHostPort(c.RemoteAddr().String()); err == nil {
metadata.DstIP = netip.MustParseAddr(h)
}
}
return NewConn(c, d), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
func (d *Direct) ListenPacketContext(ctx context.Context, _ *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
opts = append(opts, dialer.WithDirect())
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
return newPacketConn(&directPacketConn{pc}, d), nil
return NewPacketConn(&directPacketConn{pc}, d), nil
}
type directPacketConn struct {

View File

@ -89,6 +89,10 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
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
}

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,22 +6,49 @@ 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
}
// ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return newPacketConn(&nopPacketConn{}, r), nil
return NewPacketConn(&nopPacketConn{}, r), nil
}
func NewReject() *Reject {

View File

@ -10,11 +10,10 @@ import (
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
"github.com/Dreamacro/go-shadowsocks2/core"
)
type ShadowSocks struct {
@ -74,6 +73,21 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
return c, err
}
// StreamPacketConn implements C.ProxyAdapter
func (ss *ShadowSocks) StreamPacketConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
if !IsPacketConn(c) {
return c, fmt.Errorf("%s connect error: can not convert net.Conn to net.PacketConn", ss.addr)
}
addr, err := resolveUDPAddr("udp", ss.addr)
if err != nil {
return c, err
}
pc := ss.cipher.PacketConn(c.(net.PacketConn))
return WrapConn(&ssPacketConn{PacketConn: pc, rAddr: addr}), nil
}
// DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
@ -95,14 +109,13 @@ func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Meta
return nil, err
}
addr, err := resolveUDPAddr("udp", ss.addr)
c, err := ss.StreamPacketConn(WrapConn(pc), metadata)
if err != nil {
pc.Close()
_ = pc.Close()
return nil, err
}
pc = ss.cipher.PacketConn(pc)
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
return NewPacketConn(c.(net.PacketConn), ss), nil
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {

View File

@ -8,12 +8,11 @@ import (
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
)
type ShadowSocksR struct {
@ -59,6 +58,22 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn,
return c, err
}
// StreamPacketConn implements C.ProxyAdapter
func (ssr *ShadowSocksR) StreamPacketConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
if !IsPacketConn(c) {
return c, fmt.Errorf("%s connect error: can not convert net.Conn to net.PacketConn", ssr.addr)
}
addr, err := resolveUDPAddr("udp", ssr.addr)
if err != nil {
return c, err
}
pc := ssr.cipher.PacketConn(c.(net.PacketConn))
pc = ssr.protocol.PacketConn(pc)
return WrapConn(&ssPacketConn{PacketConn: pc, rAddr: addr}), nil
}
// DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
@ -80,18 +95,22 @@ func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Me
return nil, err
}
addr, err := resolveUDPAddr("udp", ssr.addr)
c, err := ssr.StreamPacketConn(WrapConn(pc), metadata)
if err != nil {
pc.Close()
_ = pc.Close()
return nil, err
}
pc = ssr.cipher.PacketConn(pc)
pc = ssr.protocol.PacketConn(pc)
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
return NewPacketConn(c.(net.PacketConn), ssr), nil
}
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
// SSR protocol compatibility
// https://github.com/Dreamacro/clash/pull/2056
if option.Cipher == "none" {
option.Cipher = "dummy"
}
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher
password := option.Password
@ -103,13 +122,14 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
ivSize int
key []byte
)
if option.Cipher == "dummy" {
ivSize = 0
key = core.Kdf(option.Password, 16)
} else {
ciph, ok := coreCiph.(*core.StreamCipher)
if !ok {
return nil, fmt.Errorf("%s is not dummy or a supported stream cipher in ssr", cipher)
return nil, fmt.Errorf("%s is not none or a supported stream cipher in ssr", cipher)
}
ivSize = ciph.IVSize()
key = ciph.Key

View File

@ -58,6 +58,18 @@ func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, err
}
// StreamPacketConn implements C.ProxyAdapter
func (s *Snell) StreamPacketConn(c net.Conn, _ *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err := snell.WriteUDPHeader(c, s.version)
if err != nil {
return c, err
}
return WrapConn(snell.PacketConn(c)), nil
}
// DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
if s.version == snell.Version2 && len(opts) == 0 {
@ -68,7 +80,7 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close()
_ = c.Close()
return nil, err
}
return NewConn(c, s), err
@ -93,15 +105,14 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return nil, err
}
tcpKeepAlive(c)
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version)
pc, err := s.StreamPacketConn(c, metadata)
if err != nil {
_ = c.Close()
return nil, err
}
pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil
return NewPacketConn(pc.(net.PacketConn), s), nil
}
func NewSnell(option SnellOption) (*Snell, error) {

View File

@ -37,12 +37,59 @@ type Socks5Option struct {
// StreamConn implements C.ProxyAdapter
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
c, _, err = ss.streamConn(c, metadata)
return c, err
}
func (ss *Socks5) StreamSocks5PacketConn(c net.Conn, pc net.PacketConn, metadata *C.Metadata) (net.PacketConn, error) {
if c == nil {
return pc, fmt.Errorf("%s connect error: parameter net.Conn is nil", ss.addr)
}
if pc == nil {
return pc, fmt.Errorf("%s connect error: parameter net.PacketConn is nil", ss.addr)
}
cc, bindAddr, err := ss.streamConn(c, metadata)
if err != nil {
return pc, err
}
c = cc
go func() {
_, _ = io.Copy(io.Discard, c)
_ = c.Close()
// A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928
_ = pc.Close()
}()
// Support unspecified UDP bind address.
bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
return pc, errors.New("invalid UDP bind address")
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
if err != nil {
return pc, err
}
bindUDPAddr.IP = serverAddr.IP
}
return &socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, nil
}
func (ss *Socks5) streamConn(c net.Conn, metadata *C.Metadata) (_ net.Conn, bindAddr socks5.Addr, err error) {
if ss.tls {
cc := tls.Client(c, ss.tlsConfig)
err := cc.Handshake()
c = cc
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
return c, nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
}
@ -53,10 +100,14 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
Password: ss.pass,
}
}
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
return nil, err
if metadata.NetWork == C.UDP {
bindAddr, err = socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
} else {
bindAddr, err = socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user)
}
return c, nil
return c, bindAddr, err
}
// DialContext implements C.ProxyAdapter
@ -81,61 +132,24 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
return
}
if ss.tls {
cc := tls.Client(c, ss.tlsConfig)
err = cc.Handshake()
c = cc
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
defer safeConnClose(c, err)
tcpKeepAlive(c)
var user *socks5.User
if ss.user != "" {
user = &socks5.User{
Username: ss.user,
Password: ss.pass,
}
}
bindAddr, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
if err != nil {
err = fmt.Errorf("client hanshake error: %w", err)
return
}
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil {
return
}
go func() {
io.Copy(io.Discard, c)
c.Close()
// A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928
pc.Close()
}()
tcpKeepAlive(c)
// Support unspecified UDP bind address.
bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
err = errors.New("invalid UDP bind address")
pc, err = ss.StreamSocks5PacketConn(c, pc, metadata)
if err != nil {
return
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
if err != nil {
return nil, err
}
bindUDPAddr.IP = serverAddr.IP
}
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
return NewPacketConn(pc, ss), nil
}
func NewSocks5(option Socks5Option) *Socks5 {
@ -199,6 +213,6 @@ func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
}
func (uc *socksPacketConn) Close() error {
uc.tcpConn.Close()
_ = uc.tcpConn.Close()
return uc.PacketConn.Close()
}

View File

@ -72,8 +72,7 @@ func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
return t.instance.StreamConn(c)
}
// StreamConn implements C.ProxyAdapter
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
func (t *Trojan) trojanStream(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
@ -85,39 +84,63 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
c, err = t.instance.PresetXTLSConn(c)
c, err = t.instance.PrepareXTLSConn(c)
if err != nil {
return nil, err
return c, 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
}
// StreamConn implements C.ProxyAdapter
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return t.trojanStream(c, metadata)
}
// StreamPacketConn implements C.ProxyAdapter
func (t *Trojan) StreamPacketConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
c, err = t.trojanStream(c, metadata)
if err != nil {
return c, err
}
pc := t.instance.PacketConn(c)
return WrapConn(pc), nil
}
// DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport
if t.transport != nil && len(opts) == 0 {
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, err
}
c, err = t.instance.PresetXTLSConn(c)
defer safeConnClose(c, err)
c, err = t.instance.PrepareXTLSConn(c)
if err != nil {
c.Close()
return nil, err
}
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
c.Close()
return nil, err
}
return NewConn(c, t), nil
}
c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
@ -137,33 +160,44 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
var c net.Conn
// grpc transport
// gun transport
if t.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
return nil, err
}
defer safeConnClose(c, err)
} else {
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
c, err = t.instance.PrepareXTLSConn(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
return nil, err
}
defer safeConnClose(c, err)
tcpKeepAlive(c)
c, err = t.plainStream(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
if err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)); err != nil {
return nil, err
}
pc := t.instance.PacketConn(c)
return NewPacketConn(pc, t), nil
}
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = t.StreamPacketConn(c, metadata)
if err != nil {
return nil, err
}
pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err
return NewPacketConn(c.(net.PacketConn), t), nil
}
func NewTrojan(option TrojanOption) (*Trojan, error) {

View File

@ -13,8 +13,8 @@ import (
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
@ -25,14 +25,14 @@ func serializesSocksAddr(metadata *C.Metadata) []byte {
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType {
case socks5.AtypDomainName:
len := uint8(len(metadata.Host))
lenM := uint8(len(metadata.Host))
host := []byte(metadata.Host)
buf = [][]byte{{aType, len}, host, port}
buf = [][]byte{{aType, lenM}, host, port}
case socks5.AtypIPv4:
host := metadata.DstIP.To4()
host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port}
case socks5.AtypIPv6:
host := metadata.DstIP.To16()
host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port}
}
return bytes.Join(buf, nil)
@ -52,7 +52,7 @@ func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
}
func safeConnClose(c net.Conn, err error) {
if err != nil {
c.Close()
if err != nil && c != nil {
_ = c.Close()
}
}

View File

@ -58,6 +58,7 @@ type VlessOption struct {
ServerName string `proxy:"servername,omitempty"`
}
// StreamConn implements C.ProxyAdapter
func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
switch v.option.Network {
@ -147,6 +148,26 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return v.client.StreamConn(c, parseVlessAddr(metadata))
}
// StreamPacketConn implements C.ProxyAdapter
func (v *Vless) StreamPacketConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
var err error
c, err = v.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return WrapConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), nil
}
func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) {
host, _, _ := net.SplitHostPort(v.addr)
@ -219,18 +240,18 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
var c net.Conn
// gun transport
if v.transport != nil && len(opts) == 0 {
// vless use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
@ -238,22 +259,27 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
return nil, fmt.Errorf("new vless client error: %v", err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata)
return NewPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamPacketConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err)
}
return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
return NewPacketConn(c.(net.PacketConn), v), nil
}
func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
@ -263,11 +289,11 @@ func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
case C.AtypIPv4:
addrType = byte(vless.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6:
addrType = byte(vless.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName:
addrType = byte(vless.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
@ -292,7 +318,7 @@ type vlessPacketConn struct {
mux sync.Mutex
}
func (vc *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
func (vc *vlessPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) {
total := len(b)
if total == 0 {
return 0, nil

View File

@ -3,13 +3,13 @@ package outbound
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
@ -116,6 +116,9 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
} else {
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
}
c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http":
@ -135,6 +138,9 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if err != nil {
return nil, err
}
} else {
http.Header(v.option.HTTPOpts.Headers).Set("Host", convert.RandHost())
convert.SetUserAgent(v.option.HTTPOpts.Headers)
}
host, _, _ := net.SplitHostPort(v.addr)
@ -195,6 +201,26 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return v.client.StreamConn(c, parseVmessAddr(metadata))
}
// StreamPacketConn implements C.ProxyAdapter
func (v *Vmess) StreamPacketConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return c, fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
var err error
c, err = v.StreamConn(c, metadata)
if err != nil {
return c, fmt.Errorf("new vmess client error: %v", err)
}
return WrapConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), nil
}
// DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport
@ -226,18 +252,18 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
var c net.Conn
// gun transport
if v.transport != nil && len(opts) == 0 {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
@ -245,22 +271,27 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
return nil, fmt.Errorf("new vmess client error: %v", err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata)
return NewPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamPacketConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
return NewPacketConn(c.(net.PacketConn), v), nil
}
func NewVmess(option VmessOption) (*Vmess, error) {
@ -342,11 +373,11 @@ func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
@ -368,7 +399,7 @@ type vmessPacketConn struct {
rAddr net.Addr
}
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
func (uc *vmessPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) {
return uc.Conn.Write(b)
}

View File

@ -14,7 +14,7 @@ import (
type Fallback struct {
*outbound.Base
disableUDP bool
single *singledo.Single
single *singledo.Single[[]C.Proxy]
providers []provider.ProxyProvider
}
@ -73,11 +73,11 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (any, error) {
elm, _, _ := f.single.Do(func() ([]C.Proxy, error) {
return getProvidersProxies(f.providers, touch), nil
})
return elm.([]C.Proxy)
return elm
}
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
@ -99,7 +99,7 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider)
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration),
providers: providers,
disableUDP: option.DisableUDP,
}

View File

@ -22,7 +22,7 @@ type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
type LoadBalance struct {
*outbound.Base
disableUDP bool
single *singledo.Single
single *singledo.Single[[]C.Proxy]
providers []provider.ProxyProvider
strategyFn strategyFn
}
@ -50,7 +50,7 @@ func getKey(metadata *C.Metadata) string {
}
}
if metadata.DstIP == nil {
if !metadata.DstIP.IsValid() {
return ""
}
@ -140,11 +140,11 @@ func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() (any, error) {
elm, _, _ := lb.single.Do(func() ([]C.Proxy, error) {
return getProvidersProxies(lb.providers, touch), nil
})
return elm.([]C.Proxy)
return elm
}
// MarshalJSON implements C.ProxyAdapter
@ -176,7 +176,7 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration),
providers: providers,
strategyFn: strategyFn,
disableUDP: option.DisableUDP,

View File

@ -3,6 +3,7 @@ package outboundgroup
import (
"errors"
"fmt"
"regexp"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider"
@ -29,6 +30,7 @@ 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) {
@ -37,10 +39,23 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
groupOption := &GroupCommonOption{
Lazy: true,
}
if err := decoder.Decode(config, groupOption); err != nil {
var (
filterRegx *regexp.Regexp
err error
)
if err = decoder.Decode(config, groupOption); err != nil {
return nil, errFormat
}
if groupOption.Filter != "" {
filterRegx, err = regexp.Compile(groupOption.Filter)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
}
if groupOption.Type == "" || groupOption.Name == "" {
return nil, errFormat
}
@ -90,7 +105,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
}
if len(groupOption.Use) != 0 {
list, err := getProviders(providersMap, groupOption.Use)
list, err := getProviders(providersMap, groupOption, filterRegx)
if err != nil {
return nil, err
}
@ -130,8 +145,13 @@ func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
return ps, nil
}
func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) {
var ps []types.ProxyProvider
func getProviders(mapping map[string]types.ProxyProvider, groupOption *GroupCommonOption, filterRegx *regexp.Regexp) ([]types.ProxyProvider, error) {
var (
ps []types.ProxyProvider
list = groupOption.Use
groupName = groupOption.Name
)
for _, name := range list {
p, ok := mapping[name]
if !ok {
@ -141,6 +161,27 @@ func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]type
if p.VehicleType() == types.Compatible {
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
}
if filterRegx != nil {
var hc *provider.HealthCheck
if groupOption.Type == "select" || groupOption.Type == "relay" {
hc = provider.NewHealthCheck([]C.Proxy{}, "", 0, true)
} else {
if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, errMissHealthCheck
}
hc = provider.NewHealthCheck([]C.Proxy{}, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
}
if _, ok = mapping[groupName]; ok {
groupName += "->" + p.Name()
}
pd := p.(*provider.ProxySetProvider)
p = provider.NewProxyFilterProvider(groupName, pd, hc, filterRegx)
pd.RegisterProvidersInUse(p)
}
ps = append(ps, p)
}
return ps, nil

View File

@ -4,17 +4,21 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/netip"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Relay struct {
*outbound.Base
single *singledo.Single
single *singledo.Single[[]C.Proxy]
providers []provider.ProxyProvider
}
@ -34,30 +38,12 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return proxies[0].DialContext(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...)...)
c, err := r.streamContext(ctx, proxies, 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
return nil, err
}
last := proxies[len(proxies)-1]
c, err = last.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
@ -66,6 +52,147 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return outbound.NewConn(c, r), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
var proxies []C.Proxy
for _, proxy := range r.proxies(metadata, true) {
if proxy.Type() != C.Direct {
proxies = append(proxies, proxy)
}
}
length := len(proxies)
switch length {
case 0:
return outbound.NewDirect().ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
case 1:
proxy := proxies[0]
if !proxy.SupportUDP() {
return nil, fmt.Errorf("%s connect error: proxy [%s] UDP is not supported", proxy.Addr(), proxy.Name())
}
return proxy.ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
var (
firstIndex = 0
nextIndex = 1
lastUDPOverTCPIndex = -1
rawUDPRelay = false
first = proxies[firstIndex]
last = proxies[length-1]
c net.Conn
cc net.Conn
err error
currentMeta *C.Metadata
)
if !last.SupportUDP() {
return nil, fmt.Errorf("%s connect error: proxy [%s] UDP is not supported in relay chains", last.Addr(), last.Name())
}
rawUDPRelay, lastUDPOverTCPIndex = isRawUDPRelay(proxies)
if first.Type() == C.Socks5 {
cc1, err1 := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err1 != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
cc = cc1
tcpKeepAlive(cc)
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", r.Base.DialOptions(opts...)...)
c = outbound.WrapConn(pc)
} else if rawUDPRelay {
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", r.Base.DialOptions(opts...)...)
c = outbound.WrapConn(pc)
} else {
firstIndex = lastUDPOverTCPIndex
nextIndex = firstIndex + 1
first = proxies[firstIndex]
c, err = r.streamContext(ctx, proxies[:nextIndex], r.Base.DialOptions(opts...)...)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
if nextIndex < length {
for i, proxy := range proxies[nextIndex:] { // raw udp in loop
currentMeta, err = addrToMetadata(proxy.Addr())
if err != nil {
return nil, err
}
currentMeta.NetWork = C.UDP
if !isRawUDP(first) && !first.SupportUDP() {
return nil, fmt.Errorf("%s connect error: proxy [%s] UDP is not supported in relay chains", first.Addr(), first.Name())
}
if needResolveIP(first, currentMeta) {
var ip netip.Addr
ip, err = resolver.ResolveProxyServerHost(currentMeta.Host)
if err != nil {
return nil, fmt.Errorf("can't resolve ip: %w", err)
}
currentMeta.DstIP = ip
}
if cc != nil { // socks5
c, err = streamSocks5PacketConn(first, cc, c, currentMeta)
cc = nil
} else {
c, err = first.StreamPacketConn(c, currentMeta)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
if proxy.Type() == C.Socks5 {
endIndex := nextIndex + i + 1
cc, err = r.streamContext(ctx, proxies[:endIndex], r.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
}
first = proxy
}
}
if cc != nil {
c, err = streamSocks5PacketConn(last, cc, c, metadata)
} else {
c, err = last.StreamPacketConn(c, metadata)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
}
return outbound.NewPacketConn(c.(net.PacketConn), r), nil
}
// SupportUDP implements C.ProxyAdapter
func (r *Relay) SupportUDP() bool {
proxies := r.rawProxies(true)
l := len(proxies)
if l == 0 {
return true
}
last := proxies[l-1]
return isRawUDP(last) || last.SupportUDP()
}
// MarshalJSON implements C.ProxyAdapter
func (r *Relay) MarshalJSON() ([]byte, error) {
var all []string
@ -79,11 +206,11 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
}
func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() (any, error) {
elm, _, _ := r.single.Do(func() ([]C.Proxy, error) {
return getProvidersProxies(r.providers, touch), nil
})
return elm.([]C.Proxy)
return elm
}
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
@ -100,6 +227,83 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
return proxies
}
func (r *Relay) streamContext(ctx context.Context, proxies []C.Proxy, opts ...dialer.Option) (net.Conn, error) {
first := proxies[0]
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), opts...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
tcpKeepAlive(c)
if len(proxies) > 1 {
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
}
}
return c, nil
}
func streamSocks5PacketConn(proxy C.Proxy, cc, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
pc, err := proxy.(*adapter.Proxy).ProxyAdapter.(*outbound.Socks5).StreamSocks5PacketConn(cc, c.(net.PacketConn), metadata)
return outbound.WrapConn(pc), err
}
func isRawUDPRelay(proxies []C.Proxy) (bool, int) {
var (
lastIndex = len(proxies) - 1
last = proxies[lastIndex]
isLastRawUDP = isRawUDP(last)
isUDPOverTCP = false
lastUDPOverTCPIndex = -1
)
for i := lastIndex; i >= 0; i-- {
p := proxies[i]
isUDPOverTCP = isUDPOverTCP || !isRawUDP(p)
if isLastRawUDP && isUDPOverTCP && lastUDPOverTCPIndex == -1 {
lastUDPOverTCPIndex = i
}
}
if !isLastRawUDP {
lastUDPOverTCPIndex = lastIndex
}
return !isUDPOverTCP, lastUDPOverTCPIndex
}
func isRawUDP(proxy C.ProxyAdapter) bool {
if proxy.Type() == C.Shadowsocks || proxy.Type() == C.ShadowsocksR || proxy.Type() == C.Socks5 {
return true
}
return false
}
func needResolveIP(proxy C.ProxyAdapter, metadata *C.Metadata) bool {
if metadata.Resolved() {
return false
}
if proxy.Type() != C.Vmess && proxy.Type() != C.Vless {
return false
}
return true
}
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{
Base: outbound.NewBase(outbound.BaseOption{
@ -108,7 +312,7 @@ func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Re
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration),
providers: providers,
}
}

View File

@ -15,7 +15,7 @@ import (
type Selector struct {
*outbound.Base
disableUDP bool
single *singledo.Single
single *singledo.Single[C.Proxy]
selected string
providers []provider.ProxyProvider
}
@ -83,7 +83,7 @@ func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (any, error) {
elm, _, _ := s.single.Do(func() (C.Proxy, error) {
proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
@ -94,7 +94,7 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
return proxies[0], nil
})
return elm.(C.Proxy)
return elm
}
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
@ -106,7 +106,7 @@ func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider)
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
single: singledo.NewSingle[C.Proxy](defaultGetProxiesDuration),
providers: providers,
selected: selected,
disableUDP: option.DisableUDP,

View File

@ -25,8 +25,8 @@ type URLTest struct {
tolerance uint16
disableUDP bool
fastNode C.Proxy
single *singledo.Single
fastSingle *singledo.Single
single *singledo.Single[[]C.Proxy]
fastSingle *singledo.Single[C.Proxy]
providers []provider.ProxyProvider
}
@ -58,15 +58,15 @@ func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (any, error) {
elm, _, _ := u.single.Do(func() ([]C.Proxy, error) {
return getProvidersProxies(u.providers, touch), nil
})
return elm.([]C.Proxy)
return elm
}
func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (any, error) {
elm, _, _ := u.fastSingle.Do(func() (C.Proxy, error) {
proxies := u.proxies(touch)
fast := proxies[0]
min := fast.LastDelay()
@ -96,7 +96,7 @@ func (u *URLTest) fast(touch bool) C.Proxy {
return u.fastNode, nil
})
return elm.(C.Proxy)
return elm
}
// SupportUDP implements C.ProxyAdapter
@ -142,8 +142,8 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
fastSingle: singledo.NewSingle(time.Second * 10),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration),
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
providers: providers,
disableUDP: option.DisableUDP,
}

View File

@ -3,6 +3,7 @@ package outboundgroup
import (
"fmt"
"net"
"net/netip"
"time"
C "github.com/Dreamacro/clash/constant"
@ -15,20 +16,20 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
return
}
ip := net.ParseIP(host)
if ip == nil {
ip, err := netip.ParseAddr(host)
if err != nil {
addr = &C.Metadata{
AddrType: C.AtypDomainName,
Host: host,
DstIP: nil,
DstIP: netip.Addr{},
DstPort: port,
}
return
} else if ip4 := ip.To4(); ip4 != nil {
return addr, nil
} else if ip.Is4() {
addr = &C.Metadata{
AddrType: C.AtypIPv4,
Host: "",
DstIP: ip4,
DstIP: ip,
DstPort: port,
}
return
@ -45,7 +46,7 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
}
}

View File

@ -8,7 +8,7 @@ import (
C "github.com/Dreamacro/clash/constant"
)
func ParseProxy(mapping map[string]any) (C.Proxy, error) {
func ParseProxy(mapping map[string]any, forceCertVerify bool) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxyType, existType := mapping["type"].(string)
if !existType {
@ -40,6 +40,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil {
break
}
if forceCertVerify {
socksOption.SkipCertVerify = false
}
proxy = outbound.NewSocks5(*socksOption)
case "http":
httpOption := &outbound.HttpOption{}
@ -47,6 +50,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil {
break
}
if forceCertVerify {
httpOption.SkipCertVerify = false
}
proxy = outbound.NewHttp(*httpOption)
case "vmess":
vmessOption := &outbound.VmessOption{
@ -59,6 +65,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil {
break
}
if forceCertVerify {
vmessOption.SkipCertVerify = false
}
proxy, err = outbound.NewVmess(*vmessOption)
case "vless":
vlessOption := &outbound.VlessOption{}
@ -66,6 +75,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil {
break
}
if forceCertVerify {
vlessOption.SkipCertVerify = false
}
proxy, err = outbound.NewVless(*vlessOption)
case "snell":
snellOption := &outbound.SnellOption{}
@ -80,6 +92,9 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil {
break
}
if forceCertVerify {
trojanOption.SkipCertVerify = false
}
proxy, err = outbound.NewTrojan(*trojanOption)
default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)

View File

@ -16,28 +16,28 @@ var (
dirMode os.FileMode = 0o755
)
type parser = func([]byte) (any, error)
type parser[V any] func([]byte) (V, error)
type fetcher struct {
type fetcher[V any] struct {
name string
vehicle types.Vehicle
updatedAt *time.Time
ticker *time.Ticker
done chan struct{}
hash [16]byte
parser parser
onUpdate func(any)
parser parser[V]
onUpdate func(V)
}
func (f *fetcher) Name() string {
func (f *fetcher[V]) Name() string {
return f.name
}
func (f *fetcher) VehicleType() types.VehicleType {
func (f *fetcher[V]) VehicleType() types.VehicleType {
return f.vehicle.Type()
}
func (f *fetcher) Initial() (any, error) {
func (f *fetcher[V]) Initial() (V, error) {
var (
buf []byte
err error
@ -53,24 +53,24 @@ func (f *fetcher) Initial() (any, error) {
}
if err != nil {
return nil, err
return getZero[V](), err
}
proxies, err := f.parser(buf)
if err != nil {
if !isLocal {
return nil, err
return getZero[V](), err
}
// parse local file error, fallback to remote
buf, err = f.vehicle.Read()
if err != nil {
return nil, err
return getZero[V](), err
}
proxies, err = f.parser(buf)
if err != nil {
return nil, err
return getZero[V](), err
}
isLocal = false
@ -78,7 +78,7 @@ func (f *fetcher) Initial() (any, error) {
if f.vehicle.Type() != types.File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, err
return getZero[V](), err
}
}
@ -92,28 +92,28 @@ func (f *fetcher) Initial() (any, error) {
return proxies, nil
}
func (f *fetcher) Update() (any, bool, error) {
func (f *fetcher[V]) Update() (V, bool, error) {
buf, err := f.vehicle.Read()
if err != nil {
return nil, false, err
return getZero[V](), false, err
}
now := time.Now()
hash := md5.Sum(buf)
if bytes.Equal(f.hash[:], hash[:]) {
f.updatedAt = &now
os.Chtimes(f.vehicle.Path(), now, now)
return nil, true, nil
_ = os.Chtimes(f.vehicle.Path(), now, now)
return getZero[V](), true, nil
}
proxies, err := f.parser(buf)
if err != nil {
return nil, false, err
return getZero[V](), false, err
}
if f.vehicle.Type() != types.File {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, false, err
return getZero[V](), false, err
}
}
@ -123,14 +123,14 @@ func (f *fetcher) Update() (any, bool, error) {
return proxies, false, nil
}
func (f *fetcher) Destroy() error {
func (f *fetcher[V]) Destroy() error {
if f.ticker != nil {
f.done <- struct{}{}
}
return nil
}
func (f *fetcher) pullLoop() {
func (f *fetcher[V]) pullLoop() {
for {
select {
case <-f.ticker.C:
@ -168,13 +168,13 @@ func safeWrite(path string, buf []byte) error {
return os.WriteFile(path, buf, fileMode)
}
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher {
func newFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser parser[V], onUpdate func(V)) *fetcher[V] {
var ticker *time.Ticker
if interval != 0 {
ticker = time.NewTicker(interval)
}
return &fetcher{
return &fetcher[V]{
name: name,
ticker: ticker,
vehicle: vehicle,
@ -183,3 +183,8 @@ func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, pars
onUpdate: onUpdate,
}
}
func getZero[V any]() V {
var result V
return result
}

View File

@ -65,14 +65,19 @@ func (hc *HealthCheck) touch() {
}
func (hc *HealthCheck) check() {
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
for _, proxy := range hc.proxies {
proxies := hc.proxies
if len(proxies) == 0 {
return
}
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
for _, proxy := range proxies {
p := proxy
b.Go(p.Name(), func() (any, error) {
b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
p.URLTest(ctx, hc.url)
return nil, nil
_, _ = p.URLTest(ctx, hc.url)
return false, nil
})
}
b.Wait()

View File

@ -20,15 +20,18 @@ type healthCheckSchema struct {
}
type proxyProviderSchema struct {
Type string `provider:"type"`
Path string `provider:"path"`
URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
Type string `provider:"type"`
Path string `provider:"path"`
URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
ForceCertVerify bool `provider:"force-cert-verify,omitempty"`
PrefixName string `provider:"prefix-name,omitempty"`
Header map[string][]string `provider:"header,omitempty"`
}
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
func ParseProxyProvider(name string, mapping map[string]any, forceCertVerify bool) (types.ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{
@ -36,6 +39,11 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
Lazy: true,
},
}
if forceCertVerify {
schema.ForceCertVerify = true
}
if err := decoder.Decode(mapping, schema); err != nil {
return nil, err
}
@ -53,12 +61,12 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
case "file":
vehicle = NewFileVehicle(path)
case "http":
vehicle = NewHTTPVehicle(schema.URL, path)
vehicle = NewHTTPVehicle(schema.URL, path, schema.Header)
default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
}
interval := time.Duration(uint(schema.Interval)) * time.Second
filter := schema.Filter
return NewProxySetProvider(name, interval, filter, vehicle, hc)
return NewProxySetProvider(name, interval, filter, vehicle, hc, schema.ForceCertVerify, schema.PrefixName)
}

View File

@ -9,10 +9,11 @@ import (
"time"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/common/convert"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
const (
@ -23,15 +24,16 @@ type ProxySchema struct {
Proxies []map[string]any `yaml:"proxies"`
}
// for auto gc
// ProxySetProvider for auto gc
type ProxySetProvider struct {
*proxySetProvider
}
type proxySetProvider struct {
*fetcher
proxies []C.Proxy
healthCheck *HealthCheck
*fetcher[[]C.Proxy]
proxies []C.Proxy
healthCheck *HealthCheck
providersInUse []types.ProxyProvider
}
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
@ -86,17 +88,22 @@ func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() {
go pp.healthCheck.check()
for _, use := range pp.providersInUse {
_ = use.Update()
}
}
func (pp *proxySetProvider) RegisterProvidersInUse(providers ...types.ProxyProvider) {
pp.providersInUse = append(pp.providersInUse, providers...)
}
func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close()
pd.fetcher.Destroy()
_ = pd.fetcher.Destroy()
}
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck, forceCertVerify bool, prefixName string) (*ProxySetProvider, error) {
filterReg, err := regexp.Compile(filter)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
@ -111,45 +118,7 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
healthCheck: hc,
}
onUpdate := func(elm any) {
ret := elm.([]C.Proxy)
pd.setProxies(ret)
}
proxiesParseAndFilter := func(buf []byte) (any, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) {
continue
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
fetcher := newFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, filterReg, forceCertVerify, prefixName), proxiesOnUpdate(pd))
pd.fetcher = fetcher
wrapper := &ProxySetProvider{pd}
@ -157,7 +126,7 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
return wrapper, nil
}
// for auto gc
// CompatibleProvider for auto gc
type CompatibleProvider struct {
*compatibleProvider
}
@ -233,3 +202,145 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil
}
// ProxyFilterProvider for filter provider
type ProxyFilterProvider struct {
*proxyFilterProvider
}
type proxyFilterProvider struct {
name string
psd *ProxySetProvider
proxies []C.Proxy
filter *regexp.Regexp
healthCheck *HealthCheck
}
func (pf *proxyFilterProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": pf.Name(),
"type": pf.Type().String(),
"vehicleType": pf.VehicleType().String(),
"proxies": pf.Proxies(),
})
}
func (pf *proxyFilterProvider) Name() string {
return pf.name
}
func (pf *proxyFilterProvider) HealthCheck() {
pf.healthCheck.check()
}
func (pf *proxyFilterProvider) Update() error {
var proxies []C.Proxy
if pf.filter != nil {
for _, proxy := range pf.psd.Proxies() {
if !pf.filter.MatchString(proxy.Name()) {
continue
}
proxies = append(proxies, proxy)
}
} else {
proxies = pf.psd.Proxies()
}
pf.proxies = proxies
pf.healthCheck.setProxy(proxies)
return nil
}
func (pf *proxyFilterProvider) Initial() error {
return nil
}
func (pf *proxyFilterProvider) VehicleType() types.VehicleType {
return pf.psd.VehicleType()
}
func (pf *proxyFilterProvider) Type() types.ProviderType {
return types.Proxy
}
func (pf *proxyFilterProvider) Proxies() []C.Proxy {
return pf.proxies
}
func (pf *proxyFilterProvider) ProxiesWithTouch() []C.Proxy {
pf.healthCheck.touch()
return pf.Proxies()
}
func stopProxyFilterProvider(pf *ProxyFilterProvider) {
pf.healthCheck.close()
}
func NewProxyFilterProvider(name string, psd *ProxySetProvider, hc *HealthCheck, filterRegx *regexp.Regexp) *ProxyFilterProvider {
pd := &proxyFilterProvider{
psd: psd,
name: name,
healthCheck: hc,
filter: filterRegx,
}
_ = pd.Update()
if hc.auto() {
go hc.process()
}
wrapper := &ProxyFilterProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyFilterProvider)
return wrapper
}
func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
return func(elm []C.Proxy) {
pd.setProxies(elm)
}
}
func proxiesParseAndFilter(filter string, filterReg *regexp.Regexp, forceCertVerify bool, prefixName string) parser[[]C.Proxy] {
return func(buf []byte) ([]C.Proxy, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
proxies, err1 := convert.ConvertsV2Ray(buf)
if err1 != nil {
return nil, fmt.Errorf("%w, %s", err, err1.Error())
}
schema.Proxies = proxies
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) {
continue
}
if prefixName != "" {
mapping["name"] = prefixName + mapping["name"].(string)
}
proxy, err := adapter.ParseProxy(mapping, forceCertVerify)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
}

View File

@ -9,7 +9,9 @@ import (
"os"
"time"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
)
@ -34,8 +36,9 @@ func NewFileVehicle(path string) *FileVehicle {
}
type HTTPVehicle struct {
url string
path string
url string
path string
header http.Header
}
func (h *HTTPVehicle) Type() types.VehicleType {
@ -60,11 +63,17 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
return nil, err
}
if h.header != nil {
req.Header = h.header
}
if user := uri.User; user != nil {
password, _ := user.Password()
req.SetBasicAuth(user.Username(), password)
}
convert.SetUserAgent(req.Header)
req = req.WithContext(ctx)
transport := &http.Transport{
@ -73,8 +82,13 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
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)
DialContext: func(ctx context.Context, network, address string) (conn net.Conn, err error) {
conn, err = dialer.DialContext(ctx, network, address) // with direct
if err != nil {
// fallback to tun if tun enabled
conn, err = (&net.Dialer{Timeout: C.DefaultTCPTimeout}).Dial(network, address)
}
return
},
}
@ -83,7 +97,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
buf, err := io.ReadAll(resp.Body)
if err != nil {
@ -93,6 +109,6 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
return buf, nil
}
func NewHTTPVehicle(url string, path string) *HTTPVehicle {
return &HTTPVehicle{url, path}
func NewHTTPVehicle(url string, path string, header http.Header) *HTTPVehicle {
return &HTTPVehicle{url, path, header}
}

View File

@ -5,10 +5,10 @@ import (
"sync"
)
type Option = func(b *Batch)
type Option[T any] func(b *Batch[T])
type Result struct {
Value any
type Result[T any] struct {
Value T
Err error
}
@ -17,8 +17,8 @@ type Error struct {
Err error
}
func WithConcurrencyNum(n int) Option {
return func(b *Batch) {
func WithConcurrencyNum[T any](n int) Option[T] {
return func(b *Batch[T]) {
q := make(chan struct{}, n)
for i := 0; i < n; i++ {
q <- struct{}{}
@ -28,8 +28,8 @@ func WithConcurrencyNum(n int) Option {
}
// Batch similar to errgroup, but can control the maximum number of concurrent
type Batch struct {
result map[string]Result
type Batch[T any] struct {
result map[string]Result[T]
queue chan struct{}
wg sync.WaitGroup
mux sync.Mutex
@ -38,7 +38,7 @@ type Batch struct {
cancel func()
}
func (b *Batch) Go(key string, fn func() (any, error)) {
func (b *Batch[T]) Go(key string, fn func() (T, error)) {
b.wg.Add(1)
go func() {
defer b.wg.Done()
@ -59,14 +59,14 @@ func (b *Batch) Go(key string, fn func() (any, error)) {
})
}
ret := Result{value, err}
ret := Result[T]{value, err}
b.mux.Lock()
defer b.mux.Unlock()
b.result[key] = ret
}()
}
func (b *Batch) Wait() *Error {
func (b *Batch[T]) Wait() *Error {
b.wg.Wait()
if b.cancel != nil {
b.cancel()
@ -74,26 +74,26 @@ func (b *Batch) Wait() *Error {
return b.err
}
func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) {
func (b *Batch[T]) WaitAndGetResult() (map[string]Result[T], *Error) {
err := b.Wait()
return b.Result(), err
}
func (b *Batch) Result() map[string]Result {
func (b *Batch[T]) Result() map[string]Result[T] {
b.mux.Lock()
defer b.mux.Unlock()
copy := map[string]Result{}
copyM := map[string]Result[T]{}
for k, v := range b.result {
copy[k] = v
copyM[k] = v
}
return copy
return copyM
}
func New(ctx context.Context, opts ...Option) (*Batch, context.Context) {
func New[T any](ctx context.Context, opts ...Option[T]) (*Batch[T], context.Context) {
ctx, cancel := context.WithCancel(ctx)
b := &Batch{
result: map[string]Result{},
b := &Batch[T]{
result: map[string]Result[T]{},
}
for _, o := range opts {

View File

@ -11,14 +11,14 @@ import (
)
func TestBatch(t *testing.T) {
b, _ := New(context.Background())
b, _ := New[string](context.Background())
now := time.Now()
b.Go("foo", func() (any, error) {
b.Go("foo", func() (string, error) {
time.Sleep(time.Millisecond * 100)
return "foo", nil
})
b.Go("bar", func() (any, error) {
b.Go("bar", func() (string, error) {
time.Sleep(time.Millisecond * 150)
return "bar", nil
})
@ -32,20 +32,20 @@ func TestBatch(t *testing.T) {
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
assert.Equal(t, k, v.Value)
}
}
func TestBatchWithConcurrencyNum(t *testing.T) {
b, _ := New(
b, _ := New[string](
context.Background(),
WithConcurrencyNum(3),
WithConcurrencyNum[string](3),
)
now := time.Now()
for i := 0; i < 7; i++ {
idx := i
b.Go(strconv.Itoa(idx), func() (any, error) {
b.Go(strconv.Itoa(idx), func() (string, error) {
time.Sleep(time.Millisecond * 100)
return strconv.Itoa(idx), nil
})
@ -57,21 +57,21 @@ func TestBatchWithConcurrencyNum(t *testing.T) {
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
assert.Equal(t, k, v.Value)
}
}
func TestBatchContext(t *testing.T) {
b, ctx := New(context.Background())
b, ctx := New[string](context.Background())
b.Go("error", func() (any, error) {
b.Go("error", func() (string, error) {
time.Sleep(time.Millisecond * 100)
return nil, errors.New("test error")
return "", errors.New("test error")
})
b.Go("ctx", func() (any, error) {
b.Go("ctx", func() (string, error) {
<-ctx.Done()
return nil, ctx.Err()
return "", ctx.Err()
})
result, err := b.WaitAndGetResult()

50
common/cache/cache.go vendored
View File

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

View File

@ -11,48 +11,50 @@ import (
func TestCache_Basic(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
c := New(interval)
c := New[string, int](interval)
c.Put("int", 1, ttl)
c.Put("string", "a", ttl)
d := New[string, string](interval)
d.Put("string", "a", ttl)
i := c.Get("int")
assert.Equal(t, i.(int), 1, "should recv 1")
assert.Equal(t, i, 1, "should recv 1")
s := c.Get("string")
assert.Equal(t, s.(string), "a", "should recv 'a'")
s := d.Get("string")
assert.Equal(t, s, "a", "should recv 'a'")
}
func TestCache_TTL(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
now := time.Now()
c := New(interval)
c := New[string, int](interval)
c.Put("int", 1, ttl)
c.Put("int2", 2, ttl)
i := c.Get("int")
_, expired := c.GetWithExpire("int2")
assert.Equal(t, i.(int), 1, "should recv 1")
assert.Equal(t, i, 1, "should recv 1")
assert.True(t, now.Before(expired))
time.Sleep(ttl * 2)
i = c.Get("int")
j, _ := c.GetWithExpire("int2")
assert.Nil(t, i, "should recv nil")
assert.Nil(t, j, "should recv nil")
assert.True(t, i == 0, "should recv 0")
assert.True(t, j == 0, "should recv 0")
}
func TestCache_AutoCleanup(t *testing.T) {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New(interval)
c := New[string, int](interval)
c.Put("int", 1, ttl)
time.Sleep(ttl * 2)
i := c.Get("int")
j, _ := c.GetWithExpire("int")
assert.Nil(t, i, "should recv nil")
assert.Nil(t, j, "should recv nil")
assert.True(t, i == 0, "should recv 0")
assert.True(t, j == 0, "should recv 0")
}
func TestCache_AutoGC(t *testing.T) {
@ -60,7 +62,7 @@ func TestCache_AutoGC(t *testing.T) {
go func() {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New(interval)
c := New[string, int](interval)
c.Put("int", 1, ttl)
sign <- struct{}{}
}()

View File

@ -3,49 +3,50 @@ package cache
// Modified by https://github.com/die-net/lrucache
import (
"container/list"
"sync"
"time"
"github.com/Dreamacro/clash/common/generics/list"
)
// Option is part of Functional Options Pattern
type Option func(*LruCache)
type Option[K comparable, V any] func(*LruCache[K, V])
// EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback = func(key any, value any)
type EvictCallback[K comparable, V any] func(key K, value V)
// WithEvict set the evict callback
func WithEvict(cb EvictCallback) Option {
return func(l *LruCache) {
func WithEvict[K comparable, V any](cb EvictCallback[K, V]) Option[K, V] {
return func(l *LruCache[K, V]) {
l.onEvict = cb
}
}
// WithUpdateAgeOnGet update expires when Get element
func WithUpdateAgeOnGet() Option {
return func(l *LruCache) {
func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] {
return func(l *LruCache[K, V]) {
l.updateAgeOnGet = true
}
}
// WithAge defined element max age (second)
func WithAge(maxAge int64) Option {
return func(l *LruCache) {
func WithAge[K comparable, V any](maxAge int64) Option[K, V] {
return func(l *LruCache[K, V]) {
l.maxAge = maxAge
}
}
// WithSize defined max length of LruCache
func WithSize(maxSize int) Option {
return func(l *LruCache) {
func WithSize[K comparable, V any](maxSize int) Option[K, V] {
return func(l *LruCache[K, V]) {
l.maxSize = maxSize
}
}
// WithStale decide whether Stale return is enabled.
// If this feature is enabled, element will not get Evicted according to `WithAge`.
func WithStale(stale bool) Option {
return func(l *LruCache) {
func WithStale[K comparable, V any](stale bool) Option[K, V] {
return func(l *LruCache[K, V]) {
l.staleReturn = stale
}
}
@ -53,22 +54,22 @@ func WithStale(stale bool) Option {
// LruCache is a thread-safe, in-memory lru-cache that evicts the
// least recently used entries from memory when (if set) the entries are
// older than maxAge (in seconds). Use the New constructor to create one.
type LruCache struct {
type LruCache[K comparable, V any] struct {
maxAge int64
maxSize int
mu sync.Mutex
cache map[any]*list.Element
lru *list.List // Front is least-recent
cache map[K]*list.Element[*entry[K, V]]
lru *list.List[*entry[K, V]] // Front is least-recent
updateAgeOnGet bool
staleReturn bool
onEvict EvictCallback
onEvict EvictCallback[K, V]
}
// NewLRUCache creates an LruCache
func NewLRUCache(options ...Option) *LruCache {
lc := &LruCache{
lru: list.New(),
cache: make(map[any]*list.Element),
func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
lc := &LruCache[K, V]{
lru: list.New[*entry[K, V]](),
cache: make(map[K]*list.Element[*entry[K, V]]),
}
for _, option := range options {
@ -80,12 +81,12 @@ func NewLRUCache(options ...Option) *LruCache {
// Get returns the any representation of a cached response and a bool
// set to true if the key was found.
func (c *LruCache) Get(key any) (any, bool) {
entry := c.get(key)
if entry == nil {
return nil, false
func (c *LruCache[K, V]) Get(key K) (V, bool) {
el := c.get(key)
if el == nil {
return getZero[V](), false
}
value := entry.value
value := el.value
return value, true
}
@ -94,17 +95,17 @@ func (c *LruCache) Get(key any) (any, bool) {
// a time.Time Give expected expires,
// and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
entry := c.get(key)
if entry == nil {
return nil, time.Time{}, false
func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
el := c.get(key)
if el == nil {
return getZero[V](), time.Time{}, false
}
return entry.value, time.Unix(entry.expires, 0), true
return el.value, time.Unix(el.expires, 0), true
}
// Exist returns if key exist in cache but not put item to the head of linked list
func (c *LruCache) Exist(key any) bool {
func (c *LruCache[K, V]) Exist(key K) bool {
c.mu.Lock()
defer c.mu.Unlock()
@ -113,7 +114,7 @@ func (c *LruCache) Exist(key any) bool {
}
// Set stores the any representation of a response for a given key.
func (c *LruCache) Set(key any, value any) {
func (c *LruCache[K, V]) Set(key K, value V) {
expires := int64(0)
if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge
@ -123,21 +124,21 @@ func (c *LruCache) Set(key any, value any) {
// SetWithExpire stores the any representation of a response for a given key and given expires.
// The expires time will round to second.
func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le)
e := le.Value.(*entry)
e := le.Value
e.value = value
e.expires = expires.Unix()
} else {
e := &entry{key: key, value: value, expires: expires.Unix()}
e := &entry[K, V]{key: key, value: value, expires: expires.Unix()}
c.cache[key] = c.lru.PushBack(e)
if c.maxSize > 0 {
if len := c.lru.Len(); len > c.maxSize {
if elLen := c.lru.Len(); elLen > c.maxSize {
c.deleteElement(c.lru.Front())
}
}
@ -147,23 +148,23 @@ func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
}
// CloneTo clone and overwrite elements to another LruCache
func (c *LruCache) CloneTo(n *LruCache) {
func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
c.mu.Lock()
defer c.mu.Unlock()
n.mu.Lock()
defer n.mu.Unlock()
n.lru = list.New()
n.cache = make(map[any]*list.Element)
n.lru = list.New[*entry[K, V]]()
n.cache = make(map[K]*list.Element[*entry[K, V]])
for e := c.lru.Front(); e != nil; e = e.Next() {
elm := e.Value.(*entry)
elm := e.Value
n.cache[elm.key] = n.lru.PushBack(elm)
}
}
func (c *LruCache) get(key any) *entry {
func (c *LruCache[K, V]) get(key K) *entry[K, V] {
c.mu.Lock()
defer c.mu.Unlock()
@ -172,7 +173,7 @@ func (c *LruCache) get(key any) *entry {
return nil
}
if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() {
if !c.staleReturn && c.maxAge > 0 && le.Value.expires <= time.Now().Unix() {
c.deleteElement(le)
c.maybeDeleteOldest()
@ -180,15 +181,15 @@ func (c *LruCache) get(key any) *entry {
}
c.lru.MoveToBack(le)
entry := le.Value.(*entry)
el := le.Value
if c.maxAge > 0 && c.updateAgeOnGet {
entry.expires = time.Now().Unix() + c.maxAge
el.expires = time.Now().Unix() + c.maxAge
}
return entry
return el
}
// Delete removes the value associated with a key.
func (c *LruCache) Delete(key any) {
func (c *LruCache[K, V]) Delete(key K) {
c.mu.Lock()
if le, ok := c.cache[key]; ok {
@ -198,35 +199,40 @@ func (c *LruCache) Delete(key any) {
c.mu.Unlock()
}
func (c *LruCache) maybeDeleteOldest() {
func (c *LruCache[K, V]) maybeDeleteOldest() {
if !c.staleReturn && c.maxAge > 0 {
now := time.Now().Unix()
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() {
for le := c.lru.Front(); le != nil && le.Value.expires <= now; le = c.lru.Front() {
c.deleteElement(le)
}
}
}
func (c *LruCache) deleteElement(le *list.Element) {
func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
c.lru.Remove(le)
e := le.Value.(*entry)
e := le.Value
delete(c.cache, e.key)
if c.onEvict != nil {
c.onEvict(e.key, e.value)
}
}
func (c *LruCache) Clear() error {
func (c *LruCache[K, V]) Clear() error {
c.mu.Lock()
c.cache = make(map[any]*list.Element)
c.cache = make(map[K]*list.Element[*entry[K, V]])
c.mu.Unlock()
return nil
}
type entry struct {
key any
value any
type entry[K comparable, V any] struct {
key K
value V
expires int64
}
func getZero[T any]() T {
var result T
return result
}

View File

@ -19,7 +19,7 @@ var entries = []struct {
}
func TestLRUCache(t *testing.T) {
c := NewLRUCache()
c := NewLRUCache[string, string]()
for _, e := range entries {
c.Set(e.key, e.value)
@ -32,7 +32,7 @@ func TestLRUCache(t *testing.T) {
for _, e := range entries {
value, ok := c.Get(e.key)
if assert.True(t, ok) {
assert.Equal(t, e.value, value.(string))
assert.Equal(t, e.value, value)
}
}
@ -45,25 +45,25 @@ func TestLRUCache(t *testing.T) {
}
func TestLRUMaxAge(t *testing.T) {
c := NewLRUCache(WithAge(86400))
c := NewLRUCache[string, string](WithAge[string, string](86400))
now := time.Now().Unix()
expected := now + 86400
// Add one expired entry
c.Set("foo", "bar")
c.lru.Back().Value.(*entry).expires = now
c.lru.Back().Value.expires = now
// Reset
c.Set("foo", "bar")
e := c.lru.Back().Value.(*entry)
e := c.lru.Back().Value
assert.True(t, e.expires >= now)
c.lru.Back().Value.(*entry).expires = now
c.lru.Back().Value.expires = now
// Set a few and verify expiration times
for _, s := range entries {
c.Set(s.key, s.value)
e := c.lru.Back().Value.(*entry)
e := c.lru.Back().Value
assert.True(t, e.expires >= expected && e.expires <= expected+10)
}
@ -77,7 +77,7 @@ func TestLRUMaxAge(t *testing.T) {
for _, s := range entries {
le, ok := c.cache[s.key]
if assert.True(t, ok) {
le.Value.(*entry).expires = now
le.Value.expires = now
}
}
@ -88,22 +88,22 @@ func TestLRUMaxAge(t *testing.T) {
}
func TestLRUpdateOnGet(t *testing.T) {
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
c := NewLRUCache[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]())
now := time.Now().Unix()
expires := now + 86400/2
// Add one expired entry
c.Set("foo", "bar")
c.lru.Back().Value.(*entry).expires = expires
c.lru.Back().Value.expires = expires
_, ok := c.Get("foo")
assert.True(t, ok)
assert.True(t, c.lru.Back().Value.(*entry).expires > expires)
assert.True(t, c.lru.Back().Value.expires > expires)
}
func TestMaxSize(t *testing.T) {
c := NewLRUCache(WithSize(2))
c := NewLRUCache[string, string](WithSize[string, string](2))
// Add one expired entry
c.Set("foo", "bar")
_, ok := c.Get("foo")
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
}
func TestExist(t *testing.T) {
c := NewLRUCache(WithSize(1))
c := NewLRUCache[int, int](WithSize[int, int](1))
c.Set(1, 2)
assert.True(t, c.Exist(1))
c.Set(2, 3)
@ -126,11 +126,11 @@ func TestExist(t *testing.T) {
func TestEvict(t *testing.T) {
temp := 0
evict := func(key any, value any) {
temp = key.(int) + value.(int)
evict := func(key int, value int) {
temp = key + value
}
c := NewLRUCache(WithEvict(evict), WithSize(1))
c := NewLRUCache[int, int](WithEvict[int, int](evict), WithSize[int, int](1))
c.Set(1, 2)
c.Set(2, 3)
@ -138,21 +138,22 @@ func TestEvict(t *testing.T) {
}
func TestSetWithExpire(t *testing.T) {
c := NewLRUCache(WithAge(1))
c := NewLRUCache[int, *struct{}](WithAge[int, *struct{}](1))
now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0)
c.SetWithExpire(1, 2, tenSecBefore)
c.SetWithExpire(1, &struct{}{}, tenSecBefore)
// res is expected not to exist, and expires should be empty time.Time
res, expires, exist := c.GetWithExpire(1)
assert.Equal(t, nil, res)
assert.True(t, nil == res)
assert.Equal(t, time.Time{}, expires)
assert.Equal(t, false, exist)
}
func TestStale(t *testing.T) {
c := NewLRUCache(WithAge(1), WithStale(true))
c := NewLRUCache[int, int](WithAge[int, int](1), WithStale[int, int](true))
now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0)
@ -165,11 +166,11 @@ func TestStale(t *testing.T) {
}
func TestCloneTo(t *testing.T) {
o := NewLRUCache(WithSize(10))
o := NewLRUCache[string, int](WithSize[string, int](10))
o.Set("1", 1)
o.Set("2", 2)
n := NewLRUCache(WithSize(2))
n := NewLRUCache[string, int](WithSize[string, int](2))
n.Set("3", 3)
n.Set("4", 4)

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](),
}
}

344
common/convert/converter.go Normal file
View File

@ -0,0 +1,344 @@
package convert
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
)
var enc = base64.StdEncoding
func DecodeBase64(buf []byte) ([]byte, error) {
dBuf := make([]byte, enc.DecodedLen(len(buf)))
n, err := enc.Decode(dBuf, buf)
if err != nil {
return nil, err
}
return dBuf[:n], nil
}
// DecodeBase64StringToString decode base64 string to string
func DecodeBase64StringToString(s string) (string, error) {
dBuf, err := enc.DecodeString(s)
if err != nil {
return "", err
}
return string(dBuf), nil
}
// ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config
func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
data, err := DecodeBase64(buf)
if err != nil {
data = buf
}
arr := strings.Split(string(data), "\n")
proxies := make([]map[string]any, 0, len(arr))
names := make(map[string]int, 200)
for _, line := range arr {
line = strings.TrimRight(line, " \r")
if line == "" {
continue
}
scheme, body, found := strings.Cut(line, "://")
if !found {
continue
}
scheme = strings.ToLower(scheme)
switch scheme {
case "trojan":
urlTrojan, err := url.Parse(line)
if err != nil {
continue
}
query := urlTrojan.Query()
name := uniqueName(names, urlTrojan.Fragment)
trojan := make(map[string]any, 20)
trojan["name"] = name
trojan["type"] = scheme
trojan["server"] = urlTrojan.Hostname()
trojan["port"] = urlTrojan.Port()
trojan["password"] = urlTrojan.User.Username()
trojan["udp"] = true
trojan["skip-cert-verify"] = false
sni := query.Get("sni")
if sni != "" {
trojan["sni"] = sni
}
network := strings.ToLower(query.Get("type"))
if network != "" {
trojan["network"] = network
}
if network == "ws" {
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
trojan["ws-opts"] = wsOpts
}
proxies = append(proxies, trojan)
case "vmess":
dcBuf, err := enc.DecodeString(body)
if err != nil {
continue
}
jsonDc := json.NewDecoder(bytes.NewReader(dcBuf))
values := make(map[string]any, 20)
if jsonDc.Decode(&values) != nil {
continue
}
name := uniqueName(names, values["ps"].(string))
vmess := make(map[string]any, 20)
vmess["name"] = name
vmess["type"] = scheme
vmess["server"] = values["add"]
vmess["port"] = values["port"]
vmess["uuid"] = values["id"]
vmess["alterId"] = values["aid"]
vmess["cipher"] = "auto"
vmess["udp"] = true
vmess["skip-cert-verify"] = false
host := values["host"]
network := strings.ToLower(values["net"].(string))
vmess["network"] = network
tls := strings.ToLower(values["tls"].(string))
if tls != "" && tls != "0" && tls != "null" {
if host != nil {
vmess["servername"] = host
}
vmess["tls"] = true
}
if network == "ws" {
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
if values["path"] != nil {
wsOpts["path"] = values["path"]
}
wsOpts["headers"] = headers
vmess["ws-opts"] = wsOpts
}
proxies = append(proxies, vmess)
case "ss":
urlSS, err := url.Parse(line)
if err != nil {
continue
}
name := uniqueName(names, urlSS.Fragment)
port := urlSS.Port()
if port == "" {
dcBuf, err := enc.DecodeString(urlSS.Host)
if err != nil {
continue
}
urlSS, err = url.Parse("ss://" + string(dcBuf))
if err != nil {
continue
}
}
var (
cipher = urlSS.User.Username()
password string
)
if password, found = urlSS.User.Password(); !found {
dcBuf, err := enc.DecodeString(cipher)
if err != nil {
continue
}
cipher, password, found = strings.Cut(string(dcBuf), ":")
if !found {
continue
}
}
ss := make(map[string]any, 20)
ss["name"] = name
ss["type"] = scheme
ss["server"] = urlSS.Hostname()
ss["port"] = urlSS.Port()
ss["cipher"] = cipher
ss["password"] = password
ss["udp"] = true
proxies = append(proxies, ss)
case "ssr":
dcBuf, err := enc.DecodeString(body)
if err != nil {
continue
}
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1
before, after, ok := strings.Cut(string(dcBuf), "/?")
if !ok {
continue
}
beforeArr := strings.Split(before, ":")
if len(beforeArr) != 6 {
continue
}
host := beforeArr[0]
port := beforeArr[1]
protocol := beforeArr[2]
method := beforeArr[3]
obfs := beforeArr[4]
password := decodeUrlSafe(urlSafe(beforeArr[5]))
query, err := url.ParseQuery(urlSafe(after))
if err != nil {
continue
}
remarks := decodeUrlSafe(query.Get("remarks"))
name := uniqueName(names, remarks)
obfsParam := decodeUrlSafe(query.Get("obfsparam"))
protocolParam := query.Get("protoparam")
ssr := make(map[string]any, 20)
ssr["name"] = name
ssr["type"] = scheme
ssr["server"] = host
ssr["port"] = port
ssr["cipher"] = method
ssr["password"] = password
ssr["obfs"] = obfs
ssr["protocol"] = protocol
ssr["udp"] = true
if obfsParam != "" {
ssr["obfs-param"] = obfsParam
}
if protocolParam != "" {
ssr["protocol-param"] = protocolParam
}
proxies = append(proxies, ssr)
case "vless":
urlVless, err := url.Parse(line)
if err != nil {
continue
}
query := urlVless.Query()
name := uniqueName(names, urlVless.Fragment)
vless := make(map[string]any, 20)
vless["name"] = name
vless["type"] = scheme
vless["server"] = urlVless.Hostname()
vless["port"] = urlVless.Port()
vless["uuid"] = urlVless.User.Username()
vless["udp"] = true
vless["skip-cert-verify"] = false
sni := query.Get("sni")
if sni != "" {
vless["servername"] = sni
}
flow := strings.ToLower(query.Get("flow"))
if flow != "" {
vless["flow"] = flow
}
network := strings.ToLower(query.Get("type"))
if network != "" {
vless["network"] = network
}
if network == "ws" {
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
vless["ws-opts"] = wsOpts
}
proxies = append(proxies, vless)
}
}
if len(proxies) == 0 {
return nil, fmt.Errorf("convert v2ray subscribe error: format invalid")
}
return proxies, nil
}
func urlSafe(data string) string {
return strings.ReplaceAll(strings.ReplaceAll(data, "+", "-"), "/", "_")
}
func decodeUrlSafe(data string) string {
dcBuf, err := base64.URLEncoding.DecodeString(data)
if err != nil {
return ""
}
return string(dcBuf)
}
func uniqueName(names map[string]int, name string) string {
if index, ok := names[name]; ok {
index++
names[name] = index
name = fmt.Sprintf("%s-%02d", name, index)
} else {
index = 0
names[name] = index
}
return name
}

315
common/convert/util.go Normal file
View File

@ -0,0 +1,315 @@
package convert
import (
"encoding/base64"
"math/rand"
"net/http"
"strings"
"github.com/gofrs/uuid"
)
var hostsSuffix = []string{
"-cdn.aliyuncs.com",
".alicdn.com",
".pan.baidu.com",
".tbcache.com",
".aliyuncdn.com",
".vod.miguvideo.com",
".cibntv.net",
".myqcloud.com",
".smtcdns.com",
".alikunlun.com",
".smtcdns.net",
".apcdns.net",
".cdn-go.cn",
".cdntip.com",
".cdntips.com",
".alidayu.com",
".alidns.com",
".cdngslb.com",
".mxhichina.com",
}
var userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24",
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1",
"Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
}
var (
hostsLen = len(hostsSuffix)
uaLen = len(userAgents)
)
func RandHost() string {
id, _ := uuid.NewV4()
base := strings.ToLower(base64.RawURLEncoding.EncodeToString(id.Bytes()))
base = strings.ReplaceAll(base, "-", "")
base = strings.ReplaceAll(base, "_", "")
buf := []byte(base)
prefix := string(buf[:3]) + "---"
prefix += string(buf[6:8]) + "-"
prefix += string(buf[len(buf)-8:])
return prefix + hostsSuffix[rand.Intn(hostsLen)]
}
func RandUserAgent() string {
return userAgents[rand.Intn(uaLen)]
}
func SetUserAgent(header http.Header) {
if header.Get("User-Agent") != "" {
return
}
userAgent := RandUserAgent()
header.Set("User-Agent", userAgent)
}

View File

@ -0,0 +1,235 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package list implements a doubly linked list.
//
// To iterate over a list (where l is a *List):
// for e := l.Front(); e != nil; e = e.Next() {
// // do something with e.Value
// }
//
package list
// Element is an element of a linked list.
type Element[T any] struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element[T]
// The list to which this element belongs.
list *List[T]
// The value stored with this element.
Value T
}
// Next returns the next list element or nil.
func (e *Element[T]) Next() *Element[T] {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// Prev returns the previous list element or nil.
func (e *Element[T]) Prev() *Element[T] {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List[T any] struct {
root Element[T] // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
// Init initializes or clears list l.
func (l *List[T]) Init() *List[T] {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
// New returns an initialized list.
func New[T any]() *List[T] { return new(List[T]).Init() }
// Len returns the number of elements of list l.
// The complexity is O(1).
func (l *List[T]) Len() int { return l.len }
// Front returns the first element of list l or nil if the list is empty.
func (l *List[T]) Front() *Element[T] {
if l.len == 0 {
return nil
}
return l.root.next
}
// Back returns the last element of list l or nil if the list is empty.
func (l *List[T]) Back() *Element[T] {
if l.len == 0 {
return nil
}
return l.root.prev
}
// lazyInit lazily initializes a zero List value.
func (l *List[T]) lazyInit() {
if l.root.next == nil {
l.Init()
}
}
// insert inserts e after at, increments l.len, and returns e.
func (l *List[T]) insert(e, at *Element[T]) *Element[T] {
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
e.list = l
l.len++
return e
}
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
func (l *List[T]) insertValue(v T, at *Element[T]) *Element[T] {
return l.insert(&Element[T]{Value: v}, at)
}
// remove removes e from its list, decrements l.len
func (l *List[T]) remove(e *Element[T]) {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
l.len--
}
// move moves e to next to at.
func (l *List[T]) move(e, at *Element[T]) {
if e == at {
return
}
e.prev.next = e.next
e.next.prev = e.prev
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
}
// Remove removes e from l if e is an element of list l.
// It returns the element value e.Value.
// The element must not be nil.
func (l *List[T]) Remove(e *Element[T]) T {
if e.list == l {
// if e.list == l, l must have been initialized when e was inserted
// in l or l == nil (e is a zero Element) and l.remove will crash
l.remove(e)
}
return e.Value
}
// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *List[T]) PushFront(v T) *Element[T] {
l.lazyInit()
return l.insertValue(v, &l.root)
}
// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *List[T]) PushBack(v T) *Element[T] {
l.lazyInit()
return l.insertValue(v, l.root.prev)
}
// InsertBefore inserts a new element e with value v immediately before mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List[T]) InsertBefore(v T, mark *Element[T]) *Element[T] {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark.prev)
}
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List[T]) InsertAfter(v T, mark *Element[T]) *Element[T] {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark)
}
// MoveToFront moves element e to the front of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List[T]) MoveToFront(e *Element[T]) {
if e.list != l || l.root.next == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, &l.root)
}
// MoveToBack moves element e to the back of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List[T]) MoveToBack(e *Element[T]) {
if e.list != l || l.root.prev == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, l.root.prev)
}
// MoveBefore moves element e to its new position before mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List[T]) MoveBefore(e, mark *Element[T]) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark.prev)
}
// MoveAfter moves element e to its new position after mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List[T]) MoveAfter(e, mark *Element[T]) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark)
}
// PushBackList inserts a copy of another list at the back of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List[T]) PushBackList(other *List[T]) {
l.lazyInit()
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
l.insertValue(e.Value, l.root.prev)
}
}
// PushFrontList inserts a copy of another list at the front of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List[T]) PushFrontList(other *List[T]) {
l.lazyInit()
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
l.insertValue(e.Value, &l.root)
}
}

39
common/net/relay.go Normal file
View File

@ -0,0 +1,39 @@
package net
import (
"io"
"net"
"time"
"github.com/Dreamacro/clash/common/pool"
)
// Relay copies between left and right bidirectionally.
func Relay(leftConn, rightConn net.Conn) {
ch := make(chan error)
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())
ch <- err
}()
buf := pool.Get(pool.RelayBufferSize)
_, _ = 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)
}
}

53
common/nnip/netip.go Normal file
View File

@ -0,0 +1,53 @@
package nnip
import (
"encoding/binary"
"net"
"net/netip"
)
// IpToAddr converts the net.IP to netip.Addr.
// If slice's length is not 4 or 16, IpToAddr returns netip.Addr{}
func IpToAddr(slice net.IP) netip.Addr {
ip := slice
if len(ip) != 4 {
if ip = slice.To4(); ip == nil {
ip = slice
}
}
if addr, ok := netip.AddrFromSlice(ip); ok {
return addr
}
return netip.Addr{}
}
// UnMasked returns p's last IP address.
// If p is invalid, UnMasked returns netip.Addr{}
func UnMasked(p netip.Prefix) netip.Addr {
if !p.IsValid() {
return netip.Addr{}
}
buf := p.Addr().As16()
hi := binary.BigEndian.Uint64(buf[:8])
lo := binary.BigEndian.Uint64(buf[8:])
bits := p.Bits()
if bits <= 32 {
bits += 96
}
hi = hi | ^uint64(0)>>bits
lo = lo | ^(^uint64(0) << (128 - bits))
binary.BigEndian.PutUint64(buf[:8], hi)
binary.BigEndian.PutUint64(buf[8:], lo)
addr := netip.AddrFrom16(buf)
if p.Addr().Is4() {
return addr.Unmap()
}
return addr
}

View File

@ -1,3 +1,3 @@
package observable
type Iterable <-chan any
type Iterable[T any] <-chan T

View File

@ -5,14 +5,14 @@ import (
"sync"
)
type Observable struct {
iterable Iterable
listener map[Subscription]*Subscriber
type Observable[T any] struct {
iterable Iterable[T]
listener map[Subscription[T]]*Subscriber[T]
mux sync.Mutex
done bool
}
func (o *Observable) process() {
func (o *Observable[T]) process() {
for item := range o.iterable {
o.mux.Lock()
for _, sub := range o.listener {
@ -23,7 +23,7 @@ func (o *Observable) process() {
o.close()
}
func (o *Observable) close() {
func (o *Observable[T]) close() {
o.mux.Lock()
defer o.mux.Unlock()
@ -33,18 +33,18 @@ func (o *Observable) close() {
}
}
func (o *Observable) Subscribe() (Subscription, error) {
func (o *Observable[T]) Subscribe() (Subscription[T], error) {
o.mux.Lock()
defer o.mux.Unlock()
if o.done {
return nil, errors.New("Observable is closed")
return nil, errors.New("observable is closed")
}
subscriber := newSubscriber()
subscriber := newSubscriber[T]()
o.listener[subscriber.Out()] = subscriber
return subscriber.Out(), nil
}
func (o *Observable) UnSubscribe(sub Subscription) {
func (o *Observable[T]) UnSubscribe(sub Subscription[T]) {
o.mux.Lock()
defer o.mux.Unlock()
subscriber, exist := o.listener[sub]
@ -55,10 +55,10 @@ func (o *Observable) UnSubscribe(sub Subscription) {
subscriber.Close()
}
func NewObservable(any Iterable) *Observable {
observable := &Observable{
iterable: any,
listener: map[Subscription]*Subscriber{},
func NewObservable[T any](iter Iterable[T]) *Observable[T] {
observable := &Observable[T]{
iterable: iter,
listener: map[Subscription[T]]*Subscriber[T]{},
}
go observable.process()
return observable

View File

@ -9,8 +9,8 @@ import (
"go.uber.org/atomic"
)
func iterator(item []any) chan any {
ch := make(chan any)
func iterator[T any](item []T) chan T {
ch := make(chan T)
go func() {
time.Sleep(100 * time.Millisecond)
for _, elm := range item {
@ -22,8 +22,8 @@ func iterator(item []any) chan any {
}
func TestObservable(t *testing.T) {
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
iter := iterator[int]([]int{1, 2, 3, 4, 5})
src := NewObservable[int](iter)
data, err := src.Subscribe()
assert.Nil(t, err)
count := 0
@ -34,15 +34,15 @@ func TestObservable(t *testing.T) {
}
func TestObservable_MultiSubscribe(t *testing.T) {
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
iter := iterator[int]([]int{1, 2, 3, 4, 5})
src := NewObservable[int](iter)
ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe()
count := atomic.NewInt32(0)
var wg sync.WaitGroup
wg.Add(2)
waitCh := func(ch <-chan any) {
waitCh := func(ch <-chan int) {
for range ch {
count.Inc()
}
@ -55,8 +55,8 @@ func TestObservable_MultiSubscribe(t *testing.T) {
}
func TestObservable_UnSubscribe(t *testing.T) {
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
iter := iterator[int]([]int{1, 2, 3, 4, 5})
src := NewObservable[int](iter)
data, err := src.Subscribe()
assert.Nil(t, err)
src.UnSubscribe(data)
@ -65,8 +65,8 @@ func TestObservable_UnSubscribe(t *testing.T) {
}
func TestObservable_SubscribeClosedSource(t *testing.T) {
iter := iterator([]any{1})
src := NewObservable(iter)
iter := iterator[int]([]int{1})
src := NewObservable[int](iter)
data, _ := src.Subscribe()
<-data
@ -75,18 +75,18 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
}
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
sub := Subscription(make(chan any))
iter := iterator([]any{1})
src := NewObservable(iter)
sub := Subscription[int](make(chan int))
iter := iterator[int]([]int{1})
src := NewObservable[int](iter)
src.UnSubscribe(sub)
}
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
iter := iterator[int]([]int{1, 2, 3, 4, 5})
src := NewObservable[int](iter)
max := 100
var list []Subscription
var list []Subscription[int]
for i := 0; i < max; i++ {
ch, _ := src.Subscribe()
list = append(list, ch)
@ -94,7 +94,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
var wg sync.WaitGroup
wg.Add(max)
waitCh := func(ch <-chan any) {
waitCh := func(ch <-chan int) {
for range ch {
}
wg.Done()
@ -115,11 +115,11 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
}
func Benchmark_Observable_1000(b *testing.B) {
ch := make(chan any)
o := NewObservable(ch)
ch := make(chan int)
o := NewObservable[int](ch)
num := 1000
subs := []Subscription{}
subs := []Subscription[int]{}
for i := 0; i < num; i++ {
sub, _ := o.Subscribe()
subs = append(subs, sub)
@ -130,7 +130,7 @@ func Benchmark_Observable_1000(b *testing.B) {
b.ResetTimer()
for _, sub := range subs {
go func(s Subscription) {
go func(s Subscription[int]) {
for range s {
}
wg.Done()

View File

@ -4,30 +4,30 @@ import (
"sync"
)
type Subscription <-chan any
type Subscription[T any] <-chan T
type Subscriber struct {
buffer chan any
type Subscriber[T any] struct {
buffer chan T
once sync.Once
}
func (s *Subscriber) Emit(item any) {
func (s *Subscriber[T]) Emit(item T) {
s.buffer <- item
}
func (s *Subscriber) Out() Subscription {
func (s *Subscriber[T]) Out() Subscription[T] {
return s.buffer
}
func (s *Subscriber) Close() {
func (s *Subscriber[T]) Close() {
s.once.Do(func() {
close(s.buffer)
})
}
func newSubscriber() *Subscriber {
sub := &Subscriber{
buffer: make(chan any, 200),
func newSubscriber[T any]() *Subscriber[T] {
sub := &Subscriber[T]{
buffer: make(chan T, 200),
}
return sub
}

View File

@ -9,7 +9,7 @@ import (
// Picker provides synchronization, and Context cancelation
// for groups of goroutines working on subtasks of a common task.
// Inspired by errGroup
type Picker struct {
type Picker[T any] struct {
ctx context.Context
cancel func()
@ -17,12 +17,12 @@ type Picker struct {
once sync.Once
errOnce sync.Once
result any
result T
err error
}
func newPicker(ctx context.Context, cancel func()) *Picker {
return &Picker{
func newPicker[T any](ctx context.Context, cancel func()) *Picker[T] {
return &Picker[T]{
ctx: ctx,
cancel: cancel,
}
@ -30,20 +30,20 @@ func newPicker(ctx context.Context, cancel func()) *Picker {
// WithContext returns a new Picker and an associated Context derived from ctx.
// and cancel when first element return.
func WithContext(ctx context.Context) (*Picker, context.Context) {
func WithContext[T any](ctx context.Context) (*Picker[T], context.Context) {
ctx, cancel := context.WithCancel(ctx)
return newPicker(ctx, cancel), ctx
return newPicker[T](ctx, cancel), ctx
}
// WithTimeout returns a new Picker and an associated Context derived from ctx with timeout.
func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.Context) {
func WithTimeout[T any](ctx context.Context, timeout time.Duration) (*Picker[T], context.Context) {
ctx, cancel := context.WithTimeout(ctx, timeout)
return newPicker(ctx, cancel), ctx
return newPicker[T](ctx, cancel), ctx
}
// Wait blocks until all function calls from the Go method have returned,
// then returns the first nil error result (if any) from them.
func (p *Picker) Wait() any {
func (p *Picker[T]) Wait() T {
p.wg.Wait()
if p.cancel != nil {
p.cancel()
@ -52,13 +52,13 @@ func (p *Picker) Wait() any {
}
// Error return the first error (if all success return nil)
func (p *Picker) Error() error {
func (p *Picker[T]) Error() error {
return p.err
}
// Go calls the given function in a new goroutine.
// The first call to return a nil error cancels the group; its result will be returned by Wait.
func (p *Picker) Go(f func() (any, error)) {
func (p *Picker[T]) Go(f func() (T, error)) {
p.wg.Add(1)
go func() {

View File

@ -8,33 +8,38 @@ import (
"github.com/stretchr/testify/assert"
)
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
return func() (any, error) {
func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, error) {
return func() (T, error) {
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
select {
case <-timer.C:
return input, nil
case <-ctx.Done():
return nil, ctx.Err()
return getZero[T](), ctx.Err()
}
}
}
func TestPicker_Basic(t *testing.T) {
picker, ctx := WithContext(context.Background())
picker, ctx := WithContext[int](context.Background())
picker.Go(sleepAndSend(ctx, 30, 2))
picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait()
assert.NotNil(t, number)
assert.Equal(t, number.(int), 1)
assert.Equal(t, number, 1)
}
func TestPicker_Timeout(t *testing.T) {
picker, ctx := WithTimeout(context.Background(), time.Millisecond*5)
picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5)
picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait()
assert.Nil(t, number)
assert.Equal(t, number, getZero[int]())
assert.NotNil(t, picker.Error())
}
func getZero[T any]() T {
var result T
return result
}

View File

@ -52,8 +52,8 @@ func (alloc *Allocator) Put(buf []byte) error {
return errors.New("allocator Put() incorrect buffer size")
}
//lint:ignore SA6002 ignore temporarily
//nolint
//lint:ignore SA6002 ignore temporarily
alloc.buffers[bits].Put(buf)
return nil
}

View File

@ -5,13 +5,13 @@ import (
)
// Queue is a simple concurrent safe queue
type Queue struct {
items []any
type Queue[T any] struct {
items []T
lock sync.RWMutex
}
// Put add the item to the queue.
func (q *Queue) Put(items ...any) {
func (q *Queue[T]) Put(items ...T) {
if len(items) == 0 {
return
}
@ -22,9 +22,9 @@ func (q *Queue) Put(items ...any) {
}
// Pop returns the head of items.
func (q *Queue) Pop() any {
func (q *Queue[T]) Pop() T {
if len(q.items) == 0 {
return nil
return GetZero[T]()
}
q.lock.Lock()
@ -35,9 +35,9 @@ func (q *Queue) Pop() any {
}
// Last returns the last of item.
func (q *Queue) Last() any {
func (q *Queue[T]) Last() T {
if len(q.items) == 0 {
return nil
return GetZero[T]()
}
q.lock.RLock()
@ -47,8 +47,8 @@ func (q *Queue) Last() any {
}
// Copy get the copy of queue.
func (q *Queue) Copy() []any {
items := []any{}
func (q *Queue[T]) Copy() []T {
items := []T{}
q.lock.RLock()
items = append(items, q.items...)
q.lock.RUnlock()
@ -56,7 +56,7 @@ func (q *Queue) Copy() []any {
}
// Len returns the number of items in this queue.
func (q *Queue) Len() int64 {
func (q *Queue[T]) Len() int64 {
q.lock.Lock()
defer q.lock.Unlock()
@ -64,8 +64,13 @@ func (q *Queue) Len() int64 {
}
// New is a constructor for a new concurrent safe queue.
func New(hint int64) *Queue {
return &Queue{
items: make([]any, 0, hint),
func New[T any](hint int64) *Queue[T] {
return &Queue[T]{
items: make([]T, 0, hint),
}
}
func GetZero[T any]() T {
var result T
return result
}

View File

@ -5,28 +5,27 @@ import (
"time"
)
type call struct {
type call[T any] struct {
wg sync.WaitGroup
val any
val T
err error
}
type Single struct {
type Single[T any] struct {
mux sync.Mutex
last time.Time
wait time.Duration
call *call
result *Result
call *call[T]
result *Result[T]
}
type Result struct {
Val any
type Result[T any] struct {
Val T
Err error
}
// Do single.Do likes sync.singleFlight
//lint:ignore ST1008 it likes sync.singleFlight
func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) {
func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
s.mux.Lock()
now := time.Now()
if now.Before(s.last.Add(s.wait)) {
@ -34,31 +33,31 @@ func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) {
return s.result.Val, s.result.Err, true
}
if call := s.call; call != nil {
if callM := s.call; callM != nil {
s.mux.Unlock()
call.wg.Wait()
return call.val, call.err, true
callM.wg.Wait()
return callM.val, callM.err, true
}
call := &call{}
call.wg.Add(1)
s.call = call
callM := &call[T]{}
callM.wg.Add(1)
s.call = callM
s.mux.Unlock()
call.val, call.err = fn()
call.wg.Done()
callM.val, callM.err = fn()
callM.wg.Done()
s.mux.Lock()
s.call = nil
s.result = &Result{call.val, call.err}
s.result = &Result[T]{callM.val, callM.err}
s.last = now
s.mux.Unlock()
return call.val, call.err, false
return callM.val, callM.err, false
}
func (s *Single) Reset() {
func (s *Single[T]) Reset() {
s.last = time.Time{}
}
func NewSingle(wait time.Duration) *Single {
return &Single{wait: wait}
func NewSingle[T any](wait time.Duration) *Single[T] {
return &Single[T]{wait: wait}
}

View File

@ -10,13 +10,13 @@ import (
)
func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
single := NewSingle[int](time.Millisecond * 30)
foo := 0
shardCount := atomic.NewInt32(0)
call := func() (any, error) {
call := func() (int, error) {
foo++
time.Sleep(time.Millisecond * 5)
return nil, nil
return 0, nil
}
var wg sync.WaitGroup
@ -38,32 +38,32 @@ func TestBasic(t *testing.T) {
}
func TestTimer(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
single := NewSingle[int](time.Millisecond * 30)
foo := 0
call := func() (any, error) {
callM := func() (int, error) {
foo++
return nil, nil
return 0, nil
}
single.Do(call)
_, _, _ = single.Do(callM)
time.Sleep(10 * time.Millisecond)
_, _, shard := single.Do(call)
_, _, shard := single.Do(callM)
assert.Equal(t, 1, foo)
assert.True(t, shard)
}
func TestReset(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
single := NewSingle[int](time.Millisecond * 30)
foo := 0
call := func() (any, error) {
callM := func() (int, error) {
foo++
return nil, nil
return 0, nil
}
single.Do(call)
_, _, _ = single.Do(callM)
single.Reset()
single.Do(call)
_, _, _ = single.Do(callM)
assert.Equal(t, 2, foo)
}

View File

@ -159,9 +159,19 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
for valSlice.Len() <= i {
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
}
currentField := valSlice.Index(i)
fieldName := fmt.Sprintf("%s[%d]", name, i)
if currentData == nil {
// in weakly type mode, null will convert to zero value
if d.option.WeaklyTypedInput {
continue
}
// in non-weakly type mode, null will convert to nil if element's zero value is nil, otherwise return an error
if elemKind := valElemType.Kind(); elemKind == reflect.Map || elemKind == reflect.Slice {
continue
}
return fmt.Errorf("'%s' can not be null", fieldName)
}
currentField := valSlice.Index(i)
if err := d.decode(fieldName, currentData, currentField); err != nil {
return err
}

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

@ -4,7 +4,9 @@ import (
"context"
"errors"
"net"
"net/netip"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4"
@ -15,14 +17,16 @@ var (
ErrNotFound = errors.New("DNS option not found")
)
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, error) {
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]netip.Addr, error) {
conn, err := ListenDHCPClient(context, ifaceName)
if err != nil {
return nil, err
}
defer conn.Close()
defer func() {
_ = conn.Close()
}()
result := make(chan []net.IP, 1)
result := make(chan []netip.Addr, 1)
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
@ -52,7 +56,7 @@ func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, er
}
}
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []net.IP) {
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []netip.Addr) {
defer close(result)
buf := make([]byte, dhcpv4.MaxMessageSize)
@ -77,11 +81,17 @@ func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []
}
dns := pkt.DNS()
if len(dns) == 0 {
l := len(dns)
if l == 0 {
return
}
result <- dns
dnsAddr := make([]netip.Addr, l)
for i := 0; i < l; i++ {
dnsAddr[i] = nnip.IpToAddr(dns[i])
}
result <- dnsAddr
return
}

View File

@ -2,6 +2,7 @@ package dialer
import (
"net"
"net/netip"
"syscall"
"github.com/Dreamacro/clash/component/iface"
@ -19,12 +20,9 @@ func bindControl(ifaceIdx int, chain controlFn) controlFn {
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
addrPort, err := netip.ParseAddrPort(address)
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
return
}
var innerErr error
@ -45,7 +43,7 @@ func bindControl(ifaceIdx int, chain controlFn) controlFn {
}
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return err

View File

@ -2,6 +2,7 @@ package dialer
import (
"net"
"net/netip"
"syscall"
"golang.org/x/sys/unix"
@ -17,12 +18,9 @@ func bindControl(ifaceName string, chain controlFn) controlFn {
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
addrPort, err := netip.ParseAddrPort(address)
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
return
}
var innerErr error
@ -38,7 +36,7 @@ func bindControl(ifaceName string, chain controlFn) controlFn {
}
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
dialer.Control = bindControl(ifaceName, dialer.Control)
return nil

View File

@ -4,27 +4,28 @@ package dialer
import (
"net"
"net/netip"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/iface"
)
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) {
func lookupLocalAddr(ifaceName string, network string, destination netip.Addr, port int) (net.Addr, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
var addr *net.IPNet
var addr *netip.Prefix
switch network {
case "udp4", "tcp4":
addr, err = ifaceObj.PickIPv4Addr(destination)
case "tcp6", "udp6":
addr, err = ifaceObj.PickIPv6Addr(destination)
default:
if destination != nil {
if destination.To4() != nil {
if destination.IsValid() {
if destination.Is4() {
addr, err = ifaceObj.PickIPv4Addr(destination)
} else {
addr, err = ifaceObj.PickIPv6Addr(destination)
@ -39,12 +40,12 @@ func lookupLocalAddr(ifaceName string, network string, destination net.IP, port
if strings.HasPrefix(network, "tcp") {
return &net.TCPAddr{
IP: addr.IP,
IP: addr.Addr().AsSlice(),
Port: port,
}, nil
} else if strings.HasPrefix(network, "udp") {
return &net.UDPAddr{
IP: addr.IP,
IP: addr.Addr().AsSlice(),
Port: port,
}, nil
}
@ -52,7 +53,7 @@ func lookupLocalAddr(ifaceName string, network string, destination net.IP, port
return nil, iface.ErrAddrNotFound
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
if !destination.IsGlobalUnicast() {
return nil
}
@ -83,7 +84,7 @@ func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, add
local, _ := strconv.ParseUint(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
addr, err := lookupLocalAddr(ifaceName, network, netip.Addr{}, int(local))
if err != nil {
return "", err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"net"
"net/netip"
"github.com/Dreamacro/clash/component/resolver"
)
@ -29,7 +30,7 @@ func DialContext(ctx context.Context, network, address string, options ...Option
return nil, err
}
var ip net.IP
var ip netip.Addr
switch network {
case "tcp4", "udp4":
if !opt.direct {
@ -88,7 +89,7 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
return lc.ListenPacket(ctx, network, address)
}
func dialContext(ctx context.Context, network string, destination net.IP, port string, opt *option) (net.Conn, error) {
func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) {
dialer := &net.Dialer{}
if opt.interfaceName != "" {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
@ -128,12 +129,12 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
case results <- result:
case <-returned:
if result.Conn != nil {
result.Conn.Close()
_ = result.Conn.Close()
}
}
}()
var ip net.IP
var ip netip.Addr
if ipv6 {
if !direct {
ip, result.error = resolver.ResolveIPv6ProxyServerHost(host)

View File

@ -4,14 +4,15 @@ package dialer
import (
"net"
"net/netip"
"syscall"
)
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) {
dialer.Control = bindMarkToControl(mark, dialer.Control)
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
lc.Control = bindMarkToControl(mark, lc.Control)
}
@ -23,20 +24,17 @@ func bindMarkToControl(mark int, chain controlFn) controlFn {
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
addrPort, err := netip.ParseAddrPort(address)
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
return
}
return c.Control(func(fd uintptr) {
switch network {
case "tcp4", "udp4":
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
case "tcp6", "udp6":
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
}
})
}

View File

@ -4,6 +4,7 @@ package dialer
import (
"net"
"net/netip"
"sync"
"github.com/Dreamacro/clash/log"
@ -17,10 +18,10 @@ func printMarkWarn() {
})
}
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) {
printMarkWarn()
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
printMarkWarn()
}

View File

@ -3,6 +3,7 @@ package dialer
import (
"context"
"net"
"net/netip"
)
func init() {
@ -18,9 +19,9 @@ func resolverDialContext(ctx context.Context, network, address string) (net.Conn
interfaceName := DefaultInterface.Load()
if interfaceName != "" {
dstIP := net.ParseIP(address)
if dstIP != nil {
bindIfaceToDialer(interfaceName, d, network, dstIP)
dstIP, err := netip.ParseAddr(address)
if err == nil {
_ = bindIfaceToDialer(interfaceName, d, network, dstIP)
}
}

View File

@ -1,7 +1,7 @@
package fakeip
import (
"net"
"net/netip"
"github.com/Dreamacro/clash/component/profile/cachefile"
)
@ -11,22 +11,27 @@ type cachefileStore struct {
}
// GetByHost implements store.GetByHost
func (c *cachefileStore) GetByHost(host string) (net.IP, bool) {
func (c *cachefileStore) GetByHost(host string) (netip.Addr, bool) {
elm := c.cache.GetFakeip([]byte(host))
if elm == nil {
return nil, false
return netip.Addr{}, false
}
if len(elm) == 4 {
return netip.AddrFrom4(*(*[4]byte)(elm)), true
} else {
return netip.AddrFrom16(*(*[16]byte)(elm)), true
}
return net.IP(elm), true
}
// PutByHost implements store.PutByHost
func (c *cachefileStore) PutByHost(host string, ip net.IP) {
c.cache.PutFakeip([]byte(host), ip)
func (c *cachefileStore) PutByHost(host string, ip netip.Addr) {
c.cache.PutFakeip([]byte(host), ip.AsSlice())
}
// GetByIP implements store.GetByIP
func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
elm := c.cache.GetFakeip(ip.To4())
func (c *cachefileStore) GetByIP(ip netip.Addr) (string, bool) {
elm := c.cache.GetFakeip(ip.AsSlice())
if elm == nil {
return "", false
}
@ -34,18 +39,18 @@ func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
}
// PutByIP implements store.PutByIP
func (c *cachefileStore) PutByIP(ip net.IP, host string) {
c.cache.PutFakeip(ip.To4(), []byte(host))
func (c *cachefileStore) PutByIP(ip netip.Addr, host string) {
c.cache.PutFakeip(ip.AsSlice(), []byte(host))
}
// DelByIP implements store.DelByIP
func (c *cachefileStore) DelByIP(ip net.IP) {
ip = ip.To4()
c.cache.DelFakeipPair(ip, c.cache.GetFakeip(ip.To4()))
func (c *cachefileStore) DelByIP(ip netip.Addr) {
addr := ip.AsSlice()
c.cache.DelFakeipPair(addr, c.cache.GetFakeip(addr))
}
// Exist implements store.Exist
func (c *cachefileStore) Exist(ip net.IP) bool {
func (c *cachefileStore) Exist(ip netip.Addr) bool {
_, exist := c.GetByIP(ip)
return exist
}

View File

@ -1,40 +1,37 @@
package fakeip
import (
"net"
"net/netip"
"github.com/Dreamacro/clash/common/cache"
)
type memoryStore struct {
cache *cache.LruCache
cacheIP *cache.LruCache[string, netip.Addr]
cacheHost *cache.LruCache[netip.Addr, string]
}
// GetByHost implements store.GetByHost
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
if elm, exist := m.cache.Get(host); exist {
ip := elm.(net.IP)
func (m *memoryStore) GetByHost(host string) (netip.Addr, bool) {
if ip, exist := m.cacheIP.Get(host); exist {
// ensure ip --> host on head of linked list
m.cache.Get(ipToUint(ip.To4()))
m.cacheHost.Get(ip)
return ip, true
}
return nil, false
return netip.Addr{}, false
}
// PutByHost implements store.PutByHost
func (m *memoryStore) PutByHost(host string, ip net.IP) {
m.cache.Set(host, ip)
func (m *memoryStore) PutByHost(host string, ip netip.Addr) {
m.cacheIP.Set(host, ip)
}
// GetByIP implements store.GetByIP
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
host := elm.(string)
func (m *memoryStore) GetByIP(ip netip.Addr) (string, bool) {
if host, exist := m.cacheHost.Get(ip); exist {
// ensure host --> ip on head of linked list
m.cache.Get(host)
m.cacheIP.Get(host)
return host, true
}
@ -42,33 +39,41 @@ func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
}
// PutByIP implements store.PutByIP
func (m *memoryStore) PutByIP(ip net.IP, host string) {
m.cache.Set(ipToUint(ip.To4()), host)
func (m *memoryStore) PutByIP(ip netip.Addr, host string) {
m.cacheHost.Set(ip, host)
}
// DelByIP implements store.DelByIP
func (m *memoryStore) DelByIP(ip net.IP) {
ipNum := ipToUint(ip.To4())
if elm, exist := m.cache.Get(ipNum); exist {
m.cache.Delete(elm.(string))
func (m *memoryStore) DelByIP(ip netip.Addr) {
if host, exist := m.cacheHost.Get(ip); exist {
m.cacheIP.Delete(host)
}
m.cache.Delete(ipNum)
m.cacheHost.Delete(ip)
}
// Exist implements store.Exist
func (m *memoryStore) Exist(ip net.IP) bool {
return m.cache.Exist(ipToUint(ip.To4()))
func (m *memoryStore) Exist(ip netip.Addr) bool {
return m.cacheHost.Exist(ip)
}
// CloneTo implements store.CloneTo
// only for memoryStore to memoryStore
func (m *memoryStore) CloneTo(store store) {
if ms, ok := store.(*memoryStore); ok {
m.cache.CloneTo(ms.cache)
m.cacheIP.CloneTo(ms.cacheIP)
m.cacheHost.CloneTo(ms.cacheHost)
}
}
// FlushFakeIP implements store.FlushFakeIP
func (m *memoryStore) FlushFakeIP() error {
return m.cache.Clear()
_ = m.cacheIP.Clear()
return m.cacheHost.Clear()
}
func newMemoryStore(size int) *memoryStore {
return &memoryStore{
cacheIP: cache.NewLRUCache[string, netip.Addr](cache.WithSize[string, netip.Addr](size)),
cacheHost: cache.NewLRUCache[netip.Addr, string](cache.WithSize[netip.Addr, string](size)),
}
}

View File

@ -2,40 +2,45 @@ package fakeip
import (
"errors"
"net"
"net/netip"
"sync"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie"
)
const (
offsetKey = "key-offset-fake-ip"
cycleKey = "key-cycle-fake-ip"
)
type store interface {
GetByHost(host string) (net.IP, bool)
PutByHost(host string, ip net.IP)
GetByIP(ip net.IP) (string, bool)
PutByIP(ip net.IP, host string)
DelByIP(ip net.IP)
Exist(ip net.IP) bool
GetByHost(host string) (netip.Addr, bool)
PutByHost(host string, ip netip.Addr)
GetByIP(ip netip.Addr) (string, bool)
PutByIP(ip netip.Addr, host string)
DelByIP(ip netip.Addr)
Exist(ip netip.Addr) bool
CloneTo(store)
FlushFakeIP() error
}
// Pool is a implementation about fake ip generator without storage
type Pool struct {
max uint32
min uint32
gateway uint32
broadcast uint32
offset uint32
mux sync.Mutex
host *trie.DomainTrie
ipnet *net.IPNet
store store
gateway netip.Addr
first netip.Addr
last netip.Addr
offset netip.Addr
cycle bool
mux sync.Mutex
host *trie.DomainTrie[bool]
ipnet *netip.Prefix
store store
}
// Lookup return a fake ip with host
func (p *Pool) Lookup(host string) net.IP {
func (p *Pool) Lookup(host string) netip.Addr {
p.mux.Lock()
defer p.mux.Unlock()
if ip, exist := p.store.GetByHost(host); exist {
@ -48,14 +53,10 @@ func (p *Pool) Lookup(host string) net.IP {
}
// LookBack return host with the fake ip
func (p *Pool) LookBack(ip net.IP) (string, bool) {
func (p *Pool) LookBack(ip netip.Addr) (string, bool) {
p.mux.Lock()
defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return "", false
}
return p.store.GetByIP(ip)
}
@ -68,29 +69,25 @@ func (p *Pool) ShouldSkipped(domain string) bool {
}
// Exist returns if given ip exists in fake-ip pool
func (p *Pool) Exist(ip net.IP) bool {
func (p *Pool) Exist(ip netip.Addr) bool {
p.mux.Lock()
defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return false
}
return p.store.Exist(ip)
}
// Gateway return gateway ip
func (p *Pool) Gateway() net.IP {
return uintToIP(p.gateway)
func (p *Pool) Gateway() netip.Addr {
return p.gateway
}
// Broadcast return broadcast ip
func (p *Pool) Broadcast() net.IP {
return uintToIP(p.broadcast)
// Broadcast return the last ip
func (p *Pool) Broadcast() netip.Addr {
return p.last
}
// IPNet return raw ipnet
func (p *Pool) IPNet() *net.IPNet {
func (p *Pool) IPNet() *netip.Prefix {
return p.ipnet
}
@ -99,47 +96,61 @@ func (p *Pool) CloneFrom(o *Pool) {
o.store.CloneTo(p.store)
}
func (p *Pool) get(host string) net.IP {
current := p.offset
for {
p.offset = (p.offset + 1) % (p.max - p.min)
// Avoid infinite loops
if p.offset == current {
p.offset = (p.offset + 1) % (p.max - p.min)
ip := uintToIP(p.min + p.offset - 1)
p.store.DelByIP(ip)
break
}
func (p *Pool) get(host string) netip.Addr {
p.offset = p.offset.Next()
ip := uintToIP(p.min + p.offset - 1)
if !p.store.Exist(ip) {
break
}
if !p.offset.Less(p.last) {
p.cycle = true
p.offset = p.first
}
ip := uintToIP(p.min + p.offset - 1)
p.store.PutByIP(ip, host)
return ip
if p.cycle || p.store.Exist(p.offset) {
p.store.DelByIP(p.offset)
}
p.store.PutByIP(p.offset, host)
return p.offset
}
func (p *Pool) FlushFakeIP() error {
return p.store.FlushFakeIP()
err := p.store.FlushFakeIP()
if err == nil {
p.cycle = false
p.offset = p.first.Prev()
}
return err
}
func ipToUint(ip net.IP) uint32 {
v := uint32(ip[0]) << 24
v += uint32(ip[1]) << 16
v += uint32(ip[2]) << 8
v += uint32(ip[3])
return v
func (p *Pool) StoreState() {
if s, ok := p.store.(*cachefileStore); ok {
s.PutByHost(offsetKey, p.offset)
if p.cycle {
s.PutByHost(cycleKey, p.offset)
}
}
}
func uintToIP(v uint32) net.IP {
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
func (p *Pool) restoreState() {
if s, ok := p.store.(*cachefileStore); ok {
if _, exist := s.GetByHost(cycleKey); exist {
p.cycle = true
}
if offset, exist := s.GetByHost(offsetKey); exist {
if p.ipnet.Contains(offset) {
p.offset = offset
} else {
_ = p.FlushFakeIP()
}
} else if s.Exist(p.first) {
_ = p.FlushFakeIP()
}
}
}
type Options struct {
IPNet *net.IPNet
Host *trie.DomainTrie
IPNet *netip.Prefix
Host *trie.DomainTrie[bool]
// Size sets the maximum number of entries in memory
// and does not work if Persistence is true
@ -152,33 +163,35 @@ type Options struct {
// New return Pool instance
func New(options Options) (*Pool, error) {
min := ipToUint(options.IPNet.IP) + 3
var (
hostAddr = options.IPNet.Masked().Addr()
gateway = hostAddr.Next()
first = gateway.Next().Next()
last = nnip.UnMasked(*options.IPNet)
)
ones, bits := options.IPNet.Mask.Size()
total := 1<<uint(bits-ones) - 4
if total <= 0 {
if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) {
return nil, errors.New("ipnet don't have valid ip")
}
max := min + uint32(total) - 1
pool := &Pool{
min: min,
max: max,
gateway: min - 2,
broadcast: max + 1,
host: options.Host,
ipnet: options.IPNet,
gateway: gateway,
first: first,
last: last,
offset: first.Prev(),
cycle: false,
host: options.Host,
ipnet: options.IPNet,
}
if options.Persistence {
pool.store = &cachefileStore{
cache: cachefile.Cache(),
}
} else {
pool.store = &memoryStore{
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
}
pool.store = newMemoryStore(options.Size)
}
pool.restoreState()
return pool, nil
}

View File

@ -2,7 +2,7 @@ package fakeip
import (
"fmt"
"net"
"net/netip"
"os"
"testing"
"time"
@ -49,9 +49,9 @@ func createCachefileStore(options Options) (*Pool, string, error) {
}
func TestPool_Basic(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.0/28")
ipnet := netip.MustParsePrefix("192.168.0.0/28")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
assert.Nil(t, err)
@ -62,24 +62,52 @@ func TestPool_Basic(t *testing.T) {
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.True(t, first.Equal(net.IP{192, 168, 0, 3}))
assert.Equal(t, pool.Lookup("foo.com"), net.IP{192, 168, 0, 3})
assert.True(t, last.Equal(net.IP{192, 168, 0, 4}))
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 3}))
assert.True(t, pool.Lookup("foo.com") == netip.AddrFrom4([4]byte{192, 168, 0, 3}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.Equal(t, pool.Gateway(), net.IP{192, 168, 0, 1})
assert.Equal(t, pool.Broadcast(), net.IP{192, 168, 0, 15})
assert.True(t, pool.Gateway() == netip.AddrFrom4([4]byte{192, 168, 0, 1}))
assert.True(t, pool.Broadcast() == netip.AddrFrom4([4]byte{192, 168, 0, 15}))
assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(net.IP{192, 168, 0, 4}))
assert.False(t, pool.Exist(net.IP{192, 168, 0, 5}))
assert.False(t, pool.Exist(net.ParseIP("::1")))
assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 4})))
assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5})))
assert.False(t, pool.Exist(netip.MustParseAddr("::1")))
}
}
func TestPool_BasicV6(t *testing.T) {
ipnet := netip.MustParsePrefix("2001:4860:4860::8888/118")
pools, tempfile, err := createPools(Options{
IPNet: &ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
for _, pool := range pools {
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.True(t, first == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8803"))
assert.True(t, pool.Lookup("foo.com") == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8803"))
assert.True(t, last == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.True(t, pool.Gateway() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801"))
assert.True(t, pool.Broadcast() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff"))
assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")))
assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")))
assert.False(t, pool.Exist(netip.MustParseAddr("127.0.0.1")))
}
}
func TestPool_CycleUsed(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.16/28")
ipnet := netip.MustParsePrefix("192.168.0.16/28")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
assert.Nil(t, err)
@ -88,22 +116,22 @@ func TestPool_CycleUsed(t *testing.T) {
for _, pool := range pools {
foo := pool.Lookup("foo.com")
bar := pool.Lookup("bar.com")
for i := 0; i < 9; i++ {
for i := 0; i < 10; i++ {
pool.Lookup(fmt.Sprintf("%d.com", i))
}
baz := pool.Lookup("baz.com")
next := pool.Lookup("foo.com")
assert.True(t, foo.Equal(baz))
assert.True(t, next.Equal(bar))
assert.True(t, foo == baz)
assert.True(t, next == bar)
}
}
func TestPool_Skip(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
tree := trie.New()
tree.Insert("example.com", tree)
ipnet := netip.MustParsePrefix("192.168.0.1/29")
tree := trie.New[bool]()
tree.Insert("example.com", true)
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
Host: tree,
})
@ -117,9 +145,9 @@ func TestPool_Skip(t *testing.T) {
}
func TestPool_MaxCacheSize(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 2,
})
@ -128,13 +156,13 @@ func TestPool_MaxCacheSize(t *testing.T) {
pool.Lookup("baz.com")
next := pool.Lookup("foo.com")
assert.False(t, first.Equal(next))
assert.False(t, first == next)
}
func TestPool_DoubleMapping(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 2,
})
@ -158,23 +186,23 @@ func TestPool_DoubleMapping(t *testing.T) {
assert.False(t, bazExist)
assert.True(t, barExist)
assert.False(t, bazIP.Equal(newBazIP))
assert.False(t, bazIP == newBazIP)
}
func TestPool_Clone(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 2,
})
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
assert.True(t, first.Equal(net.IP{192, 168, 0, 3}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 4}))
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 3}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
newPool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 2,
})
newPool.CloneFrom(pool)
@ -185,9 +213,9 @@ func TestPool_Clone(t *testing.T) {
}
func TestPool_Error(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
ipnet := netip.MustParsePrefix("192.168.0.1/31")
_, err := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
@ -195,9 +223,9 @@ func TestPool_Error(t *testing.T) {
}
func TestPool_FlushFileCache(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/28")
ipnet := netip.MustParsePrefix("192.168.0.1/28")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
assert.Nil(t, err)
@ -212,22 +240,24 @@ func TestPool_FlushFileCache(t *testing.T) {
err = pool.FlushFakeIP()
assert.Nil(t, err)
baz := pool.Lookup("foo.com")
next := pool.Lookup("baz.com")
baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com")
assert.Equal(t, foo, fox)
assert.NotEqual(t, foo, baz)
assert.Equal(t, bar, bax)
assert.NotEqual(t, bar, next)
assert.Equal(t, baz, nero)
assert.True(t, foo == fox)
assert.True(t, foo == next)
assert.False(t, foo == baz)
assert.True(t, bar == bax)
assert.True(t, bar == baz)
assert.False(t, bar == next)
assert.True(t, baz == nero)
}
}
func TestPool_FlushMemoryCache(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/28")
ipnet := netip.MustParsePrefix("192.168.0.1/28")
pool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
@ -239,13 +269,15 @@ func TestPool_FlushMemoryCache(t *testing.T) {
err := pool.FlushFakeIP()
assert.Nil(t, err)
baz := pool.Lookup("foo.com")
next := pool.Lookup("baz.com")
baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com")
assert.Equal(t, foo, fox)
assert.NotEqual(t, foo, baz)
assert.Equal(t, bar, bax)
assert.NotEqual(t, bar, next)
assert.Equal(t, baz, nero)
assert.True(t, foo == fox)
assert.True(t, foo == next)
assert.False(t, foo == baz)
assert.True(t, bar == bax)
assert.True(t, bar == baz)
assert.False(t, bar == next)
assert.True(t, baz == nero)
}

View File

@ -33,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
@ -98,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

@ -26,7 +26,7 @@ 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(filename, country string) ([]*router.CIDR, error) {

View File

@ -1,7 +1,7 @@
package strmatcher
import (
"container/list"
"github.com/Dreamacro/clash/common/generics/list"
)
const validCharCount = 53
@ -190,7 +190,7 @@ func (ac *ACAutomaton) Add(domain string, t Type) {
}
func (ac *ACAutomaton) Build() {
queue := list.New()
queue := list.New[Edge]()
for i := 0; i < validCharCount; i++ {
if ac.trie[0][i].nextNode != 0 {
queue.PushBack(ac.trie[0][i])
@ -201,7 +201,7 @@ func (ac *ACAutomaton) Build() {
if front == nil {
break
} else {
node := front.Value.(Edge).nextNode
node := front.Value.nextNode
queue.Remove(front)
for i := 0; i < validCharCount; i++ {
if ac.trie[node][i].nextNode != 0 {

View File

@ -2,9 +2,11 @@ package geodata
import (
"github.com/Dreamacro/clash/component/geodata/router"
"golang.org/x/exp/maps"
)
func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) {
func loadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) {
geoLoaderName := "standard"
geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil {
@ -28,3 +30,33 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error)
return matcher, len(domains), nil
}
var ruleProviders = make(map[string]*router.DomainMatcher)
// HasProvider has geo site provider by county code
func HasProvider(countyCode string) (ok bool) {
_, ok = ruleProviders[countyCode]
return ok
}
// GetProvidersList get geo site providers
func GetProvidersList(countyCode string) []*router.DomainMatcher {
return maps.Values(ruleProviders)
}
// GetProviderByCode get geo site provider by county code
func GetProviderByCode(countyCode string) (matcher *router.DomainMatcher, ok bool) {
matcher, ok = ruleProviders[countyCode]
return
}
func LoadProviderByCode(countyCode string) (matcher *router.DomainMatcher, count int, err error) {
var ok bool
matcher, ok = ruleProviders[countyCode]
if !ok {
if matcher, count, err = loadGeoSiteMatcher(countyCode); err == nil {
ruleProviders[countyCode] = matcher
}
}
return
}

View File

@ -3,6 +3,7 @@ package iface
import (
"errors"
"net"
"net/netip"
"time"
"github.com/Dreamacro/clash/common/singledo"
@ -11,7 +12,7 @@ import (
type Interface struct {
Index int
Name string
Addrs []*net.IPNet
Addrs []*netip.Prefix
HardwareAddr net.HardwareAddr
}
@ -20,10 +21,10 @@ var (
ErrAddrNotFound = errors.New("addr not found")
)
var interfaces = singledo.NewSingle(time.Second * 20)
var interfaces = singledo.NewSingle[map[string]*Interface](time.Second * 20)
func ResolveInterface(name string) (*Interface, error) {
value, err, _ := interfaces.Do(func() (any, error) {
value, err, _ := interfaces.Do(func() (map[string]*Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
@ -37,14 +38,18 @@ func ResolveInterface(name string) (*Interface, error) {
continue
}
ipNets := make([]*net.IPNet, 0, len(addrs))
ipNets := make([]*netip.Prefix, 0, len(addrs))
for _, addr := range addrs {
ipNet := addr.(*net.IPNet)
if v4 := ipNet.IP.To4(); v4 != nil {
ipNet.IP = v4
ip, _ := netip.AddrFromSlice(ipNet.IP)
ones, bits := ipNet.Mask.Size()
if bits == 32 {
ip = ip.Unmap()
}
ipNets = append(ipNets, ipNet)
pf := netip.PrefixFrom(ip, ones)
ipNets = append(ipNets, &pf)
}
r[iface.Name] = &Interface{
@ -61,7 +66,7 @@ func ResolveInterface(name string) (*Interface, error) {
return nil, err
}
ifaces := value.(map[string]*Interface)
ifaces := value
iface, ok := ifaces[name]
if !ok {
return nil, ErrIfaceNotFound
@ -74,35 +79,35 @@ func FlushCache() {
interfaces.Reset()
}
func (iface *Interface) PickIPv4Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() != nil
func (iface *Interface) PickIPv4Addr(destination netip.Addr) (*netip.Prefix, error) {
return iface.pickIPAddr(destination, func(addr *netip.Prefix) bool {
return addr.Addr().Is4()
})
}
func (iface *Interface) PickIPv6Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() == nil
func (iface *Interface) PickIPv6Addr(destination netip.Addr) (*netip.Prefix, error) {
return iface.pickIPAddr(destination, func(addr *netip.Prefix) bool {
return addr.Addr().Is6()
})
}
func (iface *Interface) pickIPAddr(destination net.IP, accept func(addr *net.IPNet) bool) (*net.IPNet, error) {
var fallback *net.IPNet
func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr *netip.Prefix) bool) (*netip.Prefix, error) {
var fallback *netip.Prefix
for _, addr := range iface.Addrs {
if !accept(addr) {
continue
}
if fallback == nil && !addr.IP.IsLinkLocalUnicast() {
if fallback == nil && !addr.Addr().IsLinkLocalUnicast() {
fallback = addr
if destination == nil {
if !destination.IsValid() {
break
}
}
if destination != nil && addr.Contains(destination) {
if destination.IsValid() && addr.Contains(destination) {
return addr, nil
}
}

View File

@ -6,55 +6,55 @@ import (
"time"
)
type Factory = func(context.Context) (any, error)
type Factory[T any] func(context.Context) (T, error)
type entry struct {
elm any
type entry[T any] struct {
elm T
time time.Time
}
type Option func(*pool)
type Option[T any] func(*pool[T])
// WithEvict set the evict callback
func WithEvict(cb func(any)) Option {
return func(p *pool) {
func WithEvict[T any](cb func(T)) Option[T] {
return func(p *pool[T]) {
p.evict = cb
}
}
// WithAge defined element max age (millisecond)
func WithAge(maxAge int64) Option {
return func(p *pool) {
func WithAge[T any](maxAge int64) Option[T] {
return func(p *pool[T]) {
p.maxAge = maxAge
}
}
// WithSize defined max size of Pool
func WithSize(maxSize int) Option {
return func(p *pool) {
p.ch = make(chan any, maxSize)
func WithSize[T any](maxSize int) Option[T] {
return func(p *pool[T]) {
p.ch = make(chan *entry[T], maxSize)
}
}
// Pool is for GC, see New for detail
type Pool struct {
*pool
type Pool[T any] struct {
*pool[T]
}
type pool struct {
ch chan any
factory Factory
evict func(any)
type pool[T any] struct {
ch chan *entry[T]
factory Factory[T]
evict func(T)
maxAge int64
}
func (p *pool) GetContext(ctx context.Context) (any, error) {
func (p *pool[T]) GetContext(ctx context.Context) (T, error) {
now := time.Now()
for {
select {
case item := <-p.ch:
elm := item.(*entry)
if p.maxAge != 0 && now.Sub(item.(*entry).time).Milliseconds() > p.maxAge {
elm := item
if p.maxAge != 0 && now.Sub(item.time).Milliseconds() > p.maxAge {
if p.evict != nil {
p.evict(elm.elm)
}
@ -68,12 +68,12 @@ func (p *pool) GetContext(ctx context.Context) (any, error) {
}
}
func (p *pool) Get() (any, error) {
func (p *pool[T]) Get() (T, error) {
return p.GetContext(context.Background())
}
func (p *pool) Put(item any) {
e := &entry{
func (p *pool[T]) Put(item T) {
e := &entry[T]{
elm: item,
time: time.Now(),
}
@ -90,17 +90,17 @@ func (p *pool) Put(item any) {
}
}
func recycle(p *Pool) {
func recycle[T any](p *Pool[T]) {
for item := range p.pool.ch {
if p.pool.evict != nil {
p.pool.evict(item.(*entry).elm)
p.pool.evict(item.elm)
}
}
}
func New(factory Factory, options ...Option) *Pool {
p := &pool{
ch: make(chan any, 10),
func New[T any](factory Factory[T], options ...Option[T]) *Pool[T] {
p := &pool[T]{
ch: make(chan *entry[T], 10),
factory: factory,
}
@ -108,7 +108,7 @@ func New(factory Factory, options ...Option) *Pool {
option(p)
}
P := &Pool{p}
runtime.SetFinalizer(P, recycle)
P := &Pool[T]{p}
runtime.SetFinalizer(P, recycle[T])
return P
}

View File

@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/assert"
)
func lg() Factory {
func lg() Factory[int] {
initial := -1
return func(context.Context) (any, error) {
return func(context.Context) (int, error) {
initial++
return initial, nil
}
@ -18,23 +18,23 @@ func lg() Factory {
func TestPool_Basic(t *testing.T) {
g := lg()
pool := New(g)
pool := New[int](g)
elm, _ := pool.Get()
assert.Equal(t, 0, elm.(int))
assert.Equal(t, 0, elm)
pool.Put(elm)
elm, _ = pool.Get()
assert.Equal(t, 0, elm.(int))
assert.Equal(t, 0, elm)
elm, _ = pool.Get()
assert.Equal(t, 1, elm.(int))
assert.Equal(t, 1, elm)
}
func TestPool_MaxSize(t *testing.T) {
g := lg()
size := 5
pool := New(g, WithSize(size))
pool := New[int](g, WithSize[int](size))
var items []any
var items []int
for i := 0; i < size; i++ {
item, _ := pool.Get()
@ -42,7 +42,7 @@ func TestPool_MaxSize(t *testing.T) {
}
extra, _ := pool.Get()
assert.Equal(t, size, extra.(int))
assert.Equal(t, size, extra)
for _, item := range items {
pool.Put(item)
@ -52,22 +52,22 @@ func TestPool_MaxSize(t *testing.T) {
for _, item := range items {
elm, _ := pool.Get()
assert.Equal(t, item.(int), elm.(int))
assert.Equal(t, item, elm)
}
}
func TestPool_MaxAge(t *testing.T) {
g := lg()
pool := New(g, WithAge(20))
pool := New[int](g, WithAge[int](20))
elm, _ := pool.Get()
pool.Put(elm)
elm, _ = pool.Get()
assert.Equal(t, 0, elm.(int))
assert.Equal(t, 0, elm)
pool.Put(elm)
time.Sleep(time.Millisecond * 22)
elm, _ = pool.Get()
assert.Equal(t, 1, elm.(int))
assert.Equal(t, 1, elm)
}

View File

@ -3,7 +3,9 @@ package process
import (
"errors"
"net"
"net/netip"
"github.com/Dreamacro/clash/common/nnip"
C "github.com/Dreamacro/clash/constant"
)
@ -18,7 +20,7 @@ const (
UDP = "udp"
)
func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) {
func FindProcessName(network string, srcIP netip.Addr, srcPort int) (string, error) {
return findProcessName(network, srcIP, srcPort)
}
@ -27,23 +29,23 @@ func ShouldFindProcess(metadata *C.Metadata) bool {
return false
}
for _, ip := range localIPs {
if ip.Equal(metadata.SrcIP) {
if ip == metadata.SrcIP {
return true
}
}
return false
}
func AppendLocalIPs(ip ...net.IP) {
func AppendLocalIPs(ip ...netip.Addr) {
localIPs = append(ip, localIPs...)
}
func getLocalIPs() []net.IP {
ips := []net.IP{net.IPv4zero, net.IPv6zero}
func getLocalIPs() []netip.Addr {
ips := []netip.Addr{netip.IPv4Unspecified(), netip.IPv6Unspecified()}
netInterfaces, err := net.Interfaces()
if err != nil {
ips = append(ips, net.IPv4(127, 0, 0, 1), net.IPv6loopback)
ips = append(ips, netip.AddrFrom4([4]byte{127, 0, 0, 1}), nnip.IpToAddr(net.IPv6loopback))
return ips
}
@ -53,7 +55,7 @@ func getLocalIPs() []net.IP {
for _, address := range adds {
if ipNet, ok := address.(*net.IPNet); ok {
ips = append(ips, ipNet.IP)
ips = append(ips, nnip.IpToAddr(ipNet.IP))
}
}
}
@ -62,7 +64,7 @@ func getLocalIPs() []net.IP {
return ips
}
var localIPs []net.IP
var localIPs []netip.Addr
func init() {
localIPs = getLocalIPs()

View File

@ -2,10 +2,12 @@ package process
import (
"encoding/binary"
"net"
"net/netip"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/common/nnip"
"golang.org/x/sys/unix"
)
@ -15,7 +17,7 @@ const (
proccallnumpidinfo = 0x2
)
func findProcessName(network string, ip net.IP, port int) (string, error) {
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
var spath string
switch network {
case TCP:
@ -26,7 +28,7 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
return "", ErrInvalidNetwork
}
isIPv4 := ip.To4() != nil
isIPv4 := ip.Is4()
value, err := syscall.Sysctl(spath)
if err != nil {
@ -57,19 +59,19 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
// xinpcb_n.inp_vflag
flag := buf[inp+44]
var srcIP net.IP
var srcIP netip.Addr
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = net.IP(buf[inp+76 : inp+80])
srcIP = nnip.IpToAddr(buf[inp+76 : inp+80])
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = net.IP(buf[inp+64 : inp+80])
srcIP = nnip.IpToAddr(buf[inp+64 : inp+80])
default:
continue
}
if !ip.Equal(srcIP) && (network == TCP || !srcIP.IsUnspecified()) {
if ip != srcIP && (network == TCP || !srcIP.IsUnspecified()) {
continue
}

View File

@ -3,13 +3,14 @@ package process
import (
"encoding/binary"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"sync"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/log"
)
@ -20,7 +21,7 @@ var (
once sync.Once
)
func findProcessName(network string, ip net.IP, srcPort int) (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())
@ -102,7 +103,7 @@ type searcher struct {
pid int
}
func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint32, error) {
func (s *searcher) Search(buf []byte, ip netip.Addr, port uint16, isTCP bool) (uint32, error) {
var itemSize int
var inpOffset int
@ -116,7 +117,7 @@ func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint3
inpOffset = s.udpInpOffset
}
isIPv4 := ip.To4() != nil
isIPv4 := ip.Is4()
// skip the first xinpgen block
for i := s.headSize; i+itemSize <= len(buf); i += itemSize {
inp := i + inpOffset
@ -130,19 +131,19 @@ func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint3
// xinpcb.inp_vflag
flag := buf[inp+s.vflag]
var srcIP net.IP
var srcIP netip.Addr
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = net.IP(buf[inp+s.ip : inp+s.ip+4])
srcIP = nnip.IpToAddr(buf[inp+s.ip : inp+s.ip+4])
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = net.IP(buf[inp+s.ip-12 : inp+s.ip+4])
srcIP = nnip.IpToAddr(buf[inp+s.ip-12 : inp+s.ip+4])
default:
continue
}
if !ip.Equal(srcIP) {
if ip != srcIP {
continue
}

View File

@ -5,6 +5,7 @@ import (
"encoding/binary"
"fmt"
"net"
"net/netip"
"os"
"path"
"strings"
@ -31,7 +32,7 @@ const (
pathProc = "/proc"
)
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
inode, uid, err := resolveSocketByNetlink(network, ip, srcPort)
if err != nil {
return "", err
@ -40,7 +41,7 @@ func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
return resolveProcessNameByProcSearch(inode, uid)
}
func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int32, error) {
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
var family byte
var protocol byte
@ -53,7 +54,7 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int3
return 0, 0, ErrInvalidNetwork
}
if ip.To4() != nil {
if ip.Is4() {
family = syscall.AF_INET
} else {
family = syscall.AF_INET6
@ -65,10 +66,12 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int3
if err != nil {
return 0, 0, fmt.Errorf("dial netlink: %w", err)
}
defer syscall.Close(socket)
defer func() {
_ = syscall.Close(socket)
}()
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
_ = syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
_ = syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
if err := syscall.Connect(socket, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
@ -84,7 +87,9 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int3
}
rb := pool.Get(pool.RelayBufferSize)
defer pool.Put(rb)
defer func() {
_ = pool.Put(rb)
}()
n, err := syscall.Read(socket, rb)
if err != nil {
@ -111,14 +116,10 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int3
return inode, uid, nil
}
func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint16) []byte {
func packSocketDiagRequest(family, protocol byte, source netip.Addr, sourcePort uint16) []byte {
s := make([]byte, 16)
if v4 := source.To4(); v4 != nil {
copy(s, v4)
} else {
copy(s, source)
}
copy(s, source.AsSlice())
buf := make([]byte, sizeOfSocketDiagRequest)

View File

@ -2,8 +2,8 @@
package process
import "net"
import "net/netip"
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
return "", ErrPlatformNotSupport
}

View File

@ -2,11 +2,12 @@ package process
import (
"fmt"
"net"
"net/netip"
"sync"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/log"
"golang.org/x/sys/windows"
@ -57,7 +58,7 @@ func initWin32API() error {
return nil
}
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
once.Do(func() {
err := initWin32API()
if err != nil {
@ -67,7 +68,7 @@ func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
}
})
family := windows.AF_INET
if ip.To4() == nil {
if ip.Is6() {
family = windows.AF_INET6
}
@ -107,7 +108,7 @@ type searcher struct {
tcpState int
}
func (s *searcher) Search(b []byte, ip net.IP, port uint16) (uint32, error) {
func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) {
n := int(readNativeUint32(b[:4]))
itemSize := s.itemSize
for i := 0; i < n; i++ {
@ -131,9 +132,9 @@ func (s *searcher) Search(b []byte, ip net.IP, port uint16) (uint32, error) {
continue
}
srcIP := net.IP(row[s.ip : s.ip+s.ipSize])
srcIP := nnip.IpToAddr(row[s.ip : s.ip+s.ipSize])
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
if !ip.Equal(srcIP) && (!srcIP.IsUnspecified() || s.tcpState != -1) {
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
continue
}
@ -174,7 +175,7 @@ func newSearcher(isV4, isTCP bool) *searcher {
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
for size, buf := uint32(8), make([]byte, 8); ; {
ptr := unsafe.Pointer(&buf[0])
err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
switch err {
case 0:
@ -209,13 +210,13 @@ func getExecPathFromPID(pid uint32) (string, error) {
buf := make([]uint16, syscall.MAX_LONG_PATH)
size := uint32(len(buf))
r1, _, err := syscall.Syscall6(
queryProcName, 4,
r1, _, err := syscall.SyscallN(
queryProcName,
uintptr(h),
uintptr(1),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
0, 0)
)
if r1 == 0 {
return "", err
}

View File

@ -0,0 +1,12 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package resolver
import _ "unsafe"
//go:linkname defaultNS net.defaultNS
var defaultNS []string
func init() {
defaultNS = []string{"114.114.114.114:53", "8.8.8.8:53"}
}

View File

@ -1,20 +1,19 @@
package resolver
import (
"net"
)
import "net/netip"
var DefaultHostMapper Enhancer
type Enhancer interface {
FakeIPEnabled() bool
MappingEnabled() bool
IsFakeIP(net.IP) bool
IsFakeBroadcastIP(net.IP) bool
IsExistFakeIP(net.IP) bool
FindHostByIP(net.IP) (string, bool)
IsFakeIP(netip.Addr) bool
IsFakeBroadcastIP(netip.Addr) bool
IsExistFakeIP(netip.Addr) bool
FindHostByIP(netip.Addr) (string, bool)
FlushFakeIP() error
InsertHostByIP(net.IP, string)
InsertHostByIP(netip.Addr, string)
StoreFakePoolState()
}
func FakeIPEnabled() bool {
@ -33,7 +32,7 @@ func MappingEnabled() bool {
return false
}
func IsFakeIP(ip net.IP) bool {
func IsFakeIP(ip netip.Addr) bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsFakeIP(ip)
}
@ -41,7 +40,7 @@ func IsFakeIP(ip net.IP) bool {
return false
}
func IsFakeBroadcastIP(ip net.IP) bool {
func IsFakeBroadcastIP(ip netip.Addr) bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsFakeBroadcastIP(ip)
}
@ -49,7 +48,7 @@ func IsFakeBroadcastIP(ip net.IP) bool {
return false
}
func IsExistFakeIP(ip net.IP) bool {
func IsExistFakeIP(ip netip.Addr) bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsExistFakeIP(ip)
}
@ -57,13 +56,13 @@ func IsExistFakeIP(ip net.IP) bool {
return false
}
func InsertHostByIP(ip net.IP, host string) {
func InsertHostByIP(ip netip.Addr, host string) {
if mapper := DefaultHostMapper; mapper != nil {
mapper.InsertHostByIP(ip, host)
}
}
func FindHostByIP(ip net.IP) (string, bool) {
func FindHostByIP(ip netip.Addr) (string, bool) {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.FindHostByIP(ip)
}
@ -77,3 +76,9 @@ func FlushFakeIP() error {
}
return nil
}
func StoreFakePoolState() {
if mapper := DefaultHostMapper; mapper != nil {
mapper.StoreFakePoolState()
}
}

View File

@ -5,9 +5,10 @@ import (
"errors"
"math/rand"
"net"
"strings"
"net/netip"
"time"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/trie"
)
@ -23,7 +24,7 @@ var (
DisableIPv6 = true
// DefaultHosts aim to resolve hosts
DefaultHosts = trie.New()
DefaultHosts = trie.New[netip.Addr]()
// DefaultDNSTimeout defined the default dns request timeout
DefaultDNSTimeout = time.Second * 5
@ -36,29 +37,29 @@ var (
)
type Resolver interface {
ResolveIP(host string) (ip net.IP, err error)
ResolveIPv4(host string) (ip net.IP, err error)
ResolveIPv6(host string) (ip net.IP, err error)
ResolveIP(host string) (ip netip.Addr, err error)
ResolveIPv4(host string) (ip netip.Addr, err error)
ResolveIPv6(host string) (ip netip.Addr, err error)
}
// ResolveIPv4 with a host, return ipv4
func ResolveIPv4(host string) (net.IP, error) {
func ResolveIPv4(host string) (netip.Addr, error) {
return ResolveIPv4WithResolver(host, DefaultResolver)
}
func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) {
func ResolveIPv4WithResolver(host string, r Resolver) (netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data.(net.IP).To4(); ip != nil {
if ip := node.Data; ip.Is4() {
return ip, nil
}
}
ip := net.ParseIP(host)
if ip != nil {
if !strings.Contains(host, ":") {
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is4() {
return ip, nil
}
return nil, ErrIPVersion
return netip.Addr{}, ErrIPVersion
}
if r != nil {
@ -70,39 +71,44 @@ func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) {
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
if err != nil {
return nil, err
return netip.Addr{}, err
} else if len(ipAddrs) == 0 {
return nil, ErrIPNotFound
return netip.Addr{}, ErrIPNotFound
}
return ipAddrs[rand.Intn(len(ipAddrs))], nil
ip := ipAddrs[rand.Intn(len(ipAddrs))].To4()
if ip == nil {
return netip.Addr{}, ErrIPVersion
}
return netip.AddrFrom4(*(*[4]byte)(ip)), nil
}
return nil, ErrIPNotFound
return netip.Addr{}, ErrIPNotFound
}
// ResolveIPv6 with a host, return ipv6
func ResolveIPv6(host string) (net.IP, error) {
func ResolveIPv6(host string) (netip.Addr, error) {
return ResolveIPv6WithResolver(host, DefaultResolver)
}
func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) {
func ResolveIPv6WithResolver(host string, r Resolver) (netip.Addr, error) {
if DisableIPv6 {
return nil, ErrIPv6Disabled
return netip.Addr{}, ErrIPv6Disabled
}
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data.(net.IP).To16(); ip != nil {
if ip := node.Data; ip.Is6() {
return ip, nil
}
}
ip := net.ParseIP(host)
if ip != nil {
if strings.Contains(host, ":") {
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is6() {
return ip, nil
}
return nil, ErrIPVersion
return netip.Addr{}, ErrIPVersion
}
if r != nil {
@ -114,21 +120,21 @@ func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) {
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host)
if err != nil {
return nil, err
return netip.Addr{}, err
} else if len(ipAddrs) == 0 {
return nil, ErrIPNotFound
return netip.Addr{}, ErrIPNotFound
}
return ipAddrs[rand.Intn(len(ipAddrs))], nil
return netip.AddrFrom16(*(*[16]byte)(ipAddrs[rand.Intn(len(ipAddrs))])), nil
}
return nil, ErrIPNotFound
return netip.Addr{}, ErrIPNotFound
}
// ResolveIPWithResolver same as ResolveIP, but with a resolver
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
func ResolveIPWithResolver(host string, r Resolver) (netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil {
return node.Data.(net.IP), nil
return node.Data, nil
}
if r != nil {
@ -140,30 +146,30 @@ func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
return ResolveIPv4(host)
}
ip := net.ParseIP(host)
if ip != nil {
ip, err := netip.ParseAddr(host)
if err == nil {
return ip, nil
}
if DefaultResolver == nil {
ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return nil, err
return netip.Addr{}, err
}
return ipAddr.IP, nil
return nnip.IpToAddr(ipAddr.IP), nil
}
return nil, ErrIPNotFound
return netip.Addr{}, ErrIPNotFound
}
// ResolveIP with a host, return ip
func ResolveIP(host string) (net.IP, error) {
func ResolveIP(host string) (netip.Addr, error) {
return ResolveIPWithResolver(host, DefaultResolver)
}
// ResolveIPv4ProxyServerHost proxies server host only
func ResolveIPv4ProxyServerHost(host string) (net.IP, error) {
func ResolveIPv4ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveIPv4WithResolver(host, ProxyServerHostResolver)
}
@ -171,7 +177,7 @@ func ResolveIPv4ProxyServerHost(host string) (net.IP, error) {
}
// ResolveIPv6ProxyServerHost proxies server host only
func ResolveIPv6ProxyServerHost(host string) (net.IP, error) {
func ResolveIPv6ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveIPv6WithResolver(host, ProxyServerHostResolver)
}
@ -179,7 +185,7 @@ func ResolveIPv6ProxyServerHost(host string) (net.IP, error) {
}
// ResolveProxyServerHost proxies server host only
func ResolveProxyServerHost(host string) (net.IP, error) {
func ResolveProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveIPWithResolver(host, ProxyServerHostResolver)
}

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,743 @@
#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:s, s:H, s:s, s:H}",
"ctx", clash_context,
"type", type,
"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

@ -17,8 +17,8 @@ var ErrInvalidDomain = errors.New("invalid domain")
// DomainTrie contains the main logic for adding and searching nodes for domain segments.
// support wildcard domain (e.g *.google.com)
type DomainTrie struct {
root *Node
type DomainTrie[T comparable] struct {
root *Node[T]
}
func ValidAndSplitDomain(domain string) ([]string, bool) {
@ -51,7 +51,7 @@ func ValidAndSplitDomain(domain string) ([]string, bool) {
// 3. subdomain.*.example.com
// 4. .example.com
// 5. +.example.com
func (t *DomainTrie) Insert(domain string, data any) error {
func (t *DomainTrie[T]) Insert(domain string, data T) error {
parts, valid := ValidAndSplitDomain(domain)
if !valid {
return ErrInvalidDomain
@ -68,13 +68,13 @@ func (t *DomainTrie) Insert(domain string, data any) error {
return nil
}
func (t *DomainTrie) insert(parts []string, data any) {
func (t *DomainTrie[T]) insert(parts []string, data T) {
node := t.root
// reverse storage domain part to save space
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
if !node.hasChild(part) {
node.addChild(part, newNode(nil))
node.addChild(part, newNode(getZero[T]()))
}
node = node.getChild(part)
@ -88,7 +88,7 @@ func (t *DomainTrie) insert(parts []string, data any) {
// 1. static part
// 2. wildcard domain
// 2. dot wildcard domain
func (t *DomainTrie) Search(domain string) *Node {
func (t *DomainTrie[T]) Search(domain string) *Node[T] {
parts, valid := ValidAndSplitDomain(domain)
if !valid || parts[0] == "" {
return nil
@ -96,26 +96,26 @@ func (t *DomainTrie) Search(domain string) *Node {
n := t.search(t.root, parts)
if n == nil || n.Data == nil {
if n == nil || n.Data == getZero[T]() {
return nil
}
return n
}
func (t *DomainTrie) search(node *Node, parts []string) *Node {
func (t *DomainTrie[T]) search(node *Node[T], parts []string) *Node[T] {
if len(parts) == 0 {
return node
}
if c := node.getChild(parts[len(parts)-1]); c != nil {
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != nil {
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != getZero[T]() {
return n
}
}
if c := node.getChild(wildcard); c != nil {
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != nil {
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != getZero[T]() {
return n
}
}
@ -124,6 +124,6 @@ func (t *DomainTrie) search(node *Node, parts []string) *Node {
}
// New returns a new, empty Trie.
func New() *DomainTrie {
return &DomainTrie{root: newNode(nil)}
func New[T comparable]() *DomainTrie[T] {
return &DomainTrie[T]{root: newNode[T](getZero[T]())}
}

View File

@ -1,16 +1,16 @@
package trie
import (
"net"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
var localIP = net.IP{127, 0, 0, 1}
var localIP = netip.AddrFrom4([4]byte{127, 0, 0, 1})
func TestTrie_Basic(t *testing.T) {
tree := New()
tree := New[netip.Addr]()
domains := []string{
"example.com",
"google.com",
@ -23,7 +23,7 @@ func TestTrie_Basic(t *testing.T) {
node := tree.Search("example.com")
assert.NotNil(t, node)
assert.True(t, node.Data.(net.IP).Equal(localIP))
assert.True(t, node.Data == localIP)
assert.NotNil(t, tree.Insert("", localIP))
assert.Nil(t, tree.Search(""))
assert.NotNil(t, tree.Search("localhost"))
@ -31,7 +31,7 @@ func TestTrie_Basic(t *testing.T) {
}
func TestTrie_Wildcard(t *testing.T) {
tree := New()
tree := New[netip.Addr]()
domains := []string{
"*.example.com",
"sub.*.example.com",
@ -64,7 +64,7 @@ func TestTrie_Wildcard(t *testing.T) {
}
func TestTrie_Priority(t *testing.T) {
tree := New()
tree := New[int]()
domains := []string{
".dev",
"example.dev",
@ -79,18 +79,18 @@ func TestTrie_Priority(t *testing.T) {
}
for idx, domain := range domains {
tree.Insert(domain, idx)
tree.Insert(domain, idx+1)
}
assertFn("test.dev", 0)
assertFn("foo.bar.dev", 0)
assertFn("example.dev", 1)
assertFn("foo.example.dev", 2)
assertFn("test.example.dev", 3)
assertFn("test.dev", 1)
assertFn("foo.bar.dev", 1)
assertFn("example.dev", 2)
assertFn("foo.example.dev", 3)
assertFn("test.example.dev", 4)
}
func TestTrie_Boundary(t *testing.T) {
tree := New()
tree := New[netip.Addr]()
tree.Insert("*.dev", localIP)
assert.NotNil(t, tree.Insert(".", localIP))
@ -99,7 +99,7 @@ func TestTrie_Boundary(t *testing.T) {
}
func TestTrie_WildcardBoundary(t *testing.T) {
tree := New()
tree := New[netip.Addr]()
tree.Insert("+.*", localIP)
tree.Insert("stun.*.*.*", localIP)

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