Compare commits

..

70 Commits

Author SHA1 Message Date
14917c8af1 merge clash 1.9.0 2022-01-04 17:58:50 +08:00
3bb32d12e0 Merge remote-tracking branch 'clash/dev' into Meta
# Conflicts:
#	.github/workflows/docker.yml
#	dns/server.go
#	go.mod
#	go.sum
#	hub/executor/executor.go
#	test/go.mod
#	test/go.sum
2022-01-04 17:31:07 +08:00
013b839678 [Fix] Linux Tun 2021-12-27 07:09:45 +08:00
a06382cebc [test] 2021-12-27 06:44:17 +08:00
ebc3f36236 [fix]autoIptables 2021-12-27 03:29:14 +08:00
e2a0437685 [fix] 2021-12-27 03:16:48 +08:00
82c8e02d02 [Style] Add User-Agent for provider request 2021-12-26 22:26:53 +08:00
a210ec4197 [Feature] 添加unified-delay boolean 控制延迟测试,默认为false,当设置true时忽略握手延迟,将统一延迟结果,从而利于不同协议的url-test 2021-12-26 21:20:41 +08:00
0b72395704 Merge pull request #5 from xsxun/patch-1
Update vless.go, fix udp blocked
2021-12-20 18:13:43 +08:00
8955107d6b Update vless.go 2021-12-20 12:59:06 +08:00
69aef9cec0 [Fixed] Configure tun interface on linux 2021-12-11 22:34:45 +08:00
9e44e21406 [Fixed] launch resolver an enhancer when tun mode 2021-12-09 23:00:54 +08:00
b0fdd8dc47 [Fixed] Add retry to open tun 2021-12-09 22:52:32 +08:00
e92ef587bb [Fixed] The array may be sent out of bounds 2021-12-09 22:52:32 +08:00
7d17d53a8f [readme] 2021-12-09 17:54:53 +08:00
58ef4ddbba [Fixed]Meaningless pointer 2021-12-07 20:49:39 +08:00
a78b89d16e Revert: Revert Redir-Host, please add fallback dns and append proxy adapter
DNS pass proxy use:
- protocol://ip:port#AdapterName
- protocol://ip:port/query#AdapterName

sure as:
- tls://1.1.1.1:853#DNS
2021-12-06 22:45:59 +08:00
833b43a538 Fixed: Does RuleSet resolve ip logic modification 2021-12-06 21:47:22 +08:00
8df3efe932 [Fix] 修正因xray服务端alpn参数为http/1.1而导致无法连接的问题 2021-12-06 00:19:03 +08:00
645c3154d6 [Fix] 修正因xray服务端alpn参数为http/1.1而导致无法连接的问题 2021-12-05 03:51:26 +08:00
a847d7b58d [Fix] 修正因xray服务端alpn参数为http/1.1而导致无法连接的问题 2021-12-05 02:18:58 +08:00
37ea8aff5c README 2021-12-05 00:48:35 +08:00
cb4ce8be6a Makefile 2021-12-04 21:43:33 +08:00
a85395e777 readme 2021-12-04 20:50:57 +08:00
819b29956b readme 2021-12-04 20:40:09 +08:00
eb999b3bf1 fix AutoIptables 2021-12-04 19:59:41 +08:00
8580ee8898 [style] 2021-12-04 17:41:13 +08:00
58552447ef [fix]Linux TProxy 2021-12-04 14:34:01 +08:00
23ca356447 Fixed: Modify the trigger condition, only if it fails successively 2021-12-04 00:16:39 +08:00
fae65b97ec fix Makefile 2021-12-03 22:13:05 +08:00
99f0231a9b style 2021-12-03 21:54:45 +08:00
edf1bb476d test 2021-12-03 20:38:40 +08:00
5c53243e81 Experimental: Positive health testing 2021-12-03 14:35:21 +08:00
b99b4ad15f Fixed:Rule-Set Supported RuleExtra 2021-12-02 23:32:30 +08:00
6369921364 Merge pull request #4 from Skyxim/meta
Feature:Supported Rule-Set
2021-12-02 23:17:02 +08:00
c6f923041f Feature:Supported Rule-Set 2021-12-02 22:56:17 +08:00
53eb3f15bb Revert "[fix]code"
This reverts commit 0431969a73.
2021-12-02 20:08:34 +08:00
b15a7c8b6f Revert "[test]"
This reverts commit bf6bfdd930.
2021-12-02 20:08:28 +08:00
038f973f90 Merge remote-tracking branch 'origin/Meta' into Meta
# Conflicts:
#	tunnel/tunnel.go
2021-12-02 18:06:47 +08:00
bf6bfdd930 [test] 2021-12-02 18:06:14 +08:00
0431969a73 [fix]code 2021-12-02 03:39:37 +08:00
c7b257b188 [style] 2021-12-01 19:25:32 +08:00
885f69b81d [style] 2021-12-01 17:08:44 +08:00
cb52682790 [style] 2021-12-01 16:51:31 +08:00
c65835d9e4 [style] embed_wintun.dll 2021-11-30 18:00:19 +08:00
92bb026f70 [style] embed_wintun.dll 2021-11-30 17:58:21 +08:00
c22c7efd07 [fix] embed_windows 2021-11-27 22:10:37 +08:00
e4b30dacd4 [fix] embed_windows 2021-11-27 21:51:38 +08:00
353ae30839 [test] embed_windows 2021-11-27 21:36:10 +08:00
828ff82ff2 [test] embed_windows 2021-11-27 21:23:34 +08:00
35cf39e415 Revert "[test] rule providers"
This reverts commit 078389f4f6.
2021-11-26 00:57:41 +08:00
340efef2d8 Revert "[test] rule providers"
This reverts commit 14af94205c.
2021-11-26 00:57:36 +08:00
796eb5c95c Revert "[test] rule providers"
This reverts commit d4cc650633.
2021-11-26 00:57:33 +08:00
0f2b87497b Revert "[fix]code"
This reverts commit 06e9243fda.
2021-11-26 00:57:29 +08:00
06e9243fda [fix]code 2021-11-26 00:27:00 +08:00
d4cc650633 [test] rule providers 2021-11-25 23:33:06 +08:00
14af94205c [test] rule providers 2021-11-25 23:20:08 +08:00
078389f4f6 [test] rule providers 2021-11-25 23:14:31 +08:00
cad18b7529 [fix] rule providers 2021-11-25 21:52:07 +08:00
aeddc8eb1d fix proxies callback 2021-11-21 16:57:22 +08:00
f7393509a3 fix python310 2021-11-21 15:09:22 +08:00
223de1f3fd [update]version 2021-11-18 23:54:20 +08:00
1fb2bc07d7 [update]readme 2021-11-17 19:55:14 +08:00
eb57d246cf [test]tun 2021-11-17 19:35:34 +08:00
0001a1b844 [Fix]Vless tls must not be true 2021-11-17 19:09:01 +08:00
b20e202321 [Fix]Vless tls must not be true 2021-11-17 17:56:24 +08:00
900e852525 [test] 2021-11-17 16:03:47 +08:00
1f3968bd50 [test]core 1.8 2021-11-17 15:00:32 +08:00
5d510eb5aa [test]core 1.8 2021-11-16 20:08:52 +08:00
cec14db4a8 Merge pull request #1 from Dreamacro/master
更新
2021-11-09 16:14:04 +08:00
338 changed files with 9237 additions and 14768 deletions

View File

@ -1,76 +0,0 @@
name: Bug report
description: Create a report to help us improve
title: "[Bug] "
body:
- type: checkboxes
id: ensure
attributes:
label: Verify steps
description: "
在提交之前,请确认
Please verify that you've followed these steps
"
options:
- label: "
如果你可以自己 debug 并解决的话,提交 PR 吧
Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
"
required: true
- label: "
我已经在 [Issue Tracker](……/) 中找过我要提出的问题
I have searched on the [issue tracker](……/) for a related issue.
"
required: true
- label: "
我已经使用 dev 分支版本测试过,问题依旧存在
I have tested using the dev branch, and the issue still exists.
"
required: true
- label: "
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
"
required: true
- label: "
这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash.
"
required: true
- type: input
attributes:
label: Clash version
validations:
required: true
- type: dropdown
id: os
attributes:
label: What OS are you seeing the problem on?
multiple: true
options:
- macOS
- Windows
- Linux
- OpenBSD/FreeBSD
- type: textarea
attributes:
render: yaml
label: "Clash config"
description: "
在下方附上 Clash core 脱敏后配置文件的内容
Paste the Clash core configuration below.
"
validations:
required: true
- type: textarea
attributes:
render: shell
label: Clash log
description: "
在下方附上 Clash Core 的日志log level 使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
"
- type: textarea
attributes:
label: Description
validations:
required: true

View File

@ -1,6 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Get help in GitHub Discussions
url: https://github.com/Dreamacro/clash/discussions
about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on Clash's GitHub Discussions!

View File

@ -1,36 +0,0 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature] "
body:
- type: checkboxes
id: ensure
attributes:
label: Verify steps
description: "
在提交之前,请确认
Please verify that you've followed these steps
"
options:
- label: "
我已经在 [Issue Tracker](……/) 中找过我要提出的请求
I have searched on the [issue tracker](……/) for a related feature request.
"
required: true
- label: "
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
"
required: true
- type: textarea
attributes:
label: Description
description: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
validations:
required: true
- type: textarea
attributes:
label: Possible Solution
description: "
此项非必须,但是如果你有想法的话欢迎提出。
Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change
"

View File

@ -1,29 +0,0 @@
name: CodeQL
on:
push:
branches: [ rm ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: ['go']
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -2,8 +2,9 @@ name: Publish Docker Image
on: on:
push: push:
branches: branches:
- rm - dev
tags:
- '*'
jobs: jobs:
build: build:
@ -12,7 +13,7 @@ jobs:
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
@ -48,16 +49,14 @@ jobs:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true push: true
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev' tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Get all docker tags - name: Get all docker tags
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v6 uses: actions/github-script@v4
id: tags id: tags
with: with:
script: | script: |
const ref = context.payload.ref.replace(/\/?refs\/tags\//, '') const ref = `${context.payload.ref.replace(/\/?refs\/tags\//, '')}`
const tags = [ const tags = [
'dreamacro/clash:latest', 'dreamacro/clash:latest',
`dreamacro/clash:${ref}`, `dreamacro/clash:${ref}`,
@ -75,5 +74,3 @@ jobs:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true push: true
tags: ${{steps.tags.outputs.result}} tags: ${{steps.tags.outputs.result}}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,22 +0,0 @@
name: Linter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ steps.version.outputs.go_version }}
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

View File

@ -1,73 +0,0 @@
name: Release
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ steps.version.outputs.go_version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: 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: Get dependencies, run test
run: |
go test ./...
- name: Build
if: startsWith(github.ref, 'refs/tags/')
env:
NAME: clash
BINDIR: bin
run: make -j releases
#- name: Prepare upload
# run: |
# echo "FILE_DATE=_$(date +"%Y%m%d%H%M")" >> $GITHUB_ENV
# echo "FILE_SHA=$(git describe --tags --always 2>/dev/null)" >> $GITHUB_ENV
#
#- name: Upload files to Artifacts
# uses: actions/upload-artifact@v2
# if: startsWith(github.ref, 'refs/tags/') == false
# with:
# name: clash_${{ env.FILE_SHA }}${{ env.FILE_DATE }}
# path: |
# bin/*
- name: Upload Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: bin/*
draft: true
prerelease: true
generate_release_notes: true
#- name: Delete workflow runs
# uses: GitRML/delete-workflow-runs@main
# with:
# retain_days: 1
# keep_minimum_runs: 2

View File

@ -1,19 +0,0 @@
name: Mark stale issues and pull requests
on:
push:
branches:
- rm
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
days-before-stale: 60
days-before-close: 5

View File

@ -1,16 +0,0 @@
linters:
disable-all: true
enable:
- gofumpt
- staticcheck
- govet
- gci
linters-settings:
gci:
sections:
- standard
- prefix(github.com/Dreamacro/clash)
- default
staticcheck:
go: '1.18'

View File

@ -12,7 +12,7 @@ RUN go mod download && \
FROM alpine:latest FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash" LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
RUN apk add --no-cache ca-certificates tzdata RUN apk add --no-cache ca-certificates
COPY --from=builder /Country.mmdb /root/.config/clash/ COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash / COPY --from=builder /clash /
ENTRYPOINT ["/clash"] ENTRYPOINT ["/clash"]

View File

@ -1,41 +1,32 @@
NAME=clash NAME=Clash.Meta
BINDIR=bin BINDIR=bin
VERSION=$(shell git describe --tags --always 2>/dev/null || echo "unknown version") VERSION=$(shell git describe --tags || echo "unknown version")
BUILDTIME=$(shell date -u) BUILDTIME=$(shell date -u)
AUTOIPTABLES=Enable
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-w -s -buildid=' -w -s -buildid='
GOBUILDOP=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-X "github.com/Dreamacro/clash/constant.AutoIptables=$(AUTOIPTABLES)" \
-w -s -buildid='
PLATFORM_LIST = \ PLATFORM_LIST = \
darwin-amd64 \ darwin-amd64 \
darwin-amd64-v3 \
darwin-arm64 \ darwin-arm64 \
linux-386 \ linux-arm64 \
linux-amd64 \ linux-amd64 \
linux-amd64-v3 \ linux-arm64-AutoIptables\
linux-armv5 \ linux-amd64-AutoIptables
linux-armv6 \
linux-armv7 \
linux-armv8 \
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
WINDOWS_ARCH_LIST = \ WINDOWS_ARCH_LIST = \
windows-386 \ windows-386 \
windows-amd64 \ windows-amd64
windows-amd64-v3 \
windows-arm64 \
windows-arm32v7
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
all: linux-arm64-AutoIptables linux-amd64-AutoIptables linux-arm64 linux-amd64 darwin-amd64 darwin-arm64 windows-amd64 windows-386 # Most used
docker: docker:
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@ $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -43,9 +34,6 @@ docker:
darwin-amd64: darwin-amd64:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-v3:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64: darwin-arm64:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -55,21 +43,39 @@ linux-386:
linux-amd64: linux-amd64:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-v3: linux-amd64-AutoIptables:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-arm64:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-arm64-AutoIptables:
GOARCH=arm64 GOOS=linux $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-armv5: linux-armv5:
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv5-AutoIptables:
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-armv6: linux-armv6:
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv6-AutoIptables:
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-armv7: linux-armv7:
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv7-AutoIptables:
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-armv8: linux-armv8:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv8-AutoIptables:
GOARCH=arm64 GOOS=linux $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-mips-softfloat: linux-mips-softfloat:
GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -94,9 +100,6 @@ freebsd-386:
freebsd-amd64: freebsd-amd64:
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64-v3:
GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-arm64: freebsd-arm64:
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -106,12 +109,6 @@ windows-386:
windows-amd64: windows-amd64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.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: windows-arm32v7:
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
@ -128,16 +125,5 @@ $(zip_releases): %.zip : %
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST) all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
releases: $(gz_releases) $(zip_releases) releases: $(gz_releases) $(zip_releases)
vet:
go test ./...
lint:
GOOS=darwin golangci-lint run ./...
GOOS=windows golangci-lint run ./...
GOOS=linux golangci-lint run ./...
GOOS=freebsd golangci-lint run ./...
GOOS=openbsd golangci-lint run ./...
clean: clean:
rm -rf $(BINDIR)/* rm $(BINDIR)/*

BIN
Meta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

364
README.md
View File

@ -1,23 +1,20 @@
<h1 align="center"> <h1 align="center">
<img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200"> <img src="Meta.png" alt="Meta Kennel" width="200">
<br>Clash<br> <br>Meta Kennel<br>
</h1> </h1>
<h4 align="center">A rule-based tunnel in Go.</h4> <h3 align="center">Another Clash Kennel.</h3>
<p align="center"> <p align="center">
<a href="https://github.com/Dreamacro/clash/actions"> <a href="https://goreportcard.com/report/github.com/Clash-Mini/Clash.Meta">
<img src="https://img.shields.io/github/workflow/status/Dreamacro/clash/Go?style=flat-square" alt="Github Actions"> <img src="https://goreportcard.com/badge/github.com/Clash-Mini/Clash.Meta?style=flat-square">
</a>
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</a> </a>
<img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square"> <img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
<a href="https://github.com/yaling888/clash/releases"> <a href="https://github.com/Clash-Mini/Clash.Meta/releases">
<img src="https://img.shields.io/github/release/yaling888/clash/all.svg?style=flat-square"> <img src="https://img.shields.io/github/release/Clash-Mini/Clash.Meta/all.svg?style=flat-square">
</a> </a>
<a href="https://github.com/yaling888/clash/releases/tag/plus_pro"> <a href="https://github.com/Clash-Mini/Clash.Meta">
<img src="https://img.shields.io/badge/release-Plus Pro-00b4f0?style=flat-square"> <img src="https://img.shields.io/badge/release-Meta-00b4f0?style=flat-square">
</a> </a>
</p> </p>
@ -36,158 +33,68 @@
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki). Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
## Advanced usage for this branch ## Advanced usage for this branch
### 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 ### DNS configuration
Support resolve ip with a proxy tunnel or interface. Support resolve ip with a proxy tunnel.
Support `geosite` with `fallback-filter`. Support `geosite` with `fallback-filter`.
```yaml
Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fakeip dns:
```yaml enable: true
dns: use-hosts: true
enable: true ipv6: false
use-hosts: true enhanced-mode: fake-ip
ipv6: false fake-ip-range: 198.18.0.1/16
enhanced-mode: fake-ip listen: 127.0.0.1:6868
fake-ip-range: 198.18.0.1/16 default-nameserver:
listen: 127.0.0.1:6868 - 119.29.29.29
default-nameserver: - 114.114.114.114
- 119.29.29.29 nameserver:
- 114.114.114.114 - https://doh.pub/dns-query
nameserver: - tls://223.5.5.5:853
- https://doh.pub/dns-query fallback:
- tls://223.5.5.5:853 - 'https://1.0.0.1/dns-query#Proxy' # append the proxy adapter name to the end of DNS URL with '#' prefix.
fallback: - 'tls://8.8.4.4:853#Proxy'
- 'tls://8.8.4.4:853#proxy or interface' fallback-filter:
- 'https://1.0.0.1/dns-query#Proxy' # append the proxy adapter name to the end of DNS URL with '#' prefix. geoip: false
fallback-filter: geosite:
geoip: false - gfw # `geosite` filter only use fallback server to resolve ip, prevent DNS leaks to unsafe DNS providers.
geosite: domain:
- gfw # `geosite` filter only use fallback server to resolve ip, prevent DNS leaks to untrusted DNS providers. - +.example.com
domain: ipcidr:
- +.example.com - 0.0.0.0/32
ipcidr: ```
- 0.0.0.0/32
```
### TUN configuration ### TUN configuration
Simply add the following to the main configuration:
#### NOTE: Supports macOS, Linux and Windows.
> auto-route and auto-detect-interface only available on macOS, Windows and Linux, receive IPv4 traffic
Built-in [Wintun](https://www.wintun.net) driver.
```yaml ```yaml
# Enable the TUN listener
tun: tun:
enable: true enable: true
stack: system # or gvisor stack: gvisor # system or gvisor
# device: tun://utun8 # or fd://xxx, it's optional dns-listen: 0.0.0.0:53 # additional dns server listen on TUN
# 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 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 ### Rules configuration
- Support rule `GEOSITE`. - Support rule `GEOSITE`.
- Support rule `USER-AGENT`. - Support rule-providers `RULE-SET`.
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`. - Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
- Support `network` condition for all rules. - Support `network` condition for all rules.
- Support `process` condition for all rules.
- Support source IPCIDR condition for all rules, just append to the end. - Support source IPCIDR condition for all rules, just append to the end.
- The `GEOSITE` databases via https://github.com/Loyalsoldier/v2ray-rules-dat.
The `GEOIP` databases via [https://github.com/Loyalsoldier/geoip](https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb).
The `GEOSITE` databases via [https://github.com/Loyalsoldier/v2ray-rules-dat](https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat).
```yaml ```yaml
rules: rules:
# network condition for all rules
- DOMAIN-SUFFIX,example.com,DIRECT,tcp
- DOMAIN-SUFFIX,example.com,REJECT,udp
# process condition for all rules (add 'P:' prefix)
- DOMAIN-SUFFIX,example.com,REJECT,P:Google Chrome Helper
# network(tcp/udp) condition for all rules
- DOMAIN-SUFFIX,bilibili.com,DIRECT,tcp
- DOMAIN-SUFFIX,bilibili.com,REJECT,udp
# multiport condition for rules SRC-PORT and DST-PORT # multiport condition for rules SRC-PORT and DST-PORT
- DST-PORT,123/136/137-139,DIRECT,udp - 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 # rule GEOSITE
- GEOSITE,category-ads-all,REJECT - GEOSITE,category-ads-all,REJECT
- GEOSITE,icloud@cn,DIRECT - GEOSITE,icloud@cn,DIRECT
@ -198,181 +105,112 @@ rules:
- GEOSITE,youtube,PROXY - GEOSITE,youtube,PROXY
- GEOSITE,geolocation-cn,DIRECT - GEOSITE,geolocation-cn,DIRECT
- GEOSITE,geolocation-!cn,PROXY - GEOSITE,geolocation-!cn,PROXY
# source IPCIDR condition for all rules in gateway proxy # source IPCIDR condition for all rules in gateway proxy
#- GEOSITE,geolocation-!cn,REJECT,192.168.1.88/32,192.168.1.99/32 #- GEOSITE,geolocation-!cn,REJECT,192.168.1.88/32,192.168.1.99/32
- GEOIP,telegram,PROXY,no-resolve
- GEOIP,lan,DIRECT,no-resolve
- GEOIP,cn,DIRECT
- GEOIP,telegram,PROXY,no-resolve
- GEOIP,private,DIRECT,no-resolve
- GEOIP,cn,DIRECT
- MATCH,PROXY - MATCH,PROXY
``` ```
### Proxies configuration ### Proxies configuration
Support outbound protocol `VLESS`. Support outbound transport protocol `VLESS`.
Support `Trojan` with XTLS. The XTLS only support TCP transport by the XRAY-CORE.
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 ```yaml
proxies: proxies:
# VLESS - name: "vless-tcp"
- name: "vless-tls"
type: vless type: vless
server: server server: server
port: 443 port: 443
uuid: uuid uuid: uuid
network: tcp network: tcp
servername: example.com servername: example.com # AKA SNI
# flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS
# skip-cert-verify: true
- name: "vless-ws"
type: vless
server: server
port: 443
uuid: uuid
udp: true udp: true
network: ws
servername: example.com # priority over wss host
# skip-cert-verify: true # skip-cert-verify: true
- name: "vless-xtls" ws-path: /path
type: vless ws-headers:
server: server Host: example.com
port: 443
uuid: uuid
network: tcp
servername: example.com
flow: xtls-rprx-direct # or xtls-rprx-origin
# flow-show: true # print the XTLS direction log
# udp: true
# skip-cert-verify: true
# Trojan
- name: "trojan-xtls"
type: trojan
server: server
port: 443
password: yourpsk
network: tcp
flow: xtls-rprx-direct # or xtls-rprx-origin
# flow-show: true # print the XTLS direction log
# udp: true
# sni: example.com # aka server name
# skip-cert-verify: true
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 ### IPTABLES auto-configuration
Work on Linux OS who's supported `iptables` Only work on Linux OS who support `iptables`, Clash will auto-configuration iptables for tproxy listener when `tproxy-port` value isn't zero.
If `TPROXY` is enabled, the `TUN` must be disabled.
```yaml ```yaml
# Enable the TPROXY listener # Enable the TPROXY listener
tproxy-port: 9898 tproxy-port: 9898
# Disable the TUN listener
iptables: tun:
enable: true # default is false enable: false
inbound-interface: eth0 # detect the inbound interface, default is 'lo'
``` ```
Run Clash as a daemon. Create user given name `Clash.Meta`.
Run Meta Kennel by user `Clash.Meta` as a daemon.
Create the systemd configuration file at /etc/systemd/system/clash.service: Create the systemd configuration file at /etc/systemd/system/clash.service:
```sh
```
[Unit] [Unit]
Description=Clash daemon, A rule-based proxy in Go. Description=Clash.Meta Daemon, Another Clash Kennel.
After=network.target After=network.target
[Service] [Service]
Type=simple Type=simple
User=Clash.Meta
Group=Clash.Meta
CapabilityBoundingSet=cap_net_admin CapabilityBoundingSet=cap_net_admin
AmbientCapabilities=cap_net_admin
Restart=always Restart=always
ExecStart=/usr/local/bin/clash -d /etc/clash ExecStart=/usr/local/bin/Clash.Meta -d /etc/Clash.Meta
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
``` ```
Launch clashd on system startup with: Launch clashd on system startup with:
```sh ```shell
$ systemctl enable clash $ systemctl enable Clash.Meta
``` ```
Launch clashd immediately with: Launch clashd immediately with:
```sh
$ systemctl start clash ```shell
$ systemctl start Clash.Meta
``` ```
### Display Process name ### Display Process name
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.
You can download the [Dashboard](https://github.com/yaling888/yacd/archive/gh-pages.zip) into Clash home directory: Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
```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: To display process name in GUI please use [Dashboard For Meta](https://github.com/Clash-Mini/Dashboard).
```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 ![img.png](https://github.com/Clash-Mini/Dashboard/raw/master/View/Dashboard-Process.png)
[Release](https://github.com/yaling888/clash/releases/tag/plus_pro)
## Development ## Development
If you want to build an application that uses clash as a library, check out the the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
If you want to build an application that uses clash as a library, check out the
the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
## Credits ## Credits
* [Dreamacro/clash](https://github.com/Dreamacro/clash)
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2) * [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core) * [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
* [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go) * [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
* [yaling888/clash-plus-pro](https://github.com/yaling888/clash)
## License ## License

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/netip"
"net/url" "net/url"
"time" "time"
@ -17,9 +16,11 @@ import (
"go.uber.org/atomic" "go.uber.org/atomic"
) )
var UnifiedDelay = atomic.NewBool(false)
type Proxy struct { type Proxy struct {
C.ProxyAdapter C.ProxyAdapter
history *queue.Queue[C.DelayHistory] history *queue.Queue
alive *atomic.Bool alive *atomic.Bool
} }
@ -58,10 +59,10 @@ func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
// DelayHistory implements C.Proxy // DelayHistory implements C.Proxy
func (p *Proxy) DelayHistory() []C.DelayHistory { func (p *Proxy) DelayHistory() []C.DelayHistory {
queueM := p.history.Copy() queue := p.history.Copy()
histories := []C.DelayHistory{} histories := []C.DelayHistory{}
for _, item := range queueM { for _, item := range queue {
histories = append(histories, item) histories = append(histories, item.(C.DelayHistory))
} }
return histories return histories
} }
@ -74,7 +75,11 @@ func (p *Proxy) LastDelay() (delay uint16) {
return max return max
} }
history := p.history.Last() last := p.history.Last()
if last == nil {
return max
}
history := last.(C.DelayHistory)
if history.Delay == 0 { if history.Delay == 0 {
return max return max
} }
@ -88,8 +93,8 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
return inner, err return inner, err
} }
mapping := map[string]any{} mapping := map[string]interface{}{}
_ = json.Unmarshal(inner, &mapping) json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory() mapping["history"] = p.DelayHistory()
mapping["name"] = p.Name() mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP() mapping["udp"] = p.SupportUDP()
@ -111,6 +116,8 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
} }
}() }()
unifiedDelay := UnifiedDelay.Load()
addr, err := urlToMetadata(url) addr, err := urlToMetadata(url)
if err != nil { if err != nil {
return return
@ -121,9 +128,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
if err != nil { if err != nil {
return return
} }
defer func() { defer instance.Close()
_ = instance.Close()
}()
req, err := http.NewRequest(http.MethodHead, url, nil) req, err := http.NewRequest(http.MethodHead, url, nil)
if err != nil { if err != nil {
@ -132,7 +137,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
req = req.WithContext(ctx) req = req.WithContext(ctx)
transport := &http.Transport{ transport := &http.Transport{
DialContext: func(context.Context, string, string) (net.Conn, error) { Dial: func(string, string) (net.Conn, error) {
return instance, nil return instance, nil
}, },
// from http.DefaultTransport // from http.DefaultTransport
@ -149,18 +154,26 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
}, },
} }
defer client.CloseIdleConnections() defer client.CloseIdleConnections()
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return return
} }
_ = resp.Body.Close()
if unifiedDelay {
start = time.Now()
resp, err = client.Do(req)
if err != nil {
return
}
}
resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond) t = uint16(time.Since(start) / time.Millisecond)
return return
} }
func NewProxy(adapter C.ProxyAdapter) *Proxy { func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)} return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
} }
func urlToMetadata(rawURL string) (addr C.Metadata, err error) { func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@ -185,7 +198,7 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
addr = C.Metadata{ addr = C.Metadata{
AddrType: C.AtypDomainName, AddrType: C.AtypDomainName,
Host: u.Hostname(), Host: u.Hostname(),
DstIP: netip.Addr{}, DstIP: nil,
DstPort: port, DstPort: port,
} }
return return

View File

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

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"io"
"net" "net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
@ -31,17 +30,12 @@ func (b *Base) Type() C.AdapterType {
} }
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, _ *C.Metadata) (net.Conn, error) { func (b *Base) StreamConn(c net.Conn, metadata *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") return c, errors.New("no support")
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(_ context.Context, _ *C.Metadata, _ ...dialer.Option) (C.PacketConn, error) { func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return nil, errors.New("no support") return nil, errors.New("no support")
} }
@ -63,7 +57,7 @@ func (b *Base) Addr() string {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(_ *C.Metadata) C.Proxy { func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil return nil
} }
@ -139,40 +133,6 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name()) 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()}} 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,7 +3,6 @@ package outbound
import ( import (
"context" "context"
"net" "net"
"net/netip"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -15,31 +14,21 @@ type Direct struct {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
opts = append(opts, dialer.WithDirect())
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...) c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tcpKeepAlive(c) tcpKeepAlive(c)
if !metadata.Resolved() && c.RemoteAddr() != nil {
if h, _, err := net.SplitHostPort(c.RemoteAddr().String()); err == nil {
metadata.DstIP = netip.MustParseAddr(h)
}
}
return NewConn(c, d), nil return NewConn(c, d), nil
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, _ *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
opts = append(opts, dialer.WithDirect())
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...) pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewPacketConn(&directPacketConn{pc}, d), nil return newPacketConn(&directPacketConn{pc}, d), nil
} }
type directPacketConn struct { type directPacketConn struct {

View File

@ -89,10 +89,6 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) 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 { if err := req.Write(rw); err != nil {
return err return err
} }
@ -140,7 +136,6 @@ func NewHttp(option HttpOption) *Http {
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http, tp: C.Http,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,

View File

@ -1,50 +0,0 @@
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)
_ = c.SetLinger(0)
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,49 +6,22 @@ import (
"net" "net"
"time" "time"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" 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 { type Reject struct {
*Base *Base
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { 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 return NewConn(&nopConn{}, r), nil
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { 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 { func NewReject() *Reject {

View File

@ -10,10 +10,11 @@ import (
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
obfs "github.com/Dreamacro/clash/transport/simple-obfs" obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
"github.com/Dreamacro/go-shadowsocks2/core"
) )
type ShadowSocks struct { type ShadowSocks struct {
@ -28,14 +29,14 @@ type ShadowSocks struct {
type ShadowSocksOption struct { type ShadowSocksOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
Password string `proxy:"password"` Password string `proxy:"password"`
Cipher string `proxy:"cipher"` Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"` Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"` PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
} }
type simpleObfsOption struct { type simpleObfsOption struct {
@ -73,21 +74,6 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
return c, err 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 // DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { 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...)...) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
@ -109,13 +95,14 @@ func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Meta
return nil, err return nil, err
} }
c, err := ss.StreamPacketConn(WrapConn(pc), metadata) addr, err := resolveUDPAddr("udp", ss.addr)
if err != nil { if err != nil {
_ = pc.Close() pc.Close()
return nil, err return nil, err
} }
return NewPacketConn(c.(net.PacketConn), ss), nil pc = ss.cipher.PacketConn(pc)
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
} }
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
@ -173,7 +160,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
tp: C.Shadowsocks, tp: C.Shadowsocks,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
cipher: ciph, cipher: ciph,

View File

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

View File

@ -23,13 +23,12 @@ type Snell struct {
type SnellOption struct { type SnellOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
Psk string `proxy:"psk"` Psk string `proxy:"psk"`
UDP bool `proxy:"udp,omitempty"` Version int `proxy:"version,omitempty"`
Version int `proxy:"version,omitempty"` ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"`
} }
type streamOption struct { type streamOption struct {
@ -58,18 +57,6 @@ func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, err 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 // DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { 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 { if s.version == snell.Version2 && len(opts) == 0 {
@ -80,7 +67,7 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil { if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
_ = c.Close() c.Close()
return nil, err return nil, err
} }
return NewConn(c, s), err return NewConn(c, s), err
@ -98,23 +85,6 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return NewConn(c, s), err return NewConn(c, s), err
} }
// ListenPacketContext implements C.ProxyAdapter
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
tcpKeepAlive(c)
pc, err := s.StreamPacketConn(c, metadata)
if err != nil {
_ = c.Close()
return nil, err
}
return NewPacketConn(pc.(net.PacketConn), s), nil
}
func NewSnell(option SnellOption) (*Snell, error) { func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk) psk := []byte(option.Psk)
@ -136,13 +106,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == 0 { if option.Version == 0 {
option.Version = snell.DefaultSnellVersion option.Version = snell.DefaultSnellVersion
} }
switch option.Version { if option.Version != snell.Version1 && option.Version != snell.Version2 {
case snell.Version1, snell.Version2:
if option.UDP {
return nil, fmt.Errorf("snell version %d not support UDP", option.Version)
}
case snell.Version3:
default:
return nil, fmt.Errorf("snell version error: %d", option.Version) return nil, fmt.Errorf("snell version error: %d", option.Version)
} }
@ -151,9 +115,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Snell, tp: C.Snell,
udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
psk: psk, psk: psk,
obfsOption: obfsOption, obfsOption: obfsOption,

View File

@ -37,59 +37,12 @@ type Socks5Option struct {
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { 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 { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
err := cc.Handshake() err := cc.Handshake()
c = cc c = cc
if err != nil { if err != nil {
return c, nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
} }
@ -100,14 +53,10 @@ func (ss *Socks5) streamConn(c net.Conn, metadata *C.Metadata) (_ net.Conn, bind
Password: ss.pass, Password: ss.pass,
} }
} }
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
if metadata.NetWork == C.UDP { return nil, err
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 // DialContext implements C.ProxyAdapter
@ -132,24 +81,61 @@ 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) { 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...)...) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) 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
} }
defer safeConnClose(c, 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...)...) pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return return
} }
tcpKeepAlive(c) 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()
}()
pc, err = ss.StreamSocks5PacketConn(c, pc, metadata) // Support unspecified UDP bind address.
if err != nil { bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
err = errors.New("invalid UDP bind address")
return return
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
if err != nil {
return nil, err
}
bindUDPAddr.IP = serverAddr.IP
} }
return NewPacketConn(pc, ss), nil return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
} }
func NewSocks5(option Socks5Option) *Socks5 { func NewSocks5(option Socks5Option) *Socks5 {
@ -168,7 +154,6 @@ func NewSocks5(option Socks5Option) *Socks5 {
tp: C.Socks5, tp: C.Socks5,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,
@ -213,6 +198,6 @@ func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
} }
func (uc *socksPacketConn) Close() error { func (uc *socksPacketConn) Close() error {
_ = uc.tcpConn.Close() uc.tcpConn.Close()
return uc.PacketConn.Close() return uc.PacketConn.Close()
} }

View File

@ -12,7 +12,6 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan" "github.com/Dreamacro/clash/transport/trojan"
"github.com/Dreamacro/clash/transport/vless"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -41,8 +40,6 @@ type TrojanOption struct {
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
} }
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
@ -72,7 +69,8 @@ func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
return t.instance.StreamConn(c) return t.instance.StreamConn(c)
} }
func (t *Trojan) trojanStream(c net.Conn, metadata *C.Metadata) (net.Conn, error) { // StreamConn implements C.ProxyAdapter
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
if t.transport != nil { if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig) c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
@ -84,63 +82,28 @@ func (t *Trojan) trojanStream(c net.Conn, metadata *C.Metadata) (net.Conn, error
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
c, err = t.instance.PrepareXTLSConn(c)
if err != nil {
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)) err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return c, err 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 // DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport // gun transport
if t.transport != nil && len(opts) == 0 { 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
}
defer safeConnClose(c, err)
c, err = t.instance.PrepareXTLSConn(c)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil { if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
c.Close()
return nil, err return nil, err
} }
return NewConn(c, t), nil 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
@ -160,44 +123,33 @@ 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) { func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
var c net.Conn var c net.Conn
// gun transport // grpc transport
if t.transport != nil && len(opts) == 0 { 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 { if err != nil {
return nil, err return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
defer safeConnClose(c, err) defer safeConnClose(c, err)
} else {
c, err = t.instance.PrepareXTLSConn(c) c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
defer safeConnClose(c, err)
if err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)); err != nil { tcpKeepAlive(c)
return nil, err c, err = t.plainStream(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
pc := t.instance.PacketConn(c)
return NewPacketConn(pc, t), nil
} }
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...) err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
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 { if err != nil {
return nil, err return nil, err
} }
return NewPacketConn(c.(net.PacketConn), t), nil pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err
} }
func NewTrojan(option TrojanOption) (*Trojan, error) { func NewTrojan(option TrojanOption) (*Trojan, error) {
@ -208,17 +160,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ALPN: option.ALPN, ALPN: option.ALPN,
ServerName: option.Server, ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify, SkipCertVerify: option.SkipCertVerify,
FlowShow: option.FlowShow,
}
if option.Network != "ws" && len(option.Flow) >= 16 {
option.Flow = option.Flow[:16]
switch option.Flow {
case vless.XRO, vless.XRD, vless.XRS:
tOption.Flow = option.Flow
default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
}
} }
if option.SNI != "" { if option.SNI != "" {
@ -232,7 +173,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
tp: C.Trojan, tp: C.Trojan,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
instance: trojan.New(tOption), instance: trojan.New(tOption),
option: &option, option: &option,
@ -255,12 +195,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: tOption.ServerName, ServerName: tOption.ServerName,
} }
if t.option.Flow != "" { t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
} else {
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
t.gunTLSConfig = tlsConfig t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{ t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName, ServiceName: option.GrpcOpts.GrpcServiceName,

View File

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

View File

@ -6,7 +6,6 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@ -18,13 +17,12 @@ import (
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vless"
"github.com/Dreamacro/clash/transport/vmess" "github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
const ( const (
// max packet length // max packet length
maxLength = 1024 << 3 maxLength = 8192
) )
type Vless struct { type Vless struct {
@ -46,6 +44,7 @@ type VlessOption struct {
UUID string `proxy:"uuid"` UUID string `proxy:"uuid"`
Flow string `proxy:"flow,omitempty"` Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"` FlowShow bool `proxy:"flow-show,omitempty"`
TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
@ -58,7 +57,6 @@ type VlessOption struct {
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
} }
// StreamConn implements C.ProxyAdapter
func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
switch v.option.Network { switch v.option.Network {
@ -87,19 +85,19 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
wsOpts.Headers = header wsOpts.Headers = header
} }
wsOpts.TLS = true if v.option.TLS {
wsOpts.TLSConfig = &tls.Config{ wsOpts.TLS = true
MinVersion: tls.VersionTLS12, wsOpts.TLSConfig = &tls.Config{
ServerName: host, ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
}
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
} }
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
@ -136,7 +134,6 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
} }
default: default:
// default tcp network
// handle TLS And XTLS // handle TLS And XTLS
c, err = v.streamTLSOrXTLSConn(c, false) c, err = v.streamTLSOrXTLSConn(c, false)
} }
@ -148,26 +145,6 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return v.client.StreamConn(c, parseVlessAddr(metadata)) 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.ResolveFirstIP(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) { func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
@ -187,7 +164,7 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
return vless.StreamXTLSConn(conn, &xtlsOpts) return vless.StreamXTLSConn(conn, &xtlsOpts)
} else { } else if v.option.TLS {
tlsOpts := vmess.TLSConfig{ tlsOpts := vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
@ -203,6 +180,8 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
return vmess.StreamTLSConn(conn, &tlsOpts) return vmess.StreamTLSConn(conn, &tlsOpts)
} }
return conn, nil
} }
func (v *Vless) isXTLSEnabled() bool { func (v *Vless) isXTLSEnabled() bool {
@ -240,18 +219,18 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (v *Vless) 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 var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { 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.ResolveFirstIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -259,27 +238,22 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
defer safeConnClose(c, err) defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) c, err = v.client.StreamConn(c, parseVlessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c)
defer safeConnClose(c, err)
return NewPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil c, err = v.StreamConn(c, metadata)
} }
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 { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("new vless client error: %v", err)
} }
return NewPacketConn(c.(net.PacketConn), v), nil return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr { func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
@ -289,11 +263,11 @@ func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
case C.AtypIPv4: case C.AtypIPv4:
addrType = byte(vless.AtypIPv4) addrType = byte(vless.AtypIPv4)
addr = make([]byte, net.IPv4len) addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.To4())
case C.AtypIPv6: case C.AtypIPv6:
addrType = byte(vless.AtypIPv6) addrType = byte(vless.AtypIPv6)
addr = make([]byte, net.IPv6len) addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.To16())
case C.AtypDomainName: case C.AtypDomainName:
addrType = byte(vless.AtypDomainName) addrType = byte(vless.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1) addr = make([]byte, len(metadata.Host)+1)
@ -301,7 +275,7 @@ func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
copy(addr[1:], []byte(metadata.Host)) copy(addr[1:], []byte(metadata.Host))
} }
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) port, _ := strconv.Atoi(metadata.DstPort)
return &vless.DstAddr{ return &vless.DstAddr{
UDP: metadata.NetWork == C.UDP, UDP: metadata.NetWork == C.UDP,
AddrType: addrType, AddrType: addrType,
@ -312,97 +286,91 @@ func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
type vlessPacketConn struct { type vlessPacketConn struct {
net.Conn net.Conn
rAddr net.Addr rAddr net.Addr
cache [2]byte
remain int remain int
mux sync.Mutex mux sync.Mutex
cache []byte
} }
func (vc *vlessPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) { func (c *vlessPacketConn) writePacket(b []byte, addr net.Addr) (int, error) {
total := len(b) length := len(b)
if total == 0 { defer func() {
return 0, nil c.cache = c.cache[:0]
}()
c.cache = append(c.cache, byte(length>>8), byte(length))
c.cache = append(c.cache, b...)
n, err := c.Conn.Write(c.cache)
if n > 2 {
return n - 2, err
} }
if total < maxLength { return 0, err
return vc.writePacket(b) }
func (c *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
if len(b) <= maxLength {
return c.writePacket(b, addr)
} }
offset := 0 offset := 0
for { total := len(b)
for offset < total {
cursor := offset + maxLength cursor := offset + maxLength
if cursor > total { if cursor > total {
cursor = total cursor = total
} }
n, err := vc.writePacket(b[offset:cursor]) n, err := c.writePacket(b[offset:cursor], addr)
if err != nil { if err != nil {
return offset + n, err return offset + n, err
} }
offset = cursor offset = cursor
if offset == total {
break
}
} }
return total, nil return total, nil
} }
func (vc *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
vc.mux.Lock()
defer vc.mux.Unlock()
if vc.remain != 0 { func (c *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
length := len(b) c.mux.Lock()
if length > vc.remain { defer c.mux.Unlock()
length = vc.remain
}
n, err := vc.Conn.Read(b[:length])
if err != nil {
return 0, vc.rAddr, err
}
vc.remain -= n
return n, vc.rAddr, nil
}
if _, err := vc.Conn.Read(b[:2]); err != nil {
return 0, vc.rAddr, err
}
total := int(binary.BigEndian.Uint16(b[:2]))
if total == 0 {
return 0, vc.rAddr, nil
}
length := len(b) length := len(b)
if length > total { if c.remain > 0 {
length = total if c.remain < length {
length = c.remain
}
n, err := c.Conn.Read(b[:length])
if err != nil {
return 0, nil, err
}
c.remain -= n
return n, c.rAddr, nil
} }
if _, err := io.ReadFull(vc.Conn, b[:length]); err != nil { var packetLength uint16
return 0, vc.rAddr, errors.New("read packet error") if err := binary.Read(c.Conn, binary.BigEndian, &packetLength); err != nil {
return 0, nil, err
} }
vc.remain = total - length remain := int(packetLength)
n, err := c.Conn.Read(b[:length])
return length, vc.rAddr, nil remain -= n
if remain > 0 {
c.remain = remain
}
return n, c.rAddr, err
} }
func (vc *vlessPacketConn) writePacket(payload []byte) (int, error) {
binary.BigEndian.PutUint16(vc.cache[:], uint16(len(payload)))
if _, err := vc.Conn.Write(vc.cache[:]); err != nil {
return 0, err
}
return vc.Conn.Write(payload)
}
func NewVless(option VlessOption) (*Vless, error) { func NewVless(option VlessOption) (*Vless, error) {
if !option.TLS && option.Network == "grpc" {
return nil, fmt.Errorf("TLS must be true with vless-grpc")
}
var addons *vless.Addons var addons *vless.Addons
if option.Network != "ws" && len(option.Flow) >= 16 { if option.Network != "ws" && len(option.Flow) >= 16 {
option.Flow = option.Flow[:16] option.Flow = option.Flow[:16]
@ -412,7 +380,7 @@ func NewVless(option VlessOption) (*Vless, error) {
Flow: option.Flow, Flow: option.Flow,
} }
default: default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) return nil, fmt.Errorf("unsupported vless flow type: %s", option.Flow)
} }
} }

View File

@ -3,13 +3,13 @@ package outbound
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -47,10 +47,6 @@ type VmessOption struct {
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
// TODO: compatible with VMESS WS older version configurations
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
} }
type HTTPOptions struct { type HTTPOptions struct {
@ -80,27 +76,21 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
if v.option.WSOpts.Path == "" {
v.option.WSOpts.Path = v.option.WSPath
}
if len(v.option.WSOpts.Headers) == 0 {
v.option.WSOpts.Headers = v.option.WSHeaders
}
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{ wsOpts := &vmess.WebsocketConfig{
Host: host, Host: host,
Port: port, Port: port,
Headers: http.Header{},
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
} }
if len(v.option.WSOpts.Headers) != 0 { if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSOpts.Headers { for key, value := range v.option.WSOpts.Headers {
wsOpts.Headers.Add(key, value) header.Add(key, value)
} }
wsOpts.Headers = header
} }
if v.option.TLS { if v.option.TLS {
@ -115,11 +105,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
} else if host := wsOpts.Headers.Get("Host"); host != "" { } else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = host
} }
} else {
if wsOpts.Headers.Get("Host") == "" {
wsOpts.Headers.Set("Host", convert.RandHost())
}
convert.SetUserAgent(wsOpts.Headers)
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
@ -199,26 +184,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return v.client.StreamConn(c, parseVmessAddr(metadata)) 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.ResolveFirstIP(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 // DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport // gun transport
@ -250,18 +215,18 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { 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 var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { 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.ResolveFirstIP(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) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -269,27 +234,22 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
defer safeConnClose(c, err) defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c)
defer safeConnClose(c, err)
return NewPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil c, err = v.StreamConn(c, metadata)
} }
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 { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("new vmess client error: %v", err)
} }
return NewPacketConn(c.(net.PacketConn), v), nil return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
func NewVmess(option VmessOption) (*Vmess, error) { func NewVmess(option VmessOption) (*Vmess, error) {
@ -320,7 +280,6 @@ func NewVmess(option VmessOption) (*Vmess, error) {
tp: C.Vmess, tp: C.Vmess,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
client: client, client: client,
option: &option, option: &option,
@ -371,11 +330,11 @@ func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
case C.AtypIPv4: case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4) addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len) addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.To4())
case C.AtypIPv6: case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6) addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len) addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.To16())
case C.AtypDomainName: case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName) addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1) addr = make([]byte, len(metadata.Host)+1)
@ -397,7 +356,7 @@ type vmessPacketConn struct {
rAddr net.Addr rAddr net.Addr
} }
func (uc *vmessPacketConn) WriteTo(b []byte, _ net.Addr) (int, error) { func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
return uc.Conn.Write(b) return uc.Conn.Write(b)
} }

View File

@ -3,8 +3,6 @@ package outboundgroup
import ( import (
"time" "time"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
) )
@ -13,19 +11,14 @@ const (
defaultGetProxiesDuration = time.Second * 5 defaultGetProxiesDuration = time.Second * 5
) )
var defaultRejectProxy = adapter.NewProxy(outbound.NewReject())
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy { func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
proxies := []C.Proxy{} proxies := []C.Proxy{}
for _, pd := range providers { for _, provider := range providers {
if touch { if touch {
proxies = append(proxies, pd.ProxiesWithTouch()...) proxies = append(proxies, provider.ProxiesWithTouch()...)
} else { } else {
proxies = append(proxies, pd.Proxies()...) proxies = append(proxies, provider.Proxies()...)
} }
} }
if len(proxies) == 0 {
proxies = append(proxies, defaultRejectProxy)
}
return proxies return proxies
} }

View File

@ -3,6 +3,9 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/Dreamacro/clash/log"
"go.uber.org/atomic"
"time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
@ -13,9 +16,11 @@ import (
type Fallback struct { type Fallback struct {
*outbound.Base *outbound.Base
disableUDP bool disableUDP bool
single *singledo.Single[[]C.Proxy] single *singledo.Single
providers []provider.ProxyProvider providers []provider.ProxyProvider
failedTimes *atomic.Int32
failedTime *atomic.Int64
} }
func (f *Fallback) Now() string { func (f *Fallback) Now() string {
@ -29,7 +34,12 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts .
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...) c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(f) c.AppendToChains(f)
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
} else {
f.onDialFailed()
} }
return c, err return c, err
} }
@ -39,10 +49,39 @@ func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...) pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(f) pc.AppendToChains(f)
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
} else {
f.onDialFailed()
} }
return pc, err return pc, err
} }
func (f *Fallback) onDialFailed() {
if f.failedTime.Load() == -1 {
log.Warnln("%s first failed", f.Name())
now := time.Now().UnixMilli()
f.failedTime.Store(now)
f.failedTimes.Store(1)
} else {
if f.failedTime.Load()-time.Now().UnixMilli() > 5*1000 {
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
} else {
f.failedTimes.Inc()
failedCount := f.failedTimes.Load()
log.Warnln("%s failed count: %d", f.Name(), failedCount)
if failedCount > 5 {
log.Debugln("%s failed multiple times.", f.Name())
for _, proxyProvider := range f.providers {
go proxyProvider.HealthCheck()
}
}
}
}
}
// SupportUDP implements C.ProxyAdapter // SupportUDP implements C.ProxyAdapter
func (f *Fallback) SupportUDP() bool { func (f *Fallback) SupportUDP() bool {
if f.disableUDP { if f.disableUDP {
@ -59,7 +98,7 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
for _, proxy := range f.proxies(false) { for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]interface{}{
"type": f.Type().String(), "type": f.Type().String(),
"now": f.Now(), "now": f.Now(),
"all": all, "all": all,
@ -73,11 +112,11 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
} }
func (f *Fallback) proxies(touch bool) []C.Proxy { func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() ([]C.Proxy, error) { elm, _, _ := f.single.Do(func() (interface{}, error) {
return getProvidersProxies(f.providers, touch), nil return getProvidersProxies(f.providers, touch), nil
}) })
return elm return elm.([]C.Proxy)
} }
func (f *Fallback) findAliveProxy(touch bool) C.Proxy { func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
@ -99,8 +138,10 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider)
Interface: option.Interface, Interface: option.Interface,
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
}), }),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
} }
} }

View File

@ -22,16 +22,18 @@ type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
type LoadBalance struct { type LoadBalance struct {
*outbound.Base *outbound.Base
disableUDP bool disableUDP bool
single *singledo.Single[[]C.Proxy] single *singledo.Single
providers []provider.ProxyProvider providers []provider.ProxyProvider
strategyFn strategyFn strategyFn strategyFn
} }
var errStrategy = errors.New("unsupported strategy") var errStrategy = errors.New("unsupported strategy")
func parseStrategy(config map[string]any) string { func parseStrategy(config map[string]interface{}) string {
if strategy, ok := config["strategy"].(string); ok { if elm, ok := config["strategy"]; ok {
return strategy if strategy, ok := elm.(string); ok {
return strategy
}
} }
return "consistent-hashing" return "consistent-hashing"
} }
@ -48,7 +50,7 @@ func getKey(metadata *C.Metadata) string {
} }
} }
if !metadata.DstIP.IsValid() { if metadata.DstIP == nil {
return "" return ""
} }
@ -138,11 +140,11 @@ func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
} }
func (lb *LoadBalance) proxies(touch bool) []C.Proxy { func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() ([]C.Proxy, error) { elm, _, _ := lb.single.Do(func() (interface{}, error) {
return getProvidersProxies(lb.providers, touch), nil return getProvidersProxies(lb.providers, touch), nil
}) })
return elm return elm.([]C.Proxy)
} }
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
@ -151,7 +153,7 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
for _, proxy := range lb.proxies(false) { for _, proxy := range lb.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]interface{}{
"type": lb.Type().String(), "type": lb.Type().String(),
"all": all, "all": all,
}) })
@ -174,7 +176,7 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
Interface: option.Interface, Interface: option.Interface,
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
}), }),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
strategyFn: strategyFn, strategyFn: strategyFn,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,

View File

@ -3,7 +3,6 @@ package outboundgroup
import ( import (
"errors" "errors"
"fmt" "fmt"
"regexp"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider" "github.com/Dreamacro/clash/adapter/provider"
@ -30,32 +29,18 @@ type GroupCommonOption struct {
Interval int `group:"interval,omitempty"` Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"` Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
} }
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) { func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{ groupOption := &GroupCommonOption{
Lazy: true, 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 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 == "" { if groupOption.Type == "" || groupOption.Name == "" {
return nil, errFormat return nil, errFormat
} }
@ -78,31 +63,38 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
return nil, errDuplicateProvider return nil, errDuplicateProvider
} }
hc, err := newHealthCheck(ps, groupOption) // select don't need health check
if err != nil { if groupOption.Type == "select" || groupOption.Type == "relay" {
return nil, err hc := provider.NewHealthCheck(ps, "", 0, true)
} pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) providers = append(providers, pd)
if err != nil { providersMap[groupName] = pd
return nil, err } else {
} if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, errMissHealthCheck
}
providers = append(providers, pd) hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
providersMap[groupName] = pd pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
}
} }
if len(groupOption.Use) != 0 { if len(groupOption.Use) != 0 {
list, err := getProviders(providersMap, groupOption, filterRegx) list, err := getProviders(providersMap, groupOption.Use)
if err != nil { if err != nil {
return nil, err return nil, err
} }
providers = append(providers, list...)
if groupOption.Type == "fallback" {
providers = append(list, providers...)
} else {
providers = append(providers, list...)
}
} }
var group C.ProxyAdapter var group C.ProxyAdapter
@ -138,13 +130,8 @@ func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
return ps, nil return ps, nil
} }
func getProviders(mapping map[string]types.ProxyProvider, groupOption *GroupCommonOption, filterRegx *regexp.Regexp) ([]types.ProxyProvider, error) { func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) {
var ( var ps []types.ProxyProvider
ps []types.ProxyProvider
list = groupOption.Use
groupName = groupOption.Name
)
for _, name := range list { for _, name := range list {
p, ok := mapping[name] p, ok := mapping[name]
if !ok { if !ok {
@ -154,39 +141,7 @@ func getProviders(mapping map[string]types.ProxyProvider, groupOption *GroupComm
if p.VehicleType() == types.Compatible { if p.VehicleType() == types.Compatible {
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name) return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
} }
if filterRegx != nil {
hc, err := newHealthCheck([]C.Proxy{}, groupOption)
if err != nil {
return nil, err
}
gName := groupName
if _, ok = mapping[gName]; ok {
gName = groupName + " -> " + p.Name()
}
pd := p.(*provider.ProxySetProvider)
p = provider.NewProxyFilterProvider(gName, pd, hc, filterRegx)
pd.RegisterProvidersInUse(p)
}
ps = append(ps, p) ps = append(ps, p)
} }
return ps, nil return ps, nil
} }
func newHealthCheck(ps []C.Proxy, groupOption *GroupCommonOption) (*provider.HealthCheck, error) {
var hc *provider.HealthCheck
// select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" {
hc = provider.NewHealthCheck(ps, "", 0, true)
} else {
if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, errMissHealthCheck
}
hc = provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
}
return hc, nil
}

View File

@ -4,21 +4,17 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/netip"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
) )
type Relay struct { type Relay struct {
*outbound.Base *outbound.Base
single *singledo.Single[[]C.Proxy] single *singledo.Single
providers []provider.ProxyProvider providers []provider.ProxyProvider
} }
@ -38,12 +34,30 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...) return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
} }
c, err := r.streamContext(ctx, proxies, r.Base.DialOptions(opts...)...) first := proxies[0]
last := proxies[len(proxies)-1]
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
tcpKeepAlive(c)
var currentMeta *C.Metadata
for _, proxy := range proxies[1:] {
currentMeta, err = addrToMetadata(proxy.Addr())
if err != nil {
return nil, err
}
c, err = first.StreamConn(c, currentMeta)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
first = proxy
} }
last := proxies[len(proxies)-1]
c, err = last.StreamConn(c, metadata) c, err = last.StreamConn(c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err) return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
@ -52,165 +66,24 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return outbound.NewConn(c, r), nil 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 // MarshalJSON implements C.ProxyAdapter
func (r *Relay) MarshalJSON() ([]byte, error) { func (r *Relay) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range r.rawProxies(false) { for _, proxy := range r.rawProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]interface{}{
"type": r.Type().String(), "type": r.Type().String(),
"all": all, "all": all,
}) })
} }
func (r *Relay) rawProxies(touch bool) []C.Proxy { func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() ([]C.Proxy, error) { elm, _, _ := r.single.Do(func() (interface{}, error) {
return getProvidersProxies(r.providers, touch), nil return getProvidersProxies(r.providers, touch), nil
}) })
return elm return elm.([]C.Proxy)
} }
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy { func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
@ -227,83 +100,6 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
return proxies 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 { func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{ return &Relay{
Base: outbound.NewBase(outbound.BaseOption{ Base: outbound.NewBase(outbound.BaseOption{
@ -312,7 +108,7 @@ func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Re
Interface: option.Interface, Interface: option.Interface,
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
}), }),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
} }
} }

View File

@ -15,7 +15,7 @@ import (
type Selector struct { type Selector struct {
*outbound.Base *outbound.Base
disableUDP bool disableUDP bool
single *singledo.Single[C.Proxy] single *singledo.Single
selected string selected string
providers []provider.ProxyProvider providers []provider.ProxyProvider
} }
@ -54,7 +54,7 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]interface{}{
"type": s.Type().String(), "type": s.Type().String(),
"now": s.Now(), "now": s.Now(),
"all": all, "all": all,
@ -83,7 +83,7 @@ func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
} }
func (s *Selector) selectedProxy(touch bool) C.Proxy { func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (C.Proxy, error) { elm, _, _ := s.single.Do(func() (interface{}, error) {
proxies := getProvidersProxies(s.providers, touch) proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies { for _, proxy := range proxies {
if proxy.Name() == s.selected { if proxy.Name() == s.selected {
@ -94,15 +94,11 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
return proxies[0], nil return proxies[0], nil
}) })
return elm return elm.(C.Proxy)
} }
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector { func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
selected := "REJECT" selected := providers[0].Proxies()[0].Name()
if len(providers) != 0 && len(providers[0].Proxies()) != 0 {
selected = providers[0].Proxies()[0].Name()
}
return &Selector{ return &Selector{
Base: outbound.NewBase(outbound.BaseOption{ Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name, Name: option.Name,
@ -110,7 +106,7 @@ func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider)
Interface: option.Interface, Interface: option.Interface,
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
}), }),
single: singledo.NewSingle[C.Proxy](defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
selected: selected, selected: selected,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,

View File

@ -3,6 +3,8 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/Dreamacro/clash/log"
"go.uber.org/atomic"
"time" "time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
@ -22,12 +24,14 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
type URLTest struct { type URLTest struct {
*outbound.Base *outbound.Base
tolerance uint16 tolerance uint16
disableUDP bool disableUDP bool
fastNode C.Proxy fastNode C.Proxy
single *singledo.Single[[]C.Proxy] single *singledo.Single
fastSingle *singledo.Single[C.Proxy] fastSingle *singledo.Single
providers []provider.ProxyProvider providers []provider.ProxyProvider
failedTimes *atomic.Int32
failedTime *atomic.Int64
} }
func (u *URLTest) Now() string { func (u *URLTest) Now() string {
@ -39,7 +43,12 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...) c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(u) c.AppendToChains(u)
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
} else {
u.onDialFailed()
} }
return c, err return c, err
} }
@ -48,7 +57,12 @@ func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...) pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(u) pc.AppendToChains(u)
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
} else {
u.onDialFailed()
} }
return pc, err return pc, err
} }
@ -58,15 +72,15 @@ func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
} }
func (u *URLTest) proxies(touch bool) []C.Proxy { func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() ([]C.Proxy, error) { elm, _, _ := u.single.Do(func() (interface{}, error) {
return getProvidersProxies(u.providers, touch), nil return getProvidersProxies(u.providers, touch), nil
}) })
return elm return elm.([]C.Proxy)
} }
func (u *URLTest) fast(touch bool) C.Proxy { func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (C.Proxy, error) { elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
proxies := u.proxies(touch) proxies := u.proxies(touch)
fast := proxies[0] fast := proxies[0]
min := fast.LastDelay() min := fast.LastDelay()
@ -96,7 +110,7 @@ func (u *URLTest) fast(touch bool) C.Proxy {
return u.fastNode, nil return u.fastNode, nil
}) })
return elm return elm.(C.Proxy)
} }
// SupportUDP implements C.ProxyAdapter // SupportUDP implements C.ProxyAdapter
@ -114,19 +128,45 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
for _, proxy := range u.proxies(false) { for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]interface{}{
"type": u.Type().String(), "type": u.Type().String(),
"now": u.Now(), "now": u.Now(),
"all": all, "all": all,
}) })
} }
func parseURLTestOption(config map[string]any) []urlTestOption { func (u *URLTest) onDialFailed() {
if u.failedTime.Load() == -1 {
log.Warnln("%s first failed", u.Name())
now := time.Now().UnixMilli()
u.failedTime.Store(now)
u.failedTimes.Store(1)
} else {
if u.failedTime.Load()-time.Now().UnixMilli() > 5*1000 {
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
} else {
u.failedTimes.Inc()
failedCount := u.failedTimes.Load()
log.Warnln("%s failed count: %d", u.Name(), failedCount)
if failedCount > 5 {
log.Debugln("%s failed multiple times.", u.Name())
for _, proxyProvider := range u.providers {
go proxyProvider.HealthCheck()
}
}
}
}
}
func parseURLTestOption(config map[string]interface{}) []urlTestOption {
opts := []urlTestOption{} opts := []urlTestOption{}
// tolerance // tolerance
if tolerance, ok := config["tolerance"].(int); ok { if elm, ok := config["tolerance"]; ok {
opts = append(opts, urlTestWithTolerance(uint16(tolerance))) if tolerance, ok := elm.(int); ok {
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
}
} }
return opts return opts
@ -140,10 +180,12 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o
Interface: option.Interface, Interface: option.Interface,
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
}), }),
single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers, providers: providers,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
} }
for _, option := range options { for _, option := range options {

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import (
"crypto/md5" "crypto/md5"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"time" "time"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
@ -15,32 +14,30 @@ import (
var ( var (
fileMode os.FileMode = 0o666 fileMode os.FileMode = 0o666
dirMode os.FileMode = 0o755 dirMode os.FileMode = 0o755
commentRegx = regexp.MustCompile(`(.*#.*\n)`)
) )
type parser[V any] func([]byte) (V, error) type parser = func([]byte) (interface{}, error)
type fetcher[V any] struct { type fetcher struct {
name string name string
vehicle types.Vehicle vehicle types.Vehicle
updatedAt *time.Time updatedAt *time.Time
ticker *time.Ticker ticker *time.Ticker
done chan struct{} done chan struct{}
hash [16]byte hash [16]byte
parser parser[V] parser parser
onUpdate func(V) onUpdate func(interface{})
} }
func (f *fetcher[V]) Name() string { func (f *fetcher) Name() string {
return f.name return f.name
} }
func (f *fetcher[V]) VehicleType() types.VehicleType { func (f *fetcher) VehicleType() types.VehicleType {
return f.vehicle.Type() return f.vehicle.Type()
} }
func (f *fetcher[V]) Initial() (V, error) { func (f *fetcher) Initial() (interface{}, error) {
var ( var (
buf []byte buf []byte
err error err error
@ -56,24 +53,24 @@ func (f *fetcher[V]) Initial() (V, error) {
} }
if err != nil { if err != nil {
return getZero[V](), err return nil, err
} }
proxies, err := f.parser(buf) proxies, err := f.parser(buf)
if err != nil { if err != nil {
if !isLocal { if !isLocal {
return getZero[V](), err return nil, err
} }
// parse local file error, fallback to remote // parse local file error, fallback to remote
buf, err = f.vehicle.Read() buf, err = f.vehicle.Read()
if err != nil { if err != nil {
return getZero[V](), err return nil, err
} }
proxies, err = f.parser(buf) proxies, err = f.parser(buf)
if err != nil { if err != nil {
return getZero[V](), err return nil, err
} }
isLocal = false isLocal = false
@ -81,7 +78,7 @@ func (f *fetcher[V]) Initial() (V, error) {
if f.vehicle.Type() != types.File && !isLocal { if f.vehicle.Type() != types.File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil { if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return getZero[V](), err return nil, err
} }
} }
@ -95,28 +92,27 @@ func (f *fetcher[V]) Initial() (V, error) {
return proxies, nil return proxies, nil
} }
func (f *fetcher[V]) Update() (V, bool, error) { func (f *fetcher) Update() (interface{}, bool, error) {
buf, err := f.vehicle.Read() buf, err := f.vehicle.Read()
if err != nil { if err != nil {
return getZero[V](), false, err return nil, false, err
} }
now := time.Now() now := time.Now()
hash := md5.Sum(buf) hash := md5.Sum(buf)
if bytes.Equal(f.hash[:], hash[:]) { if bytes.Equal(f.hash[:], hash[:]) {
f.updatedAt = &now f.updatedAt = &now
_ = os.Chtimes(f.vehicle.Path(), now, now) return nil, true, nil
return getZero[V](), true, nil
} }
proxies, err := f.parser(buf) proxies, err := f.parser(buf)
if err != nil { if err != nil {
return getZero[V](), false, err return nil, false, err
} }
if f.vehicle.Type() != types.File { if f.vehicle.Type() != types.File {
if err := safeWrite(f.vehicle.Path(), buf); err != nil { if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return getZero[V](), false, err return nil, false, err
} }
} }
@ -126,14 +122,14 @@ func (f *fetcher[V]) Update() (V, bool, error) {
return proxies, false, nil return proxies, false, nil
} }
func (f *fetcher[V]) Destroy() error { func (f *fetcher) Destroy() error {
if f.ticker != nil { if f.ticker != nil {
f.done <- struct{}{} f.done <- struct{}{}
} }
return nil return nil
} }
func (f *fetcher[V]) pullLoop() { func (f *fetcher) pullLoop() {
for { for {
select { select {
case <-f.ticker.C: case <-f.ticker.C:
@ -171,25 +167,13 @@ func safeWrite(path string, buf []byte) error {
return os.WriteFile(path, buf, fileMode) return os.WriteFile(path, buf, fileMode)
} }
func removeComment(buf []byte) []byte { func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(interface{})) *fetcher {
arr := commentRegx.FindAllSubmatch(buf, -1)
for _, subs := range arr {
sub := subs[0]
if !bytes.HasPrefix(bytes.TrimLeft(sub, " "), []byte("#")) {
continue
}
buf = bytes.Replace(buf, sub, []byte(""), 1)
}
return buf
}
func newFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser parser[V], onUpdate func(V)) *fetcher[V] {
var ticker *time.Ticker var ticker *time.Ticker
if interval != 0 { if interval != 0 {
ticker = time.NewTicker(interval) ticker = time.NewTicker(interval)
} }
return &fetcher[V]{ return &fetcher{
name: name, name: name,
ticker: ticker, ticker: ticker,
vehicle: vehicle, vehicle: vehicle,
@ -198,8 +182,3 @@ func newFetcher[V any](name string, interval time.Duration, vehicle types.Vehicl
onUpdate: onUpdate, onUpdate: onUpdate,
} }
} }
func getZero[V any]() V {
var result V
return result
}

View File

@ -25,25 +25,13 @@ type HealthCheck struct {
interval uint interval uint
lazy bool lazy bool
lastTouch *atomic.Int64 lastTouch *atomic.Int64
running *atomic.Bool
done chan struct{} done chan struct{}
} }
func (hc *HealthCheck) process() { func (hc *HealthCheck) process() {
if hc.running.Load() {
return
}
hc.running.Store(true)
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second) ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
go func() { go hc.check()
t := time.NewTicker(30 * time.Second)
<-t.C
t.Stop()
hc.check()
}()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
@ -71,29 +59,20 @@ func (hc *HealthCheck) touch() {
} }
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
proxies := hc.proxies b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
if len(proxies) == 0 { for _, proxy := range hc.proxies {
return
}
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
for _, proxy := range proxies {
p := proxy p := proxy
b.Go(p.Name(), func() (bool, error) { b.Go(p.Name(), func() (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel() defer cancel()
_, _ = p.URLTest(ctx, hc.url) p.URLTest(ctx, hc.url)
return false, nil return nil, nil
}) })
} }
b.Wait() b.Wait()
} }
func (hc *HealthCheck) close() { func (hc *HealthCheck) close() {
if !hc.running.Load() {
return
}
hc.running.Store(false)
hc.done <- struct{}{} hc.done <- struct{}{}
} }
@ -104,7 +83,6 @@ func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *He
interval: interval, interval: interval,
lazy: lazy, lazy: lazy,
lastTouch: atomic.NewInt64(0), lastTouch: atomic.NewInt64(0),
running: atomic.NewBool(false),
done: make(chan struct{}, 1), done: make(chan struct{}, 1),
} }
} }

View File

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

View File

@ -9,11 +9,10 @@ import (
"time" "time"
"github.com/Dreamacro/clash/adapter" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/common/convert"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v2"
) )
const ( const (
@ -21,23 +20,22 @@ const (
) )
type ProxySchema struct { type ProxySchema struct {
Proxies []map[string]any `yaml:"proxies"` Proxies []map[string]interface{} `yaml:"proxies"`
} }
// ProxySetProvider for auto gc // for auto gc
type ProxySetProvider struct { type ProxySetProvider struct {
*proxySetProvider *proxySetProvider
} }
type proxySetProvider struct { type proxySetProvider struct {
*fetcher[[]C.Proxy] *fetcher
proxies []C.Proxy proxies []C.Proxy
healthCheck *HealthCheck healthCheck *HealthCheck
providersInUse []types.ProxyProvider
} }
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{ return json.Marshal(map[string]interface{}{
"name": pp.Name(), "name": pp.Name(),
"type": pp.Type().String(), "type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(), "vehicleType": pp.VehicleType().String(),
@ -88,22 +86,17 @@ func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies pp.proxies = proxies
pp.healthCheck.setProxy(proxies) pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() {
for _, use := range pp.providersInUse { go pp.healthCheck.check()
_ = use.Update()
} }
} }
func (pp *proxySetProvider) RegisterProvidersInUse(providers ...types.ProxyProvider) {
pp.providersInUse = append(pp.providersInUse, providers...)
}
func stopProxyProvider(pd *ProxySetProvider) { func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close() pd.healthCheck.close()
_ = pd.fetcher.Destroy() pd.fetcher.Destroy()
} }
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck, forceCertVerify bool, prefixName string) (*ProxySetProvider, error) { func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
filterReg, err := regexp.Compile(filter) filterReg, err := regexp.Compile(filter)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err) return nil, fmt.Errorf("invalid filter regex: %w", err)
@ -118,7 +111,45 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
healthCheck: hc, healthCheck: hc,
} }
fetcher := newFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, filterReg, forceCertVerify, prefixName), proxiesOnUpdate(pd)) onUpdate := func(elm interface{}) {
ret := elm.([]C.Proxy)
pd.setProxies(ret)
}
proxiesParseAndFilter := func(buf []byte) (interface{}, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) {
continue
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
pd.fetcher = fetcher pd.fetcher = fetcher
wrapper := &ProxySetProvider{pd} wrapper := &ProxySetProvider{pd}
@ -126,7 +157,7 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
return wrapper, nil return wrapper, nil
} }
// CompatibleProvider for auto gc // for auto gc
type CompatibleProvider struct { type CompatibleProvider struct {
*compatibleProvider *compatibleProvider
} }
@ -138,7 +169,7 @@ type compatibleProvider struct {
} }
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{ return json.Marshal(map[string]interface{}{
"name": cp.Name(), "name": cp.Name(),
"type": cp.Type().String(), "type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(), "vehicleType": cp.VehicleType().String(),
@ -202,148 +233,3 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
runtime.SetFinalizer(wrapper, stopCompatibleProvider) runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil return wrapper, nil
} }
// 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 {
pf.healthCheck.close()
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)
if len(proxies) != 0 && pf.healthCheck.auto() {
go pf.healthCheck.process()
}
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()
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 {
name, ok := mapping["name"].(string)
if ok && len(filter) > 0 && !filterReg.MatchString(name) {
continue
}
if prefixName != "" {
mapping["name"] = prefixName + name
}
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,7 @@ import (
"os" "os"
"time" "time"
"github.com/Dreamacro/clash/common/convert" netHttp "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
) )
@ -35,9 +35,8 @@ func NewFileVehicle(path string) *FileVehicle {
} }
type HTTPVehicle struct { type HTTPVehicle struct {
url string url string
path string path string
header http.Header
} }
func (h *HTTPVehicle) Type() types.VehicleType { func (h *HTTPVehicle) Type() types.VehicleType {
@ -58,21 +57,17 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
} }
req, err := http.NewRequest(http.MethodGet, uri.String(), nil) req, err := http.NewRequest(http.MethodGet, uri.String(), nil)
req.Header.Set("user-agent", netHttp.UA)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if h.header != nil {
req.Header = h.header
}
if user := uri.User; user != nil { if user := uri.User; user != nil {
password, _ := user.Password() password, _ := user.Password()
req.SetBasicAuth(user.Username(), password) req.SetBasicAuth(user.Username(), password)
} }
convert.SetUserAgent(req.Header)
req = req.WithContext(ctx) req = req.WithContext(ctx)
transport := &http.Transport{ transport := &http.Transport{
@ -82,10 +77,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
if req.URL.Scheme == "https" { return dialer.DialContext(ctx, network, address)
return (&net.Dialer{}).DialContext(ctx, network, address) // forward to tun if tun enabled
}
return dialer.DialContext(ctx, network, address, dialer.WithDirect()) // with direct
}, },
} }
@ -94,18 +86,16 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() { defer resp.Body.Close()
_ = resp.Body.Close()
}()
buf, err := io.ReadAll(resp.Body) buf, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return removeComment(buf), nil return buf, nil
} }
func NewHTTPVehicle(url string, path string, header http.Header) *HTTPVehicle { func NewHTTPVehicle(url string, path string) *HTTPVehicle {
return &HTTPVehicle{url, path, header} return &HTTPVehicle{url, path}
} }

View File

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

View File

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

52
common/cache/cache.go vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -1,303 +0,0 @@
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
}

View File

@ -1,104 +0,0 @@
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)
}

View File

@ -1,39 +0,0 @@
package cert
import (
"crypto/tls"
"sync"
"github.com/Dreamacro/clash/component/trie"
)
// DomainTrieCertsStorage cache wildcard certificates
type DomainTrieCertsStorage struct {
certsCache *trie.DomainTrie[*tls.Certificate]
lock sync.RWMutex
}
// Get gets the certificate from the storage
func (c *DomainTrieCertsStorage) Get(key string) (*tls.Certificate, bool) {
c.lock.RLock()
defer c.lock.RUnlock()
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.lock.Lock()
_ = c.certsCache.Insert(key, cert)
c.lock.Unlock()
}
func NewDomainTrieCertsStorage() *DomainTrieCertsStorage {
return &DomainTrieCertsStorage{
certsCache: trie.New[*tls.Certificate](),
}
}

View File

@ -1,35 +0,0 @@
package cmd
import (
"fmt"
"os/exec"
"strings"
)
func ExecCmd(cmdStr string) (string, error) {
args := splitArgs(cmdStr)
var cmd *exec.Cmd
if len(args) == 1 {
cmd = exec.Command(args[0])
} else {
cmd = exec.Command(args[0], args[1:]...)
}
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v, %s", err, string(out))
}
return string(out), nil
}
func splitArgs(cmd string) []string {
args := strings.Split(cmd, " ")
// use in pipeline
if len(args) > 2 && strings.ContainsAny(cmd, "|") {
suffix := strings.Join(args[2:], " ")
args = append(args[:2], suffix)
}
return args
}

View File

@ -1,40 +0,0 @@
package cmd
import (
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSplitArgs(t *testing.T) {
args := splitArgs("ls")
args1 := splitArgs("ls -la")
args2 := splitArgs("bash -c ls")
args3 := splitArgs("bash -c ls -lahF | grep 'cmd'")
assert.Equal(t, 1, len(args))
assert.Equal(t, 2, len(args1))
assert.Equal(t, 3, len(args2))
assert.Equal(t, 3, len(args3))
}
func TestExecCmd(t *testing.T) {
if runtime.GOOS == "windows" {
_, err := ExecCmd("dir")
assert.Nil(t, err)
return
}
_, err := ExecCmd("ls")
_, err1 := ExecCmd("ls -la")
_, err2 := ExecCmd("bash -c ls")
_, err3 := ExecCmd("bash -c ls -la")
_, err4 := ExecCmd("bash -c ls -la | grep 'cmd'")
assert.Nil(t, err)
assert.Nil(t, err1)
assert.Nil(t, err2)
assert.Nil(t, err3)
assert.Nil(t, err4)
}

View File

@ -1,347 +0,0 @@
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
}
func DecodeRawBase64(buf []byte) ([]byte, error) {
dBuf := make([]byte, base64.RawStdEncoding.DecodedLen(len(buf)))
n, err := base64.RawStdEncoding.Decode(dBuf, buf)
if err != nil {
return nil, err
}
return dBuf[:n], 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, err = DecodeRawBase64(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
}

View File

@ -1,315 +0,0 @@
package convert
import (
"encoding/base64"
"math/rand"
"net/http"
"strings"
"github.com/gofrs/uuid"
)
var hostsSuffix = []string{
"-cdn.aliyuncs.com",
".alicdn.com",
".pan.baidu.com",
".tbcache.com",
".aliyuncdn.com",
".vod.miguvideo.com",
".cibntv.net",
".myqcloud.com",
".smtcdns.com",
".alikunlun.com",
".smtcdns.net",
".apcdns.net",
".cdn-go.cn",
".cdntip.com",
".cdntips.com",
".alidayu.com",
".alidns.com",
".cdngslb.com",
".mxhichina.com",
}
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

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

5
common/net/http.go Normal file
View File

@ -0,0 +1,5 @@
package net
const (
UA = "Clash"
)

View File

@ -1,33 +0,0 @@
package net
import (
"io"
"net"
"time"
)
// Relay copies between left and right bidirectionally.
func Relay(leftConn, rightConn net.Conn) {
ch := make(chan error)
tcpKeepAlive(leftConn)
tcpKeepAlive(rightConn)
go func() {
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
// See also https://github.com/Dreamacro/clash/pull/1209
_, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn})
_ = leftConn.SetReadDeadline(time.Now())
ch <- err
}()
_, _ = io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn})
_ = rightConn.SetReadDeadline(time.Now())
<-ch
}
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ func NewAllocator() *Allocator {
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
for k := range alloc.buffers { for k := range alloc.buffers {
i := k i := k
alloc.buffers[k].New = func() any { alloc.buffers[k].New = func() interface{} {
return make([]byte, 1<<uint32(i)) return make([]byte, 1<<uint32(i))
} }
} }
@ -52,8 +52,8 @@ func (alloc *Allocator) Put(buf []byte) error {
return errors.New("allocator Put() incorrect buffer size") return errors.New("allocator Put() incorrect buffer size")
} }
//nolint
//lint:ignore SA6002 ignore temporarily //lint:ignore SA6002 ignore temporarily
//nolint
alloc.buffers[bits].Put(buf) alloc.buffers[bits].Put(buf)
return nil return nil
} }

View File

@ -5,7 +5,7 @@ import (
"sync" "sync"
) )
var bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }} var bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
func GetBuffer() *bytes.Buffer { func GetBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer) return bufferPool.Get().(*bytes.Buffer)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,159 +0,0 @@
package tls
import (
"testing"
)
func TestTLSHeaders(t *testing.T) {
cases := []struct {
input []byte
domain string
err bool
}{
{
input: []byte{
0x16, 0x03, 0x01, 0x00, 0xc8, 0x01, 0x00, 0x00,
0xc4, 0x03, 0x03, 0x1a, 0xac, 0xb2, 0xa8, 0xfe,
0xb4, 0x96, 0x04, 0x5b, 0xca, 0xf7, 0xc1, 0xf4,
0x2e, 0x53, 0x24, 0x6e, 0x34, 0x0c, 0x58, 0x36,
0x71, 0x97, 0x59, 0xe9, 0x41, 0x66, 0xe2, 0x43,
0xa0, 0x13, 0xb6, 0x00, 0x00, 0x20, 0x1a, 0x1a,
0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30,
0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13,
0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d,
0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00,
0x00, 0x7b, 0xba, 0xba, 0x00, 0x00, 0xff, 0x01,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00,
0x14, 0x00, 0x00, 0x11, 0x63, 0x2e, 0x73, 0x2d,
0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66,
0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00,
0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00,
0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04,
0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08,
0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00,
0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12,
0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c,
0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70,
0x2f, 0x31, 0x2e, 0x31, 0x00, 0x0b, 0x00, 0x02,
0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08,
0xaa, 0xaa, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18,
0xaa, 0xaa, 0x00, 0x01, 0x00,
},
domain: "c.s-microsoft.com",
err: false,
},
{
input: []byte{
0x16, 0x03, 0x01, 0x00, 0xee, 0x01, 0x00, 0x00,
0xea, 0x03, 0x03, 0xe7, 0x91, 0x9e, 0x93, 0xca,
0x78, 0x1b, 0x3c, 0xe0, 0x65, 0x25, 0x58, 0xb5,
0x93, 0xe1, 0x0f, 0x85, 0xec, 0x9a, 0x66, 0x8e,
0x61, 0x82, 0x88, 0xc8, 0xfc, 0xae, 0x1e, 0xca,
0xd7, 0xa5, 0x63, 0x20, 0xbd, 0x1c, 0x00, 0x00,
0x8b, 0xee, 0x09, 0xe3, 0x47, 0x6a, 0x0e, 0x74,
0xb0, 0xbc, 0xa3, 0x02, 0xa7, 0x35, 0xe8, 0x85,
0x70, 0x7c, 0x7a, 0xf0, 0x00, 0xdf, 0x4a, 0xea,
0x87, 0x01, 0x14, 0x91, 0x00, 0x20, 0xea, 0xea,
0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30,
0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13,
0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d,
0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00,
0x00, 0x81, 0x9a, 0x9a, 0x00, 0x00, 0xff, 0x01,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00,
0x16, 0x00, 0x00, 0x13, 0x77, 0x77, 0x77, 0x30,
0x37, 0x2e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x74,
0x61, 0x6c, 0x65, 0x2e, 0x6e, 0x65, 0x74, 0x00,
0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00,
0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08,
0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05,
0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00,
0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e,
0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74,
0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x75, 0x50,
0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00,
0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x9a, 0x9a,
0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x8a, 0x8a,
0x00, 0x01, 0x00,
},
domain: "www07.clicktale.net",
err: false,
},
{
input: []byte{
0x16, 0x03, 0x01, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe2, 0x03, 0x03, 0x81, 0x47, 0xc1,
0x66, 0xd5, 0x1b, 0xfa, 0x4b, 0xb5, 0xe0, 0x2a, 0xe1, 0xa7, 0x87, 0x13, 0x1d, 0x11, 0xaa, 0xc6,
0xce, 0xfc, 0x7f, 0xab, 0x94, 0xc8, 0x62, 0xad, 0xc8, 0xab, 0x0c, 0xdd, 0xcb, 0x20, 0x6f, 0x9d,
0x07, 0xf1, 0x95, 0x3e, 0x99, 0xd8, 0xf3, 0x6d, 0x97, 0xee, 0x19, 0x0b, 0x06, 0x1b, 0xf4, 0x84,
0x0b, 0xb6, 0x8f, 0xcc, 0xde, 0xe2, 0xd0, 0x2d, 0x6b, 0x0c, 0x1f, 0x52, 0x53, 0x13, 0x00, 0x08,
0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c,
0x00, 0x0a, 0x00, 0x00, 0x07, 0x64, 0x6f, 0x67, 0x66, 0x69, 0x73, 0x68, 0x00, 0x0b, 0x00, 0x04,
0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e,
0x00, 0x19, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00,
0x00, 0x0d, 0x00, 0x1e, 0x00, 0x1c, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08,
0x08, 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01,
0x06, 0x01, 0x00, 0x2b, 0x00, 0x07, 0x06, 0x7f, 0x1c, 0x7f, 0x1b, 0x7f, 0x1a, 0x00, 0x2d, 0x00,
0x02, 0x01, 0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2f, 0x35, 0x0c,
0xb6, 0x90, 0x0a, 0xb7, 0xd5, 0xc4, 0x1b, 0x2f, 0x60, 0xaa, 0x56, 0x7b, 0x3f, 0x71, 0xc8, 0x01,
0x7e, 0x86, 0xd3, 0xb7, 0x0c, 0x29, 0x1a, 0x9e, 0x5b, 0x38, 0x3f, 0x01, 0x72,
},
domain: "dogfish",
err: false,
},
{
input: []byte{
0x16, 0x03, 0x01, 0x01, 0x03, 0x01, 0x00, 0x00,
0xff, 0x03, 0x03, 0x3d, 0x89, 0x52, 0x9e, 0xee,
0xbe, 0x17, 0x63, 0x75, 0xef, 0x29, 0xbd, 0x14,
0x6a, 0x49, 0xe0, 0x2c, 0x37, 0x57, 0x71, 0x62,
0x82, 0x44, 0x94, 0x8f, 0x6e, 0x94, 0x08, 0x45,
0x7f, 0xdb, 0xc1, 0x00, 0x00, 0x3e, 0xc0, 0x2c,
0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8,
0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, 0x00, 0x9e,
0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23,
0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14,
0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33,
0x00, 0x9d, 0x00, 0x9c, 0x13, 0x02, 0x13, 0x03,
0x13, 0x01, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35,
0x00, 0x2f, 0x00, 0xff, 0x01, 0x00, 0x00, 0x98,
0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00,
0x0b, 0x31, 0x30, 0x2e, 0x34, 0x32, 0x2e, 0x30,
0x2e, 0x32, 0x34, 0x33, 0x00, 0x0b, 0x00, 0x04,
0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0a,
0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19,
0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d,
0x00, 0x20, 0x00, 0x1e, 0x04, 0x03, 0x05, 0x03,
0x06, 0x03, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06,
0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03,
0x02, 0x01, 0x02, 0x02, 0x04, 0x02, 0x05, 0x02,
0x06, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17,
0x00, 0x00, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x7f,
0x14, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00,
0x2d, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x28,
0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20,
0x13, 0x7c, 0x6e, 0x97, 0xc4, 0xfd, 0x09, 0x2e,
0x70, 0x2f, 0x73, 0x5a, 0x9b, 0x57, 0x4d, 0x5f,
0x2b, 0x73, 0x2c, 0xa5, 0x4a, 0x98, 0x40, 0x3d,
0x75, 0x6e, 0xb4, 0x76, 0xf9, 0x48, 0x8f, 0x36,
},
domain: "10.42.0.243",
err: false,
},
}
for _, test := range cases {
header, err := SniffTLS(test.input)
if test.err {
if err == nil {
t.Errorf("Exepct error but nil in test %v", test)
}
} else {
if err != nil {
t.Errorf("Expect no error but actually %s in test %v", err.Error(), test)
}
if header.Domain() != test.domain {
t.Error("expect domain ", test.domain, " but got ", header.Domain())
}
}
}
}

View File

@ -1,4 +1,5 @@
//go:build !linux //go:build !linux
// +build !linux
package sockopt package sockopt

View File

@ -28,8 +28,8 @@ func NewDecoder(option Option) *Decoder {
return &Decoder{option: &option} return &Decoder{option: &option}
} }
// Decode transform a map[string]any to a struct // Decode transform a map[string]interface{} to a struct
func (d *Decoder) Decode(src map[string]any, dst any) error { func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr { if reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("Decode must recive a ptr struct") return fmt.Errorf("Decode must recive a ptr struct")
} }
@ -45,8 +45,12 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
} }
tag := field.Tag.Get(d.option.TagName) tag := field.Tag.Get(d.option.TagName)
key, omitKey, found := strings.Cut(tag, ",") str := strings.SplitN(tag, ",", 2)
omitempty := found && omitKey == "omitempty" key := str[0]
omitempty := false
if len(str) > 1 {
omitempty = str[1] == "omitempty"
}
value, ok := src[key] value, ok := src[key]
if !ok || value == nil { if !ok || value == nil {
@ -64,7 +68,7 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
return nil return nil
} }
func (d *Decoder) decode(name string, data any, val reflect.Value) error { func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error {
switch val.Kind() { switch val.Kind() {
case reflect.Int: case reflect.Int:
return d.decodeInt(name, data, val) return d.decodeInt(name, data, val)
@ -85,14 +89,12 @@ func (d *Decoder) decode(name string, data any, val reflect.Value) error {
} }
} }
func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) { func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
case kind == reflect.Int: case kind == reflect.Int:
val.SetInt(dataVal.Int()) val.SetInt(dataVal.Int())
case kind == reflect.Float64 && d.option.WeaklyTypedInput:
val.SetInt(int64(dataVal.Float()))
case kind == reflect.String && d.option.WeaklyTypedInput: case kind == reflect.String && d.option.WeaklyTypedInput:
var i int64 var i int64
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits()) i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
@ -110,7 +112,7 @@ func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error
return err return err
} }
func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) { func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
@ -127,7 +129,7 @@ func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err er
return err return err
} }
func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err error) { func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
@ -144,7 +146,7 @@ func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err erro
return err return err
} }
func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error { func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data)) dataVal := reflect.Indirect(reflect.ValueOf(data))
valType := val.Type() valType := val.Type()
valElemType := valType.Elem() valElemType := valType.Elem()
@ -159,19 +161,9 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
for valSlice.Len() <= i { for valSlice.Len() <= i {
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
} }
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) currentField := valSlice.Index(i)
fieldName := fmt.Sprintf("%s[%d]", name, i)
if err := d.decode(fieldName, currentData, currentField); err != nil { if err := d.decode(fieldName, currentData, currentField); err != nil {
return err return err
} }
@ -181,7 +173,7 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
return nil return nil
} }
func (d *Decoder) decodeMap(name string, data any, val reflect.Value) error { func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error {
valType := val.Type() valType := val.Type()
valKeyType := valType.Key() valKeyType := valType.Key()
valElemType := valType.Elem() valElemType := valType.Elem()
@ -253,7 +245,7 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle
return nil return nil
} }
func (d *Decoder) decodeStruct(name string, data any, val reflect.Value) error { func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data)) dataVal := reflect.Indirect(reflect.ValueOf(data))
// If the type of the value to write to and the data match directly, // If the type of the value to write to and the data match directly,
@ -281,7 +273,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
} }
dataValKeys := make(map[reflect.Value]struct{}) dataValKeys := make(map[reflect.Value]struct{})
dataValKeysUnused := make(map[any]struct{}) dataValKeysUnused := make(map[interface{}]struct{})
for _, dataValKey := range dataVal.MapKeys() { for _, dataValKey := range dataVal.MapKeys() {
dataValKeys[dataValKey] = struct{}{} dataValKeys[dataValKey] = struct{}{}
dataValKeysUnused[dataValKey.Interface()] = struct{}{} dataValKeysUnused[dataValKey.Interface()] = struct{}{}
@ -406,7 +398,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
return nil return nil
} }
func (d *Decoder) setInterface(name string, data any, val reflect.Value) (err error) { func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
val.Set(dataVal) val.Set(dataVal)
return nil return nil

View File

@ -27,7 +27,7 @@ type BazOptional struct {
} }
func TestStructure_Basic(t *testing.T) { func TestStructure_Basic(t *testing.T) {
rawMap := map[string]any{ rawMap := map[string]interface{}{
"foo": 1, "foo": 1,
"bar": "test", "bar": "test",
"extra": false, "extra": false,
@ -45,7 +45,7 @@ func TestStructure_Basic(t *testing.T) {
} }
func TestStructure_Slice(t *testing.T) { func TestStructure_Slice(t *testing.T) {
rawMap := map[string]any{ rawMap := map[string]interface{}{
"foo": 1, "foo": 1,
"bar": []string{"one", "two"}, "bar": []string{"one", "two"},
} }
@ -62,7 +62,7 @@ func TestStructure_Slice(t *testing.T) {
} }
func TestStructure_Optional(t *testing.T) { func TestStructure_Optional(t *testing.T) {
rawMap := map[string]any{ rawMap := map[string]interface{}{
"foo": 1, "foo": 1,
} }
@ -77,7 +77,7 @@ func TestStructure_Optional(t *testing.T) {
} }
func TestStructure_MissingKey(t *testing.T) { func TestStructure_MissingKey(t *testing.T) {
rawMap := map[string]any{ rawMap := map[string]interface{}{
"foo": 1, "foo": 1,
} }
@ -87,14 +87,14 @@ func TestStructure_MissingKey(t *testing.T) {
} }
func TestStructure_ParamError(t *testing.T) { func TestStructure_ParamError(t *testing.T) {
rawMap := map[string]any{} rawMap := map[string]interface{}{}
s := Baz{} s := Baz{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
assert.NotNilf(t, err, "should throw error: %#v", s) assert.NotNilf(t, err, "should throw error: %#v", s)
} }
func TestStructure_SliceTypeError(t *testing.T) { func TestStructure_SliceTypeError(t *testing.T) {
rawMap := map[string]any{ rawMap := map[string]interface{}{
"foo": 1, "foo": 1,
"bar": []int{1, 2}, "bar": []int{1, 2},
} }
@ -105,7 +105,7 @@ func TestStructure_SliceTypeError(t *testing.T) {
} }
func TestStructure_WeakType(t *testing.T) { func TestStructure_WeakType(t *testing.T) {
rawMap := map[string]any{ rawMap := map[string]interface{}{
"foo": "1", "foo": "1",
"bar": []int{1}, "bar": []int{1},
} }
@ -122,7 +122,7 @@ func TestStructure_WeakType(t *testing.T) {
} }
func TestStructure_Nest(t *testing.T) { func TestStructure_Nest(t *testing.T) {
rawMap := map[string]any{ rawMap := map[string]interface{}{
"foo": 1, "foo": 1,
} }
@ -137,45 +137,3 @@ func TestStructure_Nest(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, s.BazOptional, goal) assert.Equal(t, s.BazOptional, goal)
} }
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
"bar": []any{"bar", nil},
}
goal := &BazSlice{
Foo: 1,
Bar: []string{"bar", ""},
}
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal.Bar, s.Bar)
s = &BazSlice{}
err = decoder.Decode(rawMap, s)
assert.NotNil(t, err)
}
func TestStructure_SliceNilValueComplex(t *testing.T) {
rawMap := map[string]any{
"bar": []any{map[string]any{"bar": "foo"}, nil},
}
s := &struct {
Bar []map[string]any `test:"bar"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Nil(t, s.Bar[1])
ss := &struct {
Bar []Baz `test:"bar"`
}{}
err = decoder.Decode(rawMap, ss)
assert.NotNil(t, err)
}

View File

@ -36,7 +36,7 @@ func NewAuthenticator(users []AuthUser) Authenticator {
au.storage.Store(user.User, user.Pass) au.storage.Store(user.User, user.Pass)
} }
usernames := make([]string, 0, len(users)) usernames := make([]string, 0, len(users))
au.storage.Range(func(key, value any) bool { au.storage.Range(func(key, value interface{}) bool {
usernames = append(usernames, key.(string)) usernames = append(usernames, key.(string))
return true return true
}) })

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"net" "net"
"net/netip"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
) )
@ -12,7 +11,6 @@ import (
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) { func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
opt := &option{ opt := &option{
interfaceName: DefaultInterface.Load(), interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
} }
for _, o := range DefaultOptions { for _, o := range DefaultOptions {
@ -30,17 +28,17 @@ func DialContext(ctx context.Context, network, address string, options ...Option
return nil, err return nil, err
} }
var ip netip.Addr var ip net.IP
switch network { switch network {
case "tcp4", "udp4": case "tcp4", "udp4":
if !opt.direct { if opt.interfaceName != "" {
ip, err = resolver.ResolveIPv4ProxyServerHost(host) ip, err = resolver.ResolveIPv4WithMain(host)
} else { } else {
ip, err = resolver.ResolveIPv4(host) ip, err = resolver.ResolveIPv4(host)
} }
default: default:
if !opt.direct { if opt.interfaceName != "" {
ip, err = resolver.ResolveIPv6ProxyServerHost(host) ip, err = resolver.ResolveIPv6WithMain(host)
} else { } else {
ip, err = resolver.ResolveIPv6(host) ip, err = resolver.ResolveIPv6(host)
} }
@ -60,7 +58,6 @@ func DialContext(ctx context.Context, network, address string, options ...Option
func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) { func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
cfg := &option{ cfg := &option{
interfaceName: DefaultInterface.Load(), interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
} }
for _, o := range DefaultOptions { for _, o := range DefaultOptions {
@ -89,7 +86,7 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
return lc.ListenPacket(ctx, network, address) return lc.ListenPacket(ctx, network, address)
} }
func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) { func dialContext(ctx context.Context, network string, destination net.IP, port string, opt *option) (net.Conn, error) {
dialer := &net.Dialer{} dialer := &net.Dialer{}
if opt.interfaceName != "" { if opt.interfaceName != "" {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil { if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
@ -122,28 +119,28 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
results := make(chan dialResult) results := make(chan dialResult)
var primary, fallback dialResult var primary, fallback dialResult
startRacer := func(ctx context.Context, network, host string, direct bool, ipv6 bool) { startRacer := func(ctx context.Context, network, host string, ipv6 bool) {
result := dialResult{ipv6: ipv6, done: true} result := dialResult{ipv6: ipv6, done: true}
defer func() { defer func() {
select { select {
case results <- result: case results <- result:
case <-returned: case <-returned:
if result.Conn != nil { if result.Conn != nil {
_ = result.Conn.Close() result.Conn.Close()
} }
} }
}() }()
var ip netip.Addr var ip net.IP
if ipv6 { if ipv6 {
if !direct { if opt.interfaceName != "" {
ip, result.error = resolver.ResolveIPv6ProxyServerHost(host) ip, result.error = resolver.ResolveIPv6WithMain(host)
} else { } else {
ip, result.error = resolver.ResolveIPv6(host) ip, result.error = resolver.ResolveIPv6(host)
} }
} else { } else {
if !direct { if opt.interfaceName != "" {
ip, result.error = resolver.ResolveIPv4ProxyServerHost(host) ip, result.error = resolver.ResolveIPv4WithMain(host)
} else { } else {
ip, result.error = resolver.ResolveIPv4(host) ip, result.error = resolver.ResolveIPv4(host)
} }
@ -156,8 +153,8 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
result.Conn, result.error = dialContext(ctx, network, ip, port, opt) result.Conn, result.error = dialContext(ctx, network, ip, port, opt)
} }
go startRacer(ctx, network+"4", host, opt.direct, false) go startRacer(ctx, network+"4", host, false)
go startRacer(ctx, network+"6", host, opt.direct, true) go startRacer(ctx, network+"6", host, true)
for res := range results { for res := range results {
if res.error == nil { if res.error == nil {

View File

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

View File

@ -1,10 +1,10 @@
//go:build !linux //go:build !linux
// +build !linux
package dialer package dialer
import ( import (
"net" "net"
"net/netip"
"sync" "sync"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
@ -18,10 +18,10 @@ func printMarkWarn() {
}) })
} }
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) { func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
printMarkWarn() printMarkWarn()
} }
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) { func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
printMarkWarn() printMarkWarn()
} }

View File

@ -3,16 +3,14 @@ package dialer
import "go.uber.org/atomic" import "go.uber.org/atomic"
var ( var (
DefaultOptions []Option DefaultOptions []Option
DefaultInterface = atomic.NewString("") DefaultInterface = atomic.NewString("")
DefaultRoutingMark = atomic.NewInt32(0)
) )
type option struct { type option struct {
interfaceName string interfaceName string
addrReuse bool addrReuse bool
routingMark int routingMark int
direct bool
} }
type Option func(opt *option) type Option func(opt *option)
@ -34,9 +32,3 @@ func WithRoutingMark(mark int) Option {
opt.routingMark = mark opt.routingMark = mark
} }
} }
func WithDirect() Option {
return func(opt *option) {
opt.direct = true
}
}

View File

@ -1,29 +0,0 @@
package dialer
import (
"context"
"net"
"net/netip"
)
func init() {
// We must use this DialContext to query DNS
// when using net default resolver.
net.DefaultResolver.PreferGo = true
net.DefaultResolver.Dial = resolverDialContext
}
func resolverDialContext(ctx context.Context, network, address string) (net.Conn, error) {
d := &net.Dialer{}
interfaceName := DefaultInterface.Load()
if interfaceName != "" {
dstIP, err := netip.ParseAddr(address)
if err == nil {
_ = bindIfaceToDialer(interfaceName, d, network, dstIP)
}
}
return d.DialContext(ctx, network, address)
}

View File

@ -1,4 +1,5 @@
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows //go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows
package dialer package dialer

View File

@ -1,4 +1,5 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
package dialer package dialer

View File

@ -1,7 +1,7 @@
package fakeip package fakeip
import ( import (
"net/netip" "net"
"github.com/Dreamacro/clash/component/profile/cachefile" "github.com/Dreamacro/clash/component/profile/cachefile"
) )
@ -11,27 +11,22 @@ type cachefileStore struct {
} }
// GetByHost implements store.GetByHost // GetByHost implements store.GetByHost
func (c *cachefileStore) GetByHost(host string) (netip.Addr, bool) { func (c *cachefileStore) GetByHost(host string) (net.IP, bool) {
elm := c.cache.GetFakeip([]byte(host)) elm := c.cache.GetFakeip([]byte(host))
if elm == nil { if elm == nil {
return netip.Addr{}, false return nil, 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 // PutByHost implements store.PutByHost
func (c *cachefileStore) PutByHost(host string, ip netip.Addr) { func (c *cachefileStore) PutByHost(host string, ip net.IP) {
c.cache.PutFakeip([]byte(host), ip.AsSlice()) c.cache.PutFakeip([]byte(host), ip)
} }
// GetByIP implements store.GetByIP // GetByIP implements store.GetByIP
func (c *cachefileStore) GetByIP(ip netip.Addr) (string, bool) { func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
elm := c.cache.GetFakeip(ip.AsSlice()) elm := c.cache.GetFakeip(ip.To4())
if elm == nil { if elm == nil {
return "", false return "", false
} }
@ -39,18 +34,18 @@ func (c *cachefileStore) GetByIP(ip netip.Addr) (string, bool) {
} }
// PutByIP implements store.PutByIP // PutByIP implements store.PutByIP
func (c *cachefileStore) PutByIP(ip netip.Addr, host string) { func (c *cachefileStore) PutByIP(ip net.IP, host string) {
c.cache.PutFakeip(ip.AsSlice(), []byte(host)) c.cache.PutFakeip(ip.To4(), []byte(host))
} }
// DelByIP implements store.DelByIP // DelByIP implements store.DelByIP
func (c *cachefileStore) DelByIP(ip netip.Addr) { func (c *cachefileStore) DelByIP(ip net.IP) {
addr := ip.AsSlice() ip = ip.To4()
c.cache.DelFakeipPair(addr, c.cache.GetFakeip(addr)) c.cache.DelFakeipPair(ip, c.cache.GetFakeip(ip.To4()))
} }
// Exist implements store.Exist // Exist implements store.Exist
func (c *cachefileStore) Exist(ip netip.Addr) bool { func (c *cachefileStore) Exist(ip net.IP) bool {
_, exist := c.GetByIP(ip) _, exist := c.GetByIP(ip)
return exist return exist
} }
@ -58,8 +53,3 @@ func (c *cachefileStore) Exist(ip netip.Addr) bool {
// CloneTo implements store.CloneTo // CloneTo implements store.CloneTo
// already persistence // already persistence
func (c *cachefileStore) CloneTo(store store) {} func (c *cachefileStore) CloneTo(store store) {}
// FlushFakeIP implements store.FlushFakeIP
func (c *cachefileStore) FlushFakeIP() error {
return c.cache.FlushFakeIP()
}

View File

@ -1,37 +1,40 @@
package fakeip package fakeip
import ( import (
"net/netip" "net"
"github.com/Dreamacro/clash/common/cache" "github.com/Dreamacro/clash/common/cache"
) )
type memoryStore struct { type memoryStore struct {
cacheIP *cache.LruCache[string, netip.Addr] cache *cache.LruCache
cacheHost *cache.LruCache[netip.Addr, string]
} }
// GetByHost implements store.GetByHost // GetByHost implements store.GetByHost
func (m *memoryStore) GetByHost(host string) (netip.Addr, bool) { func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
if ip, exist := m.cacheIP.Get(host); exist { if elm, exist := m.cache.Get(host); exist {
ip := elm.(net.IP)
// ensure ip --> host on head of linked list // ensure ip --> host on head of linked list
m.cacheHost.Get(ip) m.cache.Get(ipToUint(ip.To4()))
return ip, true return ip, true
} }
return netip.Addr{}, false return nil, false
} }
// PutByHost implements store.PutByHost // PutByHost implements store.PutByHost
func (m *memoryStore) PutByHost(host string, ip netip.Addr) { func (m *memoryStore) PutByHost(host string, ip net.IP) {
m.cacheIP.Set(host, ip) m.cache.Set(host, ip)
} }
// GetByIP implements store.GetByIP // GetByIP implements store.GetByIP
func (m *memoryStore) GetByIP(ip netip.Addr) (string, bool) { func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
if host, exist := m.cacheHost.Get(ip); exist { if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
host := elm.(string)
// ensure host --> ip on head of linked list // ensure host --> ip on head of linked list
m.cacheIP.Get(host) m.cache.Get(host)
return host, true return host, true
} }
@ -39,41 +42,28 @@ func (m *memoryStore) GetByIP(ip netip.Addr) (string, bool) {
} }
// PutByIP implements store.PutByIP // PutByIP implements store.PutByIP
func (m *memoryStore) PutByIP(ip netip.Addr, host string) { func (m *memoryStore) PutByIP(ip net.IP, host string) {
m.cacheHost.Set(ip, host) m.cache.Set(ipToUint(ip.To4()), host)
} }
// DelByIP implements store.DelByIP // DelByIP implements store.DelByIP
func (m *memoryStore) DelByIP(ip netip.Addr) { func (m *memoryStore) DelByIP(ip net.IP) {
if host, exist := m.cacheHost.Get(ip); exist { ipNum := ipToUint(ip.To4())
m.cacheIP.Delete(host) if elm, exist := m.cache.Get(ipNum); exist {
m.cache.Delete(elm.(string))
} }
m.cacheHost.Delete(ip) m.cache.Delete(ipNum)
} }
// Exist implements store.Exist // Exist implements store.Exist
func (m *memoryStore) Exist(ip netip.Addr) bool { func (m *memoryStore) Exist(ip net.IP) bool {
return m.cacheHost.Exist(ip) return m.cache.Exist(ipToUint(ip.To4()))
} }
// CloneTo implements store.CloneTo // CloneTo implements store.CloneTo
// only for memoryStore to memoryStore // only for memoryStore to memoryStore
func (m *memoryStore) CloneTo(store store) { func (m *memoryStore) CloneTo(store store) {
if ms, ok := store.(*memoryStore); ok { if ms, ok := store.(*memoryStore); ok {
m.cacheIP.CloneTo(ms.cacheIP) m.cache.CloneTo(ms.cache)
m.cacheHost.CloneTo(ms.cacheHost)
}
}
// FlushFakeIP implements store.FlushFakeIP
func (m *memoryStore) FlushFakeIP() error {
_ = 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,45 +2,38 @@ package fakeip
import ( import (
"errors" "errors"
"net/netip" "net"
"sync" "sync"
"github.com/Dreamacro/clash/common/nnip" "github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/profile/cachefile" "github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
) )
const (
offsetKey = "key-offset-fake-ip"
cycleKey = "key-cycle-fake-ip"
)
type store interface { type store interface {
GetByHost(host string) (netip.Addr, bool) GetByHost(host string) (net.IP, bool)
PutByHost(host string, ip netip.Addr) PutByHost(host string, ip net.IP)
GetByIP(ip netip.Addr) (string, bool) GetByIP(ip net.IP) (string, bool)
PutByIP(ip netip.Addr, host string) PutByIP(ip net.IP, host string)
DelByIP(ip netip.Addr) DelByIP(ip net.IP)
Exist(ip netip.Addr) bool Exist(ip net.IP) bool
CloneTo(store) CloneTo(store)
FlushFakeIP() error
} }
// Pool is a implementation about fake ip generator without storage // Pool is a implementation about fake ip generator without storage
type Pool struct { type Pool struct {
gateway netip.Addr max uint32
first netip.Addr min uint32
last netip.Addr gateway uint32
offset netip.Addr offset uint32
cycle bool
mux sync.Mutex mux sync.Mutex
host *trie.DomainTrie[bool] host *trie.DomainTrie
ipnet *netip.Prefix ipnet *net.IPNet
store store store store
} }
// Lookup return a fake ip with host // Lookup return a fake ip with host
func (p *Pool) Lookup(host string) netip.Addr { func (p *Pool) Lookup(host string) net.IP {
p.mux.Lock() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if ip, exist := p.store.GetByHost(host); exist { if ip, exist := p.store.GetByHost(host); exist {
@ -53,10 +46,14 @@ func (p *Pool) Lookup(host string) netip.Addr {
} }
// LookBack return host with the fake ip // LookBack return host with the fake ip
func (p *Pool) LookBack(ip netip.Addr) (string, bool) { func (p *Pool) LookBack(ip net.IP) (string, bool) {
p.mux.Lock() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return "", false
}
return p.store.GetByIP(ip) return p.store.GetByIP(ip)
} }
@ -69,25 +66,24 @@ func (p *Pool) ShouldSkipped(domain string) bool {
} }
// Exist returns if given ip exists in fake-ip pool // Exist returns if given ip exists in fake-ip pool
func (p *Pool) Exist(ip netip.Addr) bool { func (p *Pool) Exist(ip net.IP) bool {
p.mux.Lock() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return false
}
return p.store.Exist(ip) return p.store.Exist(ip)
} }
// Gateway return gateway ip // Gateway return gateway ip
func (p *Pool) Gateway() netip.Addr { func (p *Pool) Gateway() net.IP {
return p.gateway return uintToIP(p.gateway)
}
// Broadcast return the last ip
func (p *Pool) Broadcast() netip.Addr {
return p.last
} }
// IPNet return raw ipnet // IPNet return raw ipnet
func (p *Pool) IPNet() *netip.Prefix { func (p *Pool) IPNet() *net.IPNet {
return p.ipnet return p.ipnet
} }
@ -96,61 +92,43 @@ func (p *Pool) CloneFrom(o *Pool) {
o.store.CloneTo(p.store) o.store.CloneTo(p.store)
} }
func (p *Pool) get(host string) netip.Addr { func (p *Pool) get(host string) net.IP {
p.offset = p.offset.Next() current := p.offset
for {
if !p.offset.Less(p.last) { p.offset = (p.offset + 1) % (p.max - p.min)
p.cycle = true // Avoid infinite loops
p.offset = p.first if p.offset == current {
} p.offset = (p.offset + 1) % (p.max - p.min)
ip := uintToIP(p.min + p.offset - 1)
if p.cycle || p.store.Exist(p.offset) { p.store.DelByIP(ip)
p.store.DelByIP(p.offset) break
}
p.store.PutByIP(p.offset, host)
return p.offset
}
func (p *Pool) FlushFakeIP() error {
err := p.store.FlushFakeIP()
if err == nil {
p.cycle = false
p.offset = p.first.Prev()
}
return err
}
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 (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 { ip := uintToIP(p.min + p.offset - 1)
if p.ipnet.Contains(offset) { if !p.store.Exist(ip) {
p.offset = offset break
} else {
_ = p.FlushFakeIP()
}
} else if s.Exist(p.first) {
_ = p.FlushFakeIP()
} }
} }
ip := uintToIP(p.min + p.offset - 1)
p.store.PutByIP(ip, host)
return ip
}
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 uintToIP(v uint32) net.IP {
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
} }
type Options struct { type Options struct {
IPNet *netip.Prefix IPNet *net.IPNet
Host *trie.DomainTrie[bool] Host *trie.DomainTrie
// Size sets the maximum number of entries in memory // Size sets the maximum number of entries in memory
// and does not work if Persistence is true // and does not work if Persistence is true
@ -163,23 +141,20 @@ type Options struct {
// New return Pool instance // New return Pool instance
func New(options Options) (*Pool, error) { func New(options Options) (*Pool, error) {
var ( min := ipToUint(options.IPNet.IP) + 2
hostAddr = options.IPNet.Masked().Addr()
gateway = hostAddr.Next()
first = gateway.Next().Next()
last = nnip.UnMasked(*options.IPNet)
)
if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) { ones, bits := options.IPNet.Mask.Size()
total := 1<<uint(bits-ones) - 2
if total <= 0 {
return nil, errors.New("ipnet don't have valid ip") return nil, errors.New("ipnet don't have valid ip")
} }
max := min + uint32(total) - 1
pool := &Pool{ pool := &Pool{
gateway: gateway, min: min,
first: first, max: max,
last: last, gateway: min - 1,
offset: first.Prev(),
cycle: false,
host: options.Host, host: options.Host,
ipnet: options.IPNet, ipnet: options.IPNet,
} }
@ -188,10 +163,10 @@ func New(options Options) (*Pool, error) {
cache: cachefile.Cache(), cache: cachefile.Cache(),
} }
} else { } else {
pool.store = newMemoryStore(options.Size) pool.store = &memoryStore{
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
}
} }
pool.restoreState()
return pool, nil return pool, nil
} }

View File

@ -2,7 +2,7 @@ package fakeip
import ( import (
"fmt" "fmt"
"net/netip" "net"
"os" "os"
"testing" "testing"
"time" "time"
@ -49,9 +49,9 @@ func createCachefileStore(options Options) (*Pool, string, error) {
} }
func TestPool_Basic(t *testing.T) { func TestPool_Basic(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.0/28") _, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: &ipnet, IPNet: ipnet,
Size: 10, Size: 10,
}) })
assert.Nil(t, err) assert.Nil(t, err)
@ -62,52 +62,23 @@ func TestPool_Basic(t *testing.T) {
last := pool.Lookup("bar.com") last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last) bar, exist := pool.LookBack(last)
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 3})) assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, pool.Lookup("foo.com") == netip.AddrFrom4([4]byte{192, 168, 0, 3})) assert.Equal(t, pool.Lookup("foo.com"), net.IP{192, 168, 0, 2})
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
assert.True(t, exist) assert.True(t, exist)
assert.Equal(t, bar, "bar.com") assert.Equal(t, bar, "bar.com")
assert.True(t, pool.Gateway() == netip.AddrFrom4([4]byte{192, 168, 0, 1})) assert.Equal(t, pool.Gateway(), net.IP{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.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 4}))) assert.True(t, pool.Exist(net.IP{192, 168, 0, 3}))
assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5}))) assert.False(t, pool.Exist(net.IP{192, 168, 0, 4}))
assert.False(t, pool.Exist(netip.MustParseAddr("::1"))) assert.False(t, pool.Exist(net.ParseIP("::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) { func TestPool_CycleUsed(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.16/28") _, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: &ipnet, IPNet: ipnet,
Size: 10, Size: 10,
}) })
assert.Nil(t, err) assert.Nil(t, err)
@ -116,22 +87,22 @@ func TestPool_CycleUsed(t *testing.T) {
for _, pool := range pools { for _, pool := range pools {
foo := pool.Lookup("foo.com") foo := pool.Lookup("foo.com")
bar := pool.Lookup("bar.com") bar := pool.Lookup("bar.com")
for i := 0; i < 10; i++ { for i := 0; i < 3; i++ {
pool.Lookup(fmt.Sprintf("%d.com", i)) pool.Lookup(fmt.Sprintf("%d.com", i))
} }
baz := pool.Lookup("baz.com") baz := pool.Lookup("baz.com")
next := pool.Lookup("foo.com") next := pool.Lookup("foo.com")
assert.True(t, foo == baz) assert.True(t, foo.Equal(baz))
assert.True(t, next == bar) assert.True(t, next.Equal(bar))
} }
} }
func TestPool_Skip(t *testing.T) { func TestPool_Skip(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/29") _, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
tree := trie.New[bool]() tree := trie.New()
tree.Insert("example.com", true) tree.Insert("example.com", tree)
pools, tempfile, err := createPools(Options{ pools, tempfile, err := createPools(Options{
IPNet: &ipnet, IPNet: ipnet,
Size: 10, Size: 10,
Host: tree, Host: tree,
}) })
@ -145,9 +116,9 @@ func TestPool_Skip(t *testing.T) {
} }
func TestPool_MaxCacheSize(t *testing.T) { func TestPool_MaxCacheSize(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/24") _, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(Options{ pool, _ := New(Options{
IPNet: &ipnet, IPNet: ipnet,
Size: 2, Size: 2,
}) })
@ -156,13 +127,13 @@ func TestPool_MaxCacheSize(t *testing.T) {
pool.Lookup("baz.com") pool.Lookup("baz.com")
next := pool.Lookup("foo.com") next := pool.Lookup("foo.com")
assert.False(t, first == next) assert.False(t, first.Equal(next))
} }
func TestPool_DoubleMapping(t *testing.T) { func TestPool_DoubleMapping(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/24") _, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(Options{ pool, _ := New(Options{
IPNet: &ipnet, IPNet: ipnet,
Size: 2, Size: 2,
}) })
@ -186,23 +157,23 @@ func TestPool_DoubleMapping(t *testing.T) {
assert.False(t, bazExist) assert.False(t, bazExist)
assert.True(t, barExist) assert.True(t, barExist)
assert.False(t, bazIP == newBazIP) assert.False(t, bazIP.Equal(newBazIP))
} }
func TestPool_Clone(t *testing.T) { func TestPool_Clone(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/24") _, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(Options{ pool, _ := New(Options{
IPNet: &ipnet, IPNet: ipnet,
Size: 2, Size: 2,
}) })
first := pool.Lookup("foo.com") first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com") last := pool.Lookup("bar.com")
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 3})) assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
newPool, _ := New(Options{ newPool, _ := New(Options{
IPNet: &ipnet, IPNet: ipnet,
Size: 2, Size: 2,
}) })
newPool.CloneFrom(pool) newPool.CloneFrom(pool)
@ -213,71 +184,11 @@ func TestPool_Clone(t *testing.T) {
} }
func TestPool_Error(t *testing.T) { func TestPool_Error(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/31") _, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
_, err := New(Options{ _, err := New(Options{
IPNet: &ipnet, IPNet: ipnet,
Size: 10, Size: 10,
}) })
assert.Error(t, err) assert.Error(t, err)
} }
func TestPool_FlushFileCache(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/28")
pools, tempfile, err := createPools(Options{
IPNet: &ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
for _, pool := range pools {
foo := pool.Lookup("foo.com")
bar := pool.Lookup("baz.com")
bax := pool.Lookup("baz.com")
fox := pool.Lookup("foo.com")
err = pool.FlushFakeIP()
assert.Nil(t, err)
next := pool.Lookup("baz.com")
baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com")
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 := netip.MustParsePrefix("192.168.0.1/28")
pool, _ := New(Options{
IPNet: &ipnet,
Size: 10,
})
foo := pool.Lookup("foo.com")
bar := pool.Lookup("baz.com")
bax := pool.Lookup("baz.com")
fox := pool.Lookup("foo.com")
err := pool.FlushFakeIP()
assert.Nil(t, err)
next := pool.Lookup("baz.com")
baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com")
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

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

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.0 // protoc-gen-go v1.27.1
// protoc v3.19.1 // protoc v3.17.3
// source: component/geodata/router/config.proto // source: component/geodata/router/config.proto
package router package router

View File

@ -9,7 +9,6 @@ import (
"github.com/Dreamacro/clash/component/geodata" "github.com/Dreamacro/clash/component/geodata"
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@ -26,7 +25,7 @@ func ReadFile(path string) ([]byte, error) {
} }
func ReadAsset(file string) ([]byte, error) { func ReadAsset(file string) ([]byte, error) {
return ReadFile(C.Path.Resolve(file)) return ReadFile(C.Path.GetAssetLocation(file))
} }
func loadIP(filename, country string) ([]*router.CIDR, error) { func loadIP(filename, country string) ([]*router.CIDR, error) {

View File

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

View File

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

View File

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

View File

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

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