Compare commits

..

No commits in common. "master" and "v1.7.0" have entirely different histories.

306 changed files with 4351 additions and 12329 deletions

76
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

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

View File

@ -1,124 +0,0 @@
name: (English) Report a bug of the Clash core
description: Create a bug report to help us improve
labels:
- bug
title: "[Bug] <issue title>"
body:
- type: markdown
attributes:
value: "## Welcome to the official Clash open-source community"
- type: markdown
attributes:
value: |
Thank you for taking the time to report an issue with the Clash core.
Prior to submitting this issue, please read and follow the guidelines below to ensure that your issue can be resolved as quickly as possible. Options marked with an asterisk (*) are required, while others are optional. If the information you provide does not comply with the requirements, the maintainers may not respond and may directly close the issue.
If you can debug and fix the issue yourself, we welcome you to submit a pull request to merge your changes upstream.
- type: checkboxes
id: ensure
attributes:
label: Prerequisites
description: "If any of the following options do not apply, please do not submit this issue as we will close it"
options:
- label: "I understand that this is the official open-source version of the Clash core, **only providing support for the open-source version or Premium version**"
required: true
- label: "I am submitting an issue with the Clash core, not Clash.Meta / OpenClash / ClashX / Clash For Windows or any other derivative version"
required: true
- label: "I am using the latest version of the Clash or Clash Premium core **in this repository**"
required: true
- label: "I have searched at the [Issue Tracker](……/) **and have not found any related issues**"
required: true
- label: "I have read the [official Wiki](https://dreamacro.github.io/clash/) **and was unable to solve the issue**"
required: true
- label: "(required for Premium core) I've tried the `dev` branch and the issue still exists"
required: false
- type: markdown
attributes:
value: "## Environment"
- type: markdown
attributes:
value: |
Please provide the following information to help us locate the issue.
The issue might be closed if there's not enough information provided.
- type: input
attributes:
label: Version
description: "Run `clash -v` or look at the bottom-left corner of the Clash Dashboard to find out"
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating System
description: "Select all operating systems that apply to this issue"
multiple: true
options:
- Linux
- Windows
- macOS (darwin)
- Android
- OpenBSD / FreeBSD
- type: dropdown
id: arch
attributes:
label: Architecture
description: "Select all architectures that apply to this issue"
multiple: true
options:
- amd64
- amd64-v3
- arm64
- "386"
- armv5
- armv6
- armv7
- mips-softfloat
- mips-hardfloat
- mipsle-softfloat
- mipsle-hardfloat
- mips64
- mips64le
- riscv64
- type: markdown
attributes:
value: "## Clash related information"
- type: markdown
attributes:
value: |
Please provide relevant information about your Clash instance here. If you
do not provide enough information, the issue may be closed.
- type: textarea
attributes:
render: YAML
label: Configuration File
placeholder: "Ensure that there is no sensitive information (such as server addresses, passwords, or ports) in the configuration file, and provide the minimum reproducible configuration. Do not post configurations with thousands of lines."
validations:
required: true
- type: textarea
attributes:
render: Text
label: Log
placeholder: "Please attach the corresponding core outout (setting `log-level: debug` in the configuration provides debugging information)."
- type: textarea
attributes:
label: Description
placeholder: "Please describe your issue in detail here to help us understand (supports Markdown syntax)."
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
placeholder: "Please provide the specific steps to reproduce the issue here (supports Markdown syntax)."

View File

@ -1,121 +0,0 @@
name: (中文)提交 Clash 核心的问题
description: 如果 Clash 核心运作不符合预期,在这里提交问题
labels:
- bug
title: "[Bug] <问题标题>"
body:
- type: markdown
attributes:
value: "## 欢迎来到 Clash 官方开源社区!"
- type: markdown
attributes:
value: |
感谢你拨冗提交 Clash 内核的问题。在提交之前,请仔细阅读并遵守以下指引,以确保你的问题能够被尽快解决。
带有星号(*)的选项为必填,其他可选填。**如果你填写的资料不符合规范,维护者可能不予回复,并直接关闭这个 issue。**
如果你可以自行 debug 并且修正,我们随时欢迎你提交 Pull Request将你的修改合并到上游。
- type: checkboxes
id: ensure
attributes:
label: 先决条件
description: "若以下任意选项不适用,请勿提交这个 issue因为我们会把它关闭"
options:
- label: "我了解这里是官方开源版 Clash 核心仓库,**只提供开源版或者 Premium 内核的支持**"
required: true
- label: "我要提交 Clash 核心的问题,并非 Clash.Meta / OpenClash / ClashX / Clash For Windows 或其他任何衍生版本的问题"
required: true
- label: "我使用的是**本仓库**最新版本的 Clash 或 Clash Premium 内核"
required: true
- label: "我已经在 [Issue Tracker](……/) 中找过我要提出的 bug**并且没有找到相关问题**"
required: true
- label: "我已经仔细阅读 [官方 Wiki](https://dreamacro.github.io/clash/) 并无法自行解决问题"
required: true
- label: "(非 Premium 内核必填)我已经使用 dev 分支版本测试过,问题依旧存在"
required: false
- type: markdown
attributes:
value: "## 系统环境"
- type: markdown
attributes:
value: |
请附上这个问题适用的环境,以帮助我们迅速定位问题并解决。若你提供的信息不足,我们将关闭
这个 issue 并要求你提供更多信息。
- type: input
attributes:
label: 版本
description: "运行 `clash -v` 或者查看 Clash Dashboard 的左下角来找到你现在使用的版本"
validations:
required: true
- type: dropdown
id: os
attributes:
label: 适用的作业系统
description: "勾选所有适用于这个 issue 的系统"
multiple: true
options:
- Linux
- Windows
- macOS (darwin)
- Android
- OpenBSD / FreeBSD
- type: dropdown
id: arch
attributes:
label: 适用的硬件架构
description: "勾选所有适用于这个 issue 的架构"
multiple: true
options:
- amd64
- amd64-v3
- arm64
- "386"
- armv5
- armv6
- armv7
- mips-softfloat
- mips-hardfloat
- mipsle-softfloat
- mipsle-hardfloat
- mips64
- mips64le
- riscv64
- type: markdown
attributes:
value: "## Clash 相关信息"
- type: markdown
attributes:
value: |
请附上与这个问题直接相关的相应信息,以帮助我们迅速定位问题并解决。
若你提供的信息不足,我们将关闭这个 issue 并要求你提供更多信息。
- type: textarea
attributes:
render: YAML
label: "配置文件"
placeholder: "确保配置文件中没有敏感信息(如:服务器地址、密码、端口),并且提供最小可复现配置,严禁贴上上千行的配置"
validations:
required: true
- type: textarea
attributes:
render: Text
label: 日志输出
placeholder: "在这里附上问题对应的内核日志(在配置中设置 `log-level: debug` 可获得调试信息)"
- type: textarea
attributes:
label: 问题描述
placeholder: "在这里详细叙述你的问题,帮助我们理解(支持 Markdown 语法)"
validations:
required: true
- type: textarea
attributes:
label: 复现步骤
placeholder: "在这里提供问题的具体重现步骤(支持 Markdown 语法)"

View File

@ -1,9 +1,6 @@
blank_issues_enabled: false
contact_links:
- name: (中文)阅读 Wiki
url: https://dreamacro.github.io/clash/zh_CN/
about: 如果你是新手,或者想要了解 Clash 的更多信息,请阅读我们撰写的官方 Wiki
- name: (English) Read our Wiki page
url: https://dreamacro.github.io/clash/
about: If you are new to Clash, or want to know more about Clash, please read our Wiki page
- name: Get help in GitHub Discussions
url: https://github.com/Dreamacro/clash/discussions
about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on Clash's GitHub Discussions!

View File

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

View File

@ -1,43 +0,0 @@
name: (English) Feature request
description: Suggest an idea for this project
labels:
- enhancement
title: "[Feature] <title>"
body:
- type: markdown
attributes:
value: "## Welcome to the official Clash open-source community"
- type: markdown
attributes:
value: |
Thank you for taking the time to make a suggestion to the Clash core.
Prior to submitting this issue, please read and follow the guidelines below to ensure that your issue can be resolved as quickly as possible. Options marked with an asterisk (*) are required, while others are optional. If the information you provide does not comply with the requirements, the maintainers may not respond and may directly close the issue.
If you can implement your idea by yourself, we welcome you to submit a pull request to merge your changes upstream.
- type: checkboxes
id: ensure
attributes:
label: Prerequisites
description: "If any of the following options do not apply, please do not submit this issue as we will close it"
options:
- label: "I understand that this is the official open-source version of the Clash core, **only providing support for the open-source version or Premium version**"
required: true
- label: "I have looked for my idea in [the issue tracker](https://github.com/Dreamacro/clash/issues?q=is%3Aissue+label%3Aenhancement), **and found none of which being related**"
required: true
- label: "I have read the [official Wiki](https://dreamacro.github.io/clash/)"
required: true
- type: textarea
attributes:
label: Description
placeholder: "Please explain your suggestions in detail and in a clear manner. For instance, how does this issue impact you? What specific functionality are you hoping to achieve? Also, let us know what Clash Core is currently doing in terms of your suggestion, and what you would like it to do instead."
validations:
required: true
- type: textarea
attributes:
label: Possible Solution
placeholder: "Do you have any ideas on the implementation details?"

View File

@ -1,41 +0,0 @@
name: (中文)建议一个新功能
description: 在这里提供一个的想法或建议
labels:
- enhancement
title: "[Feature] <标题>"
body:
- type: markdown
attributes:
value: "## 欢迎来到 Clash 官方开源社区!"
- type: markdown
attributes:
value: |
感谢你拨冗为 Clash 内核提供建议。在提交之前,请仔细阅读并遵守以下指引,以确保你的建议能够被顺利采纳。
带有星号(*)的选项为必填,其他可选填。**如果你填写的资料不符合规范,维护者可能不予回复,并直接关闭这个 issue。**
如果你可以自行添加这个功能,我们随时欢迎你提交 Pull Request并将你的修改合并到上游。
- type: checkboxes
id: ensure
attributes:
label: 先决条件
description: "若以下任意选项不适用,请勿提交这个 issue因为我们会把它关闭"
options:
- label: "我了解这里是 Clash 官方仓库,并非 Clash.Meta / OpenClash / ClashX / Clash For Windows 或其他任何衍生版本"
required: true
- label: "我已经在[这里](https://github.com/Dreamacro/clash/issues?q=is%3Aissue+label%3Aenhancement)找过我要提出的建议,**并且没有找到相关问题**"
required: true
- label: "我已经仔细阅读 [官方 Wiki](https://dreamacro.github.io/clash/) "
required: true
- type: textarea
attributes:
label: 描述
placeholder: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什么?
validations:
required: true
- type: textarea
attributes:
label: 可能的解决方案
placeholder: 此项非必须,但是如果你有想法的话欢迎提出。

View File

@ -1,8 +1,8 @@
name: CodeQL
name: "CodeQL"
on:
push:
branches: [master, dev]
branches: [ master, dev ]
jobs:
analyze:
@ -12,19 +12,19 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ['go']
language: [ 'go' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v1

View File

@ -1,42 +0,0 @@
name: Deploy
on:
workflow_dispatch: {}
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
working-directory: docs
run: pnpm install --frozen-lockfile=false
- name: Build
working-directory: docs
run: pnpm run docs:build
- uses: actions/configure-pages@v2
- uses: actions/upload-pages-artifact@v1
with:
path: docs/.vitepress/dist
- name: Deploy
id: deployment
uses: actions/deploy-pages@v2

View File

@ -13,29 +13,29 @@ jobs:
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up docker buildx
id: buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v1
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to Github Package
uses: docker/login-action@v2
uses: docker/login-action@v1
with:
registry: ghcr.io
username: Dreamacro
@ -43,22 +43,20 @@ jobs:
- name: Build dev branch and push
if: github.ref == 'refs/heads/dev'
uses: docker/build-push-action@v4
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
platforms: linux/amd64,linux/arm/v7,linux/arm64
push: true
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Get all docker tags
if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v6
uses: actions/github-script@v4
id: tags
with:
script: |
const ref = context.payload.ref.replace(/\/?refs\/tags\//, '')
const ref = `${context.payload.ref.replace(/\/?refs\/tags\//, '')}`
const tags = [
'dreamacro/clash:latest',
`dreamacro/clash:${ref}`,
@ -70,11 +68,9 @@ jobs:
- name: Build release and push
if: startsWith(github.ref, 'refs/tags/')
uses: docker/build-push-action@v4
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
platforms: linux/amd64,linux/arm/v7,linux/arm64
push: true
tags: ${{steps.tags.outputs.result}}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,38 +1,40 @@
name: Release
on: [push]
name: Go
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v2
with:
check-latest: true
go-version: '1.20'
go-version: 1.17
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Cache go module
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
~/.cache/go-build
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Get dependencies, run test
- name: Get dependencies, run test and static check
run: |
go test ./...
go vet ./...
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck -- $(go list ./...)
- name: Build
if: startsWith(github.ref, 'refs/tags/')
env:
NAME: clash
BINDIR: bin
run: make -j $(go run ./test/main.go) releases
run: make -j releases
- name: Upload Release
uses: softprops/action-gh-release@v1

View File

@ -1,18 +0,0 @@
name: Linter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
check-latest: true
go-version: '1.20'
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v7
- uses: actions/stale@v4
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

16
.gitignore vendored
View File

@ -12,7 +12,7 @@ bin/*
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# go mod vendor
# dep
vendor
# GoLand
@ -20,17 +20,3 @@ vendor
# macOS file
.DS_Store
# test suite
test/config/cache*
# docs site generator
node_modules
package-lock.json
pnpm-lock.yaml
# docs site cache
docs/.vitepress/cache
# docs site build files
docs/.vitepress/dist

View File

@ -1,23 +0,0 @@
linters:
disable-all: true
enable:
- gci
- gofumpt
- gosimple
- govet
- ineffassign
- misspell
- staticcheck
- unconvert
- unused
- usestdlibvars
linters-settings:
gci:
custom-order: true
sections:
- standard
- prefix(github.com/Dreamacro/clash)
- default
staticcheck:
go: '1.20'

View File

@ -1,22 +1,18 @@
FROM --platform=${BUILDPLATFORM} golang:alpine as builder
FROM golang:alpine as builder
RUN apk add --no-cache make git ca-certificates tzdata && \
RUN apk add --no-cache make git && \
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
WORKDIR /workdir
WORKDIR /clash-src
COPY --from=tonistiigi/xx:golang / /
ARG TARGETOS TARGETARCH TARGETVARIANT
RUN --mount=target=. \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
make BINDIR= ${TARGETOS}-${TARGETARCH}${TARGETVARIANT} && \
mv /clash* /clash
COPY . /clash-src
RUN go mod download && \
make docker && \
mv ./bin/clash-docker /clash
FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
RUN apk add --no-cache ca-certificates
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash /
ENTRYPOINT ["/clash"]

View File

@ -8,43 +8,37 @@ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clas
PLATFORM_LIST = \
darwin-amd64 \
darwin-amd64-v3 \
darwin-arm64 \
linux-386 \
linux-amd64 \
linux-amd64-v3 \
linux-armv5 \
linux-armv6 \
linux-armv7 \
linux-arm64 \
linux-armv8 \
linux-mips-softfloat \
linux-mips-hardfloat \
linux-mipsle-softfloat \
linux-mipsle-hardfloat \
linux-mips64 \
linux-mips64le \
linux-riscv64 \
linux-loong64 \
freebsd-386 \
freebsd-amd64 \
freebsd-amd64-v3 \
freebsd-arm64
WINDOWS_ARCH_LIST = \
windows-386 \
windows-amd64 \
windows-amd64-v3 \
windows-arm64 \
windows-armv7
windows-arm32v7
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
docker:
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-v3:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -54,9 +48,6 @@ linux-386:
linux-amd64:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-v3:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv5:
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -66,7 +57,7 @@ linux-armv6:
linux-armv7:
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-arm64:
linux-armv8:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips-softfloat:
@ -87,21 +78,12 @@ linux-mips64:
linux-mips64le:
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-riscv64:
GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-loong64:
GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-386:
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64:
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64-v3:
GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-arm64:
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -111,13 +93,10 @@ windows-386:
windows-amd64:
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-armv7:
windows-arm32v7:
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
@ -133,16 +112,5 @@ $(zip_releases): %.zip : %
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
releases: $(gz_releases) $(zip_releases)
LINT_OS_LIST := darwin windows linux freebsd openbsd
lint: $(foreach os,$(LINT_OS_LIST),$(os)-lint)
%-lint:
GOOS=$* golangci-lint run ./...
lint-fix: $(foreach os,$(LINT_OS_LIST),$(os)-lint-fix)
%-lint-fix:
GOOS=$* golangci-lint run --fix ./...
clean:
rm $(BINDIR)/*

View File

@ -7,47 +7,56 @@
<p align="center">
<a href="https://github.com/Dreamacro/clash/actions">
<img src="https://img.shields.io/github/actions/workflow/status/Dreamacro/clash/release.yml?branch=master&style=flat-square" alt="Github Actions">
<img src="https://img.shields.io/github/workflow/status/Dreamacro/clash/Go?style=flat-square" alt="Github Actions">
</a>
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</a>
<img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
<a href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
</a>
<a href="https://github.com/Dreamacro/clash/releases/tag/premium">
<img src="https://img.shields.io/badge/release-Premium-00b4f0?style=flat-square">
</a>
</p>
## Features
This is a general overview of the features that comes with Clash.
- Local HTTP/HTTPS/SOCKS server with authentication support
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller
- Inbound: HTTP, HTTPS, SOCKS5 server, TUN device
- Outbound: Shadowsocks(R), VMess, Trojan, Snell, SOCKS5, HTTP(S), Wireguard
- Rule-based Routing: dynamic scripting, domain, IP addresses, process name and more
- Fake-IP DNS: minimises impact on DNS pollution and improves network performance
- Transparent Proxy: Redirect TCP and TProxy TCP/UDP with automatic route table/rule management
- Proxy Groups: automatic fallback, load balancing or latency testing
- Remote Providers: load remote proxy lists dynamically
- RESTful API: update configuration in-place via a comprehensive API
## Premium Features
*Some of the features may only be available in the [Premium core](https://dreamacro.github.io/clash/premium/introduction.html).*
- TUN mode on macOS, Linux and Windows. [Doc](https://github.com/Dreamacro/clash/wiki/premium-core-features#tun-device)
- Match your tunnel by [Script](https://github.com/Dreamacro/clash/wiki/premium-core-features#script)
- [Rule Provider](https://github.com/Dreamacro/clash/wiki/premium-core-features#rule-providers)
## Documentation
## Getting Started
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
You can find the latest documentation at [https://dreamacro.github.io/clash/](https://dreamacro.github.io/clash/).
## Premium Release
[Release](https://github.com/Dreamacro/clash/releases/tag/premium)
## 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)
## Credits
- [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
## License
This software is released under the GPL-3.0 license.
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
## TODO
- [x] Complementing the necessary rule operators
- [x] Redir proxy
- [x] UDP support
- [x] Connection manager

View File

@ -10,7 +10,6 @@ import (
"time"
"github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
@ -35,26 +34,14 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
}
// DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
if err != nil {
p.alive.Store(false)
}
return conn, err
}
// DialUDP implements C.ProxyAdapter
func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout)
defer cancel()
return p.ListenPacketContext(ctx, metadata)
}
// ListenPacketContext implements C.ProxyAdapter
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
return pc, err
}
// DelayHistory implements C.Proxy
func (p *Proxy) DelayHistory() []C.DelayHistory {
queue := p.history.Copy()
@ -91,10 +78,9 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
return inner, err
}
mapping := map[string]any{}
mapping := map[string]interface{}{}
json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory()
mapping["alive"] = p.Alive()
mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP()
return json.Marshal(mapping)
@ -102,13 +88,12 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
// URLTest get the delay for the specified URL
// implements C.Proxy
func (p *Proxy) URLTest(ctx context.Context, url string) (delay, meanDelay uint16, err error) {
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
defer func() {
p.alive.Store(err == nil)
record := C.DelayHistory{Time: time.Now()}
if err == nil {
record.Delay = delay
record.MeanDelay = meanDelay
record.Delay = t
}
p.history.Put(record)
if p.history.Len() > 10 {
@ -158,16 +143,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (delay, meanDelay uint1
return
}
resp.Body.Close()
delay = uint16(time.Since(start) / time.Millisecond)
resp, err = client.Do(req)
if err != nil {
// ignore error because some server will hijack the connection and close immediately
return delay, 0, nil
}
resp.Body.Close()
meanDelay = uint16(time.Since(start) / time.Millisecond / 2)
t = uint16(time.Since(start) / time.Millisecond)
return
}
@ -195,9 +171,10 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
}
addr = C.Metadata{
Host: u.Hostname(),
DstIP: nil,
DstPort: port,
AddrType: C.AtypDomainName,
Host: u.Hostname(),
DstIP: nil,
DstPort: port,
}
return
}

View File

@ -2,7 +2,6 @@ package inbound
import (
"net"
"net/netip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
@ -10,18 +9,13 @@ import (
)
// NewHTTP receive normal http request and return HTTPContext
func NewHTTP(target socks5.Addr, source net.Addr, originTarget net.Addr, conn net.Conn) *context.ConnContext {
metadata := parseSocksAddr(target)
func NewHTTP(target string, source net.Addr, conn net.Conn) *context.ConnContext {
metadata := parseSocksAddr(socks5.ParseAddr(target))
metadata.NetWork = C.TCP
metadata.Type = C.HTTP
if ip, port, err := parseAddr(source.String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if originTarget != nil {
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
metadata.OriginDst = addrPort
}
}
return context.NewConnContext(conn, metadata)
}

View File

@ -3,7 +3,6 @@ package inbound
import (
"net"
"net/http"
"net/netip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
@ -17,8 +16,5 @@ func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
metadata.OriginDst = addrPort
}
return context.NewConnContext(conn, metadata)
}

View File

@ -1,9 +1,6 @@
package inbound
import (
"net"
"net/netip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
@ -20,7 +17,7 @@ func (s *PacketAdapter) Metadata() *C.Metadata {
}
// NewPacket is PacketAdapter generator
func NewPacket(target socks5.Addr, originTarget net.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
metadata := parseSocksAddr(target)
metadata.NetWork = C.UDP
metadata.Type = source
@ -28,11 +25,7 @@ func NewPacket(target socks5.Addr, originTarget net.Addr, packet C.UDPPacket, so
metadata.SrcIP = ip
metadata.SrcPort = port
}
if originTarget != nil {
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
metadata.OriginDst = addrPort
}
}
return &PacketAdapter{
UDPPacket: packet,
metadata: metadata,

View File

@ -2,7 +2,6 @@ package inbound
import (
"net"
"net/netip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
@ -18,8 +17,6 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnCo
metadata.SrcIP = ip
metadata.SrcPort = port
}
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
metadata.OriginDst = addrPort
}
return context.NewConnContext(conn, metadata)
}

View File

@ -11,7 +11,9 @@ import (
)
func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata := &C.Metadata{}
metadata := &C.Metadata{
AddrType: int(target[0]),
}
switch target[0] {
case socks5.AtypDomainName:
@ -42,13 +44,21 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
host = strings.TrimRight(host, ".")
metadata := &C.Metadata{
NetWork: C.TCP,
Host: host,
DstIP: nil,
DstPort: port,
NetWork: C.TCP,
AddrType: C.AtypDomainName,
Host: host,
DstIP: nil,
DstPort: port,
}
if ip := net.ParseIP(host); ip != nil {
ip := net.ParseIP(host)
if ip != nil {
switch {
case ip.To4() == nil:
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
}
metadata.DstIP = ip
}

View File

@ -1,22 +1,18 @@
package outbound
import (
"context"
"encoding/json"
"errors"
"net"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
type Base struct {
name string
addr string
iface string
tp C.AdapterType
udp bool
rmark int
name string
addr string
tp C.AdapterType
udp bool
}
// Name implements C.ProxyAdapter
@ -34,8 +30,8 @@ func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
// ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
// DialUDP implements C.ProxyAdapter
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return nil, errors.New("no support")
}
@ -61,42 +57,8 @@ func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil
}
// DialOptions return []dialer.Option from struct
func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
if b.iface != "" {
opts = append(opts, dialer.WithInterface(b.iface))
}
if b.rmark != 0 {
opts = append(opts, dialer.WithRoutingMark(b.rmark))
}
return opts
}
type BasicOption struct {
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
}
type BaseOption struct {
Name string
Addr string
Type C.AdapterType
UDP bool
Interface string
RoutingMark int
}
func NewBase(opt BaseOption) *Base {
return &Base{
name: opt.Name,
addr: opt.Addr,
tp: opt.Type,
udp: opt.UDP,
iface: opt.Interface,
rmark: opt.RoutingMark,
}
func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base {
return &Base{name, addr, tp, udp}
}
type conn struct {

View File

@ -13,8 +13,8 @@ type Direct struct {
}
// DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress())
if err != nil {
return nil, err
}
@ -22,9 +22,9 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return NewConn(c, d), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
// DialUDP implements C.ProxyAdapter
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(context.Background(), "udp", "")
if err != nil {
return nil, err
}

View File

@ -22,29 +22,24 @@ type Http struct {
user string
pass string
tlsConfig *tls.Config
Headers http.Header
}
type HttpOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
}
// StreamConn implements C.ProxyAdapter
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if h.tlsConfig != nil {
cc := tls.Client(c, h.tlsConfig)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err := cc.HandshakeContext(ctx)
err := cc.Handshake()
c = cc
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
@ -58,16 +53,14 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
}
// DialContext implements C.ProxyAdapter
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...)
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", h.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
}
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = h.StreamConn(c, metadata)
if err != nil {
@ -84,12 +77,12 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
URL: &url.URL{
Host: addr,
},
Host: addr,
Header: h.Headers.Clone(),
Host: addr,
Header: http.Header{
"Proxy-Connection": []string{"Keep-Alive"},
},
}
req.Header.Add("Proxy-Connection", "Keep-Alive")
if h.user != "" && h.pass != "" {
auth := h.user + ":" + h.pass
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
@ -136,22 +129,14 @@ func NewHttp(option HttpOption) *Http {
}
}
headers := http.Header{}
for name, value := range option.Headers {
headers.Add(name, value)
}
return &Http{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http,
iface: option.Interface,
rmark: option.RoutingMark,
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http,
},
user: option.UserName,
pass: option.Password,
tlsConfig: tlsConfig,
Headers: headers,
}
}

View File

@ -2,11 +2,11 @@ package outbound
import (
"context"
"errors"
"io"
"net"
"time"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
@ -15,13 +15,13 @@ type Reject struct {
}
// DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return NewConn(&nopConn{}, r), nil
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
return NewConn(&NopConn{}, r), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return newPacketConn(&nopPacketConn{}, r), nil
// DialUDP implements C.ProxyAdapter
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return nil, errors.New("match reject rule")
}
func NewReject() *Reject {
@ -34,29 +34,30 @@ func NewReject() *Reject {
}
}
type nopConn struct{}
type NopConn struct{}
func (rw *nopConn) Read(b []byte) (int, error) {
func (rw *NopConn) Read(b []byte) (int, error) {
return 0, io.EOF
}
func (rw *nopConn) Write(b []byte) (int, error) {
func (rw *NopConn) Write(b []byte) (int, error) {
return 0, io.EOF
}
func (rw *nopConn) Close() error { return nil }
func (rw *nopConn) LocalAddr() net.Addr { return nil }
func (rw *nopConn) RemoteAddr() net.Addr { return nil }
func (rw *nopConn) SetDeadline(time.Time) error { return nil }
func (rw *nopConn) SetReadDeadline(time.Time) error { return nil }
func (rw *nopConn) SetWriteDeadline(time.Time) error { return nil }
// Close is fake function for net.Conn
func (rw *NopConn) Close() error { return nil }
type nopPacketConn struct{}
// LocalAddr is fake function for net.Conn
func (rw *NopConn) LocalAddr() net.Addr { return nil }
func (npc *nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
func (npc *nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF }
func (npc *nopPacketConn) Close() error { return nil }
func (npc *nopPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} }
func (npc *nopPacketConn) SetDeadline(time.Time) error { return nil }
func (npc *nopPacketConn) SetReadDeadline(time.Time) error { return nil }
func (npc *nopPacketConn) SetWriteDeadline(time.Time) error { return nil }
// RemoteAddr is fake function for net.Conn
func (rw *NopConn) RemoteAddr() net.Addr { return nil }
// SetDeadline is fake function for net.Conn
func (rw *NopConn) SetDeadline(time.Time) error { return nil }
// SetReadDeadline is fake function for net.Conn
func (rw *NopConn) SetReadDeadline(time.Time) error { return nil }
// SetWriteDeadline is fake function for net.Conn
func (rw *NopConn) SetWriteDeadline(time.Time) error { return nil }

View File

@ -10,10 +10,11 @@ import (
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
"github.com/Dreamacro/go-shadowsocks2/core"
)
type ShadowSocks struct {
@ -27,15 +28,14 @@ type ShadowSocks struct {
}
type ShadowSocksOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
}
type simpleObfsOption struct {
@ -74,24 +74,22 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
}
// DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = ss.StreamConn(c, metadata)
return NewConn(c, ss), err
}
// ListenPacketContext implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
// DialUDP implements C.ProxyAdapter
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(context.Background(), "udp", "")
if err != nil {
return nil, err
}
@ -156,12 +154,10 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
return &ShadowSocks{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Shadowsocks,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
name: option.Name,
addr: addr,
tp: C.Shadowsocks,
udp: option.UDP,
},
cipher: ciph,

View File

@ -8,11 +8,12 @@ import (
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
)
type ShadowSocksR struct {
@ -23,7 +24,6 @@ type ShadowSocksR struct {
}
type ShadowSocksROption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
@ -59,24 +59,22 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn,
}
// DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
}
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = ssr.StreamConn(c, metadata)
return NewConn(c, ssr), err
}
// ListenPacketContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...)
// DialUDP implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(context.Background(), "udp", "")
if err != nil {
return nil, err
}
@ -93,12 +91,6 @@ func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Me
}
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
// SSR protocol compatibility
// https://github.com/Dreamacro/clash/pull/2056
if option.Cipher == "none" {
option.Cipher = "dummy"
}
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher
password := option.Password
@ -110,14 +102,13 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
ivSize int
key []byte
)
if option.Cipher == "dummy" {
ivSize = 0
key = core.Kdf(option.Password, 16)
} else {
ciph, ok := coreCiph.(*core.StreamCipher)
if !ok {
return nil, fmt.Errorf("%s is not 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()
key = ciph.Key
@ -145,12 +136,10 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
return &ShadowSocksR{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
udp: option.UDP,
},
cipher: coreCiph,
obfs: obfs,

View File

@ -22,14 +22,12 @@ type Snell struct {
}
type SnellOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Psk string `proxy:"psk"`
UDP bool `proxy:"udp,omitempty"`
Version int `proxy:"version,omitempty"`
ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Psk string `proxy:"psk"`
Version int `proxy:"version,omitempty"`
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
}
type streamOption struct {
@ -53,20 +51,20 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
// StreamConn implements C.ProxyAdapter
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port, _ := strconv.Atoi(metadata.DstPort)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err
}
// DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
if s.version == snell.Version2 && len(opts) == 0 {
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
if s.version == snell.Version2 {
c, err := s.pool.Get()
if err != nil {
return nil, err
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port, _ := strconv.Atoi(metadata.DstPort)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close()
return nil, err
@ -74,38 +72,18 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return NewConn(c, s), err
}
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = s.StreamConn(c, metadata)
return NewConn(c, s), err
}
// 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)
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version)
if err != nil {
return nil, err
}
pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil
}
func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk)
@ -127,24 +105,15 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == 0 {
option.Version = snell.DefaultSnellVersion
}
switch option.Version {
case snell.Version1, snell.Version2:
if option.UDP {
return nil, fmt.Errorf("snell version %d not support UDP", option.Version)
}
case snell.Version3:
default:
if option.Version != snell.Version1 && option.Version != snell.Version2 {
return nil, fmt.Errorf("snell version error: %d", option.Version)
}
s := &Snell{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Snell,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
name: option.Name,
addr: addr,
tp: C.Snell,
},
psk: psk,
obfsOption: obfsOption,
@ -153,7 +122,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == snell.Version2 {
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
c, err := dialer.DialContext(ctx, "tcp", addr, s.Base.DialOptions()...)
c, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}

View File

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"strconv"
@ -24,7 +25,6 @@ type Socks5 struct {
}
type Socks5Option struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
@ -39,9 +39,7 @@ type Socks5Option struct {
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if ss.tls {
cc := tls.Client(c, ss.tlsConfig)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err := cc.HandshakeContext(ctx)
err := cc.Handshake()
c = cc
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
@ -62,16 +60,14 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
}
// DialContext implements C.ProxyAdapter
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = ss.StreamConn(c, metadata)
if err != nil {
@ -81,9 +77,11 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
return NewConn(c, ss), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
// DialUDP implements C.ProxyAdapter
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel()
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
return
@ -91,15 +89,11 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if ss.tls {
cc := tls.Client(c, ss.tlsConfig)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err = cc.HandshakeContext(ctx)
err = cc.Handshake()
c = cc
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
tcpKeepAlive(c)
var user *socks5.User
@ -116,13 +110,13 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return
}
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
pc, err := dialer.ListenPacket(context.Background(), "udp", "")
if err != nil {
return
}
go func() {
io.Copy(io.Discard, c)
io.Copy(ioutil.Discard, c)
c.Close()
// A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928
@ -157,12 +151,10 @@ func NewSocks5(option Socks5Option) *Socks5 {
return &Socks5{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5,
udp: option.UDP,
},
user: option.UserName,
pass: option.Password,

View File

@ -5,7 +5,6 @@ import (
"crypto/tls"
"fmt"
"net"
"net/http"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
@ -19,7 +18,6 @@ import (
type Trojan struct {
*Base
instance *trojan.Trojan
option *TrojanOption
// for gun mux
gunTLSConfig *tls.Config
@ -28,7 +26,6 @@ type Trojan struct {
}
type TrojanOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
@ -39,34 +36,6 @@ type TrojanOption struct {
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
}
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
if t.option.Network == "ws" {
host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{
Host: host,
Port: port,
Path: t.option.WSOpts.Path,
}
if t.option.SNI != "" {
wsOpts.Host = t.option.SNI
}
if len(t.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range t.option.WSOpts.Headers {
header.Add(key, value)
}
wsOpts.Headers = header
}
return t.instance.StreamWebsocketConn(c, wsOpts)
}
return t.instance.StreamConn(c)
}
// StreamConn implements C.ProxyAdapter
@ -75,7 +44,7 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
} else {
c, err = t.plainStream(c)
c, err = t.instance.StreamConn(c)
}
if err != nil {
@ -87,9 +56,9 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
}
// 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) (_ C.Conn, err error) {
// gun transport
if t.transport != nil && len(opts) == 0 {
if t.transport != nil {
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, err
@ -103,15 +72,13 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
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)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = t.StreamConn(c, metadata)
if err != nil {
@ -121,29 +88,27 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return NewConn(c, t), err
}
// ListenPacketContext implements C.ProxyAdapter
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// DialUDP implements C.ProxyAdapter
func (t *Trojan) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
var c net.Conn
// grpc transport
if t.transport != nil && len(opts) == 0 {
if t.transport != nil {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
} else {
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel()
c, err = dialer.DialContext(ctx, "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
tcpKeepAlive(c)
c, err = t.plainStream(c)
c, err = t.instance.StreamConn(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
@ -174,20 +139,17 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
t := &Trojan{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Trojan,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
name: option.Name,
addr: addr,
tp: C.Trojan,
udp: option.UDP,
},
instance: trojan.New(tOption),
option: &option,
}
if option.Network == "grpc" {
dialFn := func(network, addr string) (net.Conn, error) {
c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...)
c, err := dialer.DialContext(context.Background(), "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
}

View File

@ -1,6 +1,7 @@
package outbound
import (
"bytes"
"net"
"strconv"
"time"
@ -8,8 +9,6 @@ import (
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/protobytes"
)
func tcpKeepAlive(c net.Conn) {
@ -20,24 +19,23 @@ func tcpKeepAlive(c net.Conn) {
}
func serializesSocksAddr(metadata *C.Metadata) []byte {
buf := protobytes.BytesWriter{}
addrType := metadata.AddrType()
buf.PutUint8(uint8(addrType))
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
switch addrType {
var buf [][]byte
aType := uint8(metadata.AddrType)
p, _ := strconv.Atoi(metadata.DstPort)
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType {
case socks5.AtypDomainName:
buf.PutUint8(uint8(len(metadata.Host)))
buf.PutString(metadata.Host)
len := uint8(len(metadata.Host))
host := []byte(metadata.Host)
buf = [][]byte{{aType, len}, host, port}
case socks5.AtypIPv4:
buf.PutSlice(metadata.DstIP.To4())
host := metadata.DstIP.To4()
buf = [][]byte{{aType}, host, port}
case socks5.AtypIPv6:
buf.PutSlice(metadata.DstIP.To16())
host := metadata.DstIP.To16()
buf = [][]byte{{aType}, host, port}
}
buf.PutUint16be(uint16(p))
return buf.Bytes()
return bytes.Join(buf, nil)
}
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {

View File

@ -14,14 +14,11 @@ import (
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
)
var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address")
type Vmess struct {
*Base
client *vmess.Client
@ -34,22 +31,23 @@ type Vmess struct {
}
type VmessOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"`
}
type HTTPOptions struct {
@ -79,6 +77,13 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
switch v.option.Network {
case "ws":
if v.option.WSOpts.Path == "" {
v.option.WSOpts.Path = v.option.WSPath
}
if len(v.option.WSOpts.Headers) == 0 {
v.option.WSOpts.Headers = v.option.WSHeaders
}
host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{
Host: host,
@ -90,7 +95,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSOpts.Headers {
for key, value := range v.option.WSHeaders {
header.Add(key, value)
}
wsOpts.Headers = header
@ -98,16 +103,8 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if v.option.TLS {
wsOpts.TLS = true
wsOpts.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
}
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
wsOpts.SkipCertVerify = v.option.SkipCertVerify
wsOpts.ServerName = v.option.ServerName
}
c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http":
@ -188,16 +185,14 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
}
// 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) (_ C.Conn, err error) {
// gun transport
if v.transport != nil && len(opts) == 0 {
if v.transport != nil {
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
if err != nil {
@ -207,21 +202,19 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return NewConn(c, v), nil
}
c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err
}
// ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// DialUDP implements C.ProxyAdapter
func (v *Vmess) DialUDP(metadata *C.Metadata) (_ 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)
@ -233,25 +226,23 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
var c net.Conn
// gun transport
if v.transport != nil && len(opts) == 0 {
if v.transport != nil {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel()
c, err = dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata)
}
@ -286,12 +277,10 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v := &Vmess{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
udp: option.UDP,
},
client: client,
option: &option,
@ -304,7 +293,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
}
case "grpc":
dialFn := func(network, addr string) (net.Conn, error) {
c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
c, err := dialer.DialContext(context.Background(), "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@ -338,23 +327,23 @@ func NewVmess(option VmessOption) (*Vmess, error) {
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
var addrType byte
var addr []byte
switch metadata.AddrType() {
case socks5.AtypIPv4:
addrType = vmess.AtypIPv4
switch metadata.AddrType {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
case socks5.AtypIPv6:
addrType = vmess.AtypIPv6
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
case socks5.AtypDomainName:
addrType = vmess.AtypDomainName
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port, _ := strconv.Atoi(metadata.DstPort)
return &vmess.DstAddr{
UDP: metadata.NetWork == C.UDP,
AddrType: addrType,
@ -368,14 +357,7 @@ type vmessPacketConn struct {
rAddr net.Addr
}
// WriteTo implments C.PacketConn.WriteTo
// Since VMess doesn't support full cone NAT by design, we verify if addr matches uc.rAddr, and drop the packet if not.
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
allowedAddr := uc.rAddr.(*net.UDPAddr)
destAddr := addr.(*net.UDPAddr)
if !(allowedAddr.IP.Equal(destAddr.IP) && allowedAddr.Port == destAddr.Port) {
return 0, ErrUDPRemoteAddrMismatch
}
return uc.Conn.Write(b)
}

View File

@ -11,19 +11,14 @@ const (
defaultGetProxiesDuration = time.Second * 5
)
func touchProviders(providers []provider.ProxyProvider) {
for _, provider := range providers {
provider.Touch()
}
}
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range providers {
if touch {
provider.Touch()
proxies = append(proxies, provider.ProxiesWithTouch()...)
} else {
proxies = append(proxies, provider.Proxies()...)
}
proxies = append(proxies, provider.Proxies()...)
}
return proxies
}

View File

@ -6,7 +6,6 @@ import (
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
@ -24,19 +23,19 @@ func (f *Fallback) Now() string {
}
// DialContext implements C.ProxyAdapter
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxy := f.findAliveProxy(true)
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
c, err := proxy.DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(f)
}
return c, err
}
// ListenPacketContext implements C.ProxyAdapter
func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
// DialUDP implements C.ProxyAdapter
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
proxy := f.findAliveProxy(true)
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
pc, err := proxy.DialUDP(metadata)
if err == nil {
pc.AppendToChains(f)
}
@ -59,7 +58,7 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"type": f.Type().String(),
"now": f.Now(),
"all": all,
@ -73,7 +72,7 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (any, error) {
elm, _, _ := f.single.Do(func() (interface{}, error) {
return getProvidersProxies(f.providers, touch), nil
})
@ -91,16 +90,11 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
return proxies[0]
}
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Fallback,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
Base: outbound.NewBase(options.Name, "", C.Fallback, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
disableUDP: option.DisableUDP,
disableUDP: options.DisableUDP,
}
}

View File

@ -10,7 +10,6 @@ import (
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
@ -29,9 +28,11 @@ type LoadBalance struct {
var errStrategy = errors.New("unsupported strategy")
func parseStrategy(config map[string]any) string {
if strategy, ok := config["strategy"].(string); ok {
return strategy
func parseStrategy(config map[string]interface{}) string {
if elm, ok := config["strategy"]; ok {
if strategy, ok := elm.(string); ok {
return strategy
}
}
return "consistent-hashing"
}
@ -68,7 +69,7 @@ func jumpHash(key uint64, buckets int32) int32 {
}
// DialContext implements C.ProxyAdapter
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
defer func() {
if err == nil {
c.AppendToChains(lb)
@ -77,12 +78,12 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, op
proxy := lb.Unwrap(metadata)
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
c, err = proxy.DialContext(ctx, metadata)
return
}
// ListenPacketContext implements C.ProxyAdapter
func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) {
// DialUDP implements C.ProxyAdapter
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
defer func() {
if err == nil {
pc.AppendToChains(lb)
@ -90,7 +91,8 @@ func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Meta
}()
proxy := lb.Unwrap(metadata)
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
return proxy.DialUDP(metadata)
}
// SupportUDP implements C.ProxyAdapter
@ -127,13 +129,6 @@ func strategyConsistentHashing() strategyFn {
}
}
// when availability is poor, traverse the entire list to get the available nodes
for _, proxy := range proxies {
if proxy.Alive() {
return proxy
}
}
return proxies[0]
}
}
@ -145,7 +140,7 @@ func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() (any, error) {
elm, _, _ := lb.single.Do(func() (interface{}, error) {
return getProvidersProxies(lb.providers, touch), nil
})
@ -158,13 +153,13 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
for _, proxy := range lb.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"type": lb.Type().String(),
"all": all,
})
}
func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
var strategyFn strategyFn
switch strategy {
case "consistent-hashing":
@ -175,15 +170,10 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
}
return &LoadBalance{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.LoadBalance,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
Base: outbound.NewBase(options.Name, "", C.LoadBalance, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
strategyFn: strategyFn,
disableUDP: option.DisableUDP,
disableUDP: options.DisableUDP,
}, nil
}

View File

@ -4,13 +4,10 @@ import (
"errors"
"fmt"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
regexp "github.com/dlclark/regexp2"
)
var (
@ -18,11 +15,10 @@ var (
errType = errors.New("unsupport type")
errMissProxy = errors.New("`use` or `proxies` missing")
errMissHealthCheck = errors.New("`url` or `interval` missing")
errDuplicateProvider = errors.New("duplicate provider name")
errDuplicateProvider = errors.New("`duplicate provider name")
)
type GroupCommonOption struct {
outbound.BasicOption
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
@ -31,10 +27,9 @@ type GroupCommonOption struct {
Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
}
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
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})
groupOption := &GroupCommonOption{
@ -48,72 +43,67 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
return nil, errFormat
}
var (
groupName = groupOption.Name
filterReg *regexp.Regexp
)
if groupOption.Filter != "" {
f, err := regexp.Compile(groupOption.Filter, regexp.None)
if err != nil {
return nil, fmt.Errorf("%s: invalid filter regex: %w", groupName, err)
}
filterReg = f
}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
return nil, fmt.Errorf("%s: %w", groupName, errMissProxy)
}
groupName := groupOption.Name
providers := []types.ProxyProvider{}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
return nil, errMissProxy
}
if len(groupOption.Proxies) != 0 {
ps, err := getProxies(proxyMap, groupOption.Proxies)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
return nil, err
}
if _, ok := providersMap[groupName]; ok {
return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider)
}
// select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" {
// if Use not empty, drop health check options
if len(groupOption.Use) != 0 {
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
} else {
if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, fmt.Errorf("%s: %w", groupName, errMissHealthCheck)
if _, ok := providersMap[groupName]; ok {
return nil, errDuplicateProvider
}
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
}
// select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" {
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
providers = append(providers, pd)
providersMap[groupName] = pd
} else {
if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, errMissHealthCheck
}
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
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 {
list, err := getProviders(providersMap, groupOption.Use)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
}
if filterReg != nil {
pd := provider.NewFilterableProvider(groupName, list, filterReg)
providers = append(providers, pd)
} else {
providers = append(providers, list...)
return nil, err
}
providers = append(providers, list...)
}
var group C.ProxyAdapter
@ -131,7 +121,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
case "relay":
group = NewRelay(groupOption, providers)
default:
return nil, fmt.Errorf("%s %w: %s", groupName, errType, groupOption.Type)
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
}
return group, nil

View File

@ -19,7 +19,7 @@ type Relay struct {
}
// DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
var proxies []C.Proxy
for _, proxy := range r.proxies(metadata, true) {
if proxy.Type() != C.Direct {
@ -29,15 +29,15 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
switch len(proxies) {
case 0:
return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
return outbound.NewDirect().DialContext(ctx, metadata)
case 1:
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
return proxies[0].DialContext(ctx, metadata)
}
first := proxies[0]
last := proxies[len(proxies)-1]
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
c, err := dialer.DialContext(ctx, "tcp", first.Addr())
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
@ -72,14 +72,14 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
for _, proxy := range r.rawProxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"type": r.Type().String(),
"all": all,
})
}
func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() (any, error) {
elm, _, _ := r.single.Do(func() (interface{}, error) {
return getProvidersProxies(r.providers, touch), nil
})
@ -100,14 +100,9 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
return proxies
}
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Relay,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
Base: outbound.NewBase(options.Name, "", C.Relay, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
}

View File

@ -7,7 +7,6 @@ import (
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
@ -21,17 +20,17 @@ type Selector struct {
}
// DialContext implements C.ProxyAdapter
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
c, err := s.selectedProxy(true).DialContext(ctx, metadata, s.Base.DialOptions(opts...)...)
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(s)
}
return c, err
}
// ListenPacketContext implements C.ProxyAdapter
func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata, s.Base.DialOptions(opts...)...)
// DialUDP implements C.ProxyAdapter
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := s.selectedProxy(true).DialUDP(metadata)
if err == nil {
pc.AppendToChains(s)
}
@ -54,7 +53,7 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"type": s.Type().String(),
"now": s.Now(),
"all": all,
@ -83,7 +82,7 @@ func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (any, error) {
elm, _, _ := s.single.Do(func() (interface{}, error) {
proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
@ -97,18 +96,13 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
return elm.(C.Proxy)
}
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
func NewSelector(options *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
selected := providers[0].Proxies()[0].Name()
return &Selector{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Selector,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
Base: outbound.NewBase(options.Name, "", C.Selector, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
selected: selected,
disableUDP: option.DisableUDP,
disableUDP: options.DisableUDP,
}
}

View File

@ -7,7 +7,6 @@ import (
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
@ -35,17 +34,17 @@ func (u *URLTest) Now() string {
}
// DialContext implements C.ProxyAdapter
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
c, err = u.fast(true).DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(u)
}
return c, err
}
// ListenPacketContext implements C.ProxyAdapter
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
// DialUDP implements C.ProxyAdapter
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := u.fast(true).DialUDP(metadata)
if err == nil {
pc.AppendToChains(u)
}
@ -58,7 +57,7 @@ func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (any, error) {
elm, _, _ := u.single.Do(func() (interface{}, error) {
return getProvidersProxies(u.providers, touch), nil
})
@ -66,7 +65,7 @@ func (u *URLTest) proxies(touch bool) []C.Proxy {
}
func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, shared := u.fastSingle.Do(func() (any, error) {
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
proxies := u.proxies(touch)
fast := proxies[0]
min := fast.LastDelay()
@ -95,9 +94,6 @@ func (u *URLTest) fast(touch bool) C.Proxy {
return u.fastNode, nil
})
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
touchProviders(u.providers)
}
return elm.(C.Proxy)
}
@ -117,36 +113,33 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"type": u.Type().String(),
"now": u.Now(),
"all": all,
})
}
func parseURLTestOption(config map[string]any) []urlTestOption {
func parseURLTestOption(config map[string]interface{}) []urlTestOption {
opts := []urlTestOption{}
// tolerance
if tolerance, ok := config["tolerance"].(int); ok {
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
if elm, ok := config["tolerance"]; ok {
if tolerance, ok := elm.(int); ok {
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
}
}
return opts
}
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
func NewURLTest(commonOptions *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.URLTest,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
Base: outbound.NewBase(commonOptions.Name, "", C.URLTest, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers,
disableUDP: option.DisableUDP,
disableUDP: commonOptions.DisableUDP,
}
for _, option := range options {

View File

@ -18,24 +18,27 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
ip := net.ParseIP(host)
if ip == nil {
addr = &C.Metadata{
Host: host,
DstIP: nil,
DstPort: port,
AddrType: C.AtypDomainName,
Host: host,
DstIP: nil,
DstPort: port,
}
return
} else if ip4 := ip.To4(); ip4 != nil {
addr = &C.Metadata{
Host: "",
DstIP: ip4,
DstPort: port,
AddrType: C.AtypIPv4,
Host: "",
DstIP: ip4,
DstPort: port,
}
return
}
addr = &C.Metadata{
Host: "",
DstIP: ip,
DstPort: port,
AddrType: C.AtypIPv6,
Host: "",
DstIP: ip,
DstPort: port,
}
return
}

View File

@ -8,7 +8,7 @@ import (
C "github.com/Dreamacro/clash/constant"
)
func ParseProxy(mapping map[string]any) (C.Proxy, error) {
func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxyType, existType := mapping["type"].(string)
if !existType {

View File

@ -3,6 +3,7 @@ package provider
import (
"bytes"
"crypto/md5"
"io/ioutil"
"os"
"path/filepath"
"time"
@ -12,22 +13,21 @@ import (
)
var (
fileMode os.FileMode = 0o666
dirMode os.FileMode = 0o755
fileMode os.FileMode = 0666
dirMode os.FileMode = 0755
)
type parser = func([]byte) (any, error)
type parser = func([]byte) (interface{}, error)
type fetcher struct {
name string
vehicle types.Vehicle
interval time.Duration
updatedAt *time.Time
ticker *time.Ticker
done chan struct{}
hash [16]byte
parser parser
onUpdate func(any)
onUpdate func(interface{})
}
func (f *fetcher) Name() string {
@ -38,19 +38,17 @@ func (f *fetcher) VehicleType() types.VehicleType {
return f.vehicle.Type()
}
func (f *fetcher) Initial() (any, error) {
func (f *fetcher) Initial() (interface{}, error) {
var (
buf []byte
err error
isLocal bool
immediatelyUpdate bool
buf []byte
err error
isLocal bool
)
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
buf, err = os.ReadFile(f.vehicle.Path())
buf, err = ioutil.ReadFile(f.vehicle.Path())
modTime := stat.ModTime()
f.updatedAt = &modTime
isLocal = true
immediatelyUpdate = time.Since(modTime) > f.interval
} else {
buf, err = f.vehicle.Read()
}
@ -89,13 +87,13 @@ func (f *fetcher) Initial() (any, error) {
// pull proxies automatically
if f.ticker != nil {
go f.pullLoop(immediatelyUpdate)
go f.pullLoop()
}
return proxies, nil
}
func (f *fetcher) Update() (any, bool, error) {
func (f *fetcher) Update() (interface{}, bool, error) {
buf, err := f.vehicle.Read()
if err != nil {
return nil, false, err
@ -105,7 +103,6 @@ func (f *fetcher) Update() (any, bool, error) {
hash := md5.Sum(buf)
if bytes.Equal(f.hash[:], hash[:]) {
f.updatedAt = &now
os.Chtimes(f.vehicle.Path(), now, now)
return nil, true, nil
}
@ -133,33 +130,25 @@ func (f *fetcher) Destroy() error {
return nil
}
func (f *fetcher) pullLoop(immediately bool) {
update := func() {
elm, same, err := f.Update()
if err != nil {
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
return
}
if same {
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
return
}
log.Infoln("[Provider] %s's proxies update", f.Name())
if f.onUpdate != nil {
f.onUpdate(elm)
}
}
if immediately {
update()
}
func (f *fetcher) pullLoop() {
for {
select {
case <-f.ticker.C:
update()
elm, same, err := f.Update()
if err != nil {
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
continue
}
if same {
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
continue
}
log.Infoln("[Provider] %s's proxies update", f.Name())
if f.onUpdate != nil {
f.onUpdate(elm)
}
case <-f.done:
f.ticker.Stop()
return
@ -176,10 +165,10 @@ func safeWrite(path string, buf []byte) error {
}
}
return os.WriteFile(path, buf, fileMode)
return ioutil.WriteFile(path, buf, fileMode)
}
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher {
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(interface{})) *fetcher {
var ticker *time.Ticker
if interval != 0 {
ticker = time.NewTicker(interval)
@ -189,7 +178,6 @@ func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, pars
name: name,
ticker: ticker,
vehicle: vehicle,
interval: interval,
parser: parser,
done: make(chan struct{}, 1),
onUpdate: onUpdate,

View File

@ -7,7 +7,6 @@ import (
"github.com/Dreamacro/clash/common/batch"
C "github.com/Dreamacro/clash/constant"
"github.com/samber/lo"
"go.uber.org/atomic"
)
@ -32,20 +31,13 @@ type HealthCheck struct {
func (hc *HealthCheck) process() {
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
go hc.checkAll()
go hc.check()
for {
select {
case <-ticker.C:
now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.checkAll()
} else { // lazy but still need to check not alive proxies
notAliveProxies := lo.Filter(hc.proxies, func(proxy C.Proxy, _ int) bool {
return !proxy.Alive()
})
if len(notAliveProxies) != 0 {
hc.check(notAliveProxies)
}
hc.check()
}
case <-hc.done:
ticker.Stop()
@ -66,15 +58,11 @@ func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now().Unix())
}
func (hc *HealthCheck) checkAll() {
hc.check(hc.proxies)
}
func (hc *HealthCheck) check(proxies []C.Proxy) {
func (hc *HealthCheck) check() {
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
for _, proxy := range proxies {
for _, proxy := range hc.proxies {
p := proxy
b.Go(p.Name(), func() (any, error) {
b.Go(p.Name(), func() (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
p.URLTest(ctx, hc.url)

View File

@ -12,7 +12,6 @@ import (
var (
errVehicleType = errors.New("unsupport vehicle type")
errSubPath = errors.New("path is not subpath of home directory")
)
type healthCheckSchema struct {
@ -27,11 +26,10 @@ type proxyProviderSchema struct {
Path string `provider:"path"`
URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
}
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
func ParseProxyProvider(name string, mapping map[string]interface{}) (types.ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{
@ -56,15 +54,11 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
case "file":
vehicle = NewFileVehicle(path)
case "http":
if !C.Path.IsSubPath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path)
}
vehicle = NewHTTPVehicle(schema.URL, path)
default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
}
interval := time.Duration(uint(schema.Interval)) * time.Second
filter := schema.Filter
return NewProxySetProvider(name, interval, filter, vehicle, hc)
return NewProxySetProvider(name, interval, vehicle, hc), nil
}

View File

@ -8,24 +8,18 @@ import (
"time"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
regexp "github.com/dlclark/regexp2"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
"gopkg.in/yaml.v2"
)
var reject = adapter.NewProxy(outbound.NewReject())
const (
ReservedName = "default"
)
type ProxySchema struct {
Proxies []map[string]any `yaml:"proxies"`
Proxies []map[string]interface{} `yaml:"proxies"`
}
// for auto gc
@ -40,7 +34,7 @@ type proxySetProvider struct {
}
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"name": pp.Name(),
"type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(),
@ -54,7 +48,7 @@ func (pp *proxySetProvider) Name() string {
}
func (pp *proxySetProvider) HealthCheck() {
pp.healthCheck.checkAll()
pp.healthCheck.check()
}
func (pp *proxySetProvider) Update() error {
@ -83,15 +77,43 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies
}
func (pp *proxySetProvider) Touch() {
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
pp.healthCheck.touch()
return pp.Proxies()
}
func proxiesParse(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 {
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 {
return nil, errors.New("file doesn't have any valid proxy")
}
return proxies, nil
}
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() {
go pp.healthCheck.checkAll()
go pp.healthCheck.check()
}
}
@ -100,12 +122,7 @@ func stopProxyProvider(pd *ProxySetProvider) {
pd.fetcher.Destroy()
}
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
filterReg, err := regexp.Compile(filter, regexp.None)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
func NewProxySetProvider(name string, interval time.Duration, vehicle types.Vehicle, hc *HealthCheck) *ProxySetProvider {
if hc.auto() {
go hc.process()
}
@ -115,56 +132,17 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
healthCheck: hc,
}
onUpdate := func(elm any) {
onUpdate := func(elm interface{}) {
ret := elm.([]C.Proxy)
pd.setProxies(ret)
}
proxiesParseAndFilter := func(buf []byte) (any, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
if name, ok := mapping["name"].(string); ok && len(filter) > 0 {
matched, err := filterReg.MatchString(name)
if err != nil {
return nil, fmt.Errorf("regex filter failed: %w", err)
}
if !matched {
continue
}
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
fetcher := newFetcher(name, interval, vehicle, proxiesParse, onUpdate)
pd.fetcher = fetcher
wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper, nil
return wrapper
}
// for auto gc
@ -179,7 +157,7 @@ type compatibleProvider struct {
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"name": cp.Name(),
"type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(),
@ -192,7 +170,7 @@ func (cp *compatibleProvider) Name() string {
}
func (cp *compatibleProvider) HealthCheck() {
cp.healthCheck.checkAll()
cp.healthCheck.check()
}
func (cp *compatibleProvider) Update() error {
@ -215,8 +193,9 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) Touch() {
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
cp.healthCheck.touch()
return cp.Proxies()
}
func stopCompatibleProvider(pd *CompatibleProvider) {
@ -242,81 +221,3 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil
}
var _ types.ProxyProvider = (*FilterableProvider)(nil)
type FilterableProvider struct {
name string
providers []types.ProxyProvider
filterReg *regexp.Regexp
single *singledo.Single
}
func (fp *FilterableProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": fp.Name(),
"type": fp.Type().String(),
"vehicleType": fp.VehicleType().String(),
"proxies": fp.Proxies(),
})
}
func (fp *FilterableProvider) Name() string {
return fp.name
}
func (fp *FilterableProvider) HealthCheck() {
}
func (fp *FilterableProvider) Update() error {
return nil
}
func (fp *FilterableProvider) Initial() error {
return nil
}
func (fp *FilterableProvider) VehicleType() types.VehicleType {
return types.Compatible
}
func (fp *FilterableProvider) Type() types.ProviderType {
return types.Proxy
}
func (fp *FilterableProvider) Proxies() []C.Proxy {
elm, _, _ := fp.single.Do(func() (any, error) {
proxies := lo.FlatMap(
fp.providers,
func(item types.ProxyProvider, _ int) []C.Proxy {
return lo.Filter(
item.Proxies(),
func(item C.Proxy, _ int) bool {
matched, _ := fp.filterReg.MatchString(item.Name())
return matched
})
})
if len(proxies) == 0 {
proxies = append(proxies, reject)
}
return proxies, nil
})
return elm.([]C.Proxy)
}
func (fp *FilterableProvider) Touch() {
for _, provider := range fp.providers {
provider.Touch()
}
}
func NewFilterableProvider(name string, providers []types.ProxyProvider, filterReg *regexp.Regexp) *FilterableProvider {
return &FilterableProvider{
name: name,
providers: providers,
filterReg: filterReg,
single: singledo.NewSingle(time.Second * 10),
}
}

View File

@ -2,11 +2,10 @@ package provider
import (
"context"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"time"
"github.com/Dreamacro/clash/component/dialer"
@ -26,7 +25,7 @@ func (f *FileVehicle) Path() string {
}
func (f *FileVehicle) Read() ([]byte, error) {
return os.ReadFile(f.path)
return ioutil.ReadFile(f.path)
}
func NewFileVehicle(path string) *FileVehicle {
@ -85,7 +84,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
}
defer resp.Body.Close()
buf, err := io.ReadAll(resp.Body)
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -8,7 +8,7 @@ import (
type Option = func(b *Batch)
type Result struct {
Value any
Value interface{}
Err error
}
@ -38,7 +38,7 @@ type Batch struct {
cancel func()
}
func (b *Batch) Go(key string, fn func() (any, error)) {
func (b *Batch) Go(key string, fn func() (interface{}, error)) {
b.wg.Add(1)
go func() {
defer b.wg.Done()

View File

@ -14,11 +14,11 @@ func TestBatch(t *testing.T) {
b, _ := New(context.Background())
now := time.Now()
b.Go("foo", func() (any, error) {
b.Go("foo", func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return "foo", nil
})
b.Go("bar", func() (any, error) {
b.Go("bar", func() (interface{}, error) {
time.Sleep(time.Millisecond * 150)
return "bar", nil
})
@ -45,7 +45,7 @@ func TestBatchWithConcurrencyNum(t *testing.T) {
now := time.Now()
for i := 0; i < 7; i++ {
idx := i
b.Go(strconv.Itoa(idx), func() (any, error) {
b.Go(strconv.Itoa(idx), func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return strconv.Itoa(idx), nil
})
@ -64,12 +64,12 @@ func TestBatchWithConcurrencyNum(t *testing.T) {
func TestBatchContext(t *testing.T) {
b, ctx := New(context.Background())
b.Go("error", func() (any, error) {
b.Go("error", func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return nil, errors.New("test error")
})
b.Go("ctx", func() (any, error) {
b.Go("ctx", func() (interface{}, error) {
<-ctx.Done()
return nil, ctx.Err()
})

106
common/cache/cache.go vendored Normal file
View File

@ -0,0 +1,106 @@
package cache
import (
"runtime"
"sync"
"time"
)
// Cache store element with a expired time
type Cache struct {
*cache
}
type cache struct {
mapping sync.Map
janitor *janitor
}
type element struct {
Expired time.Time
Payload interface{}
}
// Put element in Cache with its ttl
func (c *cache) Put(key interface{}, payload interface{}, ttl time.Duration) {
c.mapping.Store(key, &element{
Payload: payload,
Expired: time.Now().Add(ttl),
})
}
// Get element in Cache, and drop when it expired
func (c *cache) Get(key interface{}) interface{} {
item, exist := c.mapping.Load(key)
if !exist {
return nil
}
elm := item.(*element)
// expired
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
return nil
}
return elm.Payload
}
// GetWithExpire element in Cache with Expire Time
func (c *cache) GetWithExpire(key interface{}) (payload interface{}, expired time.Time) {
item, exist := c.mapping.Load(key)
if !exist {
return
}
elm := item.(*element)
// expired
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
return
}
return elm.Payload, elm.Expired
}
func (c *cache) cleanup() {
c.mapping.Range(func(k, v interface{}) bool {
key := k.(string)
elm := v.(*element)
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
}
return true
})
}
type janitor struct {
interval time.Duration
stop chan struct{}
}
func (j *janitor) process(c *cache) {
ticker := time.NewTicker(j.interval)
for {
select {
case <-ticker.C:
c.cleanup()
case <-j.stop:
ticker.Stop()
return
}
}
}
func stopJanitor(c *Cache) {
c.janitor.stop <- struct{}{}
}
// New return *Cache
func New(interval time.Duration) *Cache {
j := &janitor{
interval: interval,
stop: make(chan struct{}),
}
c := &cache{janitor: j}
go j.process(c)
C := &Cache{c}
runtime.SetFinalizer(C, stopJanitor)
return C
}

70
common/cache/cache_test.go vendored Normal file
View File

@ -0,0 +1,70 @@
package cache
import (
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCache_Basic(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
c := New(interval)
c.Put("int", 1, ttl)
c.Put("string", "a", ttl)
i := c.Get("int")
assert.Equal(t, i.(int), 1, "should recv 1")
s := c.Get("string")
assert.Equal(t, s.(string), "a", "should recv 'a'")
}
func TestCache_TTL(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
now := time.Now()
c := New(interval)
c.Put("int", 1, ttl)
c.Put("int2", 2, ttl)
i := c.Get("int")
_, expired := c.GetWithExpire("int2")
assert.Equal(t, i.(int), 1, "should recv 1")
assert.True(t, now.Before(expired))
time.Sleep(ttl * 2)
i = c.Get("int")
j, _ := c.GetWithExpire("int2")
assert.Nil(t, i, "should recv nil")
assert.Nil(t, j, "should recv nil")
}
func TestCache_AutoCleanup(t *testing.T) {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New(interval)
c.Put("int", 1, ttl)
time.Sleep(ttl * 2)
i := c.Get("int")
j, _ := c.GetWithExpire("int")
assert.Nil(t, i, "should recv nil")
assert.Nil(t, j, "should recv nil")
}
func TestCache_AutoGC(t *testing.T) {
sign := make(chan struct{})
go func() {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New(interval)
c.Put("int", 1, ttl)
sign <- struct{}{}
}()
<-sign
runtime.GC()
}

View File

@ -12,7 +12,7 @@ import (
type Option func(*LruCache)
// EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback = func(key any, value any)
type EvictCallback = func(key interface{}, value interface{})
// WithEvict set the evict callback
func WithEvict(cb EvictCallback) Option {
@ -57,18 +57,18 @@ type LruCache struct {
maxAge int64
maxSize int
mu sync.Mutex
cache map[any]*list.Element
cache map[interface{}]*list.Element
lru *list.List // Front is least-recent
updateAgeOnGet bool
staleReturn bool
onEvict EvictCallback
}
// New creates an LruCache
func New(options ...Option) *LruCache {
// NewLRUCache creates an LruCache
func NewLRUCache(options ...Option) *LruCache {
lc := &LruCache{
lru: list.New(),
cache: make(map[any]*list.Element),
cache: make(map[interface{}]*list.Element),
}
for _, option := range options {
@ -78,9 +78,9 @@ func New(options ...Option) *LruCache {
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.
func (c *LruCache) Get(key any) (any, bool) {
func (c *LruCache) Get(key interface{}) (interface{}, bool) {
entry := c.get(key)
if entry == nil {
return nil, false
@ -90,11 +90,11 @@ func (c *LruCache) Get(key any) (any, bool) {
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,
// and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool) {
entry := c.get(key)
if entry == nil {
return nil, time.Time{}, false
@ -104,7 +104,7 @@ func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
}
// Exist returns if key exist in cache but not put item to the head of linked list
func (c *LruCache) Exist(key any) bool {
func (c *LruCache) Exist(key interface{}) bool {
c.mu.Lock()
defer c.mu.Unlock()
@ -112,8 +112,8 @@ func (c *LruCache) Exist(key any) bool {
return ok
}
// Set stores the any representation of a response for a given key.
func (c *LruCache) Set(key any, value any) {
// Set stores the interface{} representation of a response for a given key.
func (c *LruCache) Set(key interface{}, value interface{}) {
expires := int64(0)
if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge
@ -121,9 +121,9 @@ func (c *LruCache) Set(key any, value any) {
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.
func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
@ -155,7 +155,7 @@ func (c *LruCache) CloneTo(n *LruCache) {
defer n.mu.Unlock()
n.lru = list.New()
n.cache = make(map[any]*list.Element)
n.cache = make(map[interface{}]*list.Element)
for e := c.lru.Front(); e != nil; e = e.Next() {
elm := e.Value.(*entry)
@ -163,7 +163,7 @@ func (c *LruCache) CloneTo(n *LruCache) {
}
}
func (c *LruCache) get(key any) *entry {
func (c *LruCache) get(key interface{}) *entry {
c.mu.Lock()
defer c.mu.Unlock()
@ -188,7 +188,7 @@ func (c *LruCache) get(key any) *entry {
}
// Delete removes the value associated with a key.
func (c *LruCache) Delete(key any) {
func (c *LruCache) Delete(key interface{}) {
c.mu.Lock()
if le, ok := c.cache[key]; ok {
@ -217,7 +217,7 @@ func (c *LruCache) deleteElement(le *list.Element) {
}
type entry struct {
key any
value any
key interface{}
value interface{}
expires int64
}

View File

@ -19,7 +19,7 @@ var entries = []struct {
}
func TestLRUCache(t *testing.T) {
c := New()
c := NewLRUCache()
for _, e := range entries {
c.Set(e.key, e.value)
@ -45,7 +45,7 @@ func TestLRUCache(t *testing.T) {
}
func TestLRUMaxAge(t *testing.T) {
c := New(WithAge(86400))
c := NewLRUCache(WithAge(86400))
now := time.Now().Unix()
expected := now + 86400
@ -88,7 +88,7 @@ func TestLRUMaxAge(t *testing.T) {
}
func TestLRUpdateOnGet(t *testing.T) {
c := New(WithAge(86400), WithUpdateAgeOnGet())
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
now := time.Now().Unix()
expires := now + 86400/2
@ -103,7 +103,7 @@ func TestLRUpdateOnGet(t *testing.T) {
}
func TestMaxSize(t *testing.T) {
c := New(WithSize(2))
c := NewLRUCache(WithSize(2))
// Add one expired entry
c.Set("foo", "bar")
_, ok := c.Get("foo")
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
}
func TestExist(t *testing.T) {
c := New(WithSize(1))
c := NewLRUCache(WithSize(1))
c.Set(1, 2)
assert.True(t, c.Exist(1))
c.Set(2, 3)
@ -126,11 +126,11 @@ func TestExist(t *testing.T) {
func TestEvict(t *testing.T) {
temp := 0
evict := func(key any, value any) {
evict := func(key interface{}, value interface{}) {
temp = key.(int) + value.(int)
}
c := New(WithEvict(evict), WithSize(1))
c := NewLRUCache(WithEvict(evict), WithSize(1))
c.Set(1, 2)
c.Set(2, 3)
@ -138,7 +138,7 @@ func TestEvict(t *testing.T) {
}
func TestSetWithExpire(t *testing.T) {
c := New(WithAge(1))
c := NewLRUCache(WithAge(1))
now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0)
@ -149,10 +149,11 @@ func TestSetWithExpire(t *testing.T) {
assert.Equal(t, nil, res)
assert.Equal(t, time.Time{}, expires)
assert.Equal(t, false, exist)
}
func TestStale(t *testing.T) {
c := New(WithAge(1), WithStale(true))
c := NewLRUCache(WithAge(1), WithStale(true))
now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0)
@ -165,11 +166,11 @@ func TestStale(t *testing.T) {
}
func TestCloneTo(t *testing.T) {
o := New(WithSize(10))
o := NewLRUCache(WithSize(10))
o.Set("1", 1)
o.Set("2", 2)
n := New(WithSize(2))
n := NewLRUCache(WithSize(2))
n.Set("3", 3)
n.Set("4", 4)

View File

@ -67,6 +67,7 @@ func (d *digest32) bmix(p []byte) (tail []byte) {
}
func (d *digest32) Sum32() (h1 uint32) {
h1 = d.h1
var k1 uint32

View File

@ -11,9 +11,6 @@ type BufferedConn struct {
}
func NewBufferedConn(c net.Conn) *BufferedConn {
if bc, ok := c.(*BufferedConn); ok {
return bc
}
return &BufferedConn{bufio.NewReader(c), c}
}

View File

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

View File

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

View File

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

View File

@ -4,14 +4,14 @@ import (
"sync"
)
type Subscription <-chan any
type Subscription <-chan interface{}
type Subscriber struct {
buffer chan any
buffer chan interface{}
once sync.Once
}
func (s *Subscriber) Emit(item any) {
func (s *Subscriber) Emit(item interface{}) {
s.buffer <- item
}
@ -27,7 +27,7 @@ func (s *Subscriber) Close() {
func newSubscriber() *Subscriber {
sub := &Subscriber{
buffer: make(chan any, 200),
buffer: make(chan interface{}, 200),
}
return sub
}

View File

@ -17,7 +17,7 @@ type Picker struct {
once sync.Once
errOnce sync.Once
result any
result interface{}
err error
}
@ -43,7 +43,7 @@ func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.C
// Wait blocks until all function calls from the Go method have returned,
// then returns the first nil error result (if any) from them.
func (p *Picker) Wait() any {
func (p *Picker) Wait() interface{} {
p.wg.Wait()
if p.cancel != nil {
p.cancel()
@ -58,7 +58,7 @@ func (p *Picker) Error() error {
// Go calls the given function in a new goroutine.
// The first call to return a nil error cancels the group; its result will be returned by Wait.
func (p *Picker) Go(f func() (any, error)) {
func (p *Picker) Go(f func() (interface{}, error)) {
p.wg.Add(1)
go func() {

View File

@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert"
)
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
return func() (any, error) {
func sleepAndSend(ctx context.Context, delay int, input interface{}) func() (interface{}, error) {
return func() (interface{}, error) {
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
select {
case <-timer.C:

View File

@ -23,7 +23,7 @@ func NewAllocator() *Allocator {
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
for k := range alloc.buffers {
i := k
alloc.buffers[k].New = func() any {
alloc.buffers[k].New = func() interface{} {
return make([]byte, 1<<uint32(i))
}
}
@ -32,36 +32,26 @@ func NewAllocator() *Allocator {
// Get a []byte from pool with most appropriate cap
func (alloc *Allocator) Get(size int) []byte {
switch {
case size < 0:
panic("alloc.Get: len out of range")
case size == 0:
if size <= 0 || size > 65536 {
return nil
case size > 65536:
return make([]byte, size)
default:
bits := msb(size)
if size == 1<<bits {
return alloc.buffers[bits].Get().([]byte)[:size]
}
return alloc.buffers[bits+1].Get().([]byte)[:size]
}
bits := msb(size)
if size == 1<<bits {
return alloc.buffers[bits].Get().([]byte)[:size]
}
return alloc.buffers[bits+1].Get().([]byte)[:size]
}
// Put returns a []byte to pool for future use,
// which the cap must be exactly 2^n
func (alloc *Allocator) Put(buf []byte) error {
if cap(buf) == 0 || cap(buf) > 65536 {
return nil
}
bits := msb(cap(buf))
if cap(buf) != 1<<bits {
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
return errors.New("allocator Put() incorrect buffer size")
}
//nolint
//lint:ignore SA6002 ignore temporarily
alloc.buffers[bits].Put(buf)
return nil

View File

@ -19,17 +19,17 @@ func TestAllocGet(t *testing.T) {
assert.Equal(t, 1024, cap(alloc.Get(1023)))
assert.Equal(t, 1024, len(alloc.Get(1024)))
assert.Equal(t, 65536, len(alloc.Get(65536)))
assert.Equal(t, 65537, len(alloc.Get(65537)))
assert.Nil(t, alloc.Get(65537))
}
func TestAllocPut(t *testing.T) {
alloc := NewAllocator()
assert.Nil(t, alloc.Put(nil), "put nil misbehavior")
assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 65536)), "put elem:65536 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
assert.NotNil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
}
func TestAllocPutThenGet(t *testing.T) {

View File

@ -1,31 +0,0 @@
package pool
import (
"bytes"
"sync"
"github.com/Dreamacro/protobytes"
)
var (
bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
bytesBufferPool = sync.Pool{New: func() any { return &protobytes.BytesWriter{} }}
)
func GetBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func PutBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}
func GetBytesBuffer() *protobytes.BytesWriter {
return bytesBufferPool.Get().(*protobytes.BytesWriter)
}
func PutBytesBuffer(buf *protobytes.BytesWriter) {
buf.Reset()
bytesBufferPool.Put(buf)
}

View File

@ -5,11 +5,6 @@ const (
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
RelayBufferSize = 20 * 1024
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
// set to 9000, so the UDP Buffer size set to 16Kib
UDPBufferSize = 16 * 1024
)
func Get(size int) []byte {

View File

@ -6,12 +6,12 @@ import (
// Queue is a simple concurrent safe queue
type Queue struct {
items []any
items []interface{}
lock sync.RWMutex
}
// Put add the item to the queue.
func (q *Queue) Put(items ...any) {
func (q *Queue) Put(items ...interface{}) {
if len(items) == 0 {
return
}
@ -22,7 +22,7 @@ func (q *Queue) Put(items ...any) {
}
// Pop returns the head of items.
func (q *Queue) Pop() any {
func (q *Queue) Pop() interface{} {
if len(q.items) == 0 {
return nil
}
@ -35,7 +35,7 @@ func (q *Queue) Pop() any {
}
// Last returns the last of item.
func (q *Queue) Last() any {
func (q *Queue) Last() interface{} {
if len(q.items) == 0 {
return nil
}
@ -47,8 +47,8 @@ func (q *Queue) Last() any {
}
// Copy get the copy of queue.
func (q *Queue) Copy() []any {
items := []any{}
func (q *Queue) Copy() []interface{} {
items := []interface{}{}
q.lock.RLock()
items = append(items, q.items...)
q.lock.RUnlock()
@ -66,6 +66,6 @@ func (q *Queue) Len() int64 {
// New is a constructor for a new concurrent safe queue.
func New(hint int64) *Queue {
return &Queue{
items: make([]any, 0, hint),
items: make([]interface{}, 0, hint),
}
}

View File

@ -7,7 +7,7 @@ import (
type call struct {
wg sync.WaitGroup
val any
val interface{}
err error
}
@ -20,12 +20,13 @@ type Single struct {
}
type Result struct {
Val any
Val interface{}
Err error
}
// Do single.Do likes sync.singleFlight
func (s *Single) Do(fn func() (any, error)) (v any, 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()
now := time.Now()
if now.Before(s.last.Add(s.wait)) {

View File

@ -12,8 +12,8 @@ import (
func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
foo := 0
shardCount := atomic.NewInt32(0)
call := func() (any, error) {
var shardCount = atomic.NewInt32(0)
call := func() (interface{}, error) {
foo++
time.Sleep(time.Millisecond * 5)
return nil, nil
@ -40,7 +40,7 @@ func TestBasic(t *testing.T) {
func TestTimer(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
foo := 0
call := func() (any, error) {
call := func() (interface{}, error) {
foo++
return nil, nil
}
@ -56,7 +56,7 @@ func TestTimer(t *testing.T) {
func TestReset(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
foo := 0
call := func() (any, error) {
call := func() (interface{}, error) {
foo++
return nil, nil
}

View File

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

View File

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

View File

@ -1,15 +1,12 @@
package structure
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
var (
decoder = NewDecoder(Option{TagName: "test"})
weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
)
var decoder = NewDecoder(Option{TagName: "test"})
var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
type Baz struct {
Foo int `test:"foo"`
@ -27,7 +24,7 @@ type BazOptional struct {
}
func TestStructure_Basic(t *testing.T) {
rawMap := map[string]any{
rawMap := map[string]interface{}{
"foo": 1,
"bar": "test",
"extra": false,
@ -40,12 +37,16 @@ func TestStructure_Basic(t *testing.T) {
s := &Baz{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
}
func TestStructure_Slice(t *testing.T) {
rawMap := map[string]any{
rawMap := map[string]interface{}{
"foo": 1,
"bar": []string{"one", "two"},
}
@ -57,12 +58,16 @@ func TestStructure_Slice(t *testing.T) {
s := &BazSlice{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
}
func TestStructure_Optional(t *testing.T) {
rawMap := map[string]any{
rawMap := map[string]interface{}{
"foo": 1,
}
@ -72,40 +77,50 @@ func TestStructure_Optional(t *testing.T) {
s := &BazOptional{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
}
func TestStructure_MissingKey(t *testing.T) {
rawMap := map[string]any{
rawMap := map[string]interface{}{
"foo": 1,
}
s := &Baz{}
err := decoder.Decode(rawMap, s)
assert.NotNilf(t, err, "should throw error: %#v", s)
if err == nil {
t.Fatalf("should throw error: %#v", s)
}
}
func TestStructure_ParamError(t *testing.T) {
rawMap := map[string]any{}
rawMap := map[string]interface{}{}
s := Baz{}
err := decoder.Decode(rawMap, s)
assert.NotNilf(t, err, "should throw error: %#v", s)
if err == nil {
t.Fatalf("should throw error: %#v", s)
}
}
func TestStructure_SliceTypeError(t *testing.T) {
rawMap := map[string]any{
rawMap := map[string]interface{}{
"foo": 1,
"bar": []int{1, 2},
}
s := &BazSlice{}
err := decoder.Decode(rawMap, s)
assert.NotNilf(t, err, "should throw error: %#v", s)
if err == nil {
t.Fatalf("should throw error: %#v", s)
}
}
func TestStructure_WeakType(t *testing.T) {
rawMap := map[string]any{
rawMap := map[string]interface{}{
"foo": "1",
"bar": []int{1},
}
@ -117,65 +132,10 @@ func TestStructure_WeakType(t *testing.T) {
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal, s)
}
func TestStructure_Nest(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
}
goal := BazOptional{
Foo: 1,
}
s := &struct {
BazOptional
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, s.BazOptional, goal)
}
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
"bar": []any{"bar", nil},
}
goal := &BazSlice{
Foo: 1,
Bar: []string{"bar", ""},
}
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal.Bar, s.Bar)
s = &BazSlice{}
err = decoder.Decode(rawMap, s)
assert.NotNil(t, err)
}
func TestStructure_SliceNilValueComplex(t *testing.T) {
rawMap := map[string]any{
"bar": []any{map[string]any{"bar": "foo"}, nil},
}
s := &struct {
Bar []map[string]any `test:"bar"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Nil(t, s.Bar[1])
ss := &struct {
Bar []Baz `test:"bar"`
}{}
err = decoder.Decode(rawMap, ss)
assert.NotNil(t, err)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
}

View File

@ -36,7 +36,7 @@ func NewAuthenticator(users []AuthUser) Authenticator {
au.storage.Store(user.User, user.Pass)
}
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))
return true
})

View File

@ -14,15 +14,5 @@ func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, er
listenAddr = "255.255.255.255:68"
}
options := []dialer.Option{
dialer.WithInterface(ifaceName),
dialer.WithAddrReuse(true),
}
// fallback bind on windows, because syscall bind can not receive broadcast
if runtime.GOOS == "windows" {
options = append(options, dialer.WithFallbackBind(true))
}
return dialer.ListenPacket(ctx, "udp4", listenAddr, options...)
return dialer.ListenPacket(ctx, "udp4", listenAddr, dialer.WithInterface(ifaceName), dialer.WithAddrReuse(true))
}

View File

@ -3,10 +3,9 @@ package dhcp
import (
"context"
"errors"
"math/rand"
"net"
"github.com/Dreamacro/clash/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4"
)
@ -24,12 +23,7 @@ func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, er
result := make(chan []net.IP, 1)
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
discovery, err := dhcpv4.NewDiscovery(ifaceObj.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
discovery, err := dhcpv4.NewDiscovery(randomHardware(), dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
if err != nil {
return nil, err
}
@ -86,3 +80,15 @@ func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []
return
}
}
func randomHardware() net.HardwareAddr {
addr := make(net.HardwareAddr, 6)
addr[0] = 0xff
for i := 1; i < len(addr); i++ {
addr[i] = byte(rand.Intn(254) + 1)
}
return addr
}

View File

@ -4,9 +4,9 @@ import (
"net"
"syscall"
"github.com/Dreamacro/clash/component/iface"
"golang.org/x/sys/unix"
"github.com/Dreamacro/clash/component/iface"
)
type controlFn = func(network, address string, c syscall.RawConn) error
@ -27,21 +27,14 @@ func bindControl(ifaceIdx int, chain controlFn) controlFn {
}
}
var innerErr error
err = c.Control(func(fd uintptr) {
return c.Control(func(fd uintptr) {
switch network {
case "tcp4", "udp4":
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx)
unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx)
case "tcp6", "udp6":
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx)
unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx)
}
})
if innerErr != nil {
err = innerErr
}
return
}
}

View File

@ -25,16 +25,9 @@ func bindControl(ifaceName string, chain controlFn) controlFn {
}
}
var innerErr error
err = c.Control(func(fd uintptr) {
innerErr = unix.BindToDevice(int(fd), ifaceName)
return c.Control(func(fd uintptr) {
unix.BindToDevice(int(fd), ifaceName)
})
if innerErr != nil {
err = innerErr
}
return
}
}

View File

@ -1,26 +1,72 @@
//go:build !linux && !darwin && !windows
//go:build !linux && !darwin
// +build !linux,!darwin
package dialer
import (
"net"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/iface"
)
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
var addr *net.IPNet
switch network {
case "udp4", "tcp4":
addr, err = ifaceObj.PickIPv4Addr(destination)
case "tcp6", "udp6":
addr, err = ifaceObj.PickIPv6Addr(destination)
default:
if destination != nil {
if destination.To4() != nil {
addr, err = ifaceObj.PickIPv4Addr(destination)
} else {
addr, err = ifaceObj.PickIPv6Addr(destination)
}
} else {
addr, err = ifaceObj.PickIPv4Addr(destination)
}
}
if err != nil {
return nil, err
}
if strings.HasPrefix(network, "tcp") {
return &net.TCPAddr{
IP: addr.IP,
Port: port,
}, nil
} else if strings.HasPrefix(network, "udp") {
return &net.UDPAddr{
IP: addr.IP,
Port: port,
}, nil
}
return nil, iface.ErrAddrNotFound
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
if !destination.IsGlobalUnicast() {
return nil
}
local := uint64(0)
local := 0
if dialer.LocalAddr != nil {
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
if err == nil {
local, _ = strconv.ParseUint(port, 10, 16)
local, _ = strconv.Atoi(port)
}
}
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local))
addr, err := lookupLocalAddr(ifaceName, network, destination, local)
if err != nil {
return err
}
@ -36,9 +82,9 @@ func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, add
port = "0"
}
local, _ := strconv.ParseUint(port, 10, 16)
local, _ := strconv.Atoi(port)
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
addr, err := lookupLocalAddr(ifaceName, network, nil, local)
if err != nil {
return "", err
}

View File

@ -1,98 +0,0 @@
package dialer
import (
"encoding/binary"
"net"
"strings"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/component/iface"
"golang.org/x/sys/windows"
)
const (
IP_UNICAST_IF = 31
IPV6_UNICAST_IF = 31
)
type controlFn = func(network, address string, c syscall.RawConn) error
func bindControl(ifaceIdx int, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
}
var innerErr error
err = c.Control(func(fd uintptr) {
if ipStr == "" && strings.HasPrefix(network, "udp") {
// When listening udp ":0", we should bind socket to interface4 and interface6 at the same time
// and ignore the error of bind6
_ = bindSocketToInterface6(windows.Handle(fd), ifaceIdx)
innerErr = bindSocketToInterface4(windows.Handle(fd), ifaceIdx)
return
}
switch network {
case "tcp4", "udp4":
innerErr = bindSocketToInterface4(windows.Handle(fd), ifaceIdx)
case "tcp6", "udp6":
innerErr = bindSocketToInterface6(windows.Handle(fd), ifaceIdx)
}
})
if innerErr != nil {
err = innerErr
}
return
}
}
func bindSocketToInterface4(handle windows.Handle, ifaceIdx int) error {
// MSDN says for IPv4 this needs to be in net byte order, so that it's like an IP address with leading zeros.
// Ref: https://learn.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
var bytes [4]byte
binary.BigEndian.PutUint32(bytes[:], uint32(ifaceIdx))
index := *(*uint32)(unsafe.Pointer(&bytes[0]))
err := windows.SetsockoptInt(handle, windows.IPPROTO_IP, IP_UNICAST_IF, int(index))
if err != nil {
return err
}
return nil
}
func bindSocketToInterface6(handle windows.Handle, ifaceIdx int) error {
return windows.SetsockoptInt(handle, windows.IPPROTO_IPV6, IPV6_UNICAST_IF, ifaceIdx)
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return err
}
dialer.Control = bindControl(ifaceObj.Index, dialer.Control)
return nil
}
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return "", err
}
lc.Control = bindControl(ifaceObj.Index, lc.Control)
return address, nil
}

View File

@ -36,13 +36,12 @@ func DialContext(ctx context.Context, network, address string, options ...Option
}
func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
cfg := &option{
interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
}
cfg := &config{}
for _, o := range DefaultOptions {
o(cfg)
if !cfg.skipDefault {
for _, o := range DefaultOptions {
o(cfg)
}
}
for _, o := range options {
@ -51,15 +50,7 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
lc := &net.ListenConfig{}
if cfg.interfaceName != "" {
var (
addr string
err error
)
if cfg.fallbackBind {
addr, err = fallbackBindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
} else {
addr, err = bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
}
addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
if err != nil {
return nil, err
}
@ -68,21 +59,17 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
if cfg.addrReuse {
addrReuseToListenConfig(lc)
}
if cfg.routingMark != 0 {
bindMarkToListenConfig(cfg.routingMark, lc, network, address)
}
return lc.ListenPacket(ctx, network, address)
}
func dialContext(ctx context.Context, network string, destination net.IP, port string, options []Option) (net.Conn, error) {
opt := &option{
interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
}
opt := &config{}
for _, o := range DefaultOptions {
o(opt)
if !opt.skipDefault {
for _, o := range DefaultOptions {
o(opt)
}
}
for _, o := range options {
@ -91,19 +78,10 @@ func dialContext(ctx context.Context, network string, destination net.IP, port s
dialer := &net.Dialer{}
if opt.interfaceName != "" {
if opt.fallbackBind {
if err := fallbackBindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err
}
} else {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err
}
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err
}
}
if opt.routingMark != 0 {
bindMarkToDialer(opt.routingMark, dialer, network, destination)
}
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
}

View File

@ -1,90 +0,0 @@
package dialer
import (
"net"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/iface"
)
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
var addr *net.IPNet
switch network {
case "udp4", "tcp4":
addr, err = ifaceObj.PickIPv4Addr(destination)
case "tcp6", "udp6":
addr, err = ifaceObj.PickIPv6Addr(destination)
default:
if destination != nil {
if destination.To4() != nil {
addr, err = ifaceObj.PickIPv4Addr(destination)
} else {
addr, err = ifaceObj.PickIPv6Addr(destination)
}
} else {
addr, err = ifaceObj.PickIPv4Addr(destination)
}
}
if err != nil {
return nil, err
}
if strings.HasPrefix(network, "tcp") {
return &net.TCPAddr{
IP: addr.IP,
Port: port,
}, nil
} else if strings.HasPrefix(network, "udp") {
return &net.UDPAddr{
IP: addr.IP,
Port: port,
}, nil
}
return nil, iface.ErrAddrNotFound
}
func fallbackBindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
if !destination.IsGlobalUnicast() {
return nil
}
local := uint64(0)
if dialer.LocalAddr != nil {
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
if err == nil {
local, _ = strconv.ParseUint(port, 10, 16)
}
}
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local))
if err != nil {
return err
}
dialer.LocalAddr = addr
return nil
}
func fallbackBindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
_, port, err := net.SplitHostPort(address)
if err != nil {
port = "0"
}
local, _ := strconv.ParseUint(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
if err != nil {
return "", err
}
return addr.String(), nil
}

View File

@ -1,43 +0,0 @@
//go:build linux
package dialer
import (
"net"
"syscall"
)
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
dialer.Control = bindMarkToControl(mark, dialer.Control)
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
lc.Control = bindMarkToControl(mark, lc.Control)
}
func bindMarkToControl(mark int, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
}
var innerErr error
err = c.Control(func(fd uintptr) {
innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
})
if innerErr != nil {
err = innerErr
}
return
}
}

View File

@ -1,26 +0,0 @@
//go:build !linux
package dialer
import (
"net"
"sync"
"github.com/Dreamacro/clash/log"
)
var printMarkWarnOnce sync.Once
func printMarkWarn() {
printMarkWarnOnce.Do(func() {
log.Warnln("Routing mark on socket is not supported on current platform")
})
}
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
printMarkWarn()
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
printMarkWarn()
}

View File

@ -1,42 +1,31 @@
package dialer
import "go.uber.org/atomic"
var (
DefaultOptions []Option
DefaultInterface = atomic.NewString("")
DefaultRoutingMark = atomic.NewInt32(0)
DefaultOptions []Option
)
type option struct {
type config struct {
skipDefault bool
interfaceName string
fallbackBind bool
addrReuse bool
routingMark int
}
type Option func(opt *option)
type Option func(opt *config)
func WithInterface(name string) Option {
return func(opt *option) {
return func(opt *config) {
opt.interfaceName = name
}
}
func WithFallbackBind(fallback bool) Option {
return func(opt *option) {
opt.fallbackBind = fallback
}
}
func WithAddrReuse(reuse bool) Option {
return func(opt *option) {
return func(opt *config) {
opt.addrReuse = reuse
}
}
func WithRoutingMark(mark int) Option {
return func(opt *option) {
opt.routingMark = mark
func WithSkipDefault(skip bool) Option {
return func(opt *config) {
opt.skipDefault = skip
}
}

View File

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

View File

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

View File

@ -1,55 +0,0 @@
package fakeip
import (
"net"
"github.com/Dreamacro/clash/component/profile/cachefile"
)
type cachefileStore struct {
cache *cachefile.CacheFile
}
// GetByHost implements store.GetByHost
func (c *cachefileStore) GetByHost(host string) (net.IP, bool) {
elm := c.cache.GetFakeip([]byte(host))
if elm == nil {
return nil, false
}
return net.IP(elm), true
}
// PutByHost implements store.PutByHost
func (c *cachefileStore) PutByHost(host string, ip net.IP) {
c.cache.PutFakeip([]byte(host), ip)
}
// GetByIP implements store.GetByIP
func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
elm := c.cache.GetFakeip(ip.To4())
if elm == nil {
return "", false
}
return string(elm), true
}
// PutByIP implements store.PutByIP
func (c *cachefileStore) PutByIP(ip net.IP, host string) {
c.cache.PutFakeip(ip.To4(), []byte(host))
}
// DelByIP implements store.DelByIP
func (c *cachefileStore) DelByIP(ip net.IP) {
ip = ip.To4()
c.cache.DelFakeipPair(ip, c.cache.GetFakeip(ip.To4()))
}
// Exist implements store.Exist
func (c *cachefileStore) Exist(ip net.IP) bool {
_, exist := c.GetByIP(ip)
return exist
}
// CloneTo implements store.CloneTo
// already persistence
func (c *cachefileStore) CloneTo(store store) {}

View File

@ -1,69 +0,0 @@
package fakeip
import (
"net"
"github.com/Dreamacro/clash/common/cache"
)
type memoryStore struct {
cache *cache.LruCache
}
// GetByHost implements store.GetByHost
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
if elm, exist := m.cache.Get(host); exist {
ip := elm.(net.IP)
// ensure ip --> host on head of linked list
m.cache.Get(ipToUint(ip.To4()))
return ip, true
}
return nil, false
}
// PutByHost implements store.PutByHost
func (m *memoryStore) PutByHost(host string, ip net.IP) {
m.cache.Set(host, ip)
}
// GetByIP implements store.GetByIP
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
host := elm.(string)
// ensure host --> ip on head of linked list
m.cache.Get(host)
return host, true
}
return "", false
}
// PutByIP implements store.PutByIP
func (m *memoryStore) PutByIP(ip net.IP, host string) {
m.cache.Set(ipToUint(ip.To4()), host)
}
// DelByIP implements store.DelByIP
func (m *memoryStore) DelByIP(ip net.IP) {
ipNum := ipToUint(ip.To4())
if elm, exist := m.cache.Get(ipNum); exist {
m.cache.Delete(elm.(string))
}
m.cache.Delete(ipNum)
}
// Exist implements store.Exist
func (m *memoryStore) Exist(ip net.IP) bool {
return m.cache.Exist(ipToUint(ip.To4()))
}
// CloneTo implements store.CloneTo
// only for memoryStore to memoryStore
func (m *memoryStore) CloneTo(store store) {
if ms, ok := store.(*memoryStore); ok {
m.cache.CloneTo(ms.cache)
}
}

View File

@ -3,25 +3,13 @@ package fakeip
import (
"errors"
"net"
"strings"
"sync"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie"
)
type store interface {
GetByHost(host string) (net.IP, bool)
PutByHost(host string, ip net.IP)
GetByIP(ip net.IP) (string, bool)
PutByIP(ip net.IP, host string)
DelByIP(ip net.IP)
Exist(ip net.IP) bool
CloneTo(store)
}
// Pool is an implementation about fake ip generator without storage
// Pool is a implementation about fake ip generator without storage
type Pool struct {
max uint32
min uint32
@ -30,22 +18,25 @@ type Pool struct {
mux sync.Mutex
host *trie.DomainTrie
ipnet *net.IPNet
store store
cache *cache.LruCache
}
// Lookup return a fake ip with host
func (p *Pool) Lookup(host string) net.IP {
p.mux.Lock()
defer p.mux.Unlock()
if elm, exist := p.cache.Get(host); exist {
ip := elm.(net.IP)
// RFC4343: DNS Case Insensitive, we SHOULD return result with all cases.
host = strings.ToLower(host)
if ip, exist := p.store.GetByHost(host); exist {
// ensure ip --> host on head of linked list
n := ipToUint(ip.To4())
offset := n - p.min + 1
p.cache.Get(offset)
return ip
}
ip := p.get(host)
p.store.PutByHost(host, ip)
p.cache.Set(host, ip)
return ip
}
@ -58,11 +49,22 @@ func (p *Pool) LookBack(ip net.IP) (string, bool) {
return "", false
}
return p.store.GetByIP(ip)
n := ipToUint(ip.To4())
offset := n - p.min + 1
if elm, exist := p.cache.Get(offset); exist {
host := elm.(string)
// ensure host --> ip on head of linked list
p.cache.Get(host)
return host, true
}
return "", false
}
// ShouldSkipped return if domain should be skipped
func (p *Pool) ShouldSkipped(domain string) bool {
// LookupHost return if domain in host
func (p *Pool) LookupHost(domain string) bool {
if p.host == nil {
return false
}
@ -78,7 +80,9 @@ func (p *Pool) Exist(ip net.IP) bool {
return false
}
return p.store.Exist(ip)
n := ipToUint(ip.To4())
offset := n - p.min + 1
return p.cache.Exist(offset)
}
// Gateway return gateway ip
@ -91,30 +95,26 @@ func (p *Pool) IPNet() *net.IPNet {
return p.ipnet
}
// CloneFrom clone cache from old pool
func (p *Pool) CloneFrom(o *Pool) {
o.store.CloneTo(p.store)
// PatchFrom clone cache from old pool
func (p *Pool) PatchFrom(o *Pool) {
o.cache.CloneTo(p.cache)
}
func (p *Pool) get(host string) net.IP {
current := p.offset
for {
ip := uintToIP(p.min + p.offset)
if !p.store.Exist(ip) {
break
}
p.offset = (p.offset + 1) % (p.max - p.min)
// Avoid infinite loops
if p.offset == current {
p.offset = (p.offset + 1) % (p.max - p.min)
ip := uintToIP(p.min + p.offset)
p.store.DelByIP(ip)
break
}
if !p.cache.Exist(p.offset) {
break
}
}
ip := uintToIP(p.min + p.offset)
p.store.PutByIP(ip, host)
ip := uintToIP(p.min + p.offset - 1)
p.cache.Set(p.offset, host)
return ip
}
@ -130,24 +130,11 @@ func uintToIP(v uint32) net.IP {
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
}
type Options struct {
IPNet *net.IPNet
Host *trie.DomainTrie
// Size sets the maximum number of entries in memory
// and does not work if Persistence is true
Size int
// Persistence will save the data to disk.
// Size will not work and record will be fully stored.
Persistence bool
}
// New return Pool instance
func New(options Options) (*Pool, error) {
min := ipToUint(options.IPNet.IP) + 2
func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) {
min := ipToUint(ipnet.IP) + 2
ones, bits := options.IPNet.Mask.Size()
ones, bits := ipnet.Mask.Size()
total := 1<<uint(bits-ones) - 2
if total <= 0 {
@ -155,22 +142,12 @@ func New(options Options) (*Pool, error) {
}
max := min + uint32(total) - 1
pool := &Pool{
return &Pool{
min: min,
max: max,
gateway: min - 1,
host: options.Host,
ipnet: options.IPNet,
}
if options.Persistence {
pool.store = &cachefileStore{
cache: cachefile.Cache(),
}
} else {
pool.store = &memoryStore{
cache: cache.New(cache.WithSize(options.Size * 2)),
}
}
return pool, nil
host: host,
ipnet: ipnet,
cache: cache.NewLRUCache(cache.WithSize(size * 2)),
}, nil
}

View File

@ -2,143 +2,38 @@ package fakeip
import (
"net"
"os"
"testing"
"time"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie"
"github.com/stretchr/testify/assert"
"go.etcd.io/bbolt"
)
func createPools(options Options) ([]*Pool, string, error) {
pool, err := New(options)
if err != nil {
return nil, "", err
}
filePool, tempfile, err := createCachefileStore(options)
if err != nil {
return nil, "", err
}
return []*Pool{pool, filePool}, tempfile, nil
}
func createCachefileStore(options Options) (*Pool, string, error) {
pool, err := New(options)
if err != nil {
return nil, "", err
}
f, err := os.CreateTemp("", "clash")
if err != nil {
return nil, "", err
}
db, err := bbolt.Open(f.Name(), 0o666, &bbolt.Options{Timeout: time.Second})
if err != nil {
return nil, "", err
}
pool.store = &cachefileStore{
cache: &cachefile.CacheFile{DB: db},
}
return pool, f.Name(), nil
}
func TestPool_Basic(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
pool, _ := New(ipnet, 10, nil)
for _, pool := range pools {
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.Equal(t, pool.Lookup("foo.com"), net.IP{192, 168, 0, 2})
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.Equal(t, pool.Gateway(), net.IP{192, 168, 0, 1})
assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(net.IP{192, 168, 0, 3}))
assert.False(t, pool.Exist(net.IP{192, 168, 0, 4}))
assert.False(t, pool.Exist(net.ParseIP("::1")))
}
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
}
func TestPool_Case_Insensitive(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
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("Foo.Com")
foo, exist := pool.LookBack(last)
assert.True(t, first.Equal(pool.Lookup("Foo.Com")))
assert.Equal(t, pool.Lookup("fOo.cOM"), first)
assert.True(t, exist)
assert.Equal(t, foo, "foo.com")
}
}
func TestPool_CycleUsed(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
for _, pool := range pools {
assert.Equal(t, net.IP{192, 168, 0, 2}, pool.Lookup("2.com"))
assert.Equal(t, net.IP{192, 168, 0, 3}, pool.Lookup("3.com"))
assert.Equal(t, net.IP{192, 168, 0, 4}, pool.Lookup("4.com"))
assert.Equal(t, net.IP{192, 168, 0, 5}, pool.Lookup("5.com"))
assert.Equal(t, net.IP{192, 168, 0, 6}, pool.Lookup("6.com"))
assert.Equal(t, net.IP{192, 168, 0, 2}, pool.Lookup("12.com"))
assert.Equal(t, net.IP{192, 168, 0, 3}, pool.Lookup("3.com"))
}
}
func TestPool_Skip(t *testing.T) {
func TestPool_Cycle(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
tree := trie.New()
tree.Insert("example.com", tree)
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
Host: tree,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
pool, _ := New(ipnet, 10, nil)
for _, pool := range pools {
assert.True(t, pool.ShouldSkipped("example.com"))
assert.False(t, pool.ShouldSkipped("foo.com"))
}
first := pool.Lookup("foo.com")
same := pool.Lookup("baz.com")
assert.True(t, first.Equal(same))
}
func TestPool_MaxCacheSize(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
pool, _ := New(ipnet, 2, nil)
first := pool.Lookup("foo.com")
pool.Lookup("bar.com")
@ -150,10 +45,7 @@ func TestPool_MaxCacheSize(t *testing.T) {
func TestPool_DoubleMapping(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
pool, _ := New(ipnet, 2, nil)
// fill cache
fooIP := pool.Lookup("foo.com")
@ -178,35 +70,9 @@ func TestPool_DoubleMapping(t *testing.T) {
assert.False(t, bazIP.Equal(newBazIP))
}
func TestPool_Clone(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
newPool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
newPool.CloneFrom(pool)
_, firstExist := newPool.LookBack(first)
_, lastExist := newPool.LookBack(last)
assert.True(t, firstExist)
assert.True(t, lastExist)
}
func TestPool_Error(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
_, err := New(Options{
IPNet: ipnet,
Size: 10,
})
_, err := New(ipnet, 10, nil)
assert.Error(t, err)
}

View File

@ -15,15 +15,13 @@ type Interface struct {
HardwareAddr net.HardwareAddr
}
var (
ErrIfaceNotFound = errors.New("interface not found")
ErrAddrNotFound = errors.New("addr not found")
)
var ErrIfaceNotFound = errors.New("interface not found")
var ErrAddrNotFound = errors.New("addr not found")
var interfaces = singledo.NewSingle(time.Second * 20)
func ResolveInterface(name string) (*Interface, error) {
value, err, _ := interfaces.Do(func() (any, error) {
value, err, _ := interfaces.Do(func() (interface{}, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err

View File

@ -1,22 +0,0 @@
//go:build linux
package ipset
import (
"net"
"github.com/vishvananda/netlink"
)
// Test whether the ip is in the set or not
func Test(setName string, ip net.IP) (bool, error) {
return netlink.IpsetTest(setName, &netlink.IPSetEntry{
IP: ip,
})
}
// Verify dumps a specific ipset to check if we can use the set normally
func Verify(setName string) error {
_, err := netlink.IpsetList(setName)
return err
}

View File

@ -1,17 +0,0 @@
//go:build !linux
package ipset
import (
"net"
)
// Always return false in non-linux
func Test(setName string, ip net.IP) (bool, error) {
return false, nil
}
// Always pass in non-linux
func Verify(setName string) error {
return nil
}

View File

@ -9,10 +9,8 @@ import (
"github.com/oschwald/geoip2-golang"
)
var (
mmdb *geoip2.Reader
once sync.Once
)
var mmdb *geoip2.Reader
var once sync.Once
func LoadFromBytes(buffer []byte) {
once.Do(func() {

View File

@ -6,17 +6,17 @@ import (
"time"
)
type Factory = func(context.Context) (any, error)
type Factory = func(context.Context) (interface{}, error)
type entry struct {
elm any
elm interface{}
time time.Time
}
type Option func(*pool)
// WithEvict set the evict callback
func WithEvict(cb func(any)) Option {
func WithEvict(cb func(interface{})) Option {
return func(p *pool) {
p.evict = cb
}
@ -32,7 +32,7 @@ func WithAge(maxAge int64) Option {
// WithSize defined max size of Pool
func WithSize(maxSize int) Option {
return func(p *pool) {
p.ch = make(chan any, maxSize)
p.ch = make(chan interface{}, maxSize)
}
}
@ -42,13 +42,13 @@ type Pool struct {
}
type pool struct {
ch chan any
ch chan interface{}
factory Factory
evict func(any)
evict func(interface{})
maxAge int64
}
func (p *pool) GetContext(ctx context.Context) (any, error) {
func (p *pool) GetContext(ctx context.Context) (interface{}, error) {
now := time.Now()
for {
select {
@ -68,11 +68,11 @@ func (p *pool) GetContext(ctx context.Context) (any, error) {
}
}
func (p *pool) Get() (any, error) {
func (p *pool) Get() (interface{}, error) {
return p.GetContext(context.Background())
}
func (p *pool) Put(item any) {
func (p *pool) Put(item interface{}) {
e := &entry{
elm: item,
time: time.Now(),
@ -100,7 +100,7 @@ func recycle(p *Pool) {
func New(factory Factory, options ...Option) *Pool {
p := &pool{
ch: make(chan any, 10),
ch: make(chan interface{}, 10),
factory: factory,
}

View File

@ -10,7 +10,7 @@ import (
func lg() Factory {
initial := -1
return func(context.Context) (any, error) {
return func(context.Context) (interface{}, error) {
initial++
return initial, nil
}
@ -34,7 +34,7 @@ func TestPool_MaxSize(t *testing.T) {
size := 5
pool := New(g, WithSize(size))
items := []any{}
items := []interface{}{}
for i := 0; i < size; i++ {
item, _ := pool.Get()

View File

@ -2,7 +2,7 @@ package process
import (
"errors"
"net/netip"
"net"
)
var (
@ -16,6 +16,6 @@ const (
UDP = "udp"
)
func FindProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) {
return findProcessPath(network, from, to)
func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) {
return findProcessName(network, srcIP, srcPort)
}

View File

@ -2,9 +2,8 @@ package process
import (
"encoding/binary"
"net/netip"
"strconv"
"strings"
"net"
"path/filepath"
"syscall"
"unsafe"
@ -17,23 +16,7 @@ const (
proccallnumpidinfo = 0x2
)
var structSize = func() int {
value, _ := syscall.Sysctl("kern.osrelease")
major, _, _ := strings.Cut(value, ".")
n, _ := strconv.ParseInt(major, 10, 64)
switch true {
case n >= 22:
return 408
default:
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
return 384
}
}()
func findProcessPath(network string, from netip.AddrPort, _ netip.AddrPort) (string, error) {
func findProcessName(network string, ip net.IP, port int) (string, error) {
var spath string
switch network {
case TCP:
@ -44,7 +27,7 @@ func findProcessPath(network string, from netip.AddrPort, _ netip.AddrPort) (str
return "", ErrInvalidNetwork
}
isIPv4 := from.Addr().Is4()
isIPv4 := ip.To4() != nil
value, err := syscall.Sysctl(spath)
if err != nil {
@ -52,62 +35,48 @@ func findProcessPath(network string, from netip.AddrPort, _ netip.AddrPort) (str
}
buf := []byte(value)
itemSize := structSize
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
itemSize := 384
if network == TCP {
// rup8(sizeof(xtcpcb_n))
itemSize += 208
}
var fallbackUDPProcess string
// skip the first xinpgen(24 bytes) block
for i := 24; i+itemSize <= len(buf); i += itemSize {
// offset of xinpcb_n and xsocket_n
inp, so := i, i+104
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
if from.Port() != srcPort {
if uint16(port) != srcPort {
continue
}
// FIXME: add dstPort check
// xinpcb_n.inp_vflag
flag := buf[inp+44]
var (
srcIP netip.Addr
srcIPOk bool
srcIsIPv4 bool
)
var srcIP net.IP
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP, srcIPOk = netip.AddrFromSlice(buf[inp+76 : inp+80])
srcIsIPv4 = true
srcIP = net.IP(buf[inp+76 : inp+80])
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP, srcIPOk = netip.AddrFromSlice(buf[inp+64 : inp+80])
srcIP = net.IP(buf[inp+64 : inp+80])
default:
continue
}
if !srcIPOk {
if !ip.Equal(srcIP) {
continue
}
if from.Addr() == srcIP { // FIXME: add dstIP check
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
}
// udp packet connection may be not equal with srcIP
if network == UDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
fallbackUDPProcess, _ = getExecPathFromPID(readNativeUint32(buf[so+68 : so+72]))
}
}
if network == UDP && fallbackUDPProcess != "" {
return fallbackUDPProcess, nil
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
}
return "", ErrNotFound
@ -127,7 +96,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
return "", errno
}
return unix.ByteSliceToString(buf), nil
return filepath.Base(unix.ByteSliceToString(buf)), nil
}
func readNativeUint32(b []byte) uint32 {

View File

@ -1,217 +0,0 @@
package process
import (
"encoding/binary"
"fmt"
"net/netip"
"strconv"
"strings"
"unsafe"
"golang.org/x/sys/unix"
)
type Xinpgen12 [64]byte // size 64
type InEndpoints12 struct {
FPort [2]byte
LPort [2]byte
FAddr [16]byte
LAddr [16]byte
ZoneID uint32
} // size 40
type XTcpcb12 struct {
Len uint32 // offset 0
_ [20]byte // offset 4
SocketAddr uint64 // offset 24
_ [84]byte // offset 32
Family uint32 // offset 116
_ [140]byte // offset 120
InEndpoints InEndpoints12 // offset 260
_ [444]byte // offset 300
} // size 744
type XInpcb12 struct {
Len uint32 // offset 0
_ [12]byte // offset 4
SocketAddr uint64 // offset 16
_ [84]byte // offset 24
Family uint32 // offset 108
_ [140]byte // offset 112
InEndpoints InEndpoints12 // offset 252
_ [108]byte // offset 292
} // size 400
type XFile12 struct {
Size uint64 // offset 0
Pid uint32 // offset 8
_ [44]byte // offset 12
DataAddr uint64 // offset 56
_ [64]byte // offset 64
} // size 128
var majorVersion = func() int {
releaseVersion, err := unix.Sysctl("kern.osrelease")
if err != nil {
return 0
}
majorVersionText, _, _ := strings.Cut(releaseVersion, ".")
majorVersion, err := strconv.Atoi(majorVersionText)
if err != nil {
return 0
}
return majorVersion
}()
func findProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) {
switch majorVersion {
case 12, 13:
return findProcessPath12(network, from, to)
}
return "", ErrPlatformNotSupport
}
func findProcessPath12(network string, from netip.AddrPort, to netip.AddrPort) (string, error) {
switch network {
case TCP:
data, err := unix.SysctlRaw("net.inet.tcp.pcblist")
if err != nil {
return "", err
}
if len(data) < int(unsafe.Sizeof(Xinpgen12{})) {
return "", fmt.Errorf("invalid sysctl data len: %d", len(data))
}
data = data[unsafe.Sizeof(Xinpgen12{}):]
for len(data) > int(unsafe.Sizeof(XTcpcb12{}.Len)) {
tcb := (*XTcpcb12)(unsafe.Pointer(&data[0]))
if tcb.Len < uint32(unsafe.Sizeof(XTcpcb12{})) || uint32(len(data)) < tcb.Len {
break
}
data = data[tcb.Len:]
var (
connFromAddr netip.Addr
connToAddr netip.Addr
)
if tcb.Family == unix.AF_INET {
connFromAddr = netip.AddrFrom4([4]byte(tcb.InEndpoints.LAddr[12:16]))
connToAddr = netip.AddrFrom4([4]byte(tcb.InEndpoints.FAddr[12:16]))
} else if tcb.Family == unix.AF_INET6 {
connFromAddr = netip.AddrFrom16(tcb.InEndpoints.LAddr)
connToAddr = netip.AddrFrom16(tcb.InEndpoints.FAddr)
} else {
continue
}
connFrom := netip.AddrPortFrom(connFromAddr, binary.BigEndian.Uint16(tcb.InEndpoints.LPort[:]))
connTo := netip.AddrPortFrom(connToAddr, binary.BigEndian.Uint16(tcb.InEndpoints.FPort[:]))
if connFrom == from && connTo == to {
pid, err := findPidBySocketAddr12(tcb.SocketAddr)
if err != nil {
return "", err
}
return findExecutableByPid(pid)
}
}
case UDP:
data, err := unix.SysctlRaw("net.inet.udp.pcblist")
if err != nil {
return "", err
}
if len(data) < int(unsafe.Sizeof(Xinpgen12{})) {
return "", fmt.Errorf("invalid sysctl data len: %d", len(data))
}
data = data[unsafe.Sizeof(Xinpgen12{}):]
for len(data) > int(unsafe.Sizeof(XInpcb12{}.Len)) {
icb := (*XInpcb12)(unsafe.Pointer(&data[0]))
if icb.Len < uint32(unsafe.Sizeof(XInpcb12{})) || uint32(len(data)) < icb.Len {
break
}
data = data[icb.Len:]
var connFromAddr netip.Addr
if icb.Family == unix.AF_INET {
connFromAddr = netip.AddrFrom4([4]byte(icb.InEndpoints.LAddr[12:16]))
} else if icb.Family == unix.AF_INET6 {
connFromAddr = netip.AddrFrom16(icb.InEndpoints.LAddr)
} else {
continue
}
connFromPort := binary.BigEndian.Uint16(icb.InEndpoints.LPort[:])
if (connFromAddr == from.Addr() || connFromAddr.IsUnspecified()) && connFromPort == from.Port() {
pid, err := findPidBySocketAddr12(icb.SocketAddr)
if err != nil {
return "", err
}
return findExecutableByPid(pid)
}
}
}
return "", ErrNotFound
}
func findPidBySocketAddr12(socketAddr uint64) (uint32, error) {
buf, err := unix.SysctlRaw("kern.file")
if err != nil {
return 0, err
}
filesLen := len(buf) / int(unsafe.Sizeof(XFile12{}))
files := unsafe.Slice((*XFile12)(unsafe.Pointer(&buf[0])), filesLen)
for _, file := range files {
if file.Size != uint64(unsafe.Sizeof(XFile12{})) {
return 0, fmt.Errorf("invalid xfile size: %d", file.Size)
}
if file.DataAddr == socketAddr {
return file.Pid, nil
}
}
return 0, ErrNotFound
}
func findExecutableByPid(pid uint32) (string, error) {
buf := make([]byte, unix.PathMax)
size := uint64(len(buf))
mib := [4]uint32{
unix.CTL_KERN,
14, // KERN_PROC
12, // KERN_PROC_PATHNAME
pid,
}
_, _, errno := unix.Syscall6(
unix.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
uintptr(len(mib)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
0,
0,
)
if errno != 0 || size == 0 {
return "", fmt.Errorf("sysctl: get proc name: %w", errno)
}
return string(buf[:size-1]), nil
}

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