diff --git a/.github/ISSUE_TEMPLATE/bug_report_en.yml b/.github/ISSUE_TEMPLATE/bug_report_en.yml new file mode 100644 index 0000000..b807998 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_en.yml @@ -0,0 +1,124 @@ +name: (English) Report a bug of the Clash core +description: Create a bug report to help us improve +labels: + - bug +title: "[Bug] " +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)." + diff --git a/.github/ISSUE_TEMPLATE/bug_report_zh.yml b/.github/ISSUE_TEMPLATE/bug_report_zh.yml new file mode 100644 index 0000000..0ecfacb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_zh.yml @@ -0,0 +1,121 @@ +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 语法)" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..aff9d7e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +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 diff --git a/.github/ISSUE_TEMPLATE/feature_request_en.yml b/.github/ISSUE_TEMPLATE/feature_request_en.yml new file mode 100644 index 0000000..5adc13a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_en.yml @@ -0,0 +1,43 @@ +name: (English) Feature request +description: Suggest an idea for this project +labels: + - enhancement +title: "[Feature] " +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?" diff --git a/.github/ISSUE_TEMPLATE/feature_request_zh.yml b/.github/ISSUE_TEMPLATE/feature_request_zh.yml new file mode 100644 index 0000000..8025d41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_zh.yml @@ -0,0 +1,41 @@ +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: 此项非必须,但是如果你有想法的话欢迎提出。 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..52b6afc --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,30 @@ +name: CodeQL + +on: + push: + branches: [master, dev] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: ['go'] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..a289ccc --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,42 @@ +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 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..4c17e8c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,80 @@ +name: Publish Docker Image +on: + push: + branches: + - dev + tags: + - '*' +jobs: + + build: + name: Build + runs-on: ubuntu-latest + steps: + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + with: + platforms: all + + - name: Set up docker buildx + id: buildx + uses: docker/setup-buildx-action@v2 + with: + version: latest + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to Github Package + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: Dreamacro + password: ${{ secrets.PACKAGE_TOKEN }} + + - name: Build dev branch and push + if: github.ref == 'refs/heads/dev' + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 + push: true + tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev' + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Get all docker tags + if: startsWith(github.ref, 'refs/tags/') + uses: actions/github-script@v6 + id: tags + with: + script: | + const ref = context.payload.ref.replace(/\/?refs\/tags\//, '') + const tags = [ + 'dreamacro/clash:latest', + `dreamacro/clash:${ref}`, + 'ghcr.io/dreamacro/clash:latest', + `ghcr.io/dreamacro/clash:${ref}` + ] + return tags.join(',') + result-encoding: string + + - name: Build release and push + if: startsWith(github.ref, 'refs/tags/') + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 + push: true + tags: ${{steps.tags.outputs.result}} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..a9a7f61 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,18 @@ +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.21' + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9dfab87 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Release +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@v4 + with: + check-latest: true + go-version: '1.21' + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Cache go module + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Build + if: startsWith(github.ref, 'refs/tags/') + env: + NAME: clash + BINDIR: bin + run: make -j $(go run ./test/main.go) releases + + - name: Upload Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: bin/* + draft: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..864454b --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,18 @@ + +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v7 + with: + stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days' + days-before-stale: 60 + days-before-close: 5 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..4e10e0b --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,60 @@ +name: Test + +on: [push, pull_request] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@v4 + with: + check-latest: true + go-version: '1.21' + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Cache go module + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Get dependencies, run test + run: | + go test ./... + + build-test: + name: Build Test + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@v4 + with: + check-latest: true + go-version: '1.21' + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Cache go module + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Build + env: + NAME: clash + BINDIR: bin + run: make -j $(go run ./test/main.go) all diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4a739e --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# go mod vendor +vendor + +# GoLand +.idea/* + +# 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 diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..95633e8 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,26 @@ +linters: + disable-all: true + enable: + - gci + - gofumpt + - gosimple + - govet + - ineffassign + - misspell + - staticcheck + - unconvert + - unused + - usestdlibvars + - exhaustive + +linters-settings: + gci: + custom-order: true + sections: + - standard + - prefix(github.com/Dreamacro/clash) + - default + staticcheck: + go: '1.21' + exhaustive: + default-signifies-exhaustive: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..954ede7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM --platform=${BUILDPLATFORM} golang:alpine as builder + +RUN apk add --no-cache make git ca-certificates tzdata && \ + wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb +WORKDIR /workdir +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 + +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/ +COPY --from=builder /Country.mmdb /root/.config/clash/ +COPY --from=builder /clash / +ENTRYPOINT ["/clash"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f81155d --- /dev/null +++ b/Makefile @@ -0,0 +1,148 @@ +NAME=clash +BINDIR=bin +VERSION=$(shell git describe --tags || echo "unknown version") +BUILDTIME=$(shell date -u) +GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \ + -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ + -w -s -buildid=' + +PLATFORM_LIST = \ + darwin-amd64 \ + darwin-amd64-v3 \ + darwin-arm64 \ + linux-386 \ + linux-amd64 \ + linux-amd64-v3 \ + linux-armv5 \ + linux-armv6 \ + linux-armv7 \ + linux-arm64 \ + linux-mips-softfloat \ + linux-mips-hardfloat \ + linux-mipsle-softfloat \ + linux-mipsle-hardfloat \ + linux-mips64 \ + linux-mips64le \ + 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 + +all: linux-amd64 darwin-amd64 windows-amd64 # Most used + +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)-$@ + +linux-386: + GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-amd64: + GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-amd64-v3: + GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-armv5: + GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-armv6: + GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-armv7: + GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-arm64: + GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-mips-softfloat: + GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-mips-hardfloat: + GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-mipsle-softfloat: + GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-mipsle-hardfloat: + GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-mips64: + GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-mips64le: + GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +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)-$@ + +windows-386: + GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe + +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: + GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe + +gz_releases=$(addsuffix .gz, $(PLATFORM_LIST)) +zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST)) + +$(gz_releases): %.gz : % + chmod +x $(BINDIR)/$(NAME)-$(basename $@) + gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@) + +$(zip_releases): %.zip : % + zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe + +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)/* diff --git a/README.md b/README.md index 06ffddc..437da24 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ -# clash-core -backup of clash core +<h1 align="center"> + <img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200"> + <br>Clash<br> +</h1> + +<h4 align="center">A rule-based tunnel in Go.</h4> + +<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"> + </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. + +- 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 + +*Some of the features may only be available in the [Premium core](https://dreamacro.github.io/clash/premium/introduction.html).* + +## Documentation + +You can find the latest documentation at [https://dreamacro.github.io/clash/](https://dreamacro.github.io/clash/). + +## 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) + +## 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) diff --git a/adapter/adapter.go b/adapter/adapter.go new file mode 100644 index 0000000..7feae63 --- /dev/null +++ b/adapter/adapter.go @@ -0,0 +1,206 @@ +package adapter + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/Dreamacro/clash/common/queue" + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" + + "go.uber.org/atomic" +) + +type Proxy struct { + C.ProxyAdapter + history *queue.Queue + alive *atomic.Bool +} + +// Alive implements C.Proxy +func (p *Proxy) Alive() bool { + return p.alive.Load() +} + +// Dial implements C.Proxy +func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) { + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) + defer cancel() + return p.DialContext(ctx, metadata) +} + +// 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) + 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() + histories := []C.DelayHistory{} + for _, item := range queue { + histories = append(histories, item.(C.DelayHistory)) + } + return histories +} + +// LastDelay return last history record. if proxy is not alive, return the max value of uint16. +// implements C.Proxy +func (p *Proxy) LastDelay() (delay uint16) { + var max uint16 = 0xffff + if !p.alive.Load() { + return max + } + + last := p.history.Last() + if last == nil { + return max + } + history := last.(C.DelayHistory) + if history.Delay == 0 { + return max + } + return history.Delay +} + +// MarshalJSON implements C.ProxyAdapter +func (p *Proxy) MarshalJSON() ([]byte, error) { + inner, err := p.ProxyAdapter.MarshalJSON() + if err != nil { + return inner, err + } + + mapping := map[string]any{} + json.Unmarshal(inner, &mapping) + mapping["history"] = p.DelayHistory() + mapping["alive"] = p.Alive() + mapping["name"] = p.Name() + mapping["udp"] = p.SupportUDP() + return json.Marshal(mapping) +} + +// 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) { + defer func() { + p.alive.Store(err == nil) + record := C.DelayHistory{Time: time.Now()} + if err == nil { + record.Delay = delay + record.MeanDelay = meanDelay + } + p.history.Put(record) + if p.history.Len() > 10 { + p.history.Pop() + } + }() + + addr, err := urlToMetadata(url) + if err != nil { + return + } + + start := time.Now() + instance, err := p.DialContext(ctx, &addr) + if err != nil { + return + } + defer instance.Close() + + req, err := http.NewRequest(http.MethodHead, url, nil) + if err != nil { + return + } + req = req.WithContext(ctx) + + transport := &http.Transport{ + Dial: func(string, string) (net.Conn, error) { + return instance, nil + }, + // from http.DefaultTransport + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + + client := http.Client{ + Transport: transport, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + defer client.CloseIdleConnections() + + resp, err := client.Do(req) + if err != nil { + 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) + + return +} + +func NewProxy(adapter C.ProxyAdapter) *Proxy { + return &Proxy{adapter, queue.New(10), atomic.NewBool(true)} +} + +func urlToMetadata(rawURL string) (addr C.Metadata, err error) { + u, err := url.Parse(rawURL) + if err != nil { + return + } + + port := u.Port() + if port == "" { + switch u.Scheme { + case "https": + port = "443" + case "http": + port = "80" + default: + err = fmt.Errorf("%s scheme not Support", rawURL) + return + } + } + + p, _ := strconv.ParseUint(port, 10, 16) + + addr = C.Metadata{ + Host: u.Hostname(), + DstIP: nil, + DstPort: C.Port(p), + } + return +} diff --git a/adapter/inbound/http.go b/adapter/inbound/http.go new file mode 100644 index 0000000..61f7b96 --- /dev/null +++ b/adapter/inbound/http.go @@ -0,0 +1,27 @@ +package inbound + +import ( + "net" + "net/netip" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/context" + "github.com/Dreamacro/clash/transport/socks5" +) + +// 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) + metadata.NetWork = C.TCP + metadata.Type = C.HTTP + if ip, port, err := parseAddr(source); err == nil { + metadata.SrcIP = ip + metadata.SrcPort = C.Port(port) + } + if originTarget != nil { + if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil { + metadata.OriginDst = addrPort + } + } + return context.NewConnContext(conn, metadata) +} diff --git a/adapter/inbound/https.go b/adapter/inbound/https.go new file mode 100644 index 0000000..7b9591d --- /dev/null +++ b/adapter/inbound/https.go @@ -0,0 +1,24 @@ +package inbound + +import ( + "net" + "net/http" + "net/netip" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/context" +) + +// NewHTTPS receive CONNECT request and return ConnContext +func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext { + metadata := parseHTTPAddr(request) + metadata.Type = C.HTTPCONNECT + if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil { + metadata.SrcIP = ip + metadata.SrcPort = C.Port(port) + } + if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil { + metadata.OriginDst = addrPort + } + return context.NewConnContext(conn, metadata) +} diff --git a/adapter/inbound/packet.go b/adapter/inbound/packet.go new file mode 100644 index 0000000..dc1b4d1 --- /dev/null +++ b/adapter/inbound/packet.go @@ -0,0 +1,40 @@ +package inbound + +import ( + "net" + "net/netip" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +// PacketAdapter is a UDP Packet adapter for socks/redir/tun +type PacketAdapter struct { + C.UDPPacket + metadata *C.Metadata +} + +// Metadata returns destination metadata +func (s *PacketAdapter) Metadata() *C.Metadata { + return s.metadata +} + +// NewPacket is PacketAdapter generator +func NewPacket(target socks5.Addr, originTarget net.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter { + metadata := parseSocksAddr(target) + metadata.NetWork = C.UDP + metadata.Type = source + if ip, port, err := parseAddr(packet.LocalAddr()); err == nil { + metadata.SrcIP = ip + metadata.SrcPort = C.Port(port) + } + if originTarget != nil { + if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil { + metadata.OriginDst = addrPort + } + } + return &PacketAdapter{ + UDPPacket: packet, + metadata: metadata, + } +} diff --git a/adapter/inbound/socket.go b/adapter/inbound/socket.go new file mode 100644 index 0000000..aaf8539 --- /dev/null +++ b/adapter/inbound/socket.go @@ -0,0 +1,25 @@ +package inbound + +import ( + "net" + "net/netip" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/context" + "github.com/Dreamacro/clash/transport/socks5" +) + +// NewSocket receive TCP inbound and return ConnContext +func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext { + metadata := parseSocksAddr(target) + metadata.NetWork = C.TCP + metadata.Type = source + if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil { + metadata.SrcIP = ip + metadata.SrcPort = C.Port(port) + } + if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil { + metadata.OriginDst = addrPort + } + return context.NewConnContext(conn, metadata) +} diff --git a/adapter/inbound/util.go b/adapter/inbound/util.go new file mode 100644 index 0000000..6f3d4a7 --- /dev/null +++ b/adapter/inbound/util.go @@ -0,0 +1,66 @@ +package inbound + +import ( + "fmt" + "net" + "net/http" + "strconv" + "strings" + + "github.com/Dreamacro/clash/common/util" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +func parseSocksAddr(target socks5.Addr) *C.Metadata { + metadata := &C.Metadata{} + + switch target[0] { + case socks5.AtypDomainName: + // trim for FQDN + metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".") + metadata.DstPort = C.Port((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) + case socks5.AtypIPv4: + ip := net.IP(target[1 : 1+net.IPv4len]) + metadata.DstIP = ip + metadata.DstPort = C.Port((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) + case socks5.AtypIPv6: + ip := net.IP(target[1 : 1+net.IPv6len]) + metadata.DstIP = ip + metadata.DstPort = C.Port((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) + } + + return metadata +} + +func parseHTTPAddr(request *http.Request) *C.Metadata { + host := request.URL.Hostname() + port, _ := strconv.ParseUint(util.EmptyOr(request.URL.Port(), "80"), 10, 16) + + // trim FQDN (#737) + host = strings.TrimRight(host, ".") + + metadata := &C.Metadata{ + NetWork: C.TCP, + Host: host, + DstIP: nil, + DstPort: C.Port(port), + } + + if ip := net.ParseIP(host); ip != nil { + metadata.DstIP = ip + } + + return metadata +} + +func parseAddr(addr net.Addr) (net.IP, int, error) { + switch a := addr.(type) { + case *net.TCPAddr: + return a.IP, a.Port, nil + case *net.UDPAddr: + return a.IP, a.Port, nil + default: + return nil, 0, fmt.Errorf("unknown address type %s", addr.String()) + } +} diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go new file mode 100644 index 0000000..e911941 --- /dev/null +++ b/adapter/outbound/base.go @@ -0,0 +1,138 @@ +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 implements C.ProxyAdapter +func (b *Base) Name() string { + return b.name +} + +// Type implements C.ProxyAdapter +func (b *Base) Type() C.AdapterType { + return b.tp +} + +// StreamConn implements C.ProxyAdapter +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) { + return nil, errors.New("no support") +} + +// SupportUDP implements C.ProxyAdapter +func (b *Base) SupportUDP() bool { + return b.udp +} + +// MarshalJSON implements C.ProxyAdapter +func (b *Base) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]string{ + "type": b.Type().String(), + }) +} + +// Addr implements C.ProxyAdapter +func (b *Base) Addr() string { + return b.addr +} + +// Unwrap implements C.ProxyAdapter +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, + } +} + +type conn struct { + net.Conn + chain C.Chain +} + +// Chains implements C.Connection +func (c *conn) Chains() C.Chain { + return c.chain +} + +// AppendToChains implements C.Connection +func (c *conn) AppendToChains(a C.ProxyAdapter) { + c.chain = append(c.chain, a.Name()) +} + +func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { + return &conn{c, []string{a.Name()}} +} + +type packetConn struct { + net.PacketConn + chain C.Chain +} + +// Chains implements C.Connection +func (c *packetConn) Chains() C.Chain { + return c.chain +} + +// AppendToChains implements C.Connection +func (c *packetConn) AppendToChains(a C.ProxyAdapter) { + c.chain = append(c.chain, a.Name()) +} + +func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { + return &packetConn{pc, []string{a.Name()}} +} diff --git a/adapter/outbound/direct.go b/adapter/outbound/direct.go new file mode 100644 index 0000000..4c4305f --- /dev/null +++ b/adapter/outbound/direct.go @@ -0,0 +1,46 @@ +package outbound + +import ( + "context" + "net" + + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" +) + +type Direct struct { + *Base +} + +// 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...)...) + if err != nil { + return nil, err + } + tcpKeepAlive(c) + 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...)...) + if err != nil { + return nil, err + } + return newPacketConn(&directPacketConn{pc}, d), nil +} + +type directPacketConn struct { + net.PacketConn +} + +func NewDirect() *Direct { + return &Direct{ + Base: &Base{ + name: "DIRECT", + tp: C.Direct, + udp: true, + }, + } +} diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go new file mode 100644 index 0000000..c435bcc --- /dev/null +++ b/adapter/outbound/http.go @@ -0,0 +1,157 @@ +package outbound + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" +) + +type Http struct { + *Base + 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"` +} + +// 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) + c = cc + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", h.addr, err) + } + } + + if err := h.shakeHand(metadata, c); err != nil { + return nil, err + } + return c, nil +} + +// 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...)...) + 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) + + c, err = h.StreamConn(c, metadata) + if err != nil { + return nil, err + } + + return NewConn(c, h), nil +} + +func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { + addr := metadata.RemoteAddress() + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{ + Host: addr, + }, + Host: addr, + Header: h.Headers.Clone(), + } + + 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))) + } + + if err := req.Write(rw); err != nil { + return err + } + + resp, err := http.ReadResponse(bufio.NewReader(rw), req) + if err != nil { + return err + } + + if resp.StatusCode == http.StatusOK { + return nil + } + + if resp.StatusCode == http.StatusProxyAuthRequired { + return errors.New("HTTP need auth") + } + + if resp.StatusCode == http.StatusMethodNotAllowed { + return errors.New("CONNECT method not allowed by proxy") + } + + if resp.StatusCode >= http.StatusInternalServerError { + return errors.New(resp.Status) + } + + return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode) +} + +func NewHttp(option HttpOption) *Http { + var tlsConfig *tls.Config + if option.TLS { + sni := option.Server + if option.SNI != "" { + sni = option.SNI + } + tlsConfig = &tls.Config{ + InsecureSkipVerify: option.SkipCertVerify, + ServerName: sni, + } + } + + 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, + }, + user: option.UserName, + pass: option.Password, + tlsConfig: tlsConfig, + Headers: headers, + } +} diff --git a/adapter/outbound/reject.go b/adapter/outbound/reject.go new file mode 100644 index 0000000..f475210 --- /dev/null +++ b/adapter/outbound/reject.go @@ -0,0 +1,62 @@ +package outbound + +import ( + "context" + "io" + "net" + "time" + + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" +) + +type Reject struct { + *Base +} + +// 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 +} + +// 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 +} + +func NewReject() *Reject { + return &Reject{ + Base: &Base{ + name: "REJECT", + tp: C.Reject, + udp: true, + }, + } +} + +type nopConn struct{} + +func (rw *nopConn) Read(b []byte) (int, error) { + return 0, io.EOF +} + +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 } + +type nopPacketConn struct{} + +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 } diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go new file mode 100644 index 0000000..e1878df --- /dev/null +++ b/adapter/outbound/shadowsocks.go @@ -0,0 +1,205 @@ +package outbound + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + + "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" +) + +type ShadowSocks struct { + *Base + cipher core.Cipher + + // obfs + obfsMode string + obfsOption *simpleObfsOption + v2rayOption *v2rayObfs.Option +} + +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"` +} + +type simpleObfsOption struct { + Mode string `obfs:"mode,omitempty"` + Host string `obfs:"host,omitempty"` +} + +type v2rayObfsOption struct { + Mode string `obfs:"mode"` + Host string `obfs:"host,omitempty"` + Path string `obfs:"path,omitempty"` + TLS bool `obfs:"tls,omitempty"` + Headers map[string]string `obfs:"headers,omitempty"` + SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` + Mux bool `obfs:"mux,omitempty"` +} + +// StreamConn implements C.ProxyAdapter +func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + switch ss.obfsMode { + case "tls": + c = obfs.NewTLSObfs(c, ss.obfsOption.Host) + case "http": + _, port, _ := net.SplitHostPort(ss.addr) + c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port) + case "websocket": + var err error + c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) + } + } + c = ss.cipher.StreamConn(c) + _, err := c.Write(serializesSocksAddr(metadata)) + return c, err +} + +// 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...)...) + 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) + + 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...)...) + if err != nil { + return nil, err + } + + addr, err := resolveUDPAddr("udp", ss.addr) + if err != nil { + pc.Close() + return nil, err + } + + pc = ss.cipher.PacketConn(pc) + return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil +} + +func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + cipher := option.Cipher + password := option.Password + ciph, err := core.PickCipher(cipher, nil, password) + if err != nil { + return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) + } + + var v2rayOption *v2rayObfs.Option + var obfsOption *simpleObfsOption + obfsMode := "" + + decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) + if option.Plugin == "obfs" { + opts := simpleObfsOption{Host: "bing.com"} + if err := decoder.Decode(option.PluginOpts, &opts); err != nil { + return nil, fmt.Errorf("ss %s initialize obfs error: %w", addr, err) + } + + if opts.Mode != "tls" && opts.Mode != "http" { + return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode) + } + obfsMode = opts.Mode + obfsOption = &opts + } else if option.Plugin == "v2ray-plugin" { + opts := v2rayObfsOption{Host: "bing.com", Mux: true} + if err := decoder.Decode(option.PluginOpts, &opts); err != nil { + return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err) + } + + if opts.Mode != "websocket" { + return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode) + } + obfsMode = opts.Mode + v2rayOption = &v2rayObfs.Option{ + Host: opts.Host, + Path: opts.Path, + Headers: opts.Headers, + Mux: opts.Mux, + } + + if opts.TLS { + v2rayOption.TLS = true + v2rayOption.SkipCertVerify = opts.SkipCertVerify + } + } + + return &ShadowSocks{ + Base: &Base{ + name: option.Name, + addr: addr, + tp: C.Shadowsocks, + udp: option.UDP, + iface: option.Interface, + rmark: option.RoutingMark, + }, + cipher: ciph, + + obfsMode: obfsMode, + v2rayOption: v2rayOption, + obfsOption: obfsOption, + }, nil +} + +type ssPacketConn struct { + net.PacketConn + rAddr net.Addr +} + +func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) + if err != nil { + return + } + return spc.PacketConn.WriteTo(packet[3:], spc.rAddr) +} + +func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, _, e := spc.PacketConn.ReadFrom(b) + if e != nil { + return 0, nil, e + } + + addr := socks5.SplitAddr(b[:n]) + if addr == nil { + return 0, nil, errors.New("parse addr error") + } + + udpAddr := addr.UDPAddr() + if udpAddr == nil { + return 0, nil, errors.New("parse addr error") + } + + copy(b, b[len(addr):]) + return n - len(addr), udpAddr, e +} diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go new file mode 100644 index 0000000..4542eeb --- /dev/null +++ b/adapter/outbound/shadowsocksr.go @@ -0,0 +1,159 @@ +package outbound + +import ( + "context" + "fmt" + "net" + "strconv" + + "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" +) + +type ShadowSocksR struct { + *Base + cipher core.Cipher + obfs obfs.Obfs + protocol protocol.Protocol +} + +type ShadowSocksROption struct { + BasicOption + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Password string `proxy:"password"` + Cipher string `proxy:"cipher"` + Obfs string `proxy:"obfs"` + ObfsParam string `proxy:"obfs-param,omitempty"` + Protocol string `proxy:"protocol"` + ProtocolParam string `proxy:"protocol-param,omitempty"` + UDP bool `proxy:"udp,omitempty"` +} + +// StreamConn implements C.ProxyAdapter +func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + c = ssr.obfs.StreamConn(c) + c = ssr.cipher.StreamConn(c) + var ( + iv []byte + err error + ) + switch conn := c.(type) { + case *shadowstream.Conn: + iv, err = conn.ObtainWriteIV() + if err != nil { + return nil, err + } + case *shadowaead.Conn: + return nil, fmt.Errorf("invalid connection type") + } + c = ssr.protocol.StreamConn(c, iv) + _, err = c.Write(serializesSocksAddr(metadata)) + return c, err +} + +// 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...)...) + 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) + + 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...)...) + if err != nil { + return nil, err + } + + addr, err := resolveUDPAddr("udp", ssr.addr) + if err != nil { + pc.Close() + return nil, err + } + + pc = ssr.cipher.PacketConn(pc) + pc = ssr.protocol.PacketConn(pc) + return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil +} + +func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { + // 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 + coreCiph, err := core.PickCipher(cipher, nil, password) + if err != nil { + return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err) + } + var ( + 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) + } + ivSize = ciph.IVSize() + key = ciph.Key + } + + obfs, obfsOverhead, err := obfs.PickObfs(option.Obfs, &obfs.Base{ + Host: option.Server, + Port: option.Port, + Key: key, + IVSize: ivSize, + Param: option.ObfsParam, + }) + if err != nil { + return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err) + } + + protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{ + Key: key, + Overhead: obfsOverhead, + Param: option.ProtocolParam, + }) + if err != nil { + return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err) + } + + return &ShadowSocksR{ + Base: &Base{ + name: option.Name, + addr: addr, + tp: C.ShadowsocksR, + udp: option.UDP, + iface: option.Interface, + rmark: option.RoutingMark, + }, + cipher: coreCiph, + obfs: obfs, + protocol: protocol, + }, nil +} diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go new file mode 100644 index 0000000..b8fb5a3 --- /dev/null +++ b/adapter/outbound/snell.go @@ -0,0 +1,164 @@ +package outbound + +import ( + "context" + "fmt" + "net" + "strconv" + + "github.com/Dreamacro/clash/common/structure" + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" + obfs "github.com/Dreamacro/clash/transport/simple-obfs" + "github.com/Dreamacro/clash/transport/snell" +) + +type Snell struct { + *Base + psk []byte + pool *snell.Pool + obfsOption *simpleObfsOption + version int +} + +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"` +} + +type streamOption struct { + psk []byte + version int + addr string + obfsOption *simpleObfsOption +} + +func streamConn(c net.Conn, option streamOption) *snell.Snell { + switch option.obfsOption.Mode { + case "tls": + c = obfs.NewTLSObfs(c, option.obfsOption.Host) + case "http": + _, port, _ := net.SplitHostPort(option.addr) + c = obfs.NewHTTPObfs(c, option.obfsOption.Host, port) + } + return snell.StreamConn(c, option.psk, option.version) +} + +// 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}) + err := snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), 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 { + c, err := s.pool.Get() + if err != nil { + return nil, err + } + + if err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version); err != nil { + c.Close() + return nil, err + } + return NewConn(c, s), err + } + + c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...) + 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) + + 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) + + decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) + obfsOption := &simpleObfsOption{Host: "bing.com"} + if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil { + return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err) + } + + switch obfsOption.Mode { + case "tls", "http", "": + break + default: + return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode) + } + + // backward compatible + 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: + 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, + }, + psk: psk, + obfsOption: obfsOption, + version: option.Version, + } + + 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()...) + if err != nil { + return nil, err + } + + tcpKeepAlive(c) + return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil + }) + } + return s, nil +} diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go new file mode 100644 index 0000000..9b11bac --- /dev/null +++ b/adapter/outbound/socks5.go @@ -0,0 +1,214 @@ +package outbound + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/netip" + "strconv" + + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type Socks5 struct { + *Base + user string + pass string + tls bool + skipCertVerify bool + tlsConfig *tls.Config +} + +type Socks5Option 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"` + UDP bool `proxy:"udp,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` +} + +// StreamConn implements C.ProxyAdapter +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) + c = cc + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) + } + } + + var user *socks5.User + if ss.user != "" { + user = &socks5.User{ + Username: ss.user, + Password: ss.pass, + } + } + if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil { + return nil, err + } + return c, nil +} + +// 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...)...) + 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) + + c, err = ss.StreamConn(c, metadata) + if err != nil { + return nil, err + } + + 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...)...) + if err != nil { + err = fmt.Errorf("%s connect error: %w", ss.addr, err) + return + } + + if ss.tls { + cc := tls.Client(c, ss.tlsConfig) + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + err = cc.HandshakeContext(ctx) + c = cc + } + + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + + tcpKeepAlive(c) + var user *socks5.User + if ss.user != "" { + user = &socks5.User{ + Username: ss.user, + Password: ss.pass, + } + } + + udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) + bindAddr, err := socks5.ClientHandshake(c, udpAssocateAddr, socks5.CmdUDPAssociate, user) + if err != nil { + err = fmt.Errorf("client hanshake error: %w", err) + return + } + + pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...) + if err != nil { + return + } + + go func() { + io.Copy(io.Discard, c) + c.Close() + // A UDP association terminates when the TCP connection that the UDP + // ASSOCIATE request arrived on terminates. RFC1928 + pc.Close() + }() + + // Support unspecified UDP bind address. + bindUDPAddr := bindAddr.UDPAddr() + if bindUDPAddr == nil { + err = errors.New("invalid UDP bind address") + return + } else if bindUDPAddr.IP.IsUnspecified() { + serverAddr, err := resolveUDPAddr("udp", ss.Addr()) + if err != nil { + return nil, err + } + + bindUDPAddr.IP = serverAddr.IP + } + + return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil +} + +func NewSocks5(option Socks5Option) *Socks5 { + var tlsConfig *tls.Config + if option.TLS { + tlsConfig = &tls.Config{ + InsecureSkipVerify: option.SkipCertVerify, + ServerName: option.Server, + } + } + + 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, + }, + user: option.UserName, + pass: option.Password, + tls: option.TLS, + skipCertVerify: option.SkipCertVerify, + tlsConfig: tlsConfig, + } +} + +type socksPacketConn struct { + net.PacketConn + rAddr net.Addr + tcpConn net.Conn +} + +func (uc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) + if err != nil { + return + } + return uc.PacketConn.WriteTo(packet, uc.rAddr) +} + +func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, _, e := uc.PacketConn.ReadFrom(b) + if e != nil { + return 0, nil, e + } + addr, payload, err := socks5.DecodeUDPPacket(b) + if err != nil { + return 0, nil, err + } + + udpAddr := addr.UDPAddr() + if udpAddr == nil { + return 0, nil, errors.New("parse udp addr error") + } + + // due to DecodeUDPPacket is mutable, record addr length + copy(b, payload) + return n - len(addr) - 3, udpAddr, nil +} + +func (uc *socksPacketConn) Close() error { + uc.tcpConn.Close() + return uc.PacketConn.Close() +} diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go new file mode 100644 index 0000000..fa549df --- /dev/null +++ b/adapter/outbound/trojan.go @@ -0,0 +1,214 @@ +package outbound + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "strconv" + + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/gun" + "github.com/Dreamacro/clash/transport/trojan" + + "golang.org/x/net/http2" +) + +type Trojan struct { + *Base + instance *trojan.Trojan + option *TrojanOption + + // for gun mux + gunTLSConfig *tls.Config + gunConfig *gun.Config + transport *http2.Transport +} + +type TrojanOption struct { + BasicOption + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Password string `proxy:"password"` + ALPN []string `proxy:"alpn,omitempty"` + SNI string `proxy:"sni,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + 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 +func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + var err error + if t.transport != nil { + c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig) + } else { + c, err = t.plainStream(c) + } + + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) + } + + err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)) + return c, err +} + +// DialContext implements C.ProxyAdapter +func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { + // gun transport + if t.transport != nil && len(opts) == 0 { + c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig) + if err != nil { + return nil, err + } + + if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil { + c.Close() + return nil, err + } + + return NewConn(c, t), nil + } + + c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...) + 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) + + c, err = t.StreamConn(c, metadata) + if err != nil { + return nil, err + } + + 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) { + var c net.Conn + + // grpc transport + if t.transport != nil && len(opts) == 0 { + c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) + } + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + } else { + c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) + } + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + tcpKeepAlive(c) + c, err = t.plainStream(c) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) + } + } + + err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) + if err != nil { + return nil, err + } + + pc := t.instance.PacketConn(c) + return newPacketConn(pc, t), err +} + +func NewTrojan(option TrojanOption) (*Trojan, error) { + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + + tOption := &trojan.Option{ + Password: option.Password, + ALPN: option.ALPN, + ServerName: option.Server, + SkipCertVerify: option.SkipCertVerify, + } + + if option.SNI != "" { + tOption.ServerName = option.SNI + } + + t := &Trojan{ + Base: &Base{ + name: option.Name, + addr: addr, + tp: C.Trojan, + udp: option.UDP, + iface: option.Interface, + rmark: option.RoutingMark, + }, + 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()...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) + } + tcpKeepAlive(c) + return c, nil + } + + tlsConfig := &tls.Config{ + NextProtos: option.ALPN, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: tOption.SkipCertVerify, + ServerName: tOption.ServerName, + } + + t.transport = gun.NewHTTP2Client(dialFn, tlsConfig) + t.gunTLSConfig = tlsConfig + t.gunConfig = &gun.Config{ + ServiceName: option.GrpcOpts.GrpcServiceName, + Host: tOption.ServerName, + } + } + + return t, nil +} diff --git a/adapter/outbound/util.go b/adapter/outbound/util.go new file mode 100644 index 0000000..83ace7b --- /dev/null +++ b/adapter/outbound/util.go @@ -0,0 +1,58 @@ +package outbound + +import ( + "net" + "time" + + "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) { + if tcp, ok := c.(*net.TCPConn); ok { + tcp.SetKeepAlive(true) + tcp.SetKeepAlivePeriod(30 * time.Second) + } +} + +func serializesSocksAddr(metadata *C.Metadata) []byte { + buf := protobytes.BytesWriter{} + + addrType := metadata.AddrType() + buf.PutUint8(uint8(addrType)) + + switch addrType { + case socks5.AtypDomainName: + buf.PutUint8(uint8(len(metadata.Host))) + buf.PutString(metadata.Host) + case socks5.AtypIPv4: + buf.PutSlice(metadata.DstIP.To4()) + case socks5.AtypIPv6: + buf.PutSlice(metadata.DstIP.To16()) + } + + buf.PutUint16be(uint16(metadata.DstPort)) + return buf.Bytes() +} + +func resolveUDPAddr(network, address string) (*net.UDPAddr, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + ip, err := resolver.ResolveIP(host) + if err != nil { + return nil, err + } + return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port)) +} + +func safeConnClose(c net.Conn, err error) { + if err != nil { + c.Close() + } +} diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go new file mode 100644 index 0000000..dd3ba44 --- /dev/null +++ b/adapter/outbound/vmess.go @@ -0,0 +1,384 @@ +package outbound + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "net/http" + "strconv" + "strings" + + "github.com/Dreamacro/clash/component/dialer" + "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 + option *VmessOption + + // for gun mux + gunTLSConfig *tls.Config + gunConfig *gun.Config + transport *http2.Transport +} + +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"` +} + +type HTTPOptions struct { + Method string `proxy:"method,omitempty"` + Path []string `proxy:"path,omitempty"` + Headers map[string][]string `proxy:"headers,omitempty"` +} + +type HTTP2Options struct { + Host []string `proxy:"host,omitempty"` + Path string `proxy:"path,omitempty"` +} + +type GrpcOptions struct { + GrpcServiceName string `proxy:"grpc-service-name,omitempty"` +} + +type WSOptions struct { + Path string `proxy:"path,omitempty"` + Headers map[string]string `proxy:"headers,omitempty"` + MaxEarlyData int `proxy:"max-early-data,omitempty"` + EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"` +} + +// StreamConn implements C.ProxyAdapter +func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + var err error + switch v.option.Network { + case "ws": + host, port, _ := net.SplitHostPort(v.addr) + wsOpts := &vmess.WebsocketConfig{ + Host: host, + Port: port, + Path: v.option.WSOpts.Path, + MaxEarlyData: v.option.WSOpts.MaxEarlyData, + EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, + } + + if len(v.option.WSOpts.Headers) != 0 { + header := http.Header{} + for key, value := range v.option.WSOpts.Headers { + header.Add(key, value) + } + wsOpts.Headers = header + } + + 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 + } + } + c, err = vmess.StreamWebsocketConn(c, wsOpts) + case "http": + // readability first, so just copy default TLS logic + if v.option.TLS { + host, _, _ := net.SplitHostPort(v.addr) + tlsOpts := &vmess.TLSConfig{ + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + } + + if v.option.ServerName != "" { + tlsOpts.Host = v.option.ServerName + } + + c, err = vmess.StreamTLSConn(c, tlsOpts) + if err != nil { + return nil, err + } + } + + host, _, _ := net.SplitHostPort(v.addr) + httpOpts := &vmess.HTTPConfig{ + Host: host, + Method: v.option.HTTPOpts.Method, + Path: v.option.HTTPOpts.Path, + Headers: v.option.HTTPOpts.Headers, + } + + c = vmess.StreamHTTPConn(c, httpOpts) + case "h2": + host, _, _ := net.SplitHostPort(v.addr) + tlsOpts := vmess.TLSConfig{ + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + NextProtos: []string{"h2"}, + } + + if v.option.ServerName != "" { + tlsOpts.Host = v.option.ServerName + } + + c, err = vmess.StreamTLSConn(c, &tlsOpts) + if err != nil { + return nil, err + } + + h2Opts := &vmess.H2Config{ + Hosts: v.option.HTTP2Opts.Host, + Path: v.option.HTTP2Opts.Path, + } + + c, err = vmess.StreamH2Conn(c, h2Opts) + case "grpc": + c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) + default: + // handle TLS + if v.option.TLS { + host, _, _ := net.SplitHostPort(v.addr) + tlsOpts := &vmess.TLSConfig{ + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + } + + if v.option.ServerName != "" { + tlsOpts.Host = v.option.ServerName + } + + c, err = vmess.StreamTLSConn(c, tlsOpts) + } + } + + if err != nil { + return nil, err + } + + return v.client.StreamConn(c, parseVmessAddr(metadata)) +} + +// DialContext implements C.ProxyAdapter +func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { + // gun transport + if v.transport != nil && len(opts) == 0 { + c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig) + if err != nil { + return nil, err + } + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + + c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) + if err != nil { + return nil, err + } + + return NewConn(c, v), nil + } + + c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) + } + tcpKeepAlive(c) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + + 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) { + // vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr + if !metadata.Resolved() { + ip, err := resolver.ResolveIP(metadata.Host) + if err != nil { + return nil, errors.New("can't resolve ip") + } + metadata.DstIP = ip + } + + var c net.Conn + // gun transport + if v.transport != nil && len(opts) == 0 { + c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) + if err != nil { + return nil, err + } + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + + c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) + } else { + c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) + } + tcpKeepAlive(c) + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + + c, err = v.StreamConn(c, metadata) + } + + if err != nil { + return nil, fmt.Errorf("new vmess client error: %v", err) + } + + return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil +} + +func NewVmess(option VmessOption) (*Vmess, error) { + security := strings.ToLower(option.Cipher) + client, err := vmess.NewClient(vmess.Config{ + UUID: option.UUID, + AlterID: uint16(option.AlterID), + Security: security, + HostName: option.Server, + Port: strconv.Itoa(option.Port), + IsAead: option.AlterID == 0, + }) + if err != nil { + return nil, err + } + + switch option.Network { + case "h2", "grpc": + if !option.TLS { + return nil, fmt.Errorf("TLS must be true with h2/grpc network") + } + } + + 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, + }, + client: client, + option: &option, + } + + switch option.Network { + case "h2": + if len(option.HTTP2Opts.Host) == 0 { + option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") + } + case "grpc": + dialFn := func(network, addr string) (net.Conn, error) { + c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) + } + tcpKeepAlive(c) + return c, nil + } + + gunConfig := &gun.Config{ + ServiceName: v.option.GrpcOpts.GrpcServiceName, + Host: v.option.ServerName, + } + tlsConfig := &tls.Config{ + InsecureSkipVerify: v.option.SkipCertVerify, + ServerName: v.option.ServerName, + } + + if v.option.ServerName == "" { + host, _, _ := net.SplitHostPort(v.addr) + tlsConfig.ServerName = host + gunConfig.Host = host + } + + v.gunTLSConfig = tlsConfig + v.gunConfig = gunConfig + v.transport = gun.NewHTTP2Client(dialFn, tlsConfig) + } + + return v, nil +} + +func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr { + var addrType byte + var addr []byte + switch metadata.AddrType() { + case socks5.AtypIPv4: + addrType = vmess.AtypIPv4 + addr = make([]byte, net.IPv4len) + copy(addr[:], metadata.DstIP.To4()) + case socks5.AtypIPv6: + addrType = vmess.AtypIPv6 + addr = make([]byte, net.IPv6len) + copy(addr[:], metadata.DstIP.To16()) + case socks5.AtypDomainName: + addrType = vmess.AtypDomainName + addr = make([]byte, len(metadata.Host)+1) + addr[0] = byte(len(metadata.Host)) + copy(addr[1:], []byte(metadata.Host)) + } + + return &vmess.DstAddr{ + UDP: metadata.NetWork == C.UDP, + AddrType: addrType, + Addr: addr, + Port: uint(metadata.DstPort), + } +} + +type vmessPacketConn struct { + net.Conn + 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) +} + +func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, err := uc.Conn.Read(b) + return n, uc.rAddr, err +} diff --git a/adapter/outboundgroup/common.go b/adapter/outboundgroup/common.go new file mode 100644 index 0000000..b1f0570 --- /dev/null +++ b/adapter/outboundgroup/common.go @@ -0,0 +1,29 @@ +package outboundgroup + +import ( + "time" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" +) + +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.Proxies()...) + } + return proxies +} diff --git a/adapter/outboundgroup/fallback.go b/adapter/outboundgroup/fallback.go new file mode 100644 index 0000000..9af4938 --- /dev/null +++ b/adapter/outboundgroup/fallback.go @@ -0,0 +1,106 @@ +package outboundgroup + +import ( + "context" + "encoding/json" + + "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/common/singledo" + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" +) + +type Fallback struct { + *outbound.Base + disableUDP bool + single *singledo.Single + providers []provider.ProxyProvider +} + +func (f *Fallback) Now() string { + proxy := f.findAliveProxy(false) + return proxy.Name() +} + +// DialContext implements C.ProxyAdapter +func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { + proxy := f.findAliveProxy(true) + c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...) + 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) { + proxy := f.findAliveProxy(true) + pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...) + if err == nil { + pc.AppendToChains(f) + } + return pc, err +} + +// SupportUDP implements C.ProxyAdapter +func (f *Fallback) SupportUDP() bool { + if f.disableUDP { + return false + } + + proxy := f.findAliveProxy(false) + return proxy.SupportUDP() +} + +// MarshalJSON implements C.ProxyAdapter +func (f *Fallback) MarshalJSON() ([]byte, error) { + var all []string + for _, proxy := range f.proxies(false) { + all = append(all, proxy.Name()) + } + return json.Marshal(map[string]any{ + "type": f.Type().String(), + "now": f.Now(), + "all": all, + }) +} + +// Unwrap implements C.ProxyAdapter +func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy { + proxy := f.findAliveProxy(true) + return proxy +} + +func (f *Fallback) proxies(touch bool) []C.Proxy { + elm, _, _ := f.single.Do(func() (any, error) { + return getProvidersProxies(f.providers, touch), nil + }) + + return elm.([]C.Proxy) +} + +func (f *Fallback) findAliveProxy(touch bool) C.Proxy { + proxies := f.proxies(touch) + for _, proxy := range proxies { + if proxy.Alive() { + return proxy + } + } + + return proxies[0] +} + +func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { + return &Fallback{ + Base: outbound.NewBase(outbound.BaseOption{ + Name: option.Name, + Type: C.Fallback, + Interface: option.Interface, + RoutingMark: option.RoutingMark, + }), + single: singledo.NewSingle(defaultGetProxiesDuration), + providers: providers, + disableUDP: option.DisableUDP, + } +} diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go new file mode 100644 index 0000000..95fee89 --- /dev/null +++ b/adapter/outboundgroup/loadbalance.go @@ -0,0 +1,189 @@ +package outboundgroup + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + + "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" + + "golang.org/x/net/publicsuffix" +) + +type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy + +type LoadBalance struct { + *outbound.Base + disableUDP bool + single *singledo.Single + providers []provider.ProxyProvider + strategyFn strategyFn +} + +var errStrategy = errors.New("unsupported strategy") + +func parseStrategy(config map[string]any) string { + if strategy, ok := config["strategy"].(string); ok { + return strategy + } + return "consistent-hashing" +} + +func getKey(metadata *C.Metadata) string { + if metadata.Host != "" { + // ip host + if ip := net.ParseIP(metadata.Host); ip != nil { + return metadata.Host + } + + if etld, err := publicsuffix.EffectiveTLDPlusOne(metadata.Host); err == nil { + return etld + } + } + + if metadata.DstIP == nil { + return "" + } + + return metadata.DstIP.String() +} + +func jumpHash(key uint64, buckets int32) int32 { + var b, j int64 + + for j < int64(buckets) { + b = j + key = key*2862933555777941757 + 1 + j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1))) + } + + return int32(b) +} + +// DialContext implements C.ProxyAdapter +func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) { + defer func() { + if err == nil { + c.AppendToChains(lb) + } + }() + + proxy := lb.Unwrap(metadata) + + c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...) + return +} + +// ListenPacketContext implements C.ProxyAdapter +func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) { + defer func() { + if err == nil { + pc.AppendToChains(lb) + } + }() + + proxy := lb.Unwrap(metadata) + return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...) +} + +// SupportUDP implements C.ProxyAdapter +func (lb *LoadBalance) SupportUDP() bool { + return !lb.disableUDP +} + +func strategyRoundRobin() strategyFn { + idx := 0 + return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { + length := len(proxies) + for i := 0; i < length; i++ { + idx = (idx + 1) % length + proxy := proxies[idx] + if proxy.Alive() { + return proxy + } + } + + return proxies[0] + } +} + +func strategyConsistentHashing() strategyFn { + maxRetry := 5 + return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { + key := uint64(murmur3.Sum32([]byte(getKey(metadata)))) + buckets := int32(len(proxies)) + for i := 0; i < maxRetry; i, key = i+1, key+1 { + idx := jumpHash(key, buckets) + proxy := proxies[idx] + if proxy.Alive() { + return proxy + } + } + + // 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] + } +} + +// Unwrap implements C.ProxyAdapter +func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy { + proxies := lb.proxies(true) + return lb.strategyFn(proxies, metadata) +} + +func (lb *LoadBalance) proxies(touch bool) []C.Proxy { + elm, _, _ := lb.single.Do(func() (any, error) { + return getProvidersProxies(lb.providers, touch), nil + }) + + return elm.([]C.Proxy) +} + +// MarshalJSON implements C.ProxyAdapter +func (lb *LoadBalance) MarshalJSON() ([]byte, error) { + var all []string + for _, proxy := range lb.proxies(false) { + all = append(all, proxy.Name()) + } + return json.Marshal(map[string]any{ + "type": lb.Type().String(), + "all": all, + }) +} + +func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) { + var strategyFn strategyFn + switch strategy { + case "consistent-hashing": + strategyFn = strategyConsistentHashing() + case "round-robin": + strategyFn = strategyRoundRobin() + default: + 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, + }), + single: singledo.NewSingle(defaultGetProxiesDuration), + providers: providers, + strategyFn: strategyFn, + disableUDP: option.DisableUDP, + }, nil +} diff --git a/adapter/outboundgroup/parser.go b/adapter/outboundgroup/parser.go new file mode 100644 index 0000000..1bac3c3 --- /dev/null +++ b/adapter/outboundgroup/parser.go @@ -0,0 +1,166 @@ +package outboundgroup + +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 ( + errFormat = errors.New("format error") + 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") +) + +type GroupCommonOption struct { + outbound.BasicOption + Name string `group:"name"` + Type string `group:"type"` + Proxies []string `group:"proxies,omitempty"` + Use []string `group:"use,omitempty"` + URL string `group:"url,omitempty"` + 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) { + decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) + + groupOption := &GroupCommonOption{ + Lazy: true, + } + if err := decoder.Decode(config, groupOption); err != nil { + return nil, errFormat + } + + if groupOption.Type == "" || groupOption.Name == "" { + 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) + } + + providers := []types.ProxyProvider{} + + if len(groupOption.Proxies) != 0 { + ps, err := getProxies(proxyMap, groupOption.Proxies) + if err != nil { + return nil, fmt.Errorf("%s: %w", groupName, 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" { + hc := provider.NewHealthCheck(ps, "", 0, true) + pd, err := provider.NewCompatibleProvider(groupName, ps, hc) + if err != nil { + return nil, fmt.Errorf("%s: %w", groupName, err) + } + + providers = append(providers, pd) + providersMap[groupName] = pd + } else { + if groupOption.URL == "" || groupOption.Interval == 0 { + return nil, fmt.Errorf("%s: %w", groupName, errMissHealthCheck) + } + + 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) + } + + 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...) + } + } + + var group C.ProxyAdapter + switch groupOption.Type { + case "url-test": + opts := parseURLTestOption(config) + group = NewURLTest(groupOption, providers, opts...) + case "select": + group = NewSelector(groupOption, providers) + case "fallback": + group = NewFallback(groupOption, providers) + case "load-balance": + strategy := parseStrategy(config) + return NewLoadBalance(groupOption, providers, strategy) + case "relay": + group = NewRelay(groupOption, providers) + default: + return nil, fmt.Errorf("%s %w: %s", groupName, errType, groupOption.Type) + } + + return group, nil +} + +func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) { + var ps []C.Proxy + for _, name := range list { + p, ok := mapping[name] + if !ok { + return nil, fmt.Errorf("'%s' not found", name) + } + ps = append(ps, p) + } + return ps, nil +} + +func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) { + var ps []types.ProxyProvider + for _, name := range list { + p, ok := mapping[name] + if !ok { + return nil, fmt.Errorf("'%s' not found", name) + } + + if p.VehicleType() == types.Compatible { + return nil, fmt.Errorf("proxy group %s can't contains in `use`", name) + } + ps = append(ps, p) + } + return ps, nil +} diff --git a/adapter/outboundgroup/relay.go b/adapter/outboundgroup/relay.go new file mode 100644 index 0000000..03e6982 --- /dev/null +++ b/adapter/outboundgroup/relay.go @@ -0,0 +1,114 @@ +package outboundgroup + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/common/singledo" + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" +) + +type Relay struct { + *outbound.Base + single *singledo.Single + providers []provider.ProxyProvider +} + +// DialContext implements C.ProxyAdapter +func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { + var proxies []C.Proxy + for _, proxy := range r.proxies(metadata, true) { + if proxy.Type() != C.Direct { + proxies = append(proxies, proxy) + } + } + + switch len(proxies) { + case 0: + return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...) + case 1: + return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...) + } + + first := proxies[0] + last := proxies[len(proxies)-1] + + c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) + } + tcpKeepAlive(c) + + var currentMeta *C.Metadata + for _, proxy := range proxies[1:] { + currentMeta, err = addrToMetadata(proxy.Addr()) + if err != nil { + return nil, err + } + + c, err = first.StreamConn(c, currentMeta) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) + } + + first = proxy + } + + c, err = last.StreamConn(c, metadata) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err) + } + + return outbound.NewConn(c, r), nil +} + +// MarshalJSON implements C.ProxyAdapter +func (r *Relay) MarshalJSON() ([]byte, error) { + var all []string + for _, proxy := range r.rawProxies(false) { + all = append(all, proxy.Name()) + } + return json.Marshal(map[string]any{ + "type": r.Type().String(), + "all": all, + }) +} + +func (r *Relay) rawProxies(touch bool) []C.Proxy { + elm, _, _ := r.single.Do(func() (any, error) { + return getProvidersProxies(r.providers, touch), nil + }) + + return elm.([]C.Proxy) +} + +func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy { + proxies := r.rawProxies(touch) + + for n, proxy := range proxies { + subproxy := proxy.Unwrap(metadata) + for subproxy != nil { + proxies[n] = subproxy + subproxy = subproxy.Unwrap(metadata) + } + } + + return proxies +} + +func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay { + return &Relay{ + Base: outbound.NewBase(outbound.BaseOption{ + Name: option.Name, + Type: C.Relay, + Interface: option.Interface, + RoutingMark: option.RoutingMark, + }), + single: singledo.NewSingle(defaultGetProxiesDuration), + providers: providers, + } +} diff --git a/adapter/outboundgroup/selector.go b/adapter/outboundgroup/selector.go new file mode 100644 index 0000000..3975df7 --- /dev/null +++ b/adapter/outboundgroup/selector.go @@ -0,0 +1,114 @@ +package outboundgroup + +import ( + "context" + "encoding/json" + "errors" + + "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/common/singledo" + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" +) + +type Selector struct { + *outbound.Base + disableUDP bool + single *singledo.Single + selected string + providers []provider.ProxyProvider +} + +// 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...)...) + 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...)...) + if err == nil { + pc.AppendToChains(s) + } + return pc, err +} + +// SupportUDP implements C.ProxyAdapter +func (s *Selector) SupportUDP() bool { + if s.disableUDP { + return false + } + + return s.selectedProxy(false).SupportUDP() +} + +// MarshalJSON implements C.ProxyAdapter +func (s *Selector) MarshalJSON() ([]byte, error) { + var all []string + for _, proxy := range getProvidersProxies(s.providers, false) { + all = append(all, proxy.Name()) + } + + return json.Marshal(map[string]any{ + "type": s.Type().String(), + "now": s.Now(), + "all": all, + }) +} + +func (s *Selector) Now() string { + return s.selectedProxy(false).Name() +} + +func (s *Selector) Set(name string) error { + for _, proxy := range getProvidersProxies(s.providers, false) { + if proxy.Name() == name { + s.selected = name + s.single.Reset() + return nil + } + } + + return errors.New("proxy not exist") +} + +// Unwrap implements C.ProxyAdapter +func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy { + return s.selectedProxy(true) +} + +func (s *Selector) selectedProxy(touch bool) C.Proxy { + elm, _, _ := s.single.Do(func() (any, error) { + proxies := getProvidersProxies(s.providers, touch) + for _, proxy := range proxies { + if proxy.Name() == s.selected { + return proxy, nil + } + } + + return proxies[0], nil + }) + + return elm.(C.Proxy) +} + +func NewSelector(option *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, + }), + single: singledo.NewSingle(defaultGetProxiesDuration), + providers: providers, + selected: selected, + disableUDP: option.DisableUDP, + } +} diff --git a/adapter/outboundgroup/urltest.go b/adapter/outboundgroup/urltest.go new file mode 100644 index 0000000..79234ec --- /dev/null +++ b/adapter/outboundgroup/urltest.go @@ -0,0 +1,157 @@ +package outboundgroup + +import ( + "context" + "encoding/json" + "time" + + "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/common/singledo" + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" +) + +type urlTestOption func(*URLTest) + +func urlTestWithTolerance(tolerance uint16) urlTestOption { + return func(u *URLTest) { + u.tolerance = tolerance + } +} + +type URLTest struct { + *outbound.Base + tolerance uint16 + disableUDP bool + fastNode C.Proxy + single *singledo.Single + fastSingle *singledo.Single + providers []provider.ProxyProvider +} + +func (u *URLTest) Now() string { + return u.fast(false).Name() +} + +// 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...)...) + 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...)...) + if err == nil { + pc.AppendToChains(u) + } + return pc, err +} + +// Unwrap implements C.ProxyAdapter +func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy { + return u.fast(true) +} + +func (u *URLTest) proxies(touch bool) []C.Proxy { + elm, _, _ := u.single.Do(func() (any, error) { + return getProvidersProxies(u.providers, touch), nil + }) + + return elm.([]C.Proxy) +} + +func (u *URLTest) fast(touch bool) C.Proxy { + elm, _, shared := u.fastSingle.Do(func() (any, error) { + proxies := u.proxies(touch) + fast := proxies[0] + min := fast.LastDelay() + fastNotExist := true + + for _, proxy := range proxies[1:] { + if u.fastNode != nil && proxy.Name() == u.fastNode.Name() { + fastNotExist = false + } + + if !proxy.Alive() { + continue + } + + delay := proxy.LastDelay() + if delay < min { + fast = proxy + min = delay + } + } + + // tolerance + if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance { + u.fastNode = fast + } + + 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) +} + +// SupportUDP implements C.ProxyAdapter +func (u *URLTest) SupportUDP() bool { + if u.disableUDP { + return false + } + + return u.fast(false).SupportUDP() +} + +// MarshalJSON implements C.ProxyAdapter +func (u *URLTest) MarshalJSON() ([]byte, error) { + var all []string + for _, proxy := range u.proxies(false) { + all = append(all, proxy.Name()) + } + return json.Marshal(map[string]any{ + "type": u.Type().String(), + "now": u.Now(), + "all": all, + }) +} + +func parseURLTestOption(config map[string]any) []urlTestOption { + opts := []urlTestOption{} + + // tolerance + if tolerance, ok := config["tolerance"].(int); ok { + opts = append(opts, urlTestWithTolerance(uint16(tolerance))) + } + + return opts +} + +func NewURLTest(option *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, + }), + single: singledo.NewSingle(defaultGetProxiesDuration), + fastSingle: singledo.NewSingle(time.Second * 10), + providers: providers, + disableUDP: option.DisableUDP, + } + + for _, option := range options { + option(urlTest) + } + + return urlTest +} diff --git a/adapter/outboundgroup/util.go b/adapter/outboundgroup/util.go new file mode 100644 index 0000000..15cb87e --- /dev/null +++ b/adapter/outboundgroup/util.go @@ -0,0 +1,50 @@ +package outboundgroup + +import ( + "fmt" + "net" + "strconv" + "time" + + C "github.com/Dreamacro/clash/constant" +) + +func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) { + host, port, err := net.SplitHostPort(rawAddress) + if err != nil { + err = fmt.Errorf("addrToMetadata failed: %w", err) + return + } + + ip := net.ParseIP(host) + p, _ := strconv.ParseUint(port, 10, 16) + if ip == nil { + addr = &C.Metadata{ + Host: host, + DstIP: nil, + DstPort: C.Port(p), + } + return + } else if ip4 := ip.To4(); ip4 != nil { + addr = &C.Metadata{ + Host: "", + DstIP: ip4, + DstPort: C.Port(p), + } + return + } + + addr = &C.Metadata{ + Host: "", + DstIP: ip, + DstPort: C.Port(p), + } + return +} + +func tcpKeepAlive(c net.Conn) { + if tcp, ok := c.(*net.TCPConn); ok { + tcp.SetKeepAlive(true) + tcp.SetKeepAlivePeriod(30 * time.Second) + } +} diff --git a/adapter/parser.go b/adapter/parser.go new file mode 100644 index 0000000..4ee7b48 --- /dev/null +++ b/adapter/parser.go @@ -0,0 +1,86 @@ +package adapter + +import ( + "fmt" + + "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/common/structure" + C "github.com/Dreamacro/clash/constant" +) + +func ParseProxy(mapping map[string]any) (C.Proxy, error) { + decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true}) + proxyType, existType := mapping["type"].(string) + if !existType { + return nil, fmt.Errorf("missing type") + } + + var ( + proxy C.ProxyAdapter + err error + ) + switch proxyType { + case "ss": + ssOption := &outbound.ShadowSocksOption{} + err = decoder.Decode(mapping, ssOption) + if err != nil { + break + } + proxy, err = outbound.NewShadowSocks(*ssOption) + case "ssr": + ssrOption := &outbound.ShadowSocksROption{} + err = decoder.Decode(mapping, ssrOption) + if err != nil { + break + } + proxy, err = outbound.NewShadowSocksR(*ssrOption) + case "socks5": + socksOption := &outbound.Socks5Option{} + err = decoder.Decode(mapping, socksOption) + if err != nil { + break + } + proxy = outbound.NewSocks5(*socksOption) + case "http": + httpOption := &outbound.HttpOption{} + err = decoder.Decode(mapping, httpOption) + if err != nil { + break + } + proxy = outbound.NewHttp(*httpOption) + case "vmess": + vmessOption := &outbound.VmessOption{ + HTTPOpts: outbound.HTTPOptions{ + Method: "GET", + Path: []string{"/"}, + }, + } + err = decoder.Decode(mapping, vmessOption) + if err != nil { + break + } + proxy, err = outbound.NewVmess(*vmessOption) + case "snell": + snellOption := &outbound.SnellOption{} + err = decoder.Decode(mapping, snellOption) + if err != nil { + break + } + proxy, err = outbound.NewSnell(*snellOption) + case "trojan": + trojanOption := &outbound.TrojanOption{} + err = decoder.Decode(mapping, trojanOption) + if err != nil { + break + } + proxy, err = outbound.NewTrojan(*trojanOption) + default: + return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) + } + + if err != nil { + return nil, err + } + + return NewProxy(proxy), nil +} diff --git a/adapter/provider/fetcher.go b/adapter/provider/fetcher.go new file mode 100644 index 0000000..297e82f --- /dev/null +++ b/adapter/provider/fetcher.go @@ -0,0 +1,197 @@ +package provider + +import ( + "bytes" + "crypto/md5" + "os" + "path/filepath" + "time" + + types "github.com/Dreamacro/clash/constant/provider" + "github.com/Dreamacro/clash/log" +) + +var ( + fileMode os.FileMode = 0o666 + dirMode os.FileMode = 0o755 +) + +type parser = func([]byte) (any, error) + +type fetcher struct { + name string + vehicle types.Vehicle + interval time.Duration + updatedAt *time.Time + ticker *time.Ticker + done chan struct{} + hash [16]byte + parser parser + onUpdate func(any) +} + +func (f *fetcher) Name() string { + return f.name +} + +func (f *fetcher) VehicleType() types.VehicleType { + return f.vehicle.Type() +} + +func (f *fetcher) Initial() (any, error) { + var ( + buf []byte + err error + isLocal bool + immediatelyUpdate bool + ) + if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil { + buf, err = os.ReadFile(f.vehicle.Path()) + modTime := stat.ModTime() + f.updatedAt = &modTime + isLocal = true + immediatelyUpdate = time.Since(modTime) > f.interval + } else { + buf, err = f.vehicle.Read() + } + + if err != nil { + return nil, err + } + + proxies, err := f.parser(buf) + if err != nil { + if !isLocal { + return nil, err + } + + // parse local file error, fallback to remote + buf, err = f.vehicle.Read() + if err != nil { + return nil, err + } + + proxies, err = f.parser(buf) + if err != nil { + return nil, err + } + + isLocal = false + } + + if f.vehicle.Type() != types.File && !isLocal { + if err := safeWrite(f.vehicle.Path(), buf); err != nil { + return nil, err + } + } + + f.hash = md5.Sum(buf) + + // pull proxies automatically + if f.ticker != nil { + go f.pullLoop(immediatelyUpdate) + } + + return proxies, nil +} + +func (f *fetcher) Update() (any, bool, error) { + buf, err := f.vehicle.Read() + if err != nil { + return nil, false, err + } + + now := time.Now() + hash := md5.Sum(buf) + if bytes.Equal(f.hash[:], hash[:]) { + f.updatedAt = &now + os.Chtimes(f.vehicle.Path(), now, now) + return nil, true, nil + } + + proxies, err := f.parser(buf) + if err != nil { + return nil, false, err + } + + if f.vehicle.Type() != types.File { + if err := safeWrite(f.vehicle.Path(), buf); err != nil { + return nil, false, err + } + } + + f.updatedAt = &now + f.hash = hash + + return proxies, false, nil +} + +func (f *fetcher) Destroy() error { + if f.ticker != nil { + f.done <- struct{}{} + } + return nil +} + +func (f *fetcher) pullLoop(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() + } + + for { + select { + case <-f.ticker.C: + update() + case <-f.done: + f.ticker.Stop() + return + } + } +} + +func safeWrite(path string, buf []byte) error { + dir := filepath.Dir(path) + + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, dirMode); err != nil { + return err + } + } + + return os.WriteFile(path, buf, fileMode) +} + +func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher { + var ticker *time.Ticker + if interval != 0 { + ticker = time.NewTicker(interval) + } + + return &fetcher{ + name: name, + ticker: ticker, + vehicle: vehicle, + interval: interval, + parser: parser, + done: make(chan struct{}, 1), + onUpdate: onUpdate, + } +} diff --git a/adapter/provider/healthcheck.go b/adapter/provider/healthcheck.go new file mode 100644 index 0000000..8f66a95 --- /dev/null +++ b/adapter/provider/healthcheck.go @@ -0,0 +1,100 @@ +package provider + +import ( + "context" + "time" + + "github.com/Dreamacro/clash/common/batch" + C "github.com/Dreamacro/clash/constant" + + "github.com/samber/lo" + "go.uber.org/atomic" +) + +const ( + defaultURLTestTimeout = time.Second * 5 +) + +type HealthCheckOption struct { + URL string + Interval uint +} + +type HealthCheck struct { + url string + proxies []C.Proxy + interval uint + lazy bool + lastTouch *atomic.Int64 + done chan struct{} +} + +func (hc *HealthCheck) process() { + ticker := time.NewTicker(time.Duration(hc.interval) * time.Second) + + go hc.checkAll() + 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) + } + } + case <-hc.done: + ticker.Stop() + return + } + } +} + +func (hc *HealthCheck) setProxy(proxies []C.Proxy) { + hc.proxies = proxies +} + +func (hc *HealthCheck) auto() bool { + return hc.interval != 0 +} + +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) { + b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10)) + for _, proxy := range proxies { + p := proxy + b.Go(p.Name(), func() (any, error) { + ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) + defer cancel() + p.URLTest(ctx, hc.url) + return nil, nil + }) + } + b.Wait() +} + +func (hc *HealthCheck) close() { + hc.done <- struct{}{} +} + +func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck { + return &HealthCheck{ + proxies: proxies, + url: url, + interval: interval, + lazy: lazy, + lastTouch: atomic.NewInt64(0), + done: make(chan struct{}, 1), + } +} diff --git a/adapter/provider/parser.go b/adapter/provider/parser.go new file mode 100644 index 0000000..e248d2c --- /dev/null +++ b/adapter/provider/parser.go @@ -0,0 +1,70 @@ +package provider + +import ( + "errors" + "fmt" + "time" + + "github.com/Dreamacro/clash/common/structure" + C "github.com/Dreamacro/clash/constant" + types "github.com/Dreamacro/clash/constant/provider" +) + +var ( + errVehicleType = errors.New("unsupport vehicle type") + errSubPath = errors.New("path is not subpath of home directory") +) + +type healthCheckSchema struct { + Enable bool `provider:"enable"` + URL string `provider:"url"` + Interval int `provider:"interval"` + Lazy bool `provider:"lazy,omitempty"` +} + +type proxyProviderSchema struct { + Type string `provider:"type"` + Path string `provider:"path"` + URL string `provider:"url,omitempty"` + Interval int `provider:"interval,omitempty"` + Filter string `provider:"filter,omitempty"` + HealthCheck healthCheckSchema `provider:"health-check,omitempty"` +} + +func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) { + decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) + + schema := &proxyProviderSchema{ + HealthCheck: healthCheckSchema{ + Lazy: true, + }, + } + if err := decoder.Decode(mapping, schema); err != nil { + return nil, err + } + + var hcInterval uint + if schema.HealthCheck.Enable { + hcInterval = uint(schema.HealthCheck.Interval) + } + hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy) + + path := C.Path.Resolve(schema.Path) + + var vehicle types.Vehicle + switch schema.Type { + 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) +} diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go new file mode 100644 index 0000000..c187307 --- /dev/null +++ b/adapter/provider/provider.go @@ -0,0 +1,322 @@ +package provider + +import ( + "encoding/json" + "errors" + "fmt" + "runtime" + "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" +) + +var reject = adapter.NewProxy(outbound.NewReject()) + +const ( + ReservedName = "default" +) + +type ProxySchema struct { + Proxies []map[string]any `yaml:"proxies"` +} + +// for auto gc +type ProxySetProvider struct { + *proxySetProvider +} + +type proxySetProvider struct { + *fetcher + proxies []C.Proxy + healthCheck *HealthCheck +} + +func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "name": pp.Name(), + "type": pp.Type().String(), + "vehicleType": pp.VehicleType().String(), + "proxies": pp.Proxies(), + "updatedAt": pp.updatedAt, + }) +} + +func (pp *proxySetProvider) Name() string { + return pp.name +} + +func (pp *proxySetProvider) HealthCheck() { + pp.healthCheck.checkAll() +} + +func (pp *proxySetProvider) Update() error { + elm, same, err := pp.fetcher.Update() + if err == nil && !same { + pp.onUpdate(elm) + } + return err +} + +func (pp *proxySetProvider) Initial() error { + elm, err := pp.fetcher.Initial() + if err != nil { + return err + } + + pp.onUpdate(elm) + return nil +} + +func (pp *proxySetProvider) Type() types.ProviderType { + return types.Proxy +} + +func (pp *proxySetProvider) Proxies() []C.Proxy { + return pp.proxies +} + +func (pp *proxySetProvider) Touch() { + pp.healthCheck.touch() +} + +func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { + pp.proxies = proxies + pp.healthCheck.setProxy(proxies) + if pp.healthCheck.auto() { + go pp.healthCheck.checkAll() + } +} + +func stopProxyProvider(pd *ProxySetProvider) { + pd.healthCheck.close() + 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) + } + + if hc.auto() { + go hc.process() + } + + pd := &proxySetProvider{ + proxies: []C.Proxy{}, + healthCheck: hc, + } + + onUpdate := func(elm any) { + ret := elm.([]C.Proxy) + pd.setProxies(ret) + } + + proxiesParseAndFilter := func(buf []byte) (any, error) { + schema := &ProxySchema{} + + if err := yaml.Unmarshal(buf, schema); err != nil { + return nil, err + } + + if schema.Proxies == nil { + return nil, errors.New("file must have a `proxies` field") + } + + proxies := []C.Proxy{} + for idx, mapping := range schema.Proxies { + if name, ok := mapping["name"].(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) + pd.fetcher = fetcher + + wrapper := &ProxySetProvider{pd} + runtime.SetFinalizer(wrapper, stopProxyProvider) + return wrapper, nil +} + +// for auto gc +type CompatibleProvider struct { + *compatibleProvider +} + +type compatibleProvider struct { + name string + healthCheck *HealthCheck + proxies []C.Proxy +} + +func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "name": cp.Name(), + "type": cp.Type().String(), + "vehicleType": cp.VehicleType().String(), + "proxies": cp.Proxies(), + }) +} + +func (cp *compatibleProvider) Name() string { + return cp.name +} + +func (cp *compatibleProvider) HealthCheck() { + cp.healthCheck.checkAll() +} + +func (cp *compatibleProvider) Update() error { + return nil +} + +func (cp *compatibleProvider) Initial() error { + return nil +} + +func (cp *compatibleProvider) VehicleType() types.VehicleType { + return types.Compatible +} + +func (cp *compatibleProvider) Type() types.ProviderType { + return types.Proxy +} + +func (cp *compatibleProvider) Proxies() []C.Proxy { + return cp.proxies +} + +func (cp *compatibleProvider) Touch() { + cp.healthCheck.touch() +} + +func stopCompatibleProvider(pd *CompatibleProvider) { + pd.healthCheck.close() +} + +func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) { + if len(proxies) == 0 { + return nil, errors.New("provider need one proxy at least") + } + + if hc.auto() { + go hc.process() + } + + pd := &compatibleProvider{ + name: name, + proxies: proxies, + healthCheck: hc, + } + + wrapper := &CompatibleProvider{pd} + 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), + } +} diff --git a/adapter/provider/vehicle.go b/adapter/provider/vehicle.go new file mode 100644 index 0000000..4f08c31 --- /dev/null +++ b/adapter/provider/vehicle.go @@ -0,0 +1,98 @@ +package provider + +import ( + "context" + "io" + "net" + "net/http" + "net/url" + "os" + "time" + + "github.com/Dreamacro/clash/component/dialer" + types "github.com/Dreamacro/clash/constant/provider" +) + +type FileVehicle struct { + path string +} + +func (f *FileVehicle) Type() types.VehicleType { + return types.File +} + +func (f *FileVehicle) Path() string { + return f.path +} + +func (f *FileVehicle) Read() ([]byte, error) { + return os.ReadFile(f.path) +} + +func NewFileVehicle(path string) *FileVehicle { + return &FileVehicle{path: path} +} + +type HTTPVehicle struct { + url string + path string +} + +func (h *HTTPVehicle) Type() types.VehicleType { + return types.HTTP +} + +func (h *HTTPVehicle) Path() string { + return h.path +} + +func (h *HTTPVehicle) Read() ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + uri, err := url.Parse(h.url) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, uri.String(), nil) + if err != nil { + return nil, err + } + + if user := uri.User; user != nil { + password, _ := user.Password() + req.SetBasicAuth(user.Username(), password) + } + + req = req.WithContext(ctx) + + transport := &http.Transport{ + // from http.DefaultTransport + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return dialer.DialContext(ctx, network, address) + }, + } + + client := http.Client{Transport: transport} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return buf, nil +} + +func NewHTTPVehicle(url string, path string) *HTTPVehicle { + return &HTTPVehicle{url, path} +} diff --git a/common/batch/batch.go b/common/batch/batch.go new file mode 100644 index 0000000..f008521 --- /dev/null +++ b/common/batch/batch.go @@ -0,0 +1,105 @@ +package batch + +import ( + "context" + "sync" +) + +type Option = func(b *Batch) + +type Result struct { + Value any + Err error +} + +type Error struct { + Key string + Err error +} + +func WithConcurrencyNum(n int) Option { + return func(b *Batch) { + q := make(chan struct{}, n) + for i := 0; i < n; i++ { + q <- struct{}{} + } + b.queue = q + } +} + +// Batch similar to errgroup, but can control the maximum number of concurrent +type Batch struct { + result map[string]Result + queue chan struct{} + wg sync.WaitGroup + mux sync.Mutex + err *Error + once sync.Once + cancel func() +} + +func (b *Batch) Go(key string, fn func() (any, error)) { + b.wg.Add(1) + go func() { + defer b.wg.Done() + if b.queue != nil { + <-b.queue + defer func() { + b.queue <- struct{}{} + }() + } + + value, err := fn() + if err != nil { + b.once.Do(func() { + b.err = &Error{key, err} + if b.cancel != nil { + b.cancel() + } + }) + } + + ret := Result{value, err} + b.mux.Lock() + defer b.mux.Unlock() + b.result[key] = ret + }() +} + +func (b *Batch) Wait() *Error { + b.wg.Wait() + if b.cancel != nil { + b.cancel() + } + return b.err +} + +func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) { + err := b.Wait() + return b.Result(), err +} + +func (b *Batch) Result() map[string]Result { + b.mux.Lock() + defer b.mux.Unlock() + copy := map[string]Result{} + for k, v := range b.result { + copy[k] = v + } + return copy +} + +func New(ctx context.Context, opts ...Option) (*Batch, context.Context) { + ctx, cancel := context.WithCancel(ctx) + + b := &Batch{ + result: map[string]Result{}, + } + + for _, o := range opts { + o(b) + } + + b.cancel = cancel + return b, ctx +} diff --git a/common/batch/batch_test.go b/common/batch/batch_test.go new file mode 100644 index 0000000..4e44158 --- /dev/null +++ b/common/batch/batch_test.go @@ -0,0 +1,83 @@ +package batch + +import ( + "context" + "errors" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestBatch(t *testing.T) { + b, _ := New(context.Background()) + + now := time.Now() + b.Go("foo", func() (any, error) { + time.Sleep(time.Millisecond * 100) + return "foo", nil + }) + b.Go("bar", func() (any, error) { + time.Sleep(time.Millisecond * 150) + return "bar", nil + }) + result, err := b.WaitAndGetResult() + + assert.Nil(t, err) + + duration := time.Since(now) + assert.Less(t, duration, time.Millisecond*200) + assert.Equal(t, 2, len(result)) + + for k, v := range result { + assert.NoError(t, v.Err) + assert.Equal(t, k, v.Value.(string)) + } +} + +func TestBatchWithConcurrencyNum(t *testing.T) { + b, _ := New( + context.Background(), + WithConcurrencyNum(3), + ) + + now := time.Now() + for i := 0; i < 7; i++ { + idx := i + b.Go(strconv.Itoa(idx), func() (any, error) { + time.Sleep(time.Millisecond * 100) + return strconv.Itoa(idx), nil + }) + } + result, _ := b.WaitAndGetResult() + duration := time.Since(now) + assert.Greater(t, duration, time.Millisecond*260) + assert.Equal(t, 7, len(result)) + + for k, v := range result { + assert.NoError(t, v.Err) + assert.Equal(t, k, v.Value.(string)) + } +} + +func TestBatchContext(t *testing.T) { + b, ctx := New(context.Background()) + + b.Go("error", func() (any, error) { + time.Sleep(time.Millisecond * 100) + return nil, errors.New("test error") + }) + + b.Go("ctx", func() (any, error) { + <-ctx.Done() + return nil, ctx.Err() + }) + + result, err := b.WaitAndGetResult() + + assert.NotNil(t, err) + assert.Equal(t, "error", err.Key) + + assert.Equal(t, ctx.Err(), result["ctx"].Err) +} diff --git a/common/cache/lrucache.go b/common/cache/lrucache.go new file mode 100644 index 0000000..e0f5d20 --- /dev/null +++ b/common/cache/lrucache.go @@ -0,0 +1,223 @@ +package cache + +// Modified by https://github.com/die-net/lrucache + +import ( + "container/list" + "sync" + "time" +) + +// Option is part of Functional Options Pattern +type Option func(*LruCache) + +// EvictCallback is used to get a callback when a cache entry is evicted +type EvictCallback = func(key any, value any) + +// WithEvict set the evict callback +func WithEvict(cb EvictCallback) Option { + return func(l *LruCache) { + l.onEvict = cb + } +} + +// WithUpdateAgeOnGet update expires when Get element +func WithUpdateAgeOnGet() Option { + return func(l *LruCache) { + l.updateAgeOnGet = true + } +} + +// WithAge defined element max age (second) +func WithAge(maxAge int64) Option { + return func(l *LruCache) { + l.maxAge = maxAge + } +} + +// WithSize defined max length of LruCache +func WithSize(maxSize int) Option { + return func(l *LruCache) { + l.maxSize = maxSize + } +} + +// WithStale decide whether Stale return is enabled. +// If this feature is enabled, element will not get Evicted according to `WithAge`. +func WithStale(stale bool) Option { + return func(l *LruCache) { + l.staleReturn = stale + } +} + +// LruCache is a thread-safe, in-memory lru-cache that evicts the +// least recently used entries from memory when (if set) the entries are +// older than maxAge (in seconds). Use the New constructor to create one. +type LruCache struct { + maxAge int64 + maxSize int + mu sync.Mutex + cache map[any]*list.Element + lru *list.List // Front is least-recent + updateAgeOnGet bool + staleReturn bool + onEvict EvictCallback +} + +// New creates an LruCache +func New(options ...Option) *LruCache { + lc := &LruCache{ + lru: list.New(), + cache: make(map[any]*list.Element), + } + + for _, option := range options { + option(lc) + } + + return lc +} + +// Get returns the any representation of a cached response and a bool +// set to true if the key was found. +func (c *LruCache) Get(key any) (any, bool) { + entry := c.get(key) + if entry == nil { + return nil, false + } + value := entry.value + + return value, true +} + +// GetWithExpire returns the any 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) { + entry := c.get(key) + if entry == nil { + return nil, time.Time{}, false + } + + return entry.value, time.Unix(entry.expires, 0), true +} + +// Exist returns if key exist in cache but not put item to the head of linked list +func (c *LruCache) Exist(key any) bool { + c.mu.Lock() + defer c.mu.Unlock() + + _, ok := c.cache[key] + return ok +} + +// Set stores the any representation of a response for a given key. +func (c *LruCache) Set(key any, value any) { + expires := int64(0) + if c.maxAge > 0 { + expires = time.Now().Unix() + c.maxAge + } + c.SetWithExpire(key, value, time.Unix(expires, 0)) +} + +// SetWithExpire stores the any representation of a response for a given key and given expires. +// The expires time will round to second. +func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) { + c.mu.Lock() + defer c.mu.Unlock() + + if le, ok := c.cache[key]; ok { + c.lru.MoveToBack(le) + e := le.Value.(*entry) + e.value = value + e.expires = expires.Unix() + } else { + e := &entry{key: key, value: value, expires: expires.Unix()} + c.cache[key] = c.lru.PushBack(e) + + if c.maxSize > 0 { + if len := c.lru.Len(); len > c.maxSize { + c.deleteElement(c.lru.Front()) + } + } + } + + c.maybeDeleteOldest() +} + +// CloneTo clone and overwrite elements to another LruCache +func (c *LruCache) CloneTo(n *LruCache) { + c.mu.Lock() + defer c.mu.Unlock() + + n.mu.Lock() + defer n.mu.Unlock() + + n.lru = list.New() + n.cache = make(map[any]*list.Element) + + for e := c.lru.Front(); e != nil; e = e.Next() { + elm := e.Value.(*entry) + n.cache[elm.key] = n.lru.PushBack(elm) + } +} + +func (c *LruCache) get(key any) *entry { + c.mu.Lock() + defer c.mu.Unlock() + + le, ok := c.cache[key] + if !ok { + return nil + } + + if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() { + c.deleteElement(le) + c.maybeDeleteOldest() + + return nil + } + + c.lru.MoveToBack(le) + entry := le.Value.(*entry) + if c.maxAge > 0 && c.updateAgeOnGet { + entry.expires = time.Now().Unix() + c.maxAge + } + return entry +} + +// Delete removes the value associated with a key. +func (c *LruCache) Delete(key any) { + c.mu.Lock() + + if le, ok := c.cache[key]; ok { + c.deleteElement(le) + } + + c.mu.Unlock() +} + +func (c *LruCache) maybeDeleteOldest() { + if !c.staleReturn && c.maxAge > 0 { + now := time.Now().Unix() + for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() { + c.deleteElement(le) + } + } +} + +func (c *LruCache) deleteElement(le *list.Element) { + c.lru.Remove(le) + e := le.Value.(*entry) + delete(c.cache, e.key) + if c.onEvict != nil { + c.onEvict(e.key, e.value) + } +} + +type entry struct { + key any + value any + expires int64 +} diff --git a/common/cache/lrucache_test.go b/common/cache/lrucache_test.go new file mode 100644 index 0000000..1a09975 --- /dev/null +++ b/common/cache/lrucache_test.go @@ -0,0 +1,183 @@ +package cache + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var entries = []struct { + key string + value string +}{ + {"1", "one"}, + {"2", "two"}, + {"3", "three"}, + {"4", "four"}, + {"5", "five"}, +} + +func TestLRUCache(t *testing.T) { + c := New() + + for _, e := range entries { + c.Set(e.key, e.value) + } + + c.Delete("missing") + _, ok := c.Get("missing") + assert.False(t, ok) + + for _, e := range entries { + value, ok := c.Get(e.key) + if assert.True(t, ok) { + assert.Equal(t, e.value, value.(string)) + } + } + + for _, e := range entries { + c.Delete(e.key) + + _, ok := c.Get(e.key) + assert.False(t, ok) + } +} + +func TestLRUMaxAge(t *testing.T) { + c := New(WithAge(86400)) + + now := time.Now().Unix() + expected := now + 86400 + + // Add one expired entry + c.Set("foo", "bar") + c.lru.Back().Value.(*entry).expires = now + + // Reset + c.Set("foo", "bar") + e := c.lru.Back().Value.(*entry) + assert.True(t, e.expires >= now) + c.lru.Back().Value.(*entry).expires = now + + // Set a few and verify expiration times + for _, s := range entries { + c.Set(s.key, s.value) + e := c.lru.Back().Value.(*entry) + assert.True(t, e.expires >= expected && e.expires <= expected+10) + } + + // Make sure we can get them all + for _, s := range entries { + _, ok := c.Get(s.key) + assert.True(t, ok) + } + + // Expire all entries + for _, s := range entries { + le, ok := c.cache[s.key] + if assert.True(t, ok) { + le.Value.(*entry).expires = now + } + } + + // Get one expired entry, which should clear all expired entries + _, ok := c.Get("3") + assert.False(t, ok) + assert.Equal(t, c.lru.Len(), 0) +} + +func TestLRUpdateOnGet(t *testing.T) { + c := New(WithAge(86400), WithUpdateAgeOnGet()) + + now := time.Now().Unix() + expires := now + 86400/2 + + // Add one expired entry + c.Set("foo", "bar") + c.lru.Back().Value.(*entry).expires = expires + + _, ok := c.Get("foo") + assert.True(t, ok) + assert.True(t, c.lru.Back().Value.(*entry).expires > expires) +} + +func TestMaxSize(t *testing.T) { + c := New(WithSize(2)) + // Add one expired entry + c.Set("foo", "bar") + _, ok := c.Get("foo") + assert.True(t, ok) + + c.Set("bar", "foo") + c.Set("baz", "foo") + + _, ok = c.Get("foo") + assert.False(t, ok) +} + +func TestExist(t *testing.T) { + c := New(WithSize(1)) + c.Set(1, 2) + assert.True(t, c.Exist(1)) + c.Set(2, 3) + assert.False(t, c.Exist(1)) +} + +func TestEvict(t *testing.T) { + temp := 0 + evict := func(key any, value any) { + temp = key.(int) + value.(int) + } + + c := New(WithEvict(evict), WithSize(1)) + c.Set(1, 2) + c.Set(2, 3) + + assert.Equal(t, temp, 3) +} + +func TestSetWithExpire(t *testing.T) { + c := New(WithAge(1)) + now := time.Now().Unix() + + tenSecBefore := time.Unix(now-10, 0) + c.SetWithExpire(1, 2, tenSecBefore) + + // res is expected not to exist, and expires should be empty time.Time + res, expires, exist := c.GetWithExpire(1) + assert.Equal(t, nil, res) + assert.Equal(t, time.Time{}, expires) + assert.Equal(t, false, exist) +} + +func TestStale(t *testing.T) { + c := New(WithAge(1), WithStale(true)) + now := time.Now().Unix() + + tenSecBefore := time.Unix(now-10, 0) + c.SetWithExpire(1, 2, tenSecBefore) + + res, expires, exist := c.GetWithExpire(1) + assert.Equal(t, 2, res) + assert.Equal(t, tenSecBefore, expires) + assert.Equal(t, true, exist) +} + +func TestCloneTo(t *testing.T) { + o := New(WithSize(10)) + o.Set("1", 1) + o.Set("2", 2) + + n := New(WithSize(2)) + n.Set("3", 3) + n.Set("4", 4) + + o.CloneTo(n) + + assert.False(t, n.Exist("3")) + assert.True(t, n.Exist("1")) + + n.Set("5", 5) + assert.False(t, n.Exist("1")) +} diff --git a/common/murmur3/murmur.go b/common/murmur3/murmur.go new file mode 100644 index 0000000..f447029 --- /dev/null +++ b/common/murmur3/murmur.go @@ -0,0 +1,50 @@ +package murmur3 + +type bmixer interface { + bmix(p []byte) (tail []byte) + Size() (n int) + reset() +} + +type digest struct { + clen int // Digested input cumulative length. + tail []byte // 0 to Size()-1 bytes view of `buf'. + buf [16]byte // Expected (but not required) to be Size() large. + seed uint32 // Seed for initializing the hash. + bmixer +} + +func (d *digest) BlockSize() int { return 1 } + +func (d *digest) Write(p []byte) (n int, err error) { + n = len(p) + d.clen += n + + if len(d.tail) > 0 { + // Stick back pending bytes. + nfree := d.Size() - len(d.tail) // nfree ∈ [1, d.Size()-1]. + if nfree < len(p) { + // One full block can be formed. + block := append(d.tail, p[:nfree]...) + p = p[nfree:] + _ = d.bmix(block) // No tail. + } else { + // Tail's buf is large enough to prevent reallocs. + p = append(d.tail, p...) + } + } + + d.tail = d.bmix(p) + + // Keep own copy of the 0 to Size()-1 pending bytes. + nn := copy(d.buf[:], d.tail) + d.tail = d.buf[:nn] + + return n, nil +} + +func (d *digest) Reset() { + d.clen = 0 + d.tail = nil + d.bmixer.reset() +} diff --git a/common/murmur3/murmur32.go b/common/murmur3/murmur32.go new file mode 100644 index 0000000..e52b793 --- /dev/null +++ b/common/murmur3/murmur32.go @@ -0,0 +1,144 @@ +package murmur3 + +// https://github.com/spaolacci/murmur3/blob/master/murmur32.go + +import ( + "hash" + "math/bits" + "unsafe" +) + +// Make sure interfaces are correctly implemented. +var ( + _ hash.Hash32 = new(digest32) + _ bmixer = new(digest32) +) + +const ( + c1_32 uint32 = 0xcc9e2d51 + c2_32 uint32 = 0x1b873593 +) + +// digest32 represents a partial evaluation of a 32 bites hash. +type digest32 struct { + digest + h1 uint32 // Unfinalized running hash. +} + +// New32 returns new 32-bit hasher +func New32() hash.Hash32 { return New32WithSeed(0) } + +// New32WithSeed returns new 32-bit hasher set with explicit seed value +func New32WithSeed(seed uint32) hash.Hash32 { + d := new(digest32) + d.seed = seed + d.bmixer = d + d.Reset() + return d +} + +func (d *digest32) Size() int { return 4 } + +func (d *digest32) reset() { d.h1 = d.seed } + +func (d *digest32) Sum(b []byte) []byte { + h := d.Sum32() + return append(b, byte(h>>24), byte(h>>16), byte(h>>8), byte(h)) +} + +// Digest as many blocks as possible. +func (d *digest32) bmix(p []byte) (tail []byte) { + h1 := d.h1 + + nblocks := len(p) / 4 + for i := 0; i < nblocks; i++ { + k1 := *(*uint32)(unsafe.Pointer(&p[i*4])) + + k1 *= c1_32 + k1 = bits.RotateLeft32(k1, 15) + k1 *= c2_32 + + h1 ^= k1 + h1 = bits.RotateLeft32(h1, 13) + h1 = h1*4 + h1 + 0xe6546b64 + } + d.h1 = h1 + return p[nblocks*d.Size():] +} + +func (d *digest32) Sum32() (h1 uint32) { + h1 = d.h1 + + var k1 uint32 + switch len(d.tail) & 3 { + case 3: + k1 ^= uint32(d.tail[2]) << 16 + fallthrough + case 2: + k1 ^= uint32(d.tail[1]) << 8 + fallthrough + case 1: + k1 ^= uint32(d.tail[0]) + k1 *= c1_32 + k1 = bits.RotateLeft32(k1, 15) + k1 *= c2_32 + h1 ^= k1 + } + + h1 ^= uint32(d.clen) + + h1 ^= h1 >> 16 + h1 *= 0x85ebca6b + h1 ^= h1 >> 13 + h1 *= 0xc2b2ae35 + h1 ^= h1 >> 16 + + return h1 +} + +func Sum32(data []byte) uint32 { return Sum32WithSeed(data, 0) } + +func Sum32WithSeed(data []byte, seed uint32) uint32 { + h1 := seed + + nblocks := len(data) / 4 + for i := 0; i < nblocks; i++ { + k1 := *(*uint32)(unsafe.Pointer(&data[i*4])) + + k1 *= c1_32 + k1 = bits.RotateLeft32(k1, 15) + k1 *= c2_32 + + h1 ^= k1 + h1 = bits.RotateLeft32(h1, 13) + h1 = h1*4 + h1 + 0xe6546b64 + } + + tail := data[nblocks*4:] + + var k1 uint32 + switch len(tail) & 3 { + case 3: + k1 ^= uint32(tail[2]) << 16 + fallthrough + case 2: + k1 ^= uint32(tail[1]) << 8 + fallthrough + case 1: + k1 ^= uint32(tail[0]) + k1 *= c1_32 + k1 = bits.RotateLeft32(k1, 15) + k1 *= c2_32 + h1 ^= k1 + } + + h1 ^= uint32(len(data)) + + h1 ^= h1 >> 16 + h1 *= 0x85ebca6b + h1 ^= h1 >> 13 + h1 *= 0xc2b2ae35 + h1 ^= h1 >> 16 + + return h1 +} diff --git a/common/net/bufconn.go b/common/net/bufconn.go new file mode 100644 index 0000000..a50c7f0 --- /dev/null +++ b/common/net/bufconn.go @@ -0,0 +1,44 @@ +package net + +import ( + "bufio" + "net" +) + +type BufferedConn struct { + r *bufio.Reader + net.Conn +} + +func NewBufferedConn(c net.Conn) *BufferedConn { + if bc, ok := c.(*BufferedConn); ok { + return bc + } + return &BufferedConn{bufio.NewReader(c), c} +} + +// Reader returns the internal bufio.Reader. +func (c *BufferedConn) Reader() *bufio.Reader { + return c.r +} + +// Peek returns the next n bytes without advancing the reader. +func (c *BufferedConn) Peek(n int) ([]byte, error) { + return c.r.Peek(n) +} + +func (c *BufferedConn) Read(p []byte) (int, error) { + return c.r.Read(p) +} + +func (c *BufferedConn) ReadByte() (byte, error) { + return c.r.ReadByte() +} + +func (c *BufferedConn) UnreadByte() error { + return c.r.UnreadByte() +} + +func (c *BufferedConn) Buffered() int { + return c.r.Buffered() +} diff --git a/common/net/io.go b/common/net/io.go new file mode 100644 index 0000000..5bb4f00 --- /dev/null +++ b/common/net/io.go @@ -0,0 +1,11 @@ +package net + +import "io" + +type ReadOnlyReader struct { + io.Reader +} + +type WriteOnlyWriter struct { + io.Writer +} diff --git a/common/net/relay.go b/common/net/relay.go new file mode 100644 index 0000000..99a0c6a --- /dev/null +++ b/common/net/relay.go @@ -0,0 +1,24 @@ +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 +} diff --git a/common/observable/iterable.go b/common/observable/iterable.go new file mode 100644 index 0000000..2ac38b4 --- /dev/null +++ b/common/observable/iterable.go @@ -0,0 +1,3 @@ +package observable + +type Iterable <-chan any diff --git a/common/observable/observable.go b/common/observable/observable.go new file mode 100644 index 0000000..64bd0a0 --- /dev/null +++ b/common/observable/observable.go @@ -0,0 +1,65 @@ +package observable + +import ( + "errors" + "sync" +) + +type Observable struct { + iterable Iterable + listener map[Subscription]*Subscriber + mux sync.Mutex + done bool +} + +func (o *Observable) process() { + for item := range o.iterable { + o.mux.Lock() + for _, sub := range o.listener { + sub.Emit(item) + } + o.mux.Unlock() + } + o.close() +} + +func (o *Observable) close() { + o.mux.Lock() + defer o.mux.Unlock() + + o.done = true + for _, sub := range o.listener { + sub.Close() + } +} + +func (o *Observable) Subscribe() (Subscription, error) { + o.mux.Lock() + defer o.mux.Unlock() + if o.done { + return nil, errors.New("Observable is closed") + } + subscriber := newSubscriber() + o.listener[subscriber.Out()] = subscriber + return subscriber.Out(), nil +} + +func (o *Observable) UnSubscribe(sub Subscription) { + o.mux.Lock() + defer o.mux.Unlock() + subscriber, exist := o.listener[sub] + if !exist { + return + } + delete(o.listener, sub) + subscriber.Close() +} + +func NewObservable(any Iterable) *Observable { + observable := &Observable{ + iterable: any, + listener: map[Subscription]*Subscriber{}, + } + go observable.process() + return observable +} diff --git a/common/observable/observable_test.go b/common/observable/observable_test.go new file mode 100644 index 0000000..da3e6d5 --- /dev/null +++ b/common/observable/observable_test.go @@ -0,0 +1,146 @@ +package observable + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/atomic" +) + +func iterator(item []any) chan any { + ch := make(chan any) + go func() { + time.Sleep(100 * time.Millisecond) + for _, elm := range item { + ch <- elm + } + close(ch) + }() + return ch +} + +func TestObservable(t *testing.T) { + iter := iterator([]any{1, 2, 3, 4, 5}) + src := NewObservable(iter) + data, err := src.Subscribe() + assert.Nil(t, err) + count := 0 + for range data { + count++ + } + assert.Equal(t, count, 5) +} + +func TestObservable_MultiSubscribe(t *testing.T) { + iter := iterator([]any{1, 2, 3, 4, 5}) + src := NewObservable(iter) + ch1, _ := src.Subscribe() + ch2, _ := src.Subscribe() + count := atomic.NewInt32(0) + + var wg sync.WaitGroup + wg.Add(2) + waitCh := func(ch <-chan any) { + for range ch { + count.Inc() + } + wg.Done() + } + go waitCh(ch1) + go waitCh(ch2) + wg.Wait() + assert.Equal(t, int32(10), count.Load()) +} + +func TestObservable_UnSubscribe(t *testing.T) { + iter := iterator([]any{1, 2, 3, 4, 5}) + src := NewObservable(iter) + data, err := src.Subscribe() + assert.Nil(t, err) + src.UnSubscribe(data) + _, open := <-data + assert.False(t, open) +} + +func TestObservable_SubscribeClosedSource(t *testing.T) { + iter := iterator([]any{1}) + src := NewObservable(iter) + data, _ := src.Subscribe() + <-data + + _, closed := src.Subscribe() + assert.NotNil(t, closed) +} + +func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) { + sub := Subscription(make(chan any)) + iter := iterator([]any{1}) + src := NewObservable(iter) + src.UnSubscribe(sub) +} + +func TestObservable_SubscribeGoroutineLeak(t *testing.T) { + iter := iterator([]any{1, 2, 3, 4, 5}) + src := NewObservable(iter) + max := 100 + + var list []Subscription + for i := 0; i < max; i++ { + ch, _ := src.Subscribe() + list = append(list, ch) + } + + var wg sync.WaitGroup + wg.Add(max) + waitCh := func(ch <-chan any) { + for range ch { + } + wg.Done() + } + + for _, ch := range list { + go waitCh(ch) + } + wg.Wait() + + for _, sub := range list { + _, more := <-sub + assert.False(t, more) + } + + _, more := <-list[0] + assert.False(t, more) +} + +func Benchmark_Observable_1000(b *testing.B) { + ch := make(chan any) + o := NewObservable(ch) + num := 1000 + + subs := []Subscription{} + for i := 0; i < num; i++ { + sub, _ := o.Subscribe() + subs = append(subs, sub) + } + + wg := sync.WaitGroup{} + wg.Add(num) + + b.ResetTimer() + for _, sub := range subs { + go func(s Subscription) { + for range s { + } + wg.Done() + }(sub) + } + + for i := 0; i < b.N; i++ { + ch <- i + } + + close(ch) + wg.Wait() +} diff --git a/common/observable/subscriber.go b/common/observable/subscriber.go new file mode 100644 index 0000000..0d8559b --- /dev/null +++ b/common/observable/subscriber.go @@ -0,0 +1,33 @@ +package observable + +import ( + "sync" +) + +type Subscription <-chan any + +type Subscriber struct { + buffer chan any + once sync.Once +} + +func (s *Subscriber) Emit(item any) { + s.buffer <- item +} + +func (s *Subscriber) Out() Subscription { + return s.buffer +} + +func (s *Subscriber) Close() { + s.once.Do(func() { + close(s.buffer) + }) +} + +func newSubscriber() *Subscriber { + sub := &Subscriber{ + buffer: make(chan any, 200), + } + return sub +} diff --git a/common/picker/picker.go b/common/picker/picker.go new file mode 100644 index 0000000..e701268 --- /dev/null +++ b/common/picker/picker.go @@ -0,0 +1,80 @@ +package picker + +import ( + "context" + "sync" + "time" +) + +// Picker provides synchronization, and Context cancelation +// for groups of goroutines working on subtasks of a common task. +// Inspired by errGroup +type Picker struct { + ctx context.Context + cancel func() + + wg sync.WaitGroup + + once sync.Once + errOnce sync.Once + result any + err error +} + +func newPicker(ctx context.Context, cancel func()) *Picker { + return &Picker{ + ctx: ctx, + cancel: cancel, + } +} + +// WithContext returns a new Picker and an associated Context derived from ctx. +// and cancel when first element return. +func WithContext(ctx context.Context) (*Picker, context.Context) { + ctx, cancel := context.WithCancel(ctx) + return newPicker(ctx, cancel), ctx +} + +// WithTimeout returns a new Picker and an associated Context derived from ctx with timeout. +func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.Context) { + ctx, cancel := context.WithTimeout(ctx, timeout) + return newPicker(ctx, cancel), ctx +} + +// Wait blocks until all function calls from the Go method have returned, +// then returns the first nil error result (if any) from them. +func (p *Picker) Wait() any { + p.wg.Wait() + if p.cancel != nil { + p.cancel() + } + return p.result +} + +// Error return the first error (if all success return nil) +func (p *Picker) Error() error { + return p.err +} + +// Go calls the given function in a new goroutine. +// The first call to return a nil error cancels the group; its result will be returned by Wait. +func (p *Picker) Go(f func() (any, error)) { + p.wg.Add(1) + + go func() { + defer p.wg.Done() + + if ret, err := f(); err == nil { + p.once.Do(func() { + p.result = ret + if p.cancel != nil { + p.cancel() + } + }) + } else { + p.errOnce.Do(func() { + p.err = err + }) + } + }() +} diff --git a/common/picker/picker_test.go b/common/picker/picker_test.go new file mode 100644 index 0000000..ca10499 --- /dev/null +++ b/common/picker/picker_test.go @@ -0,0 +1,40 @@ +package picker + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) { + return func() (any, error) { + timer := time.NewTimer(time.Millisecond * time.Duration(delay)) + select { + case <-timer.C: + return input, nil + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} + +func TestPicker_Basic(t *testing.T) { + picker, ctx := WithContext(context.Background()) + picker.Go(sleepAndSend(ctx, 30, 2)) + picker.Go(sleepAndSend(ctx, 20, 1)) + + number := picker.Wait() + assert.NotNil(t, number) + assert.Equal(t, number.(int), 1) +} + +func TestPicker_Timeout(t *testing.T) { + picker, ctx := WithTimeout(context.Background(), time.Millisecond*5) + picker.Go(sleepAndSend(ctx, 20, 1)) + + number := picker.Wait() + assert.Nil(t, number) + assert.NotNil(t, picker.Error()) +} diff --git a/common/pool/alloc.go b/common/pool/alloc.go new file mode 100644 index 0000000..f06b1a3 --- /dev/null +++ b/common/pool/alloc.go @@ -0,0 +1,73 @@ +package pool + +// Inspired by https://github.com/xtaci/smux/blob/master/alloc.go + +import ( + "errors" + "math/bits" + "sync" +) + +var defaultAllocator = NewAllocator() + +// Allocator for incoming frames, optimized to prevent overwriting after zeroing +type Allocator struct { + buffers []sync.Pool +} + +// NewAllocator initiates a []byte allocator for frames less than 65536 bytes, +// the waste(memory fragmentation) of space allocation is guaranteed to be +// no more than 50%. +func NewAllocator() *Allocator { + alloc := new(Allocator) + alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K + for k := range alloc.buffers { + i := k + alloc.buffers[k].New = func() any { + return make([]byte, 1<<uint32(i)) + } + } + return alloc +} + +// 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: + 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] + } +} + +// 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 { + return errors.New("allocator Put() incorrect buffer size") + } + + //nolint + //lint:ignore SA6002 ignore temporarily + alloc.buffers[bits].Put(buf) + return nil +} + +// msb return the pos of most significant bit +func msb(size int) uint16 { + return uint16(bits.Len32(uint32(size)) - 1) +} diff --git a/common/pool/alloc_test.go b/common/pool/alloc_test.go new file mode 100644 index 0000000..0b7c3cd --- /dev/null +++ b/common/pool/alloc_test.go @@ -0,0 +1,48 @@ +package pool + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAllocGet(t *testing.T) { + alloc := NewAllocator() + assert.Nil(t, alloc.Get(0)) + assert.Equal(t, 1, len(alloc.Get(1))) + assert.Equal(t, 2, len(alloc.Get(2))) + assert.Equal(t, 3, len(alloc.Get(3))) + assert.Equal(t, 4, cap(alloc.Get(3))) + assert.Equal(t, 4, cap(alloc.Get(4))) + assert.Equal(t, 1023, len(alloc.Get(1023))) + 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))) +} + +func TestAllocPut(t *testing.T) { + alloc := NewAllocator() + assert.Nil(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") +} + +func TestAllocPutThenGet(t *testing.T) { + alloc := NewAllocator() + data := alloc.Get(4) + alloc.Put(data) + newData := alloc.Get(4) + + assert.Equal(t, cap(data), cap(newData), "different cap while alloc.Get()") +} + +func BenchmarkMSB(b *testing.B) { + for i := 0; i < b.N; i++ { + msb(rand.Int()) + } +} diff --git a/common/pool/buffer.go b/common/pool/buffer.go new file mode 100644 index 0000000..98aa39a --- /dev/null +++ b/common/pool/buffer.go @@ -0,0 +1,31 @@ +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) +} diff --git a/common/pool/pool.go b/common/pool/pool.go new file mode 100644 index 0000000..bee4887 --- /dev/null +++ b/common/pool/pool.go @@ -0,0 +1,21 @@ +package pool + +const ( + // io.Copy default buffer size is 32 KiB + // 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 { + return defaultAllocator.Get(size) +} + +func Put(buf []byte) error { + return defaultAllocator.Put(buf) +} diff --git a/common/queue/queue.go b/common/queue/queue.go new file mode 100644 index 0000000..60257f5 --- /dev/null +++ b/common/queue/queue.go @@ -0,0 +1,71 @@ +package queue + +import ( + "sync" +) + +// Queue is a simple concurrent safe queue +type Queue struct { + items []any + lock sync.RWMutex +} + +// Put add the item to the queue. +func (q *Queue) Put(items ...any) { + if len(items) == 0 { + return + } + + q.lock.Lock() + q.items = append(q.items, items...) + q.lock.Unlock() +} + +// Pop returns the head of items. +func (q *Queue) Pop() any { + if len(q.items) == 0 { + return nil + } + + q.lock.Lock() + head := q.items[0] + q.items = q.items[1:] + q.lock.Unlock() + return head +} + +// Last returns the last of item. +func (q *Queue) Last() any { + if len(q.items) == 0 { + return nil + } + + q.lock.RLock() + last := q.items[len(q.items)-1] + q.lock.RUnlock() + return last +} + +// Copy get the copy of queue. +func (q *Queue) Copy() []any { + items := []any{} + q.lock.RLock() + items = append(items, q.items...) + q.lock.RUnlock() + return items +} + +// Len returns the number of items in this queue. +func (q *Queue) Len() int64 { + q.lock.Lock() + defer q.lock.Unlock() + + return int64(len(q.items)) +} + +// New is a constructor for a new concurrent safe queue. +func New(hint int64) *Queue { + return &Queue{ + items: make([]any, 0, hint), + } +} diff --git a/common/singledo/singledo.go b/common/singledo/singledo.go new file mode 100644 index 0000000..a50f122 --- /dev/null +++ b/common/singledo/singledo.go @@ -0,0 +1,63 @@ +package singledo + +import ( + "sync" + "time" +) + +type call struct { + wg sync.WaitGroup + val any + err error +} + +type Single struct { + mux sync.Mutex + last time.Time + wait time.Duration + call *call + result *Result +} + +type Result struct { + Val any + Err error +} + +// Do single.Do likes sync.singleFlight +func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) { + s.mux.Lock() + now := time.Now() + if now.Before(s.last.Add(s.wait)) { + s.mux.Unlock() + return s.result.Val, s.result.Err, true + } + + if call := s.call; call != nil { + s.mux.Unlock() + call.wg.Wait() + return call.val, call.err, true + } + + call := &call{} + call.wg.Add(1) + s.call = call + s.mux.Unlock() + call.val, call.err = fn() + call.wg.Done() + + s.mux.Lock() + s.call = nil + s.result = &Result{call.val, call.err} + s.last = now + s.mux.Unlock() + return call.val, call.err, false +} + +func (s *Single) Reset() { + s.last = time.Time{} +} + +func NewSingle(wait time.Duration) *Single { + return &Single{wait: wait} +} diff --git a/common/singledo/singledo_test.go b/common/singledo/singledo_test.go new file mode 100644 index 0000000..71b6ac9 --- /dev/null +++ b/common/singledo/singledo_test.go @@ -0,0 +1,69 @@ +package singledo + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/atomic" +) + +func TestBasic(t *testing.T) { + single := NewSingle(time.Millisecond * 30) + foo := 0 + shardCount := atomic.NewInt32(0) + call := func() (any, error) { + foo++ + time.Sleep(time.Millisecond * 5) + return nil, nil + } + + var wg sync.WaitGroup + const n = 5 + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + _, _, shard := single.Do(call) + if shard { + shardCount.Inc() + } + wg.Done() + }() + } + + wg.Wait() + assert.Equal(t, 1, foo) + assert.Equal(t, int32(4), shardCount.Load()) +} + +func TestTimer(t *testing.T) { + single := NewSingle(time.Millisecond * 30) + foo := 0 + call := func() (any, error) { + foo++ + return nil, nil + } + + single.Do(call) + time.Sleep(10 * time.Millisecond) + _, _, shard := single.Do(call) + + assert.Equal(t, 1, foo) + assert.True(t, shard) +} + +func TestReset(t *testing.T) { + single := NewSingle(time.Millisecond * 30) + foo := 0 + call := func() (any, error) { + foo++ + return nil, nil + } + + single.Do(call) + single.Reset() + single.Do(call) + + assert.Equal(t, 2, foo) +} diff --git a/common/sockopt/reuseaddr_linux.go b/common/sockopt/reuseaddr_linux.go new file mode 100644 index 0000000..a1d19bf --- /dev/null +++ b/common/sockopt/reuseaddr_linux.go @@ -0,0 +1,19 @@ +package sockopt + +import ( + "net" + "syscall" +) + +func UDPReuseaddr(c *net.UDPConn) (err error) { + rc, err := c.SyscallConn() + if err != nil { + return + } + + rc.Control(func(fd uintptr) { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + }) + + return +} diff --git a/common/sockopt/reuseaddr_other.go b/common/sockopt/reuseaddr_other.go new file mode 100644 index 0000000..04fc8ed --- /dev/null +++ b/common/sockopt/reuseaddr_other.go @@ -0,0 +1,11 @@ +//go:build !linux + +package sockopt + +import ( + "net" +) + +func UDPReuseaddr(c *net.UDPConn) (err error) { + return +} diff --git a/common/structure/structure.go b/common/structure/structure.go new file mode 100644 index 0000000..081dd36 --- /dev/null +++ b/common/structure/structure.go @@ -0,0 +1,413 @@ +package structure + +// references: https://github.com/mitchellh/mapstructure + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// Option is the configuration that is used to create a new decoder +type Option struct { + TagName string + WeaklyTypedInput bool +} + +// Decoder is the core of structure +type Decoder struct { + option *Option +} + +// NewDecoder return a Decoder by Option +func NewDecoder(option Option) *Decoder { + if option.TagName == "" { + option.TagName = "structure" + } + return &Decoder{option: &option} +} + +// Decode transform a map[string]any to a struct +func (d *Decoder) Decode(src map[string]any, dst any) error { + if reflect.TypeOf(dst).Kind() != reflect.Ptr { + return fmt.Errorf("Decode must recive a ptr struct") + } + t := reflect.TypeOf(dst).Elem() + 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" + + value, ok := src[key] + if !ok || value == nil { + if omitempty { + continue + } + return fmt.Errorf("key '%s' missing", key) + } + + err := d.decode(key, value, v.Field(idx)) + if err != nil { + return err + } + } + return nil +} + +func (d *Decoder) decode(name string, data any, val reflect.Value) error { + switch val.Kind() { + case reflect.Int: + return d.decodeInt(name, data, val) + case reflect.String: + return d.decodeString(name, data, val) + case reflect.Bool: + return d.decodeBool(name, data, val) + case reflect.Slice: + return d.decodeSlice(name, data, val) + case reflect.Map: + return d.decodeMap(name, data, val) + case reflect.Interface: + return d.setInterface(name, data, val) + case reflect.Struct: + return d.decodeStruct(name, data, val) + default: + return fmt.Errorf("type %s not support", val.Kind().String()) + } +} + +func (d *Decoder) decodeInt(name string, data any, 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()) + if err == nil { + val.SetInt(i) + } else { + err = fmt.Errorf("cannot parse '%s' as int: %s", name, err) + } + default: + err = fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type(), + ) + } + return err +} + +func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) { + dataVal := reflect.ValueOf(data) + kind := dataVal.Kind() + switch { + case kind == reflect.String: + val.SetString(dataVal.String()) + case kind == reflect.Int && d.option.WeaklyTypedInput: + val.SetString(strconv.FormatInt(dataVal.Int(), 10)) + default: + err = fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type(), + ) + } + return err +} + +func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err error) { + dataVal := reflect.ValueOf(data) + kind := dataVal.Kind() + switch { + case kind == reflect.Bool: + val.SetBool(dataVal.Bool()) + case kind == reflect.Int && d.option.WeaklyTypedInput: + val.SetBool(dataVal.Int() != 0) + default: + err = fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type(), + ) + } + return err +} + +func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + valType := val.Type() + valElemType := valType.Elem() + + if dataVal.Kind() != reflect.Slice { + return fmt.Errorf("'%s' is not a slice", name) + } + + valSlice := val + for i := 0; i < dataVal.Len(); i++ { + currentData := dataVal.Index(i).Interface() + 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) + if err := d.decode(fieldName, currentData, currentField); err != nil { + return err + } + } + + val.Set(valSlice) + return nil +} + +func (d *Decoder) decodeMap(name string, data any, val reflect.Value) error { + valType := val.Type() + valKeyType := valType.Key() + valElemType := valType.Elem() + + valMap := val + + if valMap.IsNil() { + mapType := reflect.MapOf(valKeyType, valElemType) + valMap = reflect.MakeMap(mapType) + } + + dataVal := reflect.Indirect(reflect.ValueOf(data)) + if dataVal.Kind() != reflect.Map { + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + } + + return d.decodeMapFromMap(name, dataVal, val, valMap) +} + +func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { + valType := val.Type() + valKeyType := valType.Key() + valElemType := valType.Elem() + + errors := make([]string, 0) + + if dataVal.Len() == 0 { + if dataVal.IsNil() { + if !val.IsNil() { + val.Set(dataVal) + } + } else { + val.Set(valMap) + } + + return nil + } + + for _, k := range dataVal.MapKeys() { + fieldName := fmt.Sprintf("%s[%s]", name, k) + + currentKey := reflect.Indirect(reflect.New(valKeyType)) + if err := d.decode(fieldName, k.Interface(), currentKey); err != nil { + errors = append(errors, err.Error()) + continue + } + + v := dataVal.MapIndex(k).Interface() + if v == nil { + errors = append(errors, fmt.Sprintf("filed %s invalid", fieldName)) + continue + } + + currentVal := reflect.Indirect(reflect.New(valElemType)) + if err := d.decode(fieldName, v, currentVal); err != nil { + errors = append(errors, err.Error()) + continue + } + + valMap.SetMapIndex(currentKey, currentVal) + } + + val.Set(valMap) + + if len(errors) > 0 { + return fmt.Errorf(strings.Join(errors, ",")) + } + + return nil +} + +func (d *Decoder) decodeStruct(name string, data any, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + + // If the type of the value to write to and the data match directly, + // then we just set it directly instead of recursing into the structure. + if dataVal.Type() == val.Type() { + val.Set(dataVal) + return nil + } + + dataValKind := dataVal.Kind() + switch dataValKind { + case reflect.Map: + return d.decodeStructFromMap(name, dataVal, val) + default: + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + } +} + +func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error { + dataValType := dataVal.Type() + if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { + return fmt.Errorf( + "'%s' needs a map with string keys, has '%s' keys", + name, dataValType.Key().Kind()) + } + + dataValKeys := make(map[reflect.Value]struct{}) + dataValKeysUnused := make(map[any]struct{}) + for _, dataValKey := range dataVal.MapKeys() { + dataValKeys[dataValKey] = struct{}{} + dataValKeysUnused[dataValKey.Interface()] = struct{}{} + } + + errors := make([]string, 0) + + // This slice will keep track of all the structs we'll be decoding. + // There can be more than one struct if there are embedded structs + // that are squashed. + structs := make([]reflect.Value, 1, 5) + structs[0] = val + + // Compile the list of all the fields that we're going to be decoding + // from all the structs. + type field struct { + field reflect.StructField + val reflect.Value + } + fields := []field{} + for len(structs) > 0 { + structVal := structs[0] + structs = structs[1:] + + structType := structVal.Type() + + for i := 0; i < structType.NumField(); i++ { + fieldType := structType.Field(i) + fieldKind := fieldType.Type.Kind() + + // If "squash" is specified in the tag, we squash the field down. + squash := false + tagParts := strings.Split(fieldType.Tag.Get(d.option.TagName), ",") + for _, tag := range tagParts[1:] { + if tag == "squash" { + squash = true + break + } + } + + if squash { + if fieldKind != reflect.Struct { + errors = append(errors, + fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldKind).Error()) + } else { + structs = append(structs, structVal.FieldByName(fieldType.Name)) + } + continue + } + + // Normal struct field, store it away + fields = append(fields, field{fieldType, structVal.Field(i)}) + } + } + + // for fieldType, field := range fields { + for _, f := range fields { + field, fieldValue := f.field, f.val + fieldName := field.Name + + tagValue := field.Tag.Get(d.option.TagName) + tagValue = strings.SplitN(tagValue, ",", 2)[0] + if tagValue != "" { + fieldName = tagValue + } + + rawMapKey := reflect.ValueOf(fieldName) + rawMapVal := dataVal.MapIndex(rawMapKey) + if !rawMapVal.IsValid() { + // Do a slower search by iterating over each key and + // doing case-insensitive search. + for dataValKey := range dataValKeys { + mK, ok := dataValKey.Interface().(string) + if !ok { + // Not a string key + continue + } + + if strings.EqualFold(mK, fieldName) { + rawMapKey = dataValKey + rawMapVal = dataVal.MapIndex(dataValKey) + break + } + } + + if !rawMapVal.IsValid() { + // There was no matching key in the map for the value in + // the struct. Just ignore. + continue + } + } + + // Delete the key we're using from the unused map so we stop tracking + delete(dataValKeysUnused, rawMapKey.Interface()) + + if !fieldValue.IsValid() { + // This should never happen + panic("field is not valid") + } + + // If we can't set the field, then it is unexported or something, + // and we just continue onwards. + if !fieldValue.CanSet() { + continue + } + + // If the name is empty string, then we're at the root, and we + // don't dot-join the fields. + if name != "" { + fieldName = fmt.Sprintf("%s.%s", name, fieldName) + } + + if err := d.decode(fieldName, rawMapVal.Interface(), fieldValue); err != nil { + errors = append(errors, err.Error()) + } + } + + if len(errors) > 0 { + return fmt.Errorf(strings.Join(errors, ",")) + } + + return nil +} + +func (d *Decoder) setInterface(name string, data any, val reflect.Value) (err error) { + dataVal := reflect.ValueOf(data) + val.Set(dataVal) + return nil +} diff --git a/common/structure/structure_test.go b/common/structure/structure_test.go new file mode 100644 index 0000000..9f31d3d --- /dev/null +++ b/common/structure/structure_test.go @@ -0,0 +1,181 @@ +package structure + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + decoder = NewDecoder(Option{TagName: "test"}) + weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true}) +) + +type Baz struct { + Foo int `test:"foo"` + Bar string `test:"bar"` +} + +type BazSlice struct { + Foo int `test:"foo"` + Bar []string `test:"bar"` +} + +type BazOptional struct { + Foo int `test:"foo,omitempty"` + Bar string `test:"bar,omitempty"` +} + +func TestStructure_Basic(t *testing.T) { + rawMap := map[string]any{ + "foo": 1, + "bar": "test", + "extra": false, + } + + goal := &Baz{ + Foo: 1, + Bar: "test", + } + + s := &Baz{} + err := decoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.Equal(t, goal, s) +} + +func TestStructure_Slice(t *testing.T) { + rawMap := map[string]any{ + "foo": 1, + "bar": []string{"one", "two"}, + } + + goal := &BazSlice{ + Foo: 1, + Bar: []string{"one", "two"}, + } + + s := &BazSlice{} + err := decoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.Equal(t, goal, s) +} + +func TestStructure_Optional(t *testing.T) { + rawMap := map[string]any{ + "foo": 1, + } + + goal := &BazOptional{ + Foo: 1, + } + + s := &BazOptional{} + err := decoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.Equal(t, goal, s) +} + +func TestStructure_MissingKey(t *testing.T) { + rawMap := map[string]any{ + "foo": 1, + } + + s := &Baz{} + err := decoder.Decode(rawMap, s) + assert.NotNilf(t, err, "should throw error: %#v", s) +} + +func TestStructure_ParamError(t *testing.T) { + rawMap := map[string]any{} + s := Baz{} + err := decoder.Decode(rawMap, s) + assert.NotNilf(t, err, "should throw error: %#v", s) +} + +func TestStructure_SliceTypeError(t *testing.T) { + rawMap := map[string]any{ + "foo": 1, + "bar": []int{1, 2}, + } + + s := &BazSlice{} + err := decoder.Decode(rawMap, s) + assert.NotNilf(t, err, "should throw error: %#v", s) +} + +func TestStructure_WeakType(t *testing.T) { + rawMap := map[string]any{ + "foo": "1", + "bar": []int{1}, + } + + goal := &BazSlice{ + Foo: 1, + Bar: []string{"1"}, + } + + 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) +} diff --git a/common/util/manipulation.go b/common/util/manipulation.go new file mode 100644 index 0000000..d2c861e --- /dev/null +++ b/common/util/manipulation.go @@ -0,0 +1,8 @@ +package util + +import "github.com/samber/lo" + +func EmptyOr[T comparable](v T, def T) T { + ret, _ := lo.Coalesce(v, def) + return ret +} diff --git a/component/auth/auth.go b/component/auth/auth.go new file mode 100644 index 0000000..9d30b92 --- /dev/null +++ b/component/auth/auth.go @@ -0,0 +1,46 @@ +package auth + +import ( + "sync" +) + +type Authenticator interface { + Verify(user string, pass string) bool + Users() []string +} + +type AuthUser struct { + User string + Pass string +} + +type inMemoryAuthenticator struct { + storage *sync.Map + usernames []string +} + +func (au *inMemoryAuthenticator) Verify(user string, pass string) bool { + realPass, ok := au.storage.Load(user) + return ok && realPass == pass +} + +func (au *inMemoryAuthenticator) Users() []string { return au.usernames } + +func NewAuthenticator(users []AuthUser) Authenticator { + if len(users) == 0 { + return nil + } + + au := &inMemoryAuthenticator{storage: &sync.Map{}} + for _, user := range users { + au.storage.Store(user.User, user.Pass) + } + usernames := make([]string, 0, len(users)) + au.storage.Range(func(key, value any) bool { + usernames = append(usernames, key.(string)) + return true + }) + au.usernames = usernames + + return au +} diff --git a/component/dhcp/conn.go b/component/dhcp/conn.go new file mode 100644 index 0000000..5b71d3c --- /dev/null +++ b/component/dhcp/conn.go @@ -0,0 +1,28 @@ +package dhcp + +import ( + "context" + "net" + "runtime" + + "github.com/Dreamacro/clash/component/dialer" +) + +func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, error) { + listenAddr := "0.0.0.0:68" + if runtime.GOOS == "linux" || runtime.GOOS == "android" { + 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...) +} diff --git a/component/dhcp/dhcp.go b/component/dhcp/dhcp.go new file mode 100644 index 0000000..b7e9f50 --- /dev/null +++ b/component/dhcp/dhcp.go @@ -0,0 +1,88 @@ +package dhcp + +import ( + "context" + "errors" + "net" + + "github.com/Dreamacro/clash/component/iface" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +var ( + ErrNotResponding = errors.New("DHCP not responding") + ErrNotFound = errors.New("DNS option not found") +) + +func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, error) { + conn, err := ListenDHCPClient(context, ifaceName) + if err != nil { + return nil, err + } + defer conn.Close() + + 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)) + if err != nil { + return nil, err + } + + go receiveOffer(conn, discovery.TransactionID, result) + + _, err = conn.WriteTo(discovery.ToBytes(), &net.UDPAddr{IP: net.IPv4bcast, Port: 67}) + if err != nil { + return nil, err + } + + select { + case r, ok := <-result: + if !ok { + return nil, ErrNotFound + } + return r, nil + case <-context.Done(): + return nil, ErrNotResponding + } +} + +func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []net.IP) { + defer close(result) + + buf := make([]byte, dhcpv4.MaxMessageSize) + + for { + n, _, err := conn.ReadFrom(buf) + if err != nil { + return + } + + pkt, err := dhcpv4.FromBytes(buf[:n]) + if err != nil { + continue + } + + if pkt.MessageType() != dhcpv4.MessageTypeOffer { + continue + } + + if pkt.TransactionID != id { + continue + } + + dns := pkt.DNS() + if len(dns) == 0 { + return + } + + result <- dns + + return + } +} diff --git a/component/dialer/bind_darwin.go b/component/dialer/bind_darwin.go new file mode 100644 index 0000000..57e09bb --- /dev/null +++ b/component/dialer/bind_darwin.go @@ -0,0 +1,66 @@ +package dialer + +import ( + "net" + "syscall" + + "github.com/Dreamacro/clash/component/iface" + + "golang.org/x/sys/unix" +) + +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) { + switch network { + case "tcp4", "udp4": + innerErr = 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) + } + }) + + if innerErr != nil { + err = innerErr + } + + return + } +} + +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 +} diff --git a/component/dialer/bind_linux.go b/component/dialer/bind_linux.go new file mode 100644 index 0000000..97d37cf --- /dev/null +++ b/component/dialer/bind_linux.go @@ -0,0 +1,51 @@ +package dialer + +import ( + "net" + "syscall" + + "golang.org/x/sys/unix" +) + +type controlFn = func(network, address string, c syscall.RawConn) error + +func bindControl(ifaceName string, 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 = unix.BindToDevice(int(fd), ifaceName) + }) + + if innerErr != nil { + err = innerErr + } + + return + } +} + +func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error { + dialer.Control = bindControl(ifaceName, dialer.Control) + + return nil +} + +func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) { + lc.Control = bindControl(ifaceName, lc.Control) + + return address, nil +} diff --git a/component/dialer/bind_others.go b/component/dialer/bind_others.go new file mode 100644 index 0000000..0b1d8b1 --- /dev/null +++ b/component/dialer/bind_others.go @@ -0,0 +1,47 @@ +//go:build !linux && !darwin && !windows + +package dialer + +import ( + "net" + "strconv" +) + +func bindIfaceToDialer(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 bindIfaceToListenConfig(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 +} diff --git a/component/dialer/bind_windows.go b/component/dialer/bind_windows.go new file mode 100644 index 0000000..e9c198e --- /dev/null +++ b/component/dialer/bind_windows.go @@ -0,0 +1,98 @@ +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 +} diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go new file mode 100644 index 0000000..fe380d7 --- /dev/null +++ b/component/dialer/dialer.go @@ -0,0 +1,182 @@ +package dialer + +import ( + "context" + "errors" + "net" + + "github.com/Dreamacro/clash/component/resolver" +) + +func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) { + switch network { + case "tcp4", "tcp6", "udp4", "udp6": + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + var ip net.IP + switch network { + case "tcp4", "udp4": + ip, err = resolver.ResolveIPv4(host) + default: + ip, err = resolver.ResolveIPv6(host) + } + if err != nil { + return nil, err + } + + return dialContext(ctx, network, ip, port, options) + case "tcp", "udp": + return dualStackDialContext(ctx, network, address, options) + default: + return nil, errors.New("network invalid") + } +} + +func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) { + cfg := &option{ + interfaceName: DefaultInterface.Load(), + routingMark: int(DefaultRoutingMark.Load()), + } + + for _, o := range DefaultOptions { + o(cfg) + } + + for _, o := range options { + o(cfg) + } + + 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) + } + if err != nil { + return nil, err + } + address = addr + } + 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()), + } + + for _, o := range DefaultOptions { + o(opt) + } + + for _, o := range options { + o(opt) + } + + 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 opt.routingMark != 0 { + bindMarkToDialer(opt.routingMark, dialer, network, destination) + } + + return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port)) +} + +func dualStackDialContext(ctx context.Context, network, address string, options []Option) (net.Conn, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + returned := make(chan struct{}) + defer close(returned) + + type dialResult struct { + net.Conn + error + resolved bool + ipv6 bool + done bool + } + results := make(chan dialResult) + var primary, fallback dialResult + + startRacer := func(ctx context.Context, network, host string, ipv6 bool) { + result := dialResult{ipv6: ipv6, done: true} + defer func() { + select { + case results <- result: + case <-returned: + if result.Conn != nil { + result.Conn.Close() + } + } + }() + + var ip net.IP + if ipv6 { + ip, result.error = resolver.ResolveIPv6(host) + } else { + ip, result.error = resolver.ResolveIPv4(host) + } + if result.error != nil { + return + } + result.resolved = true + + result.Conn, result.error = dialContext(ctx, network, ip, port, options) + } + + go startRacer(ctx, network+"4", host, false) + go startRacer(ctx, network+"6", host, true) + + for res := range results { + if res.error == nil { + return res.Conn, nil + } + + if !res.ipv6 { + primary = res + } else { + fallback = res + } + + if primary.done && fallback.done { + if primary.resolved { + return nil, primary.error + } else if fallback.resolved { + return nil, fallback.error + } else { + return nil, primary.error + } + } + } + + return nil, errors.New("never touched") +} diff --git a/component/dialer/fallbackbind.go b/component/dialer/fallbackbind.go new file mode 100644 index 0000000..961cfd3 --- /dev/null +++ b/component/dialer/fallbackbind.go @@ -0,0 +1,90 @@ +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 +} diff --git a/component/dialer/mark_linux.go b/component/dialer/mark_linux.go new file mode 100644 index 0000000..759097d --- /dev/null +++ b/component/dialer/mark_linux.go @@ -0,0 +1,43 @@ +//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 + } +} diff --git a/component/dialer/mark_nonlinux.go b/component/dialer/mark_nonlinux.go new file mode 100644 index 0000000..b3bcfd0 --- /dev/null +++ b/component/dialer/mark_nonlinux.go @@ -0,0 +1,22 @@ +//go:build !linux + +package dialer + +import ( + "net" + "sync" + + "github.com/Dreamacro/clash/log" +) + +var printMarkWarn = sync.OnceFunc(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() +} diff --git a/component/dialer/options.go b/component/dialer/options.go new file mode 100644 index 0000000..9773ebb --- /dev/null +++ b/component/dialer/options.go @@ -0,0 +1,42 @@ +package dialer + +import "go.uber.org/atomic" + +var ( + DefaultOptions []Option + DefaultInterface = atomic.NewString("") + DefaultRoutingMark = atomic.NewInt32(0) +) + +type option struct { + interfaceName string + fallbackBind bool + addrReuse bool + routingMark int +} + +type Option func(opt *option) + +func WithInterface(name string) Option { + return func(opt *option) { + opt.interfaceName = name + } +} + +func WithFallbackBind(fallback bool) Option { + return func(opt *option) { + opt.fallbackBind = fallback + } +} + +func WithAddrReuse(reuse bool) Option { + return func(opt *option) { + opt.addrReuse = reuse + } +} + +func WithRoutingMark(mark int) Option { + return func(opt *option) { + opt.routingMark = mark + } +} diff --git a/component/dialer/reuse_others.go b/component/dialer/reuse_others.go new file mode 100644 index 0000000..db67a09 --- /dev/null +++ b/component/dialer/reuse_others.go @@ -0,0 +1,9 @@ +//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows + +package dialer + +import ( + "net" +) + +func addrReuseToListenConfig(*net.ListenConfig) {} diff --git a/component/dialer/reuse_unix.go b/component/dialer/reuse_unix.go new file mode 100644 index 0000000..85fe5e5 --- /dev/null +++ b/component/dialer/reuse_unix.go @@ -0,0 +1,27 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris + +package dialer + +import ( + "net" + "syscall" + + "golang.org/x/sys/unix" +) + +func addrReuseToListenConfig(lc *net.ListenConfig) { + chain := lc.Control + + lc.Control = func(network, address string, c syscall.RawConn) (err error) { + defer func() { + if err == nil && chain != nil { + err = chain(network, address, c) + } + }() + + return c.Control(func(fd uintptr) { + unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + }) + } +} diff --git a/component/dialer/reuse_windows.go b/component/dialer/reuse_windows.go new file mode 100644 index 0000000..77fcf7a --- /dev/null +++ b/component/dialer/reuse_windows.go @@ -0,0 +1,24 @@ +package dialer + +import ( + "net" + "syscall" + + "golang.org/x/sys/windows" +) + +func addrReuseToListenConfig(lc *net.ListenConfig) { + chain := lc.Control + + lc.Control = func(network, address string, c syscall.RawConn) (err error) { + defer func() { + if err == nil && chain != nil { + err = chain(network, address, c) + } + }() + + return c.Control(func(fd uintptr) { + windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) + }) + } +} diff --git a/component/fakeip/cachefile.go b/component/fakeip/cachefile.go new file mode 100644 index 0000000..04bdc65 --- /dev/null +++ b/component/fakeip/cachefile.go @@ -0,0 +1,55 @@ +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) {} diff --git a/component/fakeip/memory.go b/component/fakeip/memory.go new file mode 100644 index 0000000..c6c6873 --- /dev/null +++ b/component/fakeip/memory.go @@ -0,0 +1,69 @@ +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) + } +} diff --git a/component/fakeip/pool.go b/component/fakeip/pool.go new file mode 100644 index 0000000..fa7fba0 --- /dev/null +++ b/component/fakeip/pool.go @@ -0,0 +1,176 @@ +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 +type Pool struct { + max uint32 + min uint32 + gateway uint32 + offset uint32 + mux sync.Mutex + host *trie.DomainTrie + ipnet *net.IPNet + store store +} + +// Lookup return a fake ip with host +func (p *Pool) Lookup(host string) net.IP { + p.mux.Lock() + defer p.mux.Unlock() + + // RFC4343: DNS Case Insensitive, we SHOULD return result with all cases. + host = strings.ToLower(host) + if ip, exist := p.store.GetByHost(host); exist { + return ip + } + + ip := p.get(host) + p.store.PutByHost(host, ip) + return ip +} + +// LookBack return host with the fake ip +func (p *Pool) LookBack(ip net.IP) (string, bool) { + p.mux.Lock() + defer p.mux.Unlock() + + if ip = ip.To4(); ip == nil { + return "", false + } + + return p.store.GetByIP(ip) +} + +// ShouldSkipped return if domain should be skipped +func (p *Pool) ShouldSkipped(domain string) bool { + if p.host == nil { + return false + } + return p.host.Search(domain) != nil +} + +// Exist returns if given ip exists in fake-ip pool +func (p *Pool) Exist(ip net.IP) bool { + p.mux.Lock() + defer p.mux.Unlock() + + if ip = ip.To4(); ip == nil { + return false + } + + return p.store.Exist(ip) +} + +// Gateway return gateway ip +func (p *Pool) Gateway() net.IP { + return uintToIP(p.gateway) +} + +// IPNet return raw ipnet +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) +} + +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 + } + } + ip := uintToIP(p.min + p.offset) + p.store.PutByIP(ip, host) + return ip +} + +func ipToUint(ip net.IP) uint32 { + v := uint32(ip[0]) << 24 + v += uint32(ip[1]) << 16 + v += uint32(ip[2]) << 8 + v += uint32(ip[3]) + return v +} + +func uintToIP(v uint32) net.IP { + return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)} +} + +type Options struct { + 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 + + ones, bits := options.IPNet.Mask.Size() + total := 1<<uint(bits-ones) - 2 + + if total <= 0 { + return nil, errors.New("ipnet don't have valid ip") + } + + max := min + uint32(total) - 1 + pool := &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 +} diff --git a/component/fakeip/pool_test.go b/component/fakeip/pool_test.go new file mode 100644 index 0000000..e5b7ec5 --- /dev/null +++ b/component/fakeip/pool_test.go @@ -0,0 +1,212 @@ +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) + + for _, pool := range pools { + 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"))) + } +} + +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) { + _, 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) + + for _, pool := range pools { + assert.True(t, pool.ShouldSkipped("example.com")) + assert.False(t, pool.ShouldSkipped("foo.com")) + } +} + +func TestPool_MaxCacheSize(t *testing.T) { + _, ipnet, _ := net.ParseCIDR("192.168.0.1/24") + pool, _ := New(Options{ + IPNet: ipnet, + Size: 2, + }) + + first := pool.Lookup("foo.com") + pool.Lookup("bar.com") + pool.Lookup("baz.com") + next := pool.Lookup("foo.com") + + assert.False(t, first.Equal(next)) +} + +func TestPool_DoubleMapping(t *testing.T) { + _, ipnet, _ := net.ParseCIDR("192.168.0.1/24") + pool, _ := New(Options{ + IPNet: ipnet, + Size: 2, + }) + + // fill cache + fooIP := pool.Lookup("foo.com") + bazIP := pool.Lookup("baz.com") + + // make foo.com hot + pool.Lookup("foo.com") + + // should drop baz.com + barIP := pool.Lookup("bar.com") + + _, fooExist := pool.LookBack(fooIP) + _, bazExist := pool.LookBack(bazIP) + _, barExist := pool.LookBack(barIP) + + newBazIP := pool.Lookup("baz.com") + + assert.True(t, fooExist) + assert.False(t, bazExist) + assert.True(t, barExist) + + 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, + }) + + assert.Error(t, err) +} diff --git a/component/iface/iface.go b/component/iface/iface.go new file mode 100644 index 0000000..df29081 --- /dev/null +++ b/component/iface/iface.go @@ -0,0 +1,115 @@ +package iface + +import ( + "errors" + "net" + "time" + + "github.com/Dreamacro/clash/common/singledo" +) + +type Interface struct { + Index int + Name string + Addrs []*net.IPNet + HardwareAddr net.HardwareAddr +} + +var ( + ErrIfaceNotFound = errors.New("interface not found") + 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) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + r := map[string]*Interface{} + + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { + continue + } + + ipNets := make([]*net.IPNet, 0, len(addrs)) + for _, addr := range addrs { + ipNet := addr.(*net.IPNet) + if v4 := ipNet.IP.To4(); v4 != nil { + ipNet.IP = v4 + } + + ipNets = append(ipNets, ipNet) + } + + r[iface.Name] = &Interface{ + Index: iface.Index, + Name: iface.Name, + Addrs: ipNets, + HardwareAddr: iface.HardwareAddr, + } + } + + return r, nil + }) + if err != nil { + return nil, err + } + + ifaces := value.(map[string]*Interface) + iface, ok := ifaces[name] + if !ok { + return nil, ErrIfaceNotFound + } + + return iface, nil +} + +func FlushCache() { + interfaces.Reset() +} + +func (iface *Interface) PickIPv4Addr(destination net.IP) (*net.IPNet, error) { + return iface.pickIPAddr(destination, func(addr *net.IPNet) bool { + return addr.IP.To4() != nil + }) +} + +func (iface *Interface) PickIPv6Addr(destination net.IP) (*net.IPNet, error) { + return iface.pickIPAddr(destination, func(addr *net.IPNet) bool { + return addr.IP.To4() == nil + }) +} + +func (iface *Interface) pickIPAddr(destination net.IP, accept func(addr *net.IPNet) bool) (*net.IPNet, error) { + var fallback *net.IPNet + + for _, addr := range iface.Addrs { + if !accept(addr) { + continue + } + + if fallback == nil && !addr.IP.IsLinkLocalUnicast() { + fallback = addr + + if destination == nil { + break + } + } + + if destination != nil && addr.Contains(destination) { + return addr, nil + } + } + + if fallback == nil { + return nil, ErrAddrNotFound + } + + return fallback, nil +} diff --git a/component/ipset/ipset_linux.go b/component/ipset/ipset_linux.go new file mode 100644 index 0000000..a00cef6 --- /dev/null +++ b/component/ipset/ipset_linux.go @@ -0,0 +1,22 @@ +//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 +} diff --git a/component/ipset/ipset_others.go b/component/ipset/ipset_others.go new file mode 100644 index 0000000..546e5c3 --- /dev/null +++ b/component/ipset/ipset_others.go @@ -0,0 +1,17 @@ +//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 +} diff --git a/component/mmdb/mmdb.go b/component/mmdb/mmdb.go new file mode 100644 index 0000000..e120055 --- /dev/null +++ b/component/mmdb/mmdb.go @@ -0,0 +1,45 @@ +package mmdb + +import ( + "sync" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + + "github.com/oschwald/geoip2-golang" +) + +var ( + mmdb *geoip2.Reader + once sync.Once +) + +func LoadFromBytes(buffer []byte) { + once.Do(func() { + var err error + mmdb, err = geoip2.FromBytes(buffer) + if err != nil { + log.Fatalln("Can't load mmdb: %s", err.Error()) + } + }) +} + +func Verify() bool { + instance, err := geoip2.Open(C.Path.MMDB()) + if err == nil { + instance.Close() + } + return err == nil +} + +func Instance() *geoip2.Reader { + once.Do(func() { + var err error + mmdb, err = geoip2.Open(C.Path.MMDB()) + if err != nil { + log.Fatalln("Can't load mmdb: %s", err.Error()) + } + }) + + return mmdb +} diff --git a/component/nat/table.go b/component/nat/table.go new file mode 100644 index 0000000..fbb16de --- /dev/null +++ b/component/nat/table.go @@ -0,0 +1,37 @@ +package nat + +import ( + "sync" + + C "github.com/Dreamacro/clash/constant" +) + +type Table struct { + mapping sync.Map +} + +func (t *Table) Set(key string, pc C.PacketConn) { + t.mapping.Store(key, pc) +} + +func (t *Table) Get(key string) C.PacketConn { + item, exist := t.mapping.Load(key) + if !exist { + return nil + } + return item.(C.PacketConn) +} + +func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) { + item, loaded := t.mapping.LoadOrStore(key, sync.NewCond(&sync.Mutex{})) + return item.(*sync.Cond), loaded +} + +func (t *Table) Delete(key string) { + t.mapping.Delete(key) +} + +// New return *Cache +func New() *Table { + return &Table{} +} diff --git a/component/pool/pool.go b/component/pool/pool.go new file mode 100644 index 0000000..ef11753 --- /dev/null +++ b/component/pool/pool.go @@ -0,0 +1,114 @@ +package pool + +import ( + "context" + "runtime" + "time" +) + +type Factory = func(context.Context) (any, error) + +type entry struct { + elm any + time time.Time +} + +type Option func(*pool) + +// WithEvict set the evict callback +func WithEvict(cb func(any)) Option { + return func(p *pool) { + p.evict = cb + } +} + +// WithAge defined element max age (millisecond) +func WithAge(maxAge int64) Option { + return func(p *pool) { + p.maxAge = maxAge + } +} + +// WithSize defined max size of Pool +func WithSize(maxSize int) Option { + return func(p *pool) { + p.ch = make(chan any, maxSize) + } +} + +// Pool is for GC, see New for detail +type Pool struct { + *pool +} + +type pool struct { + ch chan any + factory Factory + evict func(any) + maxAge int64 +} + +func (p *pool) GetContext(ctx context.Context) (any, error) { + now := time.Now() + for { + select { + case item := <-p.ch: + elm := item.(*entry) + if p.maxAge != 0 && now.Sub(item.(*entry).time).Milliseconds() > p.maxAge { + if p.evict != nil { + p.evict(elm.elm) + } + continue + } + + return elm.elm, nil + default: + return p.factory(ctx) + } + } +} + +func (p *pool) Get() (any, error) { + return p.GetContext(context.Background()) +} + +func (p *pool) Put(item any) { + e := &entry{ + elm: item, + time: time.Now(), + } + + select { + case p.ch <- e: + return + default: + // pool is full + if p.evict != nil { + p.evict(item) + } + return + } +} + +func recycle(p *Pool) { + for item := range p.pool.ch { + if p.pool.evict != nil { + p.pool.evict(item.(*entry).elm) + } + } +} + +func New(factory Factory, options ...Option) *Pool { + p := &pool{ + ch: make(chan any, 10), + factory: factory, + } + + for _, option := range options { + option(p) + } + + P := &Pool{p} + runtime.SetFinalizer(P, recycle) + return P +} diff --git a/component/pool/pool_test.go b/component/pool/pool_test.go new file mode 100644 index 0000000..10f4996 --- /dev/null +++ b/component/pool/pool_test.go @@ -0,0 +1,73 @@ +package pool + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func lg() Factory { + initial := -1 + return func(context.Context) (any, error) { + initial++ + return initial, nil + } +} + +func TestPool_Basic(t *testing.T) { + g := lg() + pool := New(g) + + elm, _ := pool.Get() + assert.Equal(t, 0, elm.(int)) + pool.Put(elm) + elm, _ = pool.Get() + assert.Equal(t, 0, elm.(int)) + elm, _ = pool.Get() + assert.Equal(t, 1, elm.(int)) +} + +func TestPool_MaxSize(t *testing.T) { + g := lg() + size := 5 + pool := New(g, WithSize(size)) + + items := []any{} + + for i := 0; i < size; i++ { + item, _ := pool.Get() + items = append(items, item) + } + + extra, _ := pool.Get() + assert.Equal(t, size, extra.(int)) + + for _, item := range items { + pool.Put(item) + } + + pool.Put(extra) + + for _, item := range items { + elm, _ := pool.Get() + assert.Equal(t, item.(int), elm.(int)) + } +} + +func TestPool_MaxAge(t *testing.T) { + g := lg() + pool := New(g, WithAge(20)) + + elm, _ := pool.Get() + pool.Put(elm) + + elm, _ = pool.Get() + assert.Equal(t, 0, elm.(int)) + pool.Put(elm) + + time.Sleep(time.Millisecond * 22) + elm, _ = pool.Get() + assert.Equal(t, 1, elm.(int)) +} diff --git a/component/process/process.go b/component/process/process.go new file mode 100644 index 0000000..7ca2e2b --- /dev/null +++ b/component/process/process.go @@ -0,0 +1,21 @@ +package process + +import ( + "errors" + "net/netip" +) + +var ( + ErrInvalidNetwork = errors.New("invalid network") + ErrPlatformNotSupport = errors.New("not support on this platform") + ErrNotFound = errors.New("process not found") +) + +const ( + TCP = "tcp" + UDP = "udp" +) + +func FindProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) { + return findProcessPath(network, from, to) +} diff --git a/component/process/process_darwin.go b/component/process/process_darwin.go new file mode 100644 index 0000000..625012c --- /dev/null +++ b/component/process/process_darwin.go @@ -0,0 +1,135 @@ +package process + +import ( + "encoding/binary" + "net/netip" + "strconv" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + procpidpathinfo = 0xb + procpidpathinfosize = 1024 + 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) { + var spath string + switch network { + case TCP: + spath = "net.inet.tcp.pcblist_n" + case UDP: + spath = "net.inet.udp.pcblist_n" + default: + return "", ErrInvalidNetwork + } + + isIPv4 := from.Addr().Is4() + + value, err := syscall.Sysctl(spath) + if err != nil { + return "", err + } + + buf := []byte(value) + itemSize := structSize + 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 { + continue + } + + // FIXME: add dstPort check + + // xinpcb_n.inp_vflag + flag := buf[inp+44] + + var ( + srcIP netip.Addr + srcIPOk bool + srcIsIPv4 bool + ) + switch { + case flag&0x1 > 0 && isIPv4: + // ipv4 + srcIP, srcIPOk = netip.AddrFromSlice(buf[inp+76 : inp+80]) + srcIsIPv4 = true + case flag&0x2 > 0 && !isIPv4: + // ipv6 + srcIP, srcIPOk = netip.AddrFromSlice(buf[inp+64 : inp+80]) + default: + continue + } + if !srcIPOk { + 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 + } + + return "", ErrNotFound +} + +func getExecPathFromPID(pid uint32) (string, error) { + buf := make([]byte, procpidpathinfosize) + _, _, errno := syscall.Syscall6( + syscall.SYS_PROC_INFO, + proccallnumpidinfo, + uintptr(pid), + procpidpathinfo, + 0, + uintptr(unsafe.Pointer(&buf[0])), + procpidpathinfosize) + if errno != 0 { + return "", errno + } + + return unix.ByteSliceToString(buf), nil +} + +func readNativeUint32(b []byte) uint32 { + return *(*uint32)(unsafe.Pointer(&b[0])) +} diff --git a/component/process/process_freebsd.go b/component/process/process_freebsd.go new file mode 100644 index 0000000..769c684 --- /dev/null +++ b/component/process/process_freebsd.go @@ -0,0 +1,217 @@ +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 +} diff --git a/component/process/process_freebsd_test.go b/component/process/process_freebsd_test.go new file mode 100644 index 0000000..e84fa72 --- /dev/null +++ b/component/process/process_freebsd_test.go @@ -0,0 +1,35 @@ +//go:build freebsd + +package process + +import ( + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" +) + +func TestEnforceStructValid12(t *testing.T) { + if majorVersion != 12 && majorVersion != 13 { + t.Skipf("Unsupported freebsd version: %d", majorVersion) + + return + } + + assert.Equal(t, 0, int(unsafe.Offsetof(XTcpcb12{}.Len))) + assert.Equal(t, 24, int(unsafe.Offsetof(XTcpcb12{}.SocketAddr))) + assert.Equal(t, 116, int(unsafe.Offsetof(XTcpcb12{}.Family))) + assert.Equal(t, 260, int(unsafe.Offsetof(XTcpcb12{}.InEndpoints))) + assert.Equal(t, 0, int(unsafe.Offsetof(XInpcb12{}.Len))) + assert.Equal(t, 16, int(unsafe.Offsetof(XInpcb12{}.SocketAddr))) + assert.Equal(t, 108, int(unsafe.Offsetof(XInpcb12{}.Family))) + assert.Equal(t, 252, int(unsafe.Offsetof(XInpcb12{}.InEndpoints))) + assert.Equal(t, 0, int(unsafe.Offsetof(XFile12{}.Size))) + assert.Equal(t, 8, int(unsafe.Offsetof(XFile12{}.Pid))) + assert.Equal(t, 56, int(unsafe.Offsetof(XFile12{}.DataAddr))) + assert.Equal(t, 64, int(unsafe.Sizeof(Xinpgen12{}))) + assert.Equal(t, 744, int(unsafe.Sizeof(XTcpcb12{}))) + assert.Equal(t, 400, int(unsafe.Sizeof(XInpcb12{}))) + assert.Equal(t, 40, int(unsafe.Sizeof(InEndpoints12{}))) + assert.Equal(t, 128, int(unsafe.Sizeof(XFile12{}))) +} diff --git a/component/process/process_linux.go b/component/process/process_linux.go new file mode 100644 index 0000000..5d41248 --- /dev/null +++ b/component/process/process_linux.go @@ -0,0 +1,232 @@ +package process + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "net/netip" + "os" + "unsafe" + + "github.com/Dreamacro/clash/common/pool" + + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +type inetDiagRequest struct { + Family byte + Protocol byte + Ext byte + Pad byte + States uint32 + + SrcPort [2]byte + DstPort [2]byte + Src [16]byte + Dst [16]byte + If uint32 + Cookie [2]uint32 +} + +type inetDiagResponse struct { + Family byte + State byte + Timer byte + ReTrans byte + + SrcPort [2]byte + DstPort [2]byte + Src [16]byte + Dst [16]byte + If uint32 + Cookie [2]uint32 + + Expires uint32 + RQueue uint32 + WQueue uint32 + UID uint32 + INode uint32 +} + +func findProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) { + inode, uid, err := resolveSocketByNetlink(network, from, to) + if err != nil { + return "", err + } + + return resolveProcessPathByProcSearch(inode, uid) +} + +func resolveSocketByNetlink(network string, from netip.AddrPort, to netip.AddrPort) (inode uint32, uid uint32, err error) { + var families []byte + if from.Addr().Unmap().Is4() { + families = []byte{unix.AF_INET, unix.AF_INET6} + } else { + families = []byte{unix.AF_INET6, unix.AF_INET} + } + + var protocol byte + switch network { + case TCP: + protocol = unix.IPPROTO_TCP + case UDP: + protocol = unix.IPPROTO_UDP + default: + return 0, 0, ErrInvalidNetwork + } + + if protocol == unix.IPPROTO_UDP { + // Swap from & to for udp + // See also https://www.mail-archive.com/netdev@vger.kernel.org/msg248638.html + from, to = to, from + } + + for _, family := range families { + inode, uid, err = resolveSocketByNetlinkExact(family, protocol, from, to, netlink.Request) + if err == nil { + return inode, uid, err + } + } + + return 0, 0, ErrNotFound +} + +func resolveSocketByNetlinkExact(family byte, protocol byte, from netip.AddrPort, to netip.AddrPort, flags netlink.HeaderFlags) (inode uint32, uid uint32, err error) { + request := &inetDiagRequest{ + Family: family, + Protocol: protocol, + States: 0xffffffff, + Cookie: [2]uint32{0xffffffff, 0xffffffff}, + } + + var ( + fromAddr []byte + toAddr []byte + ) + if family == unix.AF_INET { + fromAddr = net.IP(from.Addr().AsSlice()).To4() + toAddr = net.IP(to.Addr().AsSlice()).To4() + } else { + fromAddr = net.IP(from.Addr().AsSlice()).To16() + toAddr = net.IP(to.Addr().AsSlice()).To16() + } + + copy(request.Src[:], fromAddr) + copy(request.Dst[:], toAddr) + + binary.BigEndian.PutUint16(request.SrcPort[:], from.Port()) + binary.BigEndian.PutUint16(request.DstPort[:], to.Port()) + + conn, err := netlink.Dial(unix.NETLINK_INET_DIAG, nil) + if err != nil { + return 0, 0, err + } + defer conn.Close() + + message := netlink.Message{ + Header: netlink.Header{ + Type: 20, // SOCK_DIAG_BY_FAMILY + Flags: flags, + }, + Data: (*(*[unsafe.Sizeof(*request)]byte)(unsafe.Pointer(request)))[:], + } + + messages, err := conn.Execute(message) + if err != nil { + return 0, 0, err + } + + for _, msg := range messages { + if len(msg.Data) < int(unsafe.Sizeof(inetDiagResponse{})) { + continue + } + + response := (*inetDiagResponse)(unsafe.Pointer(&msg.Data[0])) + + return response.INode, response.UID, nil + } + + return 0, 0, ErrNotFound +} + +func resolveProcessPathByProcSearch(inode, uid uint32) (string, error) { + procDir, err := os.Open("/proc") + if err != nil { + return "", err + } + defer procDir.Close() + + pids, err := procDir.Readdirnames(-1) + if err != nil { + return "", err + } + + expectedSocketName := fmt.Appendf(nil, "socket:[%d]", inode) + + pathBuffer := pool.Get(64) + defer pool.Put(pathBuffer) + + readlinkBuffer := pool.Get(32) + defer pool.Put(readlinkBuffer) + + copy(pathBuffer, "/proc/") + + for _, pid := range pids { + if !isPid(pid) { + continue + } + + pathBuffer = append(pathBuffer[:len("/proc/")], pid...) + + stat := &unix.Stat_t{} + err = unix.Stat(string(pathBuffer), stat) + if err != nil { + continue + } else if stat.Uid != uid { + continue + } + + pathBuffer = append(pathBuffer, "/fd/"...) + fdsPrefixLength := len(pathBuffer) + + fdDir, err := os.Open(string(pathBuffer)) + if err != nil { + continue + } + + fds, err := fdDir.Readdirnames(-1) + fdDir.Close() + if err != nil { + continue + } + + for _, fd := range fds { + pathBuffer = pathBuffer[:fdsPrefixLength] + + pathBuffer = append(pathBuffer, fd...) + + n, err := unix.Readlink(string(pathBuffer), readlinkBuffer) + if err != nil { + continue + } + + if bytes.Equal(readlinkBuffer[:n], expectedSocketName) { + return os.Readlink("/proc/" + pid + "/exe") + } + } + } + + return "", fmt.Errorf("inode %d of uid %d not found", inode, uid) +} + +func isPid(name string) bool { + for _, c := range name { + if c < '0' || c > '9' { + return false + } + } + + return true +} diff --git a/component/process/process_other.go b/component/process/process_other.go new file mode 100644 index 0000000..0a1a60c --- /dev/null +++ b/component/process/process_other.go @@ -0,0 +1,11 @@ +//go:build !darwin && !linux && !windows && !freebsd + +package process + +import ( + "net/netip" +) + +func findProcessPath(_ string, _, _ netip.AddrPort) (string, error) { + return "", ErrPlatformNotSupport +} diff --git a/component/process/process_test.go b/component/process/process_test.go new file mode 100644 index 0000000..2a71f29 --- /dev/null +++ b/component/process/process_test.go @@ -0,0 +1,112 @@ +package process + +import ( + "net" + "net/netip" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func testConn(t *testing.T, network, address string) { + l, err := net.Listen(network, address) + if err != nil { + assert.FailNow(t, "Listen failed", err) + } + defer l.Close() + + conn, err := net.Dial("tcp", l.Addr().String()) + if err != nil { + assert.FailNow(t, "Dial failed", err) + } + defer conn.Close() + + rConn, err := l.Accept() + if err != nil { + assert.FailNow(t, "Accept conn failed", err) + } + defer rConn.Close() + + path, err := FindProcessPath(TCP, conn.LocalAddr().(*net.TCPAddr).AddrPort(), conn.RemoteAddr().(*net.TCPAddr).AddrPort()) + if err != nil { + assert.FailNow(t, "Find process path failed", err) + } + + exePath, err := os.Executable() + if err != nil { + assert.FailNow(t, "Get executable failed", err) + } + + assert.Equal(t, exePath, path) +} + +func TestFindProcessPathTCP(t *testing.T) { + t.Run("v4", func(t *testing.T) { + testConn(t, "tcp4", "127.0.0.1:0") + }) + t.Run("v6", func(t *testing.T) { + testConn(t, "tcp6", "[::1]:0") + }) +} + +func testPacketConn(t *testing.T, network, lAddress, rAddress string) { + lConn, err := net.ListenPacket(network, lAddress) + if err != nil { + assert.FailNow(t, "ListenPacket failed", err) + } + defer lConn.Close() + + rConn, err := net.ListenPacket(network, rAddress) + if err != nil { + assert.FailNow(t, "ListenPacket failed", err) + } + defer rConn.Close() + + _, err = lConn.WriteTo([]byte{0}, rConn.LocalAddr()) + if err != nil { + assert.FailNow(t, "Send message failed", err) + } + + _, lAddr, err := rConn.ReadFrom([]byte{0}) + if err != nil { + assert.FailNow(t, "Receive message failed", err) + } + + path, err := FindProcessPath(UDP, lAddr.(*net.UDPAddr).AddrPort(), rConn.LocalAddr().(*net.UDPAddr).AddrPort()) + if err != nil { + assert.FailNow(t, "Find process path", err) + } + + exePath, err := os.Executable() + if err != nil { + assert.FailNow(t, "Find executable", err) + } + + assert.Equal(t, exePath, path) +} + +func TestFindProcessPathUDP(t *testing.T) { + t.Run("v4", func(t *testing.T) { + testPacketConn(t, "udp4", "127.0.0.1:0", "127.0.0.1:0") + }) + t.Run("v6", func(t *testing.T) { + testPacketConn(t, "udp6", "[::1]:0", "[::1]:0") + }) + t.Run("v4AnyLocal", func(t *testing.T) { + testPacketConn(t, "udp4", "0.0.0.0:0", "127.0.0.1:0") + }) + t.Run("v6AnyLocal", func(t *testing.T) { + testPacketConn(t, "udp6", "[::]:0", "[::1]:0") + }) +} + +func BenchmarkFindProcessName(b *testing.B) { + from := netip.MustParseAddrPort("127.0.0.1:11447") + to := netip.MustParseAddrPort("127.0.0.1:33669") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + FindProcessPath(TCP, from, to) + } +} diff --git a/component/process/process_windows.go b/component/process/process_windows.go new file mode 100644 index 0000000..47ede05 --- /dev/null +++ b/component/process/process_windows.go @@ -0,0 +1,229 @@ +package process + +import ( + "errors" + "fmt" + "net/netip" + "unsafe" + + "github.com/Dreamacro/clash/common/pool" + + "golang.org/x/sys/windows" +) + +var ( + modIphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") + + procGetExtendedTcpTable = modIphlpapi.NewProc("GetExtendedTcpTable") + procGetExtendedUdpTable = modIphlpapi.NewProc("GetExtendedUdpTable") +) + +func findProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) { + family := uint32(windows.AF_INET) + if from.Addr().Is6() { + family = windows.AF_INET6 + } + + var protocol uint32 + switch network { + case TCP: + protocol = windows.IPPROTO_TCP + case UDP: + protocol = windows.IPPROTO_UDP + default: + return "", ErrInvalidNetwork + } + + pid, err := findPidByConnectionEndpoint(family, protocol, from, to) + if err != nil { + return "", err + } + + return getExecPathFromPID(pid) +} + +func findPidByConnectionEndpoint(family uint32, protocol uint32, from netip.AddrPort, to netip.AddrPort) (uint32, error) { + buf := pool.Get(0) + defer pool.Put(buf) + + bufSize := uint32(len(buf)) + +loop: + for { + var ret uintptr + + switch protocol { + case windows.IPPROTO_TCP: + ret, _, _ = procGetExtendedTcpTable.Call( + uintptr(unsafe.Pointer(unsafe.SliceData(buf))), + uintptr(unsafe.Pointer(&bufSize)), + 0, + uintptr(family), + 4, // TCP_TABLE_OWNER_PID_CONNECTIONS + 0, + ) + case windows.IPPROTO_UDP: + ret, _, _ = procGetExtendedUdpTable.Call( + uintptr(unsafe.Pointer(unsafe.SliceData(buf))), + uintptr(unsafe.Pointer(&bufSize)), + 0, + uintptr(family), + 1, // UDP_TABLE_OWNER_PID + 0, + ) + default: + return 0, errors.New("unsupported network") + } + + switch ret { + case 0: + buf = buf[:bufSize] + + break loop + case uintptr(windows.ERROR_INSUFFICIENT_BUFFER): + pool.Put(buf) + buf = pool.Get(int(bufSize)) + + continue loop + default: + return 0, fmt.Errorf("syscall error: %d", ret) + } + } + + if len(buf) < int(unsafe.Sizeof(uint32(0))) { + return 0, fmt.Errorf("invalid table size: %d", len(buf)) + } + + entriesSize := *(*uint32)(unsafe.Pointer(&buf[0])) + + switch protocol { + case windows.IPPROTO_TCP: + if family == windows.AF_INET { + type MibTcpRowOwnerPid struct { + State uint32 + LocalAddr [4]byte + LocalPort uint32 + RemoteAddr [4]byte + RemotePort uint32 + OwningPid uint32 + } + + if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibTcpRowOwnerPid{})) { + return 0, fmt.Errorf("invalid tables size: %d", len(buf)) + } + + entries := unsafe.Slice((*MibTcpRowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize) + for _, entry := range entries { + localAddr := netip.AddrFrom4(entry.LocalAddr) + localPort := windows.Ntohs(uint16(entry.LocalPort)) + remoteAddr := netip.AddrFrom4(entry.RemoteAddr) + remotePort := windows.Ntohs(uint16(entry.RemotePort)) + + if localAddr == from.Addr() && remoteAddr == to.Addr() && localPort == from.Port() && remotePort == to.Port() { + return entry.OwningPid, nil + } + } + } else { + type MibTcp6RowOwnerPid struct { + LocalAddr [16]byte + LocalScopeID uint32 + LocalPort uint32 + RemoteAddr [16]byte + RemoteScopeID uint32 + RemotePort uint32 + State uint32 + OwningPid uint32 + } + + if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibTcp6RowOwnerPid{})) { + return 0, fmt.Errorf("invalid tables size: %d", len(buf)) + } + + entries := unsafe.Slice((*MibTcp6RowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize) + for _, entry := range entries { + localAddr := netip.AddrFrom16(entry.LocalAddr) + localPort := windows.Ntohs(uint16(entry.LocalPort)) + remoteAddr := netip.AddrFrom16(entry.RemoteAddr) + remotePort := windows.Ntohs(uint16(entry.RemotePort)) + + if localAddr == from.Addr() && remoteAddr == to.Addr() && localPort == from.Port() && remotePort == to.Port() { + return entry.OwningPid, nil + } + } + } + case windows.IPPROTO_UDP: + if family == windows.AF_INET { + type MibUdpRowOwnerPid struct { + LocalAddr [4]byte + LocalPort uint32 + OwningPid uint32 + } + + if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibUdpRowOwnerPid{})) { + return 0, fmt.Errorf("invalid tables size: %d", len(buf)) + } + + entries := unsafe.Slice((*MibUdpRowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize) + for _, entry := range entries { + localAddr := netip.AddrFrom4(entry.LocalAddr) + localPort := windows.Ntohs(uint16(entry.LocalPort)) + + if (localAddr == from.Addr() || localAddr.IsUnspecified()) && localPort == from.Port() { + return entry.OwningPid, nil + } + } + } else { + type MibUdp6RowOwnerPid struct { + LocalAddr [16]byte + LocalScopeId uint32 + LocalPort uint32 + OwningPid uint32 + } + + if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibUdp6RowOwnerPid{})) { + return 0, fmt.Errorf("invalid tables size: %d", len(buf)) + } + + entries := unsafe.Slice((*MibUdp6RowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize) + for _, entry := range entries { + localAddr := netip.AddrFrom16(entry.LocalAddr) + localPort := windows.Ntohs(uint16(entry.LocalPort)) + + if (localAddr == from.Addr() || localAddr.IsUnspecified()) && localPort == from.Port() { + return entry.OwningPid, nil + } + } + } + default: + return 0, ErrInvalidNetwork + } + + return 0, ErrNotFound +} + +func getExecPathFromPID(pid uint32) (string, error) { + // kernel process starts with a colon in order to distinguish with normal processes + switch pid { + case 0: + // reserved pid for system idle process + return ":System Idle Process", nil + case 4: + // reserved pid for windows kernel image + return ":System", nil + } + h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) + if err != nil { + return "", err + } + defer windows.CloseHandle(h) + + buf := make([]uint16, windows.MAX_LONG_PATH) + size := uint32(len(buf)) + + err = windows.QueryFullProcessImageName(h, 0, &buf[0], &size) + if err != nil { + return "", err + } + + return windows.UTF16ToString(buf[:size]), nil +} diff --git a/component/profile/cachefile/cache.go b/component/profile/cachefile/cache.go new file mode 100644 index 0000000..3cc3c7d --- /dev/null +++ b/component/profile/cachefile/cache.go @@ -0,0 +1,157 @@ +package cachefile + +import ( + "os" + "sync" + "time" + + "github.com/Dreamacro/clash/component/profile" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + + "go.etcd.io/bbolt" +) + +var ( + fileMode os.FileMode = 0o666 + + bucketSelected = []byte("selected") + bucketFakeip = []byte("fakeip") +) + +// CacheFile store and update the cache file +type CacheFile struct { + DB *bbolt.DB +} + +func (c *CacheFile) SetSelected(group, selected string) { + if !profile.StoreSelected.Load() { + return + } else if c.DB == nil { + return + } + + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketSelected) + if err != nil { + return err + } + return bucket.Put([]byte(group), []byte(selected)) + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + return + } +} + +func (c *CacheFile) SelectedMap() map[string]string { + if !profile.StoreSelected.Load() { + return nil + } else if c.DB == nil { + return nil + } + + mapping := map[string]string{} + c.DB.View(func(t *bbolt.Tx) error { + bucket := t.Bucket(bucketSelected) + if bucket == nil { + return nil + } + + c := bucket.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + mapping[string(k)] = string(v) + } + return nil + }) + return mapping +} + +func (c *CacheFile) PutFakeip(key, value []byte) error { + if c.DB == nil { + return nil + } + + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketFakeip) + if err != nil { + return err + } + return bucket.Put(key, value) + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + } + + return err +} + +func (c *CacheFile) DelFakeipPair(ip, host []byte) error { + if c.DB == nil { + return nil + } + + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketFakeip) + if err != nil { + return err + } + err = bucket.Delete(ip) + if len(host) > 0 { + if err := bucket.Delete(host); err != nil { + return err + } + } + return err + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + } + + return err +} + +func (c *CacheFile) GetFakeip(key []byte) []byte { + if c.DB == nil { + return nil + } + + tx, err := c.DB.Begin(false) + if err != nil { + return nil + } + defer tx.Rollback() + + bucket := tx.Bucket(bucketFakeip) + if bucket == nil { + return nil + } + + return bucket.Get(key) +} + +func (c *CacheFile) Close() error { + return c.DB.Close() +} + +// Cache return singleton of CacheFile +var Cache = sync.OnceValue(func() *CacheFile { + options := bbolt.Options{Timeout: time.Second} + db, err := bbolt.Open(C.Path.Cache(), fileMode, &options) + switch err { + case bbolt.ErrInvalid, bbolt.ErrChecksum, bbolt.ErrVersionMismatch: + if err = os.Remove(C.Path.Cache()); err != nil { + log.Warnln("[CacheFile] remove invalid cache file error: %s", err.Error()) + break + } + log.Infoln("[CacheFile] remove invalid cache file and create new one") + db, err = bbolt.Open(C.Path.Cache(), fileMode, &options) + } + if err != nil { + log.Warnln("[CacheFile] can't open cache file: %s", err.Error()) + } + + return &CacheFile{ + DB: db, + } +}) diff --git a/component/profile/profile.go b/component/profile/profile.go new file mode 100644 index 0000000..e3d9e78 --- /dev/null +++ b/component/profile/profile.go @@ -0,0 +1,8 @@ +package profile + +import ( + "go.uber.org/atomic" +) + +// StoreSelected is a global switch for storing selected proxy to cache +var StoreSelected = atomic.NewBool(true) diff --git a/component/resolver/defaults.go b/component/resolver/defaults.go new file mode 100644 index 0000000..8a04bd1 --- /dev/null +++ b/component/resolver/defaults.go @@ -0,0 +1,12 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris + +package resolver + +import _ "unsafe" + +//go:linkname defaultNS net.defaultNS +var defaultNS []string + +func init() { + defaultNS = []string{"114.114.114.114:53", "8.8.8.8:53"} +} diff --git a/component/resolver/enhancer.go b/component/resolver/enhancer.go new file mode 100644 index 0000000..c096f87 --- /dev/null +++ b/component/resolver/enhancer.go @@ -0,0 +1,55 @@ +package resolver + +import ( + "net" +) + +var DefaultHostMapper Enhancer + +type Enhancer interface { + FakeIPEnabled() bool + MappingEnabled() bool + IsFakeIP(net.IP) bool + IsExistFakeIP(net.IP) bool + FindHostByIP(net.IP) (string, bool) +} + +func FakeIPEnabled() bool { + if mapper := DefaultHostMapper; mapper != nil { + return mapper.FakeIPEnabled() + } + + return false +} + +func MappingEnabled() bool { + if mapper := DefaultHostMapper; mapper != nil { + return mapper.MappingEnabled() + } + + return false +} + +func IsFakeIP(ip net.IP) bool { + if mapper := DefaultHostMapper; mapper != nil { + return mapper.IsFakeIP(ip) + } + + return false +} + +func IsExistFakeIP(ip net.IP) bool { + if mapper := DefaultHostMapper; mapper != nil { + return mapper.IsExistFakeIP(ip) + } + + return false +} + +func FindHostByIP(ip net.IP) (string, bool) { + if mapper := DefaultHostMapper; mapper != nil { + return mapper.FindHostByIP(ip) + } + + return "", false +} diff --git a/component/resolver/resolver.go b/component/resolver/resolver.go new file mode 100644 index 0000000..c69bc51 --- /dev/null +++ b/component/resolver/resolver.go @@ -0,0 +1,182 @@ +package resolver + +import ( + "context" + "errors" + "fmt" + "math/rand" + "net" + "strings" + "time" + + "github.com/Dreamacro/clash/component/trie" + + "github.com/miekg/dns" +) + +var ( + // DefaultResolver aim to resolve ip + DefaultResolver Resolver + + // DisableIPv6 means don't resolve ipv6 host + // default value is true + DisableIPv6 = true + + // DefaultHosts aim to resolve hosts + DefaultHosts = trie.New() + + // DefaultDNSTimeout defined the default dns request timeout + DefaultDNSTimeout = time.Second * 5 +) + +var ( + ErrIPNotFound = errors.New("couldn't find ip") + ErrIPVersion = errors.New("ip version error") + ErrIPv6Disabled = errors.New("ipv6 disabled") +) + +type Resolver interface { + LookupIP(ctx context.Context, host string) ([]net.IP, error) + LookupIPv4(ctx context.Context, host string) ([]net.IP, error) + LookupIPv6(ctx context.Context, host string) ([]net.IP, error) + ResolveIP(host string) (ip net.IP, err error) + ResolveIPv4(host string) (ip net.IP, err error) + ResolveIPv6(host string) (ip net.IP, err error) + ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error) +} + +// LookupIPv4 with a host, return ipv4 list +func LookupIPv4(ctx context.Context, host string) ([]net.IP, error) { + if node := DefaultHosts.Search(host); node != nil { + if ip := node.Data.(net.IP).To4(); ip != nil { + return []net.IP{ip}, nil + } + } + + ip := net.ParseIP(host) + if ip != nil { + if !strings.Contains(host, ":") { + return []net.IP{ip}, nil + } + return nil, ErrIPVersion + } + + if DefaultResolver != nil { + return DefaultResolver.LookupIPv4(ctx, host) + } + + ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout) + defer cancel() + ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host) + if err != nil { + return nil, err + } else if len(ipAddrs) == 0 { + return nil, ErrIPNotFound + } + + return ipAddrs, nil +} + +// ResolveIPv4 with a host, return ipv4 +func ResolveIPv4(host string) (net.IP, error) { + ips, err := LookupIPv4(context.Background(), host) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, fmt.Errorf("%w: %s", ErrIPNotFound, host) + } + return ips[rand.Intn(len(ips))], nil +} + +// LookupIPv6 with a host, return ipv6 list +func LookupIPv6(ctx context.Context, host string) ([]net.IP, error) { + if DisableIPv6 { + return nil, ErrIPv6Disabled + } + + if node := DefaultHosts.Search(host); node != nil { + if ip := node.Data.(net.IP).To16(); ip != nil { + return []net.IP{ip}, nil + } + } + + ip := net.ParseIP(host) + if ip != nil { + if strings.Contains(host, ":") { + return []net.IP{ip}, nil + } + return nil, ErrIPVersion + } + + if DefaultResolver != nil { + return DefaultResolver.LookupIPv6(ctx, host) + } + + ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout) + defer cancel() + ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host) + if err != nil { + return nil, err + } else if len(ipAddrs) == 0 { + return nil, ErrIPNotFound + } + + return ipAddrs, nil +} + +// ResolveIPv6 with a host, return ipv6 +func ResolveIPv6(host string) (net.IP, error) { + ips, err := LookupIPv6(context.Background(), host) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, fmt.Errorf("%w: %s", ErrIPNotFound, host) + } + return ips[rand.Intn(len(ips))], nil +} + +// LookupIPWithResolver same as ResolveIP, but with a resolver +func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]net.IP, error) { + if node := DefaultHosts.Search(host); node != nil { + return []net.IP{node.Data.(net.IP)}, nil + } + + if r != nil { + if DisableIPv6 { + return r.LookupIPv4(ctx, host) + } + return r.LookupIP(ctx, host) + } else if DisableIPv6 { + return LookupIPv4(ctx, host) + } + + ip := net.ParseIP(host) + if ip != nil { + return []net.IP{ip}, nil + } + + ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, ErrIPNotFound + } + + return ips, nil +} + +// ResolveIP with a host, return ip +func LookupIP(ctx context.Context, host string) ([]net.IP, error) { + return LookupIPWithResolver(ctx, host, DefaultResolver) +} + +// ResolveIP with a host, return ip +func ResolveIP(host string) (net.IP, error) { + ips, err := LookupIP(context.Background(), host) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, fmt.Errorf("%w: %s", ErrIPNotFound, host) + } + return ips[rand.Intn(len(ips))], nil +} diff --git a/component/trie/domain.go b/component/trie/domain.go new file mode 100644 index 0000000..8915eda --- /dev/null +++ b/component/trie/domain.go @@ -0,0 +1,129 @@ +package trie + +import ( + "errors" + "strings" +) + +const ( + wildcard = "*" + dotWildcard = "" + complexWildcard = "+" + domainStep = "." +) + +// ErrInvalidDomain means insert domain is invalid +var ErrInvalidDomain = errors.New("invalid domain") + +// DomainTrie contains the main logic for adding and searching nodes for domain segments. +// support wildcard domain (e.g *.google.com) +type DomainTrie struct { + root *Node +} + +func ValidAndSplitDomain(domain string) ([]string, bool) { + if domain != "" && domain[len(domain)-1] == '.' { + return nil, false + } + + parts := strings.Split(domain, domainStep) + if len(parts) == 1 { + if parts[0] == "" { + return nil, false + } + + return parts, true + } + + for _, part := range parts[1:] { + if part == "" { + return nil, false + } + } + + return parts, true +} + +// Insert adds a node to the trie. +// Support +// 1. www.example.com +// 2. *.example.com +// 3. subdomain.*.example.com +// 4. .example.com +// 5. +.example.com +func (t *DomainTrie) Insert(domain string, data any) error { + parts, valid := ValidAndSplitDomain(domain) + if !valid { + return ErrInvalidDomain + } + + if parts[0] == complexWildcard { + t.insert(parts[1:], data) + parts[0] = dotWildcard + t.insert(parts, data) + } else { + t.insert(parts, data) + } + + return nil +} + +func (t *DomainTrie) insert(parts []string, data any) { + node := t.root + // reverse storage domain part to save space + for i := len(parts) - 1; i >= 0; i-- { + part := parts[i] + if !node.hasChild(part) { + node.addChild(part, newNode(nil)) + } + + node = node.getChild(part) + } + + node.Data = data +} + +// Search is the most important part of the Trie. +// Priority as: +// 1. static part +// 2. wildcard domain +// 2. dot wildcard domain +func (t *DomainTrie) Search(domain string) *Node { + parts, valid := ValidAndSplitDomain(domain) + if !valid || parts[0] == "" { + return nil + } + + n := t.search(t.root, parts) + + if n == nil || n.Data == nil { + return nil + } + + return n +} + +func (t *DomainTrie) search(node *Node, parts []string) *Node { + if len(parts) == 0 { + return node + } + + if c := node.getChild(parts[len(parts)-1]); c != nil { + if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != nil { + return n + } + } + + if c := node.getChild(wildcard); c != nil { + if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != nil { + return n + } + } + + return node.getChild(dotWildcard) +} + +// New returns a new, empty Trie. +func New() *DomainTrie { + return &DomainTrie{root: newNode(nil)} +} diff --git a/component/trie/domain_test.go b/component/trie/domain_test.go new file mode 100644 index 0000000..4322699 --- /dev/null +++ b/component/trie/domain_test.go @@ -0,0 +1,107 @@ +package trie + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +var localIP = net.IP{127, 0, 0, 1} + +func TestTrie_Basic(t *testing.T) { + tree := New() + domains := []string{ + "example.com", + "google.com", + "localhost", + } + + for _, domain := range domains { + tree.Insert(domain, localIP) + } + + node := tree.Search("example.com") + assert.NotNil(t, node) + assert.True(t, node.Data.(net.IP).Equal(localIP)) + assert.NotNil(t, tree.Insert("", localIP)) + assert.Nil(t, tree.Search("")) + assert.NotNil(t, tree.Search("localhost")) + assert.Nil(t, tree.Search("www.google.com")) +} + +func TestTrie_Wildcard(t *testing.T) { + tree := New() + domains := []string{ + "*.example.com", + "sub.*.example.com", + "*.dev", + ".org", + ".example.net", + ".apple.*", + "+.foo.com", + "+.stun.*.*", + "+.stun.*.*.*", + "+.stun.*.*.*.*", + "stun.l.google.com", + } + + for _, domain := range domains { + tree.Insert(domain, localIP) + } + + assert.NotNil(t, tree.Search("sub.example.com")) + assert.NotNil(t, tree.Search("sub.foo.example.com")) + assert.NotNil(t, tree.Search("test.org")) + assert.NotNil(t, tree.Search("test.example.net")) + assert.NotNil(t, tree.Search("test.apple.com")) + assert.NotNil(t, tree.Search("test.foo.com")) + assert.NotNil(t, tree.Search("foo.com")) + assert.NotNil(t, tree.Search("global.stun.website.com")) + assert.Nil(t, tree.Search("foo.sub.example.com")) + assert.Nil(t, tree.Search("foo.example.dev")) + assert.Nil(t, tree.Search("example.com")) +} + +func TestTrie_Priority(t *testing.T) { + tree := New() + domains := []string{ + ".dev", + "example.dev", + "*.example.dev", + "test.example.dev", + } + + assertFn := func(domain string, data int) { + node := tree.Search(domain) + assert.NotNil(t, node) + assert.Equal(t, data, node.Data) + } + + for idx, domain := range domains { + tree.Insert(domain, idx) + } + + assertFn("test.dev", 0) + assertFn("foo.bar.dev", 0) + assertFn("example.dev", 1) + assertFn("foo.example.dev", 2) + assertFn("test.example.dev", 3) +} + +func TestTrie_Boundary(t *testing.T) { + tree := New() + tree.Insert("*.dev", localIP) + + assert.NotNil(t, tree.Insert(".", localIP)) + assert.NotNil(t, tree.Insert("..dev", localIP)) + assert.Nil(t, tree.Search("dev")) +} + +func TestTrie_WildcardBoundary(t *testing.T) { + tree := New() + tree.Insert("+.*", localIP) + tree.Insert("stun.*.*.*", localIP) + + assert.NotNil(t, tree.Search("example.com")) +} diff --git a/component/trie/node.go b/component/trie/node.go new file mode 100644 index 0000000..67ef64a --- /dev/null +++ b/component/trie/node.go @@ -0,0 +1,26 @@ +package trie + +// Node is the trie's node +type Node struct { + children map[string]*Node + Data any +} + +func (n *Node) getChild(s string) *Node { + return n.children[s] +} + +func (n *Node) hasChild(s string) bool { + return n.getChild(s) != nil +} + +func (n *Node) addChild(s string, child *Node) { + n.children[s] = child +} + +func newNode(data any) *Node { + return &Node{ + Data: data, + children: map[string]*Node{}, + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a0ccc0b --- /dev/null +++ b/config/config.go @@ -0,0 +1,733 @@ +package config + +import ( + "errors" + "fmt" + "net" + "net/url" + "os" + "strings" + + "github.com/Dreamacro/clash/adapter" + "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/adapter/outboundgroup" + "github.com/Dreamacro/clash/adapter/provider" + "github.com/Dreamacro/clash/component/auth" + "github.com/Dreamacro/clash/component/fakeip" + "github.com/Dreamacro/clash/component/trie" + C "github.com/Dreamacro/clash/constant" + providerTypes "github.com/Dreamacro/clash/constant/provider" + "github.com/Dreamacro/clash/dns" + "github.com/Dreamacro/clash/log" + R "github.com/Dreamacro/clash/rule" + T "github.com/Dreamacro/clash/tunnel" + + "github.com/samber/lo" + "gopkg.in/yaml.v3" +) + +// General config +type General struct { + LegacyInbound + Controller + Authentication []string `json:"authentication"` + Mode T.TunnelMode `json:"mode"` + LogLevel log.LogLevel `json:"log-level"` + IPv6 bool `json:"ipv6"` + Interface string `json:"-"` + RoutingMark int `json:"-"` +} + +// Controller +type Controller struct { + ExternalController string `json:"-"` + ExternalUI string `json:"-"` + Secret string `json:"-"` +} + +type LegacyInbound struct { + Port int `json:"port"` + SocksPort int `json:"socks-port"` + RedirPort int `json:"redir-port"` + TProxyPort int `json:"tproxy-port"` + MixedPort int `json:"mixed-port"` + AllowLan bool `json:"allow-lan"` + BindAddress string `json:"bind-address"` +} + +// DNS config +type DNS struct { + Enable bool `yaml:"enable"` + IPv6 bool `yaml:"ipv6"` + NameServer []dns.NameServer `yaml:"nameserver"` + Fallback []dns.NameServer `yaml:"fallback"` + FallbackFilter FallbackFilter `yaml:"fallback-filter"` + Listen string `yaml:"listen"` + EnhancedMode C.DNSMode `yaml:"enhanced-mode"` + DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` + FakeIPRange *fakeip.Pool + Hosts *trie.DomainTrie + NameServerPolicy map[string]dns.NameServer + SearchDomains []string +} + +// FallbackFilter config +type FallbackFilter struct { + GeoIP bool `yaml:"geoip"` + GeoIPCode string `yaml:"geoip-code"` + IPCIDR []*net.IPNet `yaml:"ipcidr"` + Domain []string `yaml:"domain"` +} + +// Profile config +type Profile struct { + StoreSelected bool `yaml:"store-selected"` + StoreFakeIP bool `yaml:"store-fake-ip"` +} + +// Experimental config +type Experimental struct { + UDPFallbackMatch bool `yaml:"udp-fallback-match"` +} + +// Config is clash config manager +type Config struct { + General *General + DNS *DNS + Experimental *Experimental + Hosts *trie.DomainTrie + Profile *Profile + Inbounds []C.Inbound + Rules []C.Rule + Users []auth.AuthUser + Proxies map[string]C.Proxy + Providers map[string]providerTypes.ProxyProvider + Tunnels []Tunnel +} + +type RawDNS struct { + Enable bool `yaml:"enable"` + IPv6 *bool `yaml:"ipv6"` + UseHosts bool `yaml:"use-hosts"` + NameServer []string `yaml:"nameserver"` + Fallback []string `yaml:"fallback"` + FallbackFilter RawFallbackFilter `yaml:"fallback-filter"` + Listen string `yaml:"listen"` + EnhancedMode C.DNSMode `yaml:"enhanced-mode"` + FakeIPRange string `yaml:"fake-ip-range"` + FakeIPFilter []string `yaml:"fake-ip-filter"` + DefaultNameserver []string `yaml:"default-nameserver"` + NameServerPolicy map[string]string `yaml:"nameserver-policy"` + SearchDomains []string `yaml:"search-domains"` +} + +type RawFallbackFilter struct { + GeoIP bool `yaml:"geoip"` + GeoIPCode string `yaml:"geoip-code"` + IPCIDR []string `yaml:"ipcidr"` + Domain []string `yaml:"domain"` +} + +type tunnel struct { + Network []string `yaml:"network"` + Address string `yaml:"address"` + Target string `yaml:"target"` + Proxy string `yaml:"proxy"` +} + +type Tunnel tunnel + +// UnmarshalYAML implements yaml.Unmarshaler +func (t *Tunnel) UnmarshalYAML(unmarshal func(any) error) error { + var tp string + if err := unmarshal(&tp); err != nil { + var inner tunnel + if err := unmarshal(&inner); err != nil { + return err + } + + *t = Tunnel(inner) + return nil + } + + // parse udp/tcp,address,target,proxy + parts := lo.Map(strings.Split(tp, ","), func(s string, _ int) string { + return strings.TrimSpace(s) + }) + if len(parts) != 4 { + return fmt.Errorf("invalid tunnel config %s", tp) + } + network := strings.Split(parts[0], "/") + + // validate network + for _, n := range network { + switch n { + case "tcp", "udp": + default: + return fmt.Errorf("invalid tunnel network %s", n) + } + } + + // validate address and target + address := parts[1] + target := parts[2] + for _, addr := range []string{address, target} { + if _, _, err := net.SplitHostPort(addr); err != nil { + return fmt.Errorf("invalid tunnel target or address %s", addr) + } + } + + *t = Tunnel(tunnel{ + Network: network, + Address: address, + Target: target, + Proxy: parts[3], + }) + return nil +} + +type RawConfig struct { + Port int `yaml:"port"` + SocksPort int `yaml:"socks-port"` + RedirPort int `yaml:"redir-port"` + TProxyPort int `yaml:"tproxy-port"` + MixedPort int `yaml:"mixed-port"` + Authentication []string `yaml:"authentication"` + AllowLan bool `yaml:"allow-lan"` + BindAddress string `yaml:"bind-address"` + Mode T.TunnelMode `yaml:"mode"` + LogLevel log.LogLevel `yaml:"log-level"` + IPv6 bool `yaml:"ipv6"` + ExternalController string `yaml:"external-controller"` + ExternalUI string `yaml:"external-ui"` + Secret string `yaml:"secret"` + Interface string `yaml:"interface-name"` + RoutingMark int `yaml:"routing-mark"` + Tunnels []Tunnel `yaml:"tunnels"` + + ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` + Hosts map[string]string `yaml:"hosts"` + Inbounds []C.Inbound `yaml:"inbounds"` + DNS RawDNS `yaml:"dns"` + Experimental Experimental `yaml:"experimental"` + Profile Profile `yaml:"profile"` + Proxy []map[string]any `yaml:"proxies"` + ProxyGroup []map[string]any `yaml:"proxy-groups"` + Rule []string `yaml:"rules"` +} + +// Parse config +func Parse(buf []byte) (*Config, error) { + rawCfg, err := UnmarshalRawConfig(buf) + if err != nil { + return nil, err + } + + return ParseRawConfig(rawCfg) +} + +func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { + // config with default value + rawCfg := &RawConfig{ + AllowLan: false, + BindAddress: "*", + Mode: T.Rule, + Authentication: []string{}, + LogLevel: log.INFO, + Hosts: map[string]string{}, + Rule: []string{}, + Proxy: []map[string]any{}, + ProxyGroup: []map[string]any{}, + DNS: RawDNS{ + Enable: false, + UseHosts: true, + FakeIPRange: "198.18.0.1/16", + FallbackFilter: RawFallbackFilter{ + GeoIP: true, + GeoIPCode: "CN", + IPCIDR: []string{}, + }, + DefaultNameserver: []string{ + "114.114.114.114", + "8.8.8.8", + }, + }, + Profile: Profile{ + StoreSelected: true, + }, + } + + if err := yaml.Unmarshal(buf, rawCfg); err != nil { + return nil, err + } + + return rawCfg, nil +} + +func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { + config := &Config{} + + config.Experimental = &rawCfg.Experimental + config.Profile = &rawCfg.Profile + + general, err := parseGeneral(rawCfg) + if err != nil { + return nil, err + } + config.General = general + + config.Inbounds = rawCfg.Inbounds + + proxies, providers, err := parseProxies(rawCfg) + if err != nil { + return nil, err + } + config.Proxies = proxies + config.Providers = providers + + rules, err := parseRules(rawCfg, proxies) + if err != nil { + return nil, err + } + config.Rules = rules + + hosts, err := parseHosts(rawCfg) + if err != nil { + return nil, err + } + config.Hosts = hosts + + dnsCfg, err := parseDNS(rawCfg, hosts) + if err != nil { + return nil, err + } + config.DNS = dnsCfg + + config.Users = parseAuthentication(rawCfg.Authentication) + + config.Tunnels = rawCfg.Tunnels + // verify tunnels + for _, t := range config.Tunnels { + if _, ok := config.Proxies[t.Proxy]; !ok { + return nil, fmt.Errorf("tunnel proxy %s not found", t.Proxy) + } + } + + return config, nil +} + +func parseGeneral(cfg *RawConfig) (*General, error) { + externalUI := cfg.ExternalUI + + // checkout externalUI exist + if externalUI != "" { + externalUI = C.Path.Resolve(externalUI) + + if _, err := os.Stat(externalUI); os.IsNotExist(err) { + return nil, fmt.Errorf("external-ui: %s not exist", externalUI) + } + } + + return &General{ + LegacyInbound: LegacyInbound{ + Port: cfg.Port, + SocksPort: cfg.SocksPort, + RedirPort: cfg.RedirPort, + TProxyPort: cfg.TProxyPort, + MixedPort: cfg.MixedPort, + AllowLan: cfg.AllowLan, + BindAddress: cfg.BindAddress, + }, + Controller: Controller{ + ExternalController: cfg.ExternalController, + ExternalUI: cfg.ExternalUI, + Secret: cfg.Secret, + }, + Mode: cfg.Mode, + LogLevel: cfg.LogLevel, + IPv6: cfg.IPv6, + Interface: cfg.Interface, + RoutingMark: cfg.RoutingMark, + }, nil +} + +func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[string]providerTypes.ProxyProvider, err error) { + proxies = make(map[string]C.Proxy) + providersMap = make(map[string]providerTypes.ProxyProvider) + proxyList := []string{} + proxiesConfig := cfg.Proxy + groupsConfig := cfg.ProxyGroup + providersConfig := cfg.ProxyProvider + + proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect()) + proxies["REJECT"] = adapter.NewProxy(outbound.NewReject()) + proxyList = append(proxyList, "DIRECT", "REJECT") + + // parse proxy + for idx, mapping := range proxiesConfig { + proxy, err := adapter.ParseProxy(mapping) + if err != nil { + return nil, nil, fmt.Errorf("proxy %d: %w", idx, err) + } + + if _, exist := proxies[proxy.Name()]; exist { + return nil, nil, fmt.Errorf("proxy %s is the duplicate name", proxy.Name()) + } + proxies[proxy.Name()] = proxy + proxyList = append(proxyList, proxy.Name()) + } + + // keep the original order of ProxyGroups in config file + for idx, mapping := range groupsConfig { + groupName, existName := mapping["name"].(string) + if !existName { + return nil, nil, fmt.Errorf("proxy group %d: missing name", idx) + } + proxyList = append(proxyList, groupName) + } + + // check if any loop exists and sort the ProxyGroups + if err := proxyGroupsDagSort(groupsConfig); err != nil { + return nil, nil, err + } + + // parse and initial providers + for name, mapping := range providersConfig { + if name == provider.ReservedName { + return nil, nil, fmt.Errorf("can not defined a provider called `%s`", provider.ReservedName) + } + + pd, err := provider.ParseProxyProvider(name, mapping) + if err != nil { + return nil, nil, fmt.Errorf("parse proxy provider %s error: %w", name, err) + } + + providersMap[name] = pd + } + + for _, provider := range providersMap { + log.Infoln("Start initial provider %s", provider.Name()) + if err := provider.Initial(); err != nil { + return nil, nil, fmt.Errorf("initial proxy provider %s error: %w", provider.Name(), err) + } + } + + // parse proxy group + for idx, mapping := range groupsConfig { + group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap) + if err != nil { + return nil, nil, fmt.Errorf("proxy group[%d]: %w", idx, err) + } + + groupName := group.Name() + if _, exist := proxies[groupName]; exist { + return nil, nil, fmt.Errorf("proxy group %s: the duplicate name", groupName) + } + + proxies[groupName] = adapter.NewProxy(group) + } + + // initial compatible provider + for _, pd := range providersMap { + if pd.VehicleType() != providerTypes.Compatible { + continue + } + + log.Infoln("Start initial compatible provider %s", pd.Name()) + if err := pd.Initial(); err != nil { + return nil, nil, err + } + } + + ps := []C.Proxy{} + for _, v := range proxyList { + ps = append(ps, proxies[v]) + } + hc := provider.NewHealthCheck(ps, "", 0, true) + pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) + providersMap[provider.ReservedName] = pd + + global := outboundgroup.NewSelector( + &outboundgroup.GroupCommonOption{ + Name: "GLOBAL", + }, + []providerTypes.ProxyProvider{pd}, + ) + proxies["GLOBAL"] = adapter.NewProxy(global) + return proxies, providersMap, nil +} + +func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) { + rules := []C.Rule{} + rulesConfig := cfg.Rule + + // parse rules + for idx, line := range rulesConfig { + rule := trimArr(strings.Split(line, ",")) + var ( + payload string + target string + params = []string{} + ) + + switch l := len(rule); { + case l == 2: + target = rule[1] + case l == 3: + payload = rule[1] + target = rule[2] + case l >= 4: + payload = rule[1] + target = rule[2] + params = rule[3:] + default: + return nil, fmt.Errorf("rules[%d] [%s] error: format invalid", idx, line) + } + + if _, ok := proxies[target]; !ok { + return nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target) + } + + rule = trimArr(rule) + params = trimArr(params) + + parsed, parseErr := R.ParseRule(rule[0], payload, target, params) + if parseErr != nil { + return nil, fmt.Errorf("rules[%d] [%s] error: %s", idx, line, parseErr.Error()) + } + + rules = append(rules, parsed) + } + + return rules, nil +} + +func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) { + tree := trie.New() + + // add default hosts + if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil { + log.Errorln("insert localhost to host error: %s", err.Error()) + } + + if len(cfg.Hosts) != 0 { + for domain, ipStr := range cfg.Hosts { + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("%s is not a valid IP", ipStr) + } + tree.Insert(domain, ip) + } + } + + return tree, nil +} + +func hostWithDefaultPort(host string, defPort string) (string, error) { + if !strings.Contains(host, ":") { + host += ":" + } + + hostname, port, err := net.SplitHostPort(host) + if err != nil { + return "", err + } + + if port == "" { + port = defPort + } + + return net.JoinHostPort(hostname, port), nil +} + +func parseNameServer(servers []string) ([]dns.NameServer, error) { + nameservers := []dns.NameServer{} + + for idx, server := range servers { + // parse without scheme .e.g 8.8.8.8:53 + if !strings.Contains(server, "://") { + server = "udp://" + server + } + u, err := url.Parse(server) + if err != nil { + return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) + } + + // parse with specific interface + // .e.g 10.0.0.1#en0 + interfaceName := u.Fragment + + var addr, dnsNetType string + switch u.Scheme { + case "udp": + addr, err = hostWithDefaultPort(u.Host, "53") + dnsNetType = "" // UDP + case "tcp": + addr, err = hostWithDefaultPort(u.Host, "53") + dnsNetType = "tcp" // TCP + case "tls": + addr, err = hostWithDefaultPort(u.Host, "853") + dnsNetType = "tcp-tls" // DNS over TLS + case "https": + clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path, User: u.User} + addr = clearURL.String() + dnsNetType = "https" // DNS over HTTPS + case "dhcp": + addr = u.Host + dnsNetType = "dhcp" // UDP from DHCP + default: + return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) + } + + if err != nil { + return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) + } + + nameservers = append( + nameservers, + dns.NameServer{ + Net: dnsNetType, + Addr: addr, + Interface: interfaceName, + }, + ) + } + return nameservers, nil +} + +func parseNameServerPolicy(nsPolicy map[string]string) (map[string]dns.NameServer, error) { + policy := map[string]dns.NameServer{} + + for domain, server := range nsPolicy { + nameservers, err := parseNameServer([]string{server}) + if err != nil { + return nil, err + } + if _, valid := trie.ValidAndSplitDomain(domain); !valid { + return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain) + } + policy[domain] = nameservers[0] + } + + return policy, nil +} + +func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) { + ipNets := []*net.IPNet{} + + for idx, ip := range ips { + _, ipnet, err := net.ParseCIDR(ip) + if err != nil { + return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error()) + } + ipNets = append(ipNets, ipnet) + } + + return ipNets, nil +} + +func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie) (*DNS, error) { + cfg := rawCfg.DNS + if cfg.Enable && len(cfg.NameServer) == 0 { + return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") + } + + dnsCfg := &DNS{ + Enable: cfg.Enable, + Listen: cfg.Listen, + IPv6: lo.FromPtrOr(cfg.IPv6, rawCfg.IPv6), + EnhancedMode: cfg.EnhancedMode, + FallbackFilter: FallbackFilter{ + IPCIDR: []*net.IPNet{}, + }, + } + var err error + if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer); err != nil { + return nil, err + } + + if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback); err != nil { + return nil, err + } + + if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy); err != nil { + return nil, err + } + + if len(cfg.DefaultNameserver) == 0 { + return nil, errors.New("default nameserver should have at least one nameserver") + } + if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver); err != nil { + return nil, err + } + // check default nameserver is pure ip addr + for _, ns := range dnsCfg.DefaultNameserver { + host, _, err := net.SplitHostPort(ns.Addr) + if err != nil || net.ParseIP(host) == nil { + return nil, errors.New("default nameserver should be pure IP") + } + } + + if cfg.EnhancedMode == C.DNSFakeIP { + _, ipnet, err := net.ParseCIDR(cfg.FakeIPRange) + if err != nil { + return nil, err + } + + var host *trie.DomainTrie + // fake ip skip host filter + if len(cfg.FakeIPFilter) != 0 { + host = trie.New() + for _, domain := range cfg.FakeIPFilter { + host.Insert(domain, true) + } + } + + pool, err := fakeip.New(fakeip.Options{ + IPNet: ipnet, + Size: 1000, + Host: host, + Persistence: rawCfg.Profile.StoreFakeIP, + }) + if err != nil { + return nil, err + } + + dnsCfg.FakeIPRange = pool + } + + dnsCfg.FallbackFilter.GeoIP = cfg.FallbackFilter.GeoIP + dnsCfg.FallbackFilter.GeoIPCode = cfg.FallbackFilter.GeoIPCode + if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil { + dnsCfg.FallbackFilter.IPCIDR = fallbackip + } + dnsCfg.FallbackFilter.Domain = cfg.FallbackFilter.Domain + + if cfg.UseHosts { + dnsCfg.Hosts = hosts + } + + if len(cfg.SearchDomains) != 0 { + for _, domain := range cfg.SearchDomains { + if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") { + return nil, errors.New("search domains should not start or end with '.'") + } + if strings.Contains(domain, ":") { + return nil, errors.New("search domains are for ipv4 only and should not contain ports") + } + } + dnsCfg.SearchDomains = cfg.SearchDomains + } + + return dnsCfg, nil +} + +func parseAuthentication(rawRecords []string) []auth.AuthUser { + users := []auth.AuthUser{} + for _, line := range rawRecords { + if user, pass, found := strings.Cut(line, ":"); found { + users = append(users, auth.AuthUser{User: user, Pass: pass}) + } + } + return users +} diff --git a/config/initial.go b/config/initial.go new file mode 100644 index 0000000..9d1a2db --- /dev/null +++ b/config/initial.go @@ -0,0 +1,78 @@ +package config + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/Dreamacro/clash/component/mmdb" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" +) + +func downloadMMDB(path string) (err error) { + resp, err := http.Get("https://cdn.jsdelivr.net/gh/Dreamacro/maxmind-geoip@release/Country.mmdb") + if err != nil { + return + } + defer resp.Body.Close() + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + + return err +} + +func initMMDB() error { + if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) { + log.Infoln("Can't find MMDB, start download") + if err := downloadMMDB(C.Path.MMDB()); err != nil { + return fmt.Errorf("can't download MMDB: %s", err.Error()) + } + } + + if !mmdb.Verify() { + log.Warnln("MMDB invalid, remove and download") + if err := os.Remove(C.Path.MMDB()); err != nil { + return fmt.Errorf("can't remove invalid MMDB: %s", err.Error()) + } + + if err := downloadMMDB(C.Path.MMDB()); err != nil { + return fmt.Errorf("can't download MMDB: %s", err.Error()) + } + } + + return nil +} + +// Init prepare necessary files +func Init(dir string) error { + // initial homedir + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0o777); err != nil { + return fmt.Errorf("can't create config directory %s: %s", dir, err.Error()) + } + } + + // initial config.yaml + if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) { + log.Infoln("Can't find config, create a initial config file") + f, err := os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return fmt.Errorf("can't create file %s: %s", C.Path.Config(), err.Error()) + } + f.Write([]byte(`mixed-port: 7890`)) + f.Close() + } + + // initial mmdb + if err := initMMDB(); err != nil { + return fmt.Errorf("can't initial MMDB: %w", err) + } + return nil +} diff --git a/config/utils.go b/config/utils.go new file mode 100644 index 0000000..1d49552 --- /dev/null +++ b/config/utils.go @@ -0,0 +1,148 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/Dreamacro/clash/adapter/outboundgroup" + "github.com/Dreamacro/clash/common/structure" +) + +func trimArr(arr []string) (r []string) { + for _, e := range arr { + r = append(r, strings.Trim(e, " ")) + } + return +} + +// Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order. +// Meanwhile, record the original index in the config file. +// If loop is detected, return an error with location of loop. +func proxyGroupsDagSort(groupsConfig []map[string]any) error { + type graphNode struct { + indegree int + // topological order + topo int + // the original data in `groupsConfig` + data map[string]any + // `outdegree` and `from` are used in loop locating + outdegree int + option *outboundgroup.GroupCommonOption + from []string + } + + decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) + graph := make(map[string]*graphNode) + + // Step 1.1 build dependency graph + for _, mapping := range groupsConfig { + option := &outboundgroup.GroupCommonOption{} + if err := decoder.Decode(mapping, option); err != nil { + return fmt.Errorf("ProxyGroup %s: %s", option.Name, err.Error()) + } + + groupName := option.Name + if node, ok := graph[groupName]; ok { + if node.data != nil { + return fmt.Errorf("ProxyGroup %s: duplicate group name", groupName) + } + node.data = mapping + node.option = option + } else { + graph[groupName] = &graphNode{0, -1, mapping, 0, option, nil} + } + + for _, proxy := range option.Proxies { + if node, ex := graph[proxy]; ex { + node.indegree++ + } else { + graph[proxy] = &graphNode{1, -1, nil, 0, nil, nil} + } + } + } + // Step 1.2 Topological Sort + // topological index of **ProxyGroup** + index := 0 + queue := make([]string, 0) + for name, node := range graph { + // in the beginning, put nodes that have `node.indegree == 0` into queue. + if node.indegree == 0 { + queue = append(queue, name) + } + } + // every element in queue have indegree == 0 + for ; len(queue) > 0; queue = queue[1:] { + name := queue[0] + node := graph[name] + if node.option != nil { + index++ + groupsConfig[len(groupsConfig)-index] = node.data + if len(node.option.Proxies) == 0 { + delete(graph, name) + continue + } + + for _, proxy := range node.option.Proxies { + child := graph[proxy] + child.indegree-- + if child.indegree == 0 { + queue = append(queue, proxy) + } + } + } + delete(graph, name) + } + + // no loop is detected, return sorted ProxyGroup + if len(graph) == 0 { + return nil + } + + // if loop is detected, locate the loop and throw an error + // Step 2.1 rebuild the graph, fill `outdegree` and `from` filed + for name, node := range graph { + if node.option == nil { + continue + } + + if len(node.option.Proxies) == 0 { + continue + } + + for _, proxy := range node.option.Proxies { + node.outdegree++ + child := graph[proxy] + if child.from == nil { + child.from = make([]string, 0, child.indegree) + } + child.from = append(child.from, name) + } + } + // Step 2.2 remove nodes outside the loop. so that we have only the loops remain in `graph` + queue = make([]string, 0) + // initialize queue with node have outdegree == 0 + for name, node := range graph { + if node.outdegree == 0 { + queue = append(queue, name) + } + } + // every element in queue have outdegree == 0 + for ; len(queue) > 0; queue = queue[1:] { + name := queue[0] + node := graph[name] + for _, f := range node.from { + graph[f].outdegree-- + if graph[f].outdegree == 0 { + queue = append(queue, f) + } + } + delete(graph, name) + } + // Step 2.3 report the elements in loop + loopElements := make([]string, 0, len(graph)) + for name := range graph { + loopElements = append(loopElements, name) + delete(graph, name) + } + return fmt.Errorf("loop is detected in ProxyGroup, please check following ProxyGroups: %v", loopElements) +} diff --git a/constant/adapters.go b/constant/adapters.go new file mode 100644 index 0000000..8200e46 --- /dev/null +++ b/constant/adapters.go @@ -0,0 +1,180 @@ +package constant + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/Dreamacro/clash/component/dialer" +) + +// Adapter Type +const ( + Direct AdapterType = iota + Reject + + Shadowsocks + ShadowsocksR + Snell + Socks5 + Http + Vmess + Trojan + + Relay + Selector + Fallback + URLTest + LoadBalance +) + +const ( + DefaultTCPTimeout = 5 * time.Second + DefaultUDPTimeout = DefaultTCPTimeout + DefaultTLSTimeout = DefaultTCPTimeout +) + +type Connection interface { + Chains() Chain + AppendToChains(adapter ProxyAdapter) +} + +type Chain []string + +func (c Chain) String() string { + switch len(c) { + case 0: + return "" + case 1: + return c[0] + default: + return fmt.Sprintf("%s[%s]", c[len(c)-1], c[0]) + } +} + +func (c Chain) Last() string { + switch len(c) { + case 0: + return "" + default: + return c[0] + } +} + +type Conn interface { + net.Conn + Connection +} + +type PacketConn interface { + net.PacketConn + Connection + // Deprecate WriteWithMetadata because of remote resolve DNS cause TURN failed + // WriteWithMetadata(p []byte, metadata *Metadata) (n int, err error) +} + +type ProxyAdapter interface { + Name() string + Type() AdapterType + Addr() string + SupportUDP() bool + MarshalJSON() ([]byte, error) + + // StreamConn wraps a protocol around net.Conn with Metadata. + // + // Examples: + // conn, _ := net.DialContext(context.Background(), "tcp", "host:port") + // conn, _ = adapter.StreamConn(conn, metadata) + // + // It returns a C.Conn with protocol which start with + // a new session (if any) + StreamConn(c net.Conn, metadata *Metadata) (net.Conn, error) + + // DialContext return a C.Conn with protocol which + // contains multiplexing-related reuse logic (if any) + DialContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (Conn, error) + ListenPacketContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (PacketConn, error) + + // Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract. + Unwrap(metadata *Metadata) Proxy +} + +type DelayHistory struct { + Time time.Time `json:"time"` + Delay uint16 `json:"delay"` + MeanDelay uint16 `json:"meanDelay"` +} + +type Proxy interface { + ProxyAdapter + Alive() bool + DelayHistory() []DelayHistory + LastDelay() uint16 + URLTest(ctx context.Context, url string) (uint16, uint16, error) + + // Deprecated: use DialContext instead. + Dial(metadata *Metadata) (Conn, error) + + // Deprecated: use DialPacketConn instead. + DialUDP(metadata *Metadata) (PacketConn, error) +} + +// AdapterType is enum of adapter type +type AdapterType int + +func (at AdapterType) String() string { + switch at { + case Direct: + return "Direct" + case Reject: + return "Reject" + + case Shadowsocks: + return "Shadowsocks" + case ShadowsocksR: + return "ShadowsocksR" + case Snell: + return "Snell" + case Socks5: + return "Socks5" + case Http: + return "Http" + case Vmess: + return "Vmess" + case Trojan: + return "Trojan" + + case Relay: + return "Relay" + case Selector: + return "Selector" + case Fallback: + return "Fallback" + case URLTest: + return "URLTest" + case LoadBalance: + return "LoadBalance" + + default: + return "Unknown" + } +} + +// UDPPacket contains the data of UDP packet, and offers control/info of UDP packet's source +type UDPPacket interface { + // Data get the payload of UDP Packet + Data() []byte + + // WriteBack writes the payload with source IP/Port equals addr + // - variable source IP/Port is important to STUN + // - if addr is not provided, WriteBack will write out UDP packet with SourceIP/Port equals to original Target, + // this is important when using Fake-IP. + WriteBack(b []byte, addr net.Addr) (n int, err error) + + // Drop call after packet is used, could recycle buffer in this function. + Drop() + + // LocalAddr returns the source IP/Port of packet + LocalAddr() net.Addr +} diff --git a/constant/context.go b/constant/context.go new file mode 100644 index 0000000..a2c77c9 --- /dev/null +++ b/constant/context.go @@ -0,0 +1,23 @@ +package constant + +import ( + "net" + + "github.com/gofrs/uuid/v5" +) + +type PlainContext interface { + ID() uuid.UUID +} + +type ConnContext interface { + PlainContext + Metadata() *Metadata + Conn() net.Conn +} + +type PacketConnContext interface { + PlainContext + Metadata() *Metadata + PacketConn() net.PacketConn +} diff --git a/constant/dns.go b/constant/dns.go new file mode 100644 index 0000000..a6f1b9c --- /dev/null +++ b/constant/dns.go @@ -0,0 +1,70 @@ +package constant + +import ( + "encoding/json" + "errors" + "fmt" +) + +// DNSModeMapping is a mapping for EnhancedMode enum +var DNSModeMapping = map[string]DNSMode{ + DNSNormal.String(): DNSNormal, + DNSFakeIP.String(): DNSFakeIP, +} + +const ( + DNSNormal DNSMode = iota + DNSFakeIP + DNSMapping +) + +type DNSMode int + +// UnmarshalYAML unserialize EnhancedMode with yaml +func (e *DNSMode) UnmarshalYAML(unmarshal func(any) error) error { + var tp string + if err := unmarshal(&tp); err != nil { + return err + } + mode, exist := DNSModeMapping[tp] + if !exist { + return fmt.Errorf("invalid mode: %s", tp) + } + *e = mode + return nil +} + +// MarshalYAML serialize EnhancedMode with yaml +func (e DNSMode) MarshalYAML() (any, error) { + return e.String(), nil +} + +// UnmarshalJSON unserialize EnhancedMode with json +func (e *DNSMode) UnmarshalJSON(data []byte) error { + var tp string + json.Unmarshal(data, &tp) + mode, exist := DNSModeMapping[tp] + if !exist { + return errors.New("invalid mode") + } + *e = mode + return nil +} + +// MarshalJSON serialize EnhancedMode with json +func (e DNSMode) MarshalJSON() ([]byte, error) { + return json.Marshal(e.String()) +} + +func (e DNSMode) String() string { + switch e { + case DNSNormal: + return "normal" + case DNSFakeIP: + return "fake-ip" + case DNSMapping: + return "redir-host" + default: + return "unknown" + } +} diff --git a/constant/listener.go b/constant/listener.go new file mode 100644 index 0000000..7bce491 --- /dev/null +++ b/constant/listener.go @@ -0,0 +1,90 @@ +package constant + +import ( + "fmt" + "net" + "net/url" + "strconv" +) + +type Listener interface { + RawAddress() string + Address() string + Close() error +} + +type InboundType string + +const ( + InboundTypeSocks InboundType = "socks" + InboundTypeRedir InboundType = "redir" + InboundTypeTproxy InboundType = "tproxy" + InboundTypeHTTP InboundType = "http" + InboundTypeMixed InboundType = "mixed" +) + +var supportInboundTypes = map[InboundType]bool{ + InboundTypeSocks: true, + InboundTypeRedir: true, + InboundTypeTproxy: true, + InboundTypeHTTP: true, + InboundTypeMixed: true, +} + +type inbound struct { + Type InboundType `json:"type" yaml:"type"` + BindAddress string `json:"bind-address" yaml:"bind-address"` + IsFromPortCfg bool `json:"-" yaml:"-"` +} + +// Inbound +type Inbound inbound + +// UnmarshalYAML implements yaml.Unmarshaler +func (i *Inbound) UnmarshalYAML(unmarshal func(any) error) error { + var tp string + if err := unmarshal(&tp); err != nil { + var inner inbound + if err := unmarshal(&inner); err != nil { + return err + } + + *i = Inbound(inner) + } else { + inner, err := parseInbound(tp) + if err != nil { + return err + } + + *i = Inbound(*inner) + } + + if !supportInboundTypes[i.Type] { + return fmt.Errorf("not support inbound type: %s", i.Type) + } + _, portStr, err := net.SplitHostPort(i.BindAddress) + if err != nil { + return fmt.Errorf("bind address parse error. addr: %s, err: %w", i.BindAddress, err) + } + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil || port == 0 { + return fmt.Errorf("invalid bind port. addr: %s", i.BindAddress) + } + return nil +} + +func parseInbound(alias string) (*inbound, error) { + u, err := url.Parse(alias) + if err != nil { + return nil, err + } + listenerType := InboundType(u.Scheme) + return &inbound{ + Type: listenerType, + BindAddress: u.Host, + }, nil +} + +func (i *Inbound) ToAlias() string { + return string(i.Type) + "://" + i.BindAddress +} diff --git a/constant/metadata.go b/constant/metadata.go new file mode 100644 index 0000000..2570312 --- /dev/null +++ b/constant/metadata.go @@ -0,0 +1,150 @@ +package constant + +import ( + "encoding/json" + "net" + "net/netip" + "strconv" + + "github.com/Dreamacro/clash/transport/socks5" +) + +// Socks addr type +const ( + TCP NetWork = iota + UDP + + HTTP Type = iota + HTTPCONNECT + SOCKS4 + SOCKS5 + REDIR + TPROXY + TUNNEL +) + +type NetWork int + +func (n NetWork) String() string { + if n == TCP { + return "tcp" + } + return "udp" +} + +func (n NetWork) MarshalJSON() ([]byte, error) { + return json.Marshal(n.String()) +} + +type Type int + +func (t Type) String() string { + switch t { + case HTTP: + return "HTTP" + case HTTPCONNECT: + return "HTTP Connect" + case SOCKS4: + return "Socks4" + case SOCKS5: + return "Socks5" + case REDIR: + return "Redir" + case TPROXY: + return "TProxy" + case TUNNEL: + return "Tunnel" + default: + return "Unknown" + } +} + +func (t Type) MarshalJSON() ([]byte, error) { + return json.Marshal(t.String()) +} + +// Metadata is used to store connection address +type Metadata struct { + NetWork NetWork `json:"network"` + Type Type `json:"type"` + SrcIP net.IP `json:"sourceIP"` + DstIP net.IP `json:"destinationIP"` + SrcPort Port `json:"sourcePort"` + DstPort Port `json:"destinationPort"` + Host string `json:"host"` + DNSMode DNSMode `json:"dnsMode"` + ProcessPath string `json:"processPath"` + SpecialProxy string `json:"specialProxy"` + + OriginDst netip.AddrPort `json:"-"` +} + +func (m *Metadata) RemoteAddress() string { + return net.JoinHostPort(m.String(), m.DstPort.String()) +} + +func (m *Metadata) SourceAddress() string { + return net.JoinHostPort(m.SrcIP.String(), m.SrcPort.String()) +} + +func (m *Metadata) AddrType() int { + switch true { + case m.Host != "" || m.DstIP == nil: + return socks5.AtypDomainName + case m.DstIP.To4() != nil: + return socks5.AtypIPv4 + default: + return socks5.AtypIPv6 + } +} + +func (m *Metadata) Resolved() bool { + return m.DstIP != nil +} + +// Pure is used to solve unexpected behavior +// when dialing proxy connection in DNSMapping mode. +func (m *Metadata) Pure() *Metadata { + if m.DNSMode == DNSMapping && m.DstIP != nil { + copy := *m + copy.Host = "" + return © + } + + return m +} + +func (m *Metadata) UDPAddr() *net.UDPAddr { + if m.NetWork != UDP || m.DstIP == nil { + return nil + } + return &net.UDPAddr{ + IP: m.DstIP, + Port: int(m.DstPort), + } +} + +func (m *Metadata) String() string { + if m.Host != "" { + return m.Host + } else if m.DstIP != nil { + return m.DstIP.String() + } else { + return "<nil>" + } +} + +func (m *Metadata) Valid() bool { + return m.Host != "" || m.DstIP != nil +} + +// Port is used to compatible with old version +type Port uint16 + +func (n Port) MarshalJSON() ([]byte, error) { + return json.Marshal(n.String()) +} + +func (n Port) String() string { + return strconv.FormatUint(uint64(n), 10) +} diff --git a/constant/path.go b/constant/path.go new file mode 100644 index 0000000..add7820 --- /dev/null +++ b/constant/path.go @@ -0,0 +1,77 @@ +package constant + +import ( + "os" + P "path" + "path/filepath" + "strings" +) + +const Name = "clash" + +// Path is used to get the configuration path +var Path = func() *path { + homeDir, err := os.UserHomeDir() + if err != nil { + homeDir, _ = os.Getwd() + } + + homeDir = P.Join(homeDir, ".config", Name) + return &path{homeDir: homeDir, configFile: "config.yaml"} +}() + +type path struct { + homeDir string + configFile string +} + +// SetHomeDir is used to set the configuration path +func SetHomeDir(root string) { + Path.homeDir = root +} + +// SetConfig is used to set the configuration file +func SetConfig(file string) { + Path.configFile = file +} + +func (p *path) HomeDir() string { + return p.homeDir +} + +func (p *path) Config() string { + return p.configFile +} + +// Resolve return a absolute path or a relative path with homedir +func (p *path) Resolve(path string) string { + if !filepath.IsAbs(path) { + return filepath.Join(p.HomeDir(), path) + } + + return path +} + +// IsSubPath return true if path is a subpath of homedir +func (p *path) IsSubPath(path string) bool { + homedir := p.HomeDir() + path = p.Resolve(path) + rel, err := filepath.Rel(homedir, path) + if err != nil { + return false + } + + return !strings.Contains(rel, "..") +} + +func (p *path) MMDB() string { + return P.Join(p.homeDir, "Country.mmdb") +} + +func (p *path) OldCache() string { + return P.Join(p.homeDir, ".cache") +} + +func (p *path) Cache() string { + return P.Join(p.homeDir, "cache.db") +} diff --git a/constant/provider/interface.go b/constant/provider/interface.go new file mode 100644 index 0000000..1864f36 --- /dev/null +++ b/constant/provider/interface.go @@ -0,0 +1,105 @@ +package provider + +import ( + "github.com/Dreamacro/clash/constant" +) + +// Vehicle Type +const ( + File VehicleType = iota + HTTP + Compatible +) + +// VehicleType defined +type VehicleType int + +func (v VehicleType) String() string { + switch v { + case File: + return "File" + case HTTP: + return "HTTP" + case Compatible: + return "Compatible" + default: + return "Unknown" + } +} + +type Vehicle interface { + Read() ([]byte, error) + Path() string + Type() VehicleType +} + +// Provider Type +const ( + Proxy ProviderType = iota + Rule +) + +// ProviderType defined +type ProviderType int + +func (pt ProviderType) String() string { + switch pt { + case Proxy: + return "Proxy" + case Rule: + return "Rule" + default: + return "Unknown" + } +} + +// Provider interface +type Provider interface { + Name() string + VehicleType() VehicleType + Type() ProviderType + Initial() error + Update() error +} + +// ProxyProvider interface +type ProxyProvider interface { + Provider + Proxies() []constant.Proxy + // Touch is used to inform the provider that the proxy is actually being used while getting the list of proxies. + // Commonly used in DialContext and DialPacketConn + Touch() + HealthCheck() +} + +// Rule Type +const ( + Domain RuleType = iota + IPCIDR + Classical +) + +// RuleType defined +type RuleType int + +func (rt RuleType) String() string { + switch rt { + case Domain: + return "Domain" + case IPCIDR: + return "IPCIDR" + case Classical: + return "Classical" + default: + return "Unknown" + } +} + +// RuleProvider interface +type RuleProvider interface { + Provider + Behavior() RuleType + Match(*constant.Metadata) bool + ShouldResolveIP() bool + AsRule(adaptor string) constant.Rule +} diff --git a/constant/rule.go b/constant/rule.go new file mode 100644 index 0000000..2dfb51a --- /dev/null +++ b/constant/rule.go @@ -0,0 +1,84 @@ +package constant + +const ( + RuleConfigDomain RuleConfig = "DOMAIN" + RuleConfigDomainSuffix RuleConfig = "DOMAIN-SUFFIX" + RuleConfigDomainKeyword RuleConfig = "DOMAIN-KEYWORD" + RuleConfigGeoIP RuleConfig = "GEOIP" + RuleConfigIPCIDR RuleConfig = "IP-CIDR" + RuleConfigIPCIDR6 RuleConfig = "IP-CIDR6" + RuleConfigSrcIPCIDR RuleConfig = "SRC-IP-CIDR" + RuleConfigSrcPort RuleConfig = "SRC-PORT" + RuleConfigDstPort RuleConfig = "DST-PORT" + RuleConfigInboundPort RuleConfig = "INBOUND-PORT" + RuleConfigProcessName RuleConfig = "PROCESS-NAME" + RuleConfigProcessPath RuleConfig = "PROCESS-PATH" + RuleConfigIPSet RuleConfig = "IPSET" + RuleConfigRuleSet RuleConfig = "RULE-SET" + RuleConfigScript RuleConfig = "SCRIPT" + RuleConfigMatch RuleConfig = "MATCH" +) + +// Rule Config Type String represents a rule type in configuration files. +type RuleConfig string + +// Rule Type +const ( + Domain RuleType = iota + DomainSuffix + DomainKeyword + GEOIP + IPCIDR + SrcIPCIDR + SrcPort + DstPort + InboundPort + Process + ProcessPath + IPSet + MATCH +) + +type RuleType int + +func (rt RuleType) String() string { + switch rt { + case Domain: + return "Domain" + case DomainSuffix: + return "DomainSuffix" + case DomainKeyword: + return "DomainKeyword" + case GEOIP: + return "GeoIP" + case IPCIDR: + return "IPCIDR" + case SrcIPCIDR: + return "SrcIPCIDR" + case SrcPort: + return "SrcPort" + case DstPort: + return "DstPort" + case InboundPort: + return "InboundPort" + case Process: + return "Process" + case ProcessPath: + return "ProcessPath" + case IPSet: + return "IPSet" + case MATCH: + return "Match" + default: + return "Unknown" + } +} + +type Rule interface { + RuleType() RuleType + Match(metadata *Metadata) bool + Adapter() string + Payload() string + ShouldResolveIP() bool + ShouldFindProcess() bool +} diff --git a/constant/version.go b/constant/version.go new file mode 100644 index 0000000..17a26f8 --- /dev/null +++ b/constant/version.go @@ -0,0 +1,6 @@ +package constant + +var ( + Version = "unknown version" + BuildTime = "unknown time" +) diff --git a/context/conn.go b/context/conn.go new file mode 100644 index 0000000..46cb68a --- /dev/null +++ b/context/conn.go @@ -0,0 +1,39 @@ +package context + +import ( + "net" + + C "github.com/Dreamacro/clash/constant" + + "github.com/gofrs/uuid/v5" +) + +type ConnContext struct { + id uuid.UUID + metadata *C.Metadata + conn net.Conn +} + +func NewConnContext(conn net.Conn, metadata *C.Metadata) *ConnContext { + id, _ := uuid.NewV4() + return &ConnContext{ + id: id, + metadata: metadata, + conn: conn, + } +} + +// ID implement C.ConnContext ID +func (c *ConnContext) ID() uuid.UUID { + return c.id +} + +// Metadata implement C.ConnContext Metadata +func (c *ConnContext) Metadata() *C.Metadata { + return c.metadata +} + +// Conn implement C.ConnContext Conn +func (c *ConnContext) Conn() net.Conn { + return c.conn +} diff --git a/context/dns.go b/context/dns.go new file mode 100644 index 0000000..68babce --- /dev/null +++ b/context/dns.go @@ -0,0 +1,41 @@ +package context + +import ( + "github.com/gofrs/uuid/v5" + "github.com/miekg/dns" +) + +const ( + DNSTypeHost = "host" + DNSTypeFakeIP = "fakeip" + DNSTypeRaw = "raw" +) + +type DNSContext struct { + id uuid.UUID + msg *dns.Msg + tp string +} + +func NewDNSContext(msg *dns.Msg) *DNSContext { + id, _ := uuid.NewV4() + return &DNSContext{ + id: id, + msg: msg, + } +} + +// ID implement C.PlainContext ID +func (c *DNSContext) ID() uuid.UUID { + return c.id +} + +// SetType set type of response +func (c *DNSContext) SetType(tp string) { + c.tp = tp +} + +// Type return type of response +func (c *DNSContext) Type() string { + return c.tp +} diff --git a/context/packetconn.go b/context/packetconn.go new file mode 100644 index 0000000..571977a --- /dev/null +++ b/context/packetconn.go @@ -0,0 +1,43 @@ +package context + +import ( + "net" + + C "github.com/Dreamacro/clash/constant" + + "github.com/gofrs/uuid/v5" +) + +type PacketConnContext struct { + id uuid.UUID + metadata *C.Metadata + packetConn net.PacketConn +} + +func NewPacketConnContext(metadata *C.Metadata) *PacketConnContext { + id, _ := uuid.NewV4() + return &PacketConnContext{ + id: id, + metadata: metadata, + } +} + +// ID implement C.PacketConnContext ID +func (pc *PacketConnContext) ID() uuid.UUID { + return pc.id +} + +// Metadata implement C.PacketConnContext Metadata +func (pc *PacketConnContext) Metadata() *C.Metadata { + return pc.metadata +} + +// PacketConn implement C.PacketConnContext PacketConn +func (pc *PacketConnContext) PacketConn() net.PacketConn { + return pc.packetConn +} + +// InjectPacketConn injectPacketConn manually +func (pc *PacketConnContext) InjectPacketConn(pconn C.PacketConn) { + pc.packetConn = pconn +} diff --git a/dns/client.go b/dns/client.go new file mode 100644 index 0000000..366a179 --- /dev/null +++ b/dns/client.go @@ -0,0 +1,92 @@ +package dns + +import ( + "context" + "crypto/tls" + "fmt" + "math/rand" + "net" + "strings" + + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/resolver" + + D "github.com/miekg/dns" +) + +type client struct { + *D.Client + r *Resolver + port string + host string + iface string +} + +func (c *client) Exchange(m *D.Msg) (*D.Msg, error) { + return c.ExchangeContext(context.Background(), m) +} + +func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { + var ( + ip net.IP + err error + ) + if c.r == nil { + // a default ip dns + if ip = net.ParseIP(c.host); ip == nil { + return nil, fmt.Errorf("dns %s not a valid ip", c.host) + } + } else { + ips, err := resolver.LookupIPWithResolver(ctx, c.host, c.r) + if err != nil { + return nil, fmt.Errorf("use default dns resolve failed: %w", err) + } else if len(ips) == 0 { + return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, c.host) + } + ip = ips[rand.Intn(len(ips))] + } + + network := "udp" + if strings.HasPrefix(c.Client.Net, "tcp") { + network = "tcp" + } + + options := []dialer.Option{} + if c.iface != "" { + options = append(options, dialer.WithInterface(c.iface)) + } + conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), c.port), options...) + if err != nil { + return nil, err + } + defer conn.Close() + + // miekg/dns ExchangeContext doesn't respond to context cancel. + // this is a workaround + type result struct { + msg *D.Msg + err error + } + ch := make(chan result, 1) + go func() { + if strings.HasSuffix(c.Client.Net, "tls") { + conn = tls.Client(conn, c.Client.TLSConfig) + } + + msg, _, err := c.Client.ExchangeWithConn(m, &D.Conn{ + Conn: conn, + UDPSize: c.Client.UDPSize, + TsigSecret: c.Client.TsigSecret, + TsigProvider: c.Client.TsigProvider, + }) + + ch <- result{msg, err} + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case ret := <-ch: + return ret.msg, ret.err + } +} diff --git a/dns/dhcp.go b/dns/dhcp.go new file mode 100644 index 0000000..8e0d5d4 --- /dev/null +++ b/dns/dhcp.go @@ -0,0 +1,146 @@ +package dns + +import ( + "bytes" + "context" + "net" + "sync" + "time" + + "github.com/Dreamacro/clash/component/dhcp" + "github.com/Dreamacro/clash/component/iface" + "github.com/Dreamacro/clash/component/resolver" + + D "github.com/miekg/dns" +) + +const ( + IfaceTTL = time.Second * 20 + DHCPTTL = time.Hour + DHCPTimeout = time.Minute +) + +type dhcpClient struct { + ifaceName string + + lock sync.Mutex + ifaceInvalidate time.Time + dnsInvalidate time.Time + + ifaceAddr *net.IPNet + done chan struct{} + clients []dnsClient + err error +} + +func (d *dhcpClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { + ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) + defer cancel() + + return d.ExchangeContext(ctx, m) +} + +func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + clients, err := d.resolve(ctx) + if err != nil { + return nil, err + } + + return batchExchange(ctx, clients, m) +} + +func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) { + d.lock.Lock() + + invalidated, err := d.invalidate() + if err != nil { + d.err = err + } else if invalidated { + done := make(chan struct{}) + + d.done = done + + go func() { + ctx, cancel := context.WithTimeout(context.Background(), DHCPTimeout) + defer cancel() + + var res []dnsClient + dns, err := dhcp.ResolveDNSFromDHCP(ctx, d.ifaceName) + // dns never empty if err is nil + if err == nil { + nameserver := make([]NameServer, 0, len(dns)) + for _, item := range dns { + nameserver = append(nameserver, NameServer{ + Addr: net.JoinHostPort(item.String(), "53"), + Interface: d.ifaceName, + }) + } + + res = transform(nameserver, nil) + } + + d.lock.Lock() + defer d.lock.Unlock() + + close(done) + + d.done = nil + d.clients = res + d.err = err + }() + } + + d.lock.Unlock() + + for { + d.lock.Lock() + + res, err, done := d.clients, d.err, d.done + + d.lock.Unlock() + + // initializing + if res == nil && err == nil { + select { + case <-done: + continue + case <-ctx.Done(): + return nil, ctx.Err() + } + } + + // dirty return + return res, err + } +} + +func (d *dhcpClient) invalidate() (bool, error) { + if time.Now().Before(d.ifaceInvalidate) { + return false, nil + } + + d.ifaceInvalidate = time.Now().Add(IfaceTTL) + + ifaceObj, err := iface.ResolveInterface(d.ifaceName) + if err != nil { + return false, err + } + + addr, err := ifaceObj.PickIPv4Addr(nil) + if err != nil { + return false, err + } + + if time.Now().Before(d.dnsInvalidate) && d.ifaceAddr.IP.Equal(addr.IP) && bytes.Equal(d.ifaceAddr.Mask, addr.Mask) { + return false, nil + } + + d.dnsInvalidate = time.Now().Add(DHCPTTL) + d.ifaceAddr = addr + + return d.done == nil, nil +} + +func newDHCPClient(ifaceName string) *dhcpClient { + return &dhcpClient{ifaceName: ifaceName} +} diff --git a/dns/doh.go b/dns/doh.go new file mode 100644 index 0000000..79820f9 --- /dev/null +++ b/dns/doh.go @@ -0,0 +1,117 @@ +package dns + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io" + "math/rand" + "net" + "net/http" + + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/resolver" + + D "github.com/miekg/dns" +) + +const ( + // dotMimeType is the DoH mimetype that should be used. + dotMimeType = "application/dns-message" +) + +type dohClient struct { + url string + transport *http.Transport +} + +func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { + return dc.ExchangeContext(context.Background(), m) +} + +func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + // https://datatracker.ietf.org/doc/html/rfc8484#section-4.1 + // In order to maximize cache friendliness, SHOULD use a DNS ID of 0 in every DNS request. + newM := *m + newM.Id = 0 + req, err := dc.newRequest(&newM) + if err != nil { + return nil, err + } + + req = req.WithContext(ctx) + msg, err = dc.doRequest(req) + if err == nil { + msg.Id = m.Id + } + return +} + +// newRequest returns a new DoH request given a dns.Msg. +func (dc *dohClient) newRequest(m *D.Msg) (*http.Request, error) { + buf, err := m.Pack() + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, dc.url, bytes.NewReader(buf)) + if err != nil { + return req, err + } + + req.Header.Set("content-type", dotMimeType) + req.Header.Set("accept", dotMimeType) + return req, nil +} + +func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) { + client := &http.Client{Transport: dc.transport} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + msg = &D.Msg{} + err = msg.Unpack(buf) + return msg, err +} + +func newDoHClient(url, iface string, r *Resolver) *dohClient { + return &dohClient{ + url: url, + transport: &http.Transport{ + ForceAttemptHTTP2: true, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + ips, err := resolver.LookupIPWithResolver(ctx, host, r) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) + } + ip := ips[rand.Intn(len(ips))] + + options := []dialer.Option{} + if iface != "" { + options = append(options, dialer.WithInterface(iface)) + } + + return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port), options...) + }, + TLSClientConfig: &tls.Config{ + // alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6 + NextProtos: []string{"dns"}, + }, + }, + } +} diff --git a/dns/enhancer.go b/dns/enhancer.go new file mode 100644 index 0000000..faaa072 --- /dev/null +++ b/dns/enhancer.go @@ -0,0 +1,89 @@ +package dns + +import ( + "net" + + "github.com/Dreamacro/clash/common/cache" + "github.com/Dreamacro/clash/component/fakeip" + C "github.com/Dreamacro/clash/constant" +) + +type ResolverEnhancer struct { + mode C.DNSMode + fakePool *fakeip.Pool + mapping *cache.LruCache +} + +func (h *ResolverEnhancer) FakeIPEnabled() bool { + return h.mode == C.DNSFakeIP +} + +func (h *ResolverEnhancer) MappingEnabled() bool { + return h.mode == C.DNSFakeIP || h.mode == C.DNSMapping +} + +func (h *ResolverEnhancer) IsExistFakeIP(ip net.IP) bool { + if !h.FakeIPEnabled() { + return false + } + + if pool := h.fakePool; pool != nil { + return pool.Exist(ip) + } + + return false +} + +func (h *ResolverEnhancer) IsFakeIP(ip net.IP) bool { + if !h.FakeIPEnabled() { + return false + } + + if pool := h.fakePool; pool != nil { + return pool.IPNet().Contains(ip) && !pool.Gateway().Equal(ip) + } + + return false +} + +func (h *ResolverEnhancer) FindHostByIP(ip net.IP) (string, bool) { + if pool := h.fakePool; pool != nil { + if host, existed := pool.LookBack(ip); existed { + return host, true + } + } + + if mapping := h.mapping; mapping != nil { + if host, existed := h.mapping.Get(ip.String()); existed { + return host.(string), true + } + } + + return "", false +} + +func (h *ResolverEnhancer) PatchFrom(o *ResolverEnhancer) { + if h.mapping != nil && o.mapping != nil { + o.mapping.CloneTo(h.mapping) + } + + if h.fakePool != nil && o.fakePool != nil { + h.fakePool.CloneFrom(o.fakePool) + } +} + +func NewEnhancer(cfg Config) *ResolverEnhancer { + var fakePool *fakeip.Pool + var mapping *cache.LruCache + + if cfg.EnhancedMode != C.DNSNormal { + fakePool = cfg.Pool + mapping = cache.New(cache.WithSize(4096)) + } + + return &ResolverEnhancer{ + mode: cfg.EnhancedMode, + fakePool: fakePool, + mapping: mapping, + } +} diff --git a/dns/filters.go b/dns/filters.go new file mode 100644 index 0000000..30825a4 --- /dev/null +++ b/dns/filters.go @@ -0,0 +1,50 @@ +package dns + +import ( + "net" + "strings" + + "github.com/Dreamacro/clash/component/mmdb" + "github.com/Dreamacro/clash/component/trie" +) + +type fallbackIPFilter interface { + Match(net.IP) bool +} + +type geoipFilter struct { + code string +} + +func (gf *geoipFilter) Match(ip net.IP) bool { + record, _ := mmdb.Instance().Country(ip) + return !strings.EqualFold(record.Country.IsoCode, gf.code) && !ip.IsPrivate() +} + +type ipnetFilter struct { + ipnet *net.IPNet +} + +func (inf *ipnetFilter) Match(ip net.IP) bool { + return inf.ipnet.Contains(ip) +} + +type fallbackDomainFilter interface { + Match(domain string) bool +} + +type domainFilter struct { + tree *trie.DomainTrie +} + +func NewDomainFilter(domains []string) *domainFilter { + df := domainFilter{tree: trie.New()} + for _, domain := range domains { + df.tree.Insert(domain, "") + } + return &df +} + +func (df *domainFilter) Match(domain string) bool { + return df.tree.Search(domain) != nil +} diff --git a/dns/middleware.go b/dns/middleware.go new file mode 100644 index 0000000..7ec2890 --- /dev/null +++ b/dns/middleware.go @@ -0,0 +1,197 @@ +package dns + +import ( + "net" + "strings" + "time" + + "github.com/Dreamacro/clash/common/cache" + "github.com/Dreamacro/clash/component/fakeip" + "github.com/Dreamacro/clash/component/trie" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/context" + "github.com/Dreamacro/clash/log" + + D "github.com/miekg/dns" +) + +type ( + handler func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) + middleware func(next handler) handler +) + +func withHosts(hosts *trie.DomainTrie) middleware { + return func(next handler) handler { + return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + q := r.Question[0] + + if !isIPRequest(q) { + return next(ctx, r) + } + + record := hosts.Search(strings.TrimRight(q.Name, ".")) + if record == nil { + return next(ctx, r) + } + + ip := record.Data.(net.IP) + msg := r.Copy() + + if v4 := ip.To4(); v4 != nil && q.Qtype == D.TypeA { + rr := &D.A{} + rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL} + rr.A = v4 + + msg.Answer = []D.RR{rr} + } else if v6 := ip.To16(); v6 != nil && q.Qtype == D.TypeAAAA { + rr := &D.AAAA{} + rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: dnsDefaultTTL} + rr.AAAA = v6 + + msg.Answer = []D.RR{rr} + } else { + return next(ctx, r) + } + + ctx.SetType(context.DNSTypeHost) + msg.SetRcode(r, D.RcodeSuccess) + msg.Authoritative = true + msg.RecursionAvailable = true + + return msg, nil + } + } +} + +func withMapping(mapping *cache.LruCache) middleware { + return func(next handler) handler { + return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + q := r.Question[0] + + if !isIPRequest(q) { + return next(ctx, r) + } + + msg, err := next(ctx, r) + if err != nil { + return nil, err + } + + host := strings.TrimRight(q.Name, ".") + + for _, ans := range msg.Answer { + var ip net.IP + var ttl uint32 + + switch a := ans.(type) { + case *D.A: + ip = a.A + ttl = a.Hdr.Ttl + if !ip.IsGlobalUnicast() { + continue + } + case *D.AAAA: + ip = a.AAAA + ttl = a.Hdr.Ttl + if !ip.IsGlobalUnicast() { + continue + } + default: + continue + } + + if ttl < 1 { + ttl = 1 + } + mapping.SetWithExpire(ip.String(), host, time.Now().Add(time.Second*time.Duration(ttl))) + } + + return msg, nil + } + } +} + +func withFakeIP(fakePool *fakeip.Pool) middleware { + return func(next handler) handler { + return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + q := r.Question[0] + + host := strings.TrimRight(q.Name, ".") + if fakePool.ShouldSkipped(host) { + return next(ctx, r) + } + + switch q.Qtype { + case D.TypeAAAA, D.TypeSVCB, D.TypeHTTPS: + return handleMsgWithEmptyAnswer(r), nil + } + + if q.Qtype != D.TypeA { + return next(ctx, r) + } + + rr := &D.A{} + rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL} + ip := fakePool.Lookup(host) + rr.A = ip + msg := r.Copy() + msg.Answer = []D.RR{rr} + + ctx.SetType(context.DNSTypeFakeIP) + setMsgTTL(msg, 1) + msg.SetRcode(r, D.RcodeSuccess) + msg.Authoritative = true + msg.RecursionAvailable = true + + return msg, nil + } + } +} + +func withResolver(resolver *Resolver) handler { + return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + ctx.SetType(context.DNSTypeRaw) + q := r.Question[0] + + // return a empty AAAA msg when ipv6 disabled + if !resolver.ipv6 && q.Qtype == D.TypeAAAA { + return handleMsgWithEmptyAnswer(r), nil + } + + msg, err := resolver.Exchange(r) + if err != nil { + log.Debugln("[DNS Server] Exchange %s failed: %v", q.String(), err) + return msg, err + } + msg.SetRcode(r, msg.Rcode) + msg.Authoritative = true + + return msg, nil + } +} + +func compose(middlewares []middleware, endpoint handler) handler { + length := len(middlewares) + h := endpoint + for i := length - 1; i >= 0; i-- { + middleware := middlewares[i] + h = middleware(h) + } + + return h +} + +func newHandler(resolver *Resolver, mapper *ResolverEnhancer) handler { + middlewares := []middleware{} + + if resolver.hosts != nil { + middlewares = append(middlewares, withHosts(resolver.hosts)) + } + + if mapper.mode == C.DNSFakeIP { + middlewares = append(middlewares, withFakeIP(mapper.fakePool)) + middlewares = append(middlewares, withMapping(mapper.mapping)) + } + + return compose(middlewares, withResolver(resolver)) +} diff --git a/dns/resolver.go b/dns/resolver.go new file mode 100644 index 0000000..75c907c --- /dev/null +++ b/dns/resolver.go @@ -0,0 +1,407 @@ +package dns + +import ( + "context" + "errors" + "fmt" + "math/rand" + "net" + "strings" + "time" + + "github.com/Dreamacro/clash/common/cache" + "github.com/Dreamacro/clash/component/fakeip" + "github.com/Dreamacro/clash/component/resolver" + "github.com/Dreamacro/clash/component/trie" + C "github.com/Dreamacro/clash/constant" + + D "github.com/miekg/dns" + "golang.org/x/sync/singleflight" +) + +type dnsClient interface { + Exchange(m *D.Msg) (msg *D.Msg, err error) + ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) +} + +type result struct { + Msg *D.Msg + Error error +} + +type Resolver struct { + ipv6 bool + hosts *trie.DomainTrie + main []dnsClient + fallback []dnsClient + fallbackDomainFilters []fallbackDomainFilter + fallbackIPFilters []fallbackIPFilter + group singleflight.Group + lruCache *cache.LruCache + policy *trie.DomainTrie + searchDomains []string +} + +// LookupIP request with TypeA and TypeAAAA, priority return TypeA +func (r *Resolver) LookupIP(ctx context.Context, host string) (ip []net.IP, err error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + ch := make(chan []net.IP, 1) + + go func() { + defer close(ch) + ip, err := r.lookupIP(ctx, host, D.TypeAAAA) + if err != nil { + return + } + ch <- ip + }() + + ip, err = r.lookupIP(ctx, host, D.TypeA) + if err == nil { + return + } + + ip, open := <-ch + if !open { + return nil, resolver.ErrIPNotFound + } + + return ip, nil +} + +// ResolveIP request with TypeA and TypeAAAA, priority return TypeA +func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) { + ips, err := r.LookupIP(context.Background(), host) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) + } + return ips[rand.Intn(len(ips))], nil +} + +// LookupIPv4 request with TypeA +func (r *Resolver) LookupIPv4(ctx context.Context, host string) ([]net.IP, error) { + return r.lookupIP(ctx, host, D.TypeA) +} + +// ResolveIPv4 request with TypeA +func (r *Resolver) ResolveIPv4(host string) (ip net.IP, err error) { + ips, err := r.lookupIP(context.Background(), host, D.TypeA) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) + } + return ips[rand.Intn(len(ips))], nil +} + +// LookupIPv6 request with TypeAAAA +func (r *Resolver) LookupIPv6(ctx context.Context, host string) ([]net.IP, error) { + return r.lookupIP(ctx, host, D.TypeAAAA) +} + +// ResolveIPv6 request with TypeAAAA +func (r *Resolver) ResolveIPv6(host string) (ip net.IP, err error) { + ips, err := r.lookupIP(context.Background(), host, D.TypeAAAA) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) + } + return ips[rand.Intn(len(ips))], nil +} + +func (r *Resolver) shouldIPFallback(ip net.IP) bool { + for _, filter := range r.fallbackIPFilters { + if filter.Match(ip) { + return true + } + } + return false +} + +// Exchange a batch of dns request, and it use cache +func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) { + return r.ExchangeContext(context.Background(), m) +} + +// ExchangeContext a batch of dns request with context.Context, and it use cache +func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + if len(m.Question) == 0 { + return nil, errors.New("should have one question at least") + } + + q := m.Question[0] + cache, expireTime, hit := r.lruCache.GetWithExpire(q.String()) + if hit { + now := time.Now() + msg = cache.(*D.Msg).Copy() + if expireTime.Before(now) { + setMsgTTL(msg, uint32(1)) // Continue fetch + go func() { + ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) + r.exchangeWithoutCache(ctx, m) + cancel() + }() + } else { + // updating TTL by subtracting common delta time from each DNS record + updateMsgTTL(msg, uint32(time.Until(expireTime).Seconds())) + } + return + } + return r.exchangeWithoutCache(ctx, m) +} + +// ExchangeWithoutCache a batch of dns request, and it do NOT GET from cache +func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + q := m.Question[0] + + ret, err, shared := r.group.Do(q.String(), func() (result any, err error) { + defer func() { + if err != nil { + return + } + + msg := result.(*D.Msg) + + putMsgToCache(r.lruCache, q.String(), q, msg) + }() + + isIPReq := isIPRequest(q) + if isIPReq { + return r.ipExchange(ctx, m) + } + + if matched := r.matchPolicy(m); len(matched) != 0 { + return r.batchExchange(ctx, matched, m) + } + return r.batchExchange(ctx, r.main, m) + }) + + if err == nil { + msg = ret.(*D.Msg) + if shared { + msg = msg.Copy() + } + } + + return +} + +func (r *Resolver) batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) { + ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDNSTimeout) + defer cancel() + + return batchExchange(ctx, clients, m) +} + +func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient { + if r.policy == nil { + return nil + } + + domain := r.msgToDomain(m) + if domain == "" { + return nil + } + + record := r.policy.Search(domain) + if record == nil { + return nil + } + + return record.Data.([]dnsClient) +} + +func (r *Resolver) shouldOnlyQueryFallback(m *D.Msg) bool { + if r.fallback == nil || len(r.fallbackDomainFilters) == 0 { + return false + } + + domain := r.msgToDomain(m) + + if domain == "" { + return false + } + + for _, df := range r.fallbackDomainFilters { + if df.Match(domain) { + return true + } + } + + return false +} + +func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + if matched := r.matchPolicy(m); len(matched) != 0 { + res := <-r.asyncExchange(ctx, matched, m) + return res.Msg, res.Error + } + + onlyFallback := r.shouldOnlyQueryFallback(m) + + if onlyFallback { + res := <-r.asyncExchange(ctx, r.fallback, m) + return res.Msg, res.Error + } + + msgCh := r.asyncExchange(ctx, r.main, m) + + if r.fallback == nil { // directly return if no fallback servers are available + res := <-msgCh + msg, err = res.Msg, res.Error + return + } + + fallbackMsg := r.asyncExchange(ctx, r.fallback, m) + res := <-msgCh + if res.Error == nil { + if ips := msgToIP(res.Msg); len(ips) != 0 { + if !r.shouldIPFallback(ips[0]) { + msg = res.Msg // no need to wait for fallback result + err = res.Error + return msg, err + } + } + } + + res = <-fallbackMsg + msg, err = res.Msg, res.Error + return +} + +func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) ([]net.IP, error) { + ip := net.ParseIP(host) + if ip != nil { + ip4 := ip.To4() + isIPv4 := ip4 != nil + if dnsType == D.TypeAAAA && !isIPv4 { + return []net.IP{ip}, nil + } else if dnsType == D.TypeA && isIPv4 { + return []net.IP{ip4}, nil + } else { + return nil, resolver.ErrIPVersion + } + } + + query := &D.Msg{} + query.SetQuestion(D.Fqdn(host), dnsType) + + msg, err := r.ExchangeContext(ctx, query) + if err != nil { + return nil, err + } + + ips := msgToIP(msg) + if len(ips) != 0 { + return ips, nil + } else if len(r.searchDomains) == 0 { + return nil, resolver.ErrIPNotFound + } + + // query provided search domains serially + for _, domain := range r.searchDomains { + q := &D.Msg{} + q.SetQuestion(D.Fqdn(fmt.Sprintf("%s.%s", host, domain)), dnsType) + msg, err := r.ExchangeContext(ctx, q) + if err != nil { + return nil, err + } + ips := msgToIP(msg) + if len(ips) != 0 { + return ips, nil + } + } + + return nil, resolver.ErrIPNotFound +} + +func (r *Resolver) msgToDomain(msg *D.Msg) string { + if len(msg.Question) > 0 { + return strings.TrimRight(msg.Question[0].Name, ".") + } + + return "" +} + +func (r *Resolver) asyncExchange(ctx context.Context, client []dnsClient, msg *D.Msg) <-chan *result { + ch := make(chan *result, 1) + go func() { + res, err := r.batchExchange(ctx, client, msg) + ch <- &result{Msg: res, Error: err} + }() + return ch +} + +type NameServer struct { + Net string + Addr string + Interface string +} + +type FallbackFilter struct { + GeoIP bool + GeoIPCode string + IPCIDR []*net.IPNet + Domain []string +} + +type Config struct { + Main, Fallback []NameServer + Default []NameServer + IPv6 bool + EnhancedMode C.DNSMode + FallbackFilter FallbackFilter + Pool *fakeip.Pool + Hosts *trie.DomainTrie + Policy map[string]NameServer + SearchDomains []string +} + +func NewResolver(config Config) *Resolver { + defaultResolver := &Resolver{ + main: transform(config.Default, nil), + lruCache: cache.New(cache.WithSize(4096), cache.WithStale(true)), + } + + r := &Resolver{ + ipv6: config.IPv6, + main: transform(config.Main, defaultResolver), + lruCache: cache.New(cache.WithSize(4096), cache.WithStale(true)), + hosts: config.Hosts, + searchDomains: config.SearchDomains, + } + + if len(config.Fallback) != 0 { + r.fallback = transform(config.Fallback, defaultResolver) + } + + if len(config.Policy) != 0 { + r.policy = trie.New() + for domain, nameserver := range config.Policy { + r.policy.Insert(domain, transform([]NameServer{nameserver}, defaultResolver)) + } + } + + fallbackIPFilters := []fallbackIPFilter{} + if config.FallbackFilter.GeoIP { + fallbackIPFilters = append(fallbackIPFilters, &geoipFilter{ + code: config.FallbackFilter.GeoIPCode, + }) + } + for _, ipnet := range config.FallbackFilter.IPCIDR { + fallbackIPFilters = append(fallbackIPFilters, &ipnetFilter{ipnet: ipnet}) + } + r.fallbackIPFilters = fallbackIPFilters + + if len(config.FallbackFilter.Domain) != 0 { + fallbackDomainFilters := []fallbackDomainFilter{NewDomainFilter(config.FallbackFilter.Domain)} + r.fallbackDomainFilters = fallbackDomainFilters + } + + return r +} diff --git a/dns/server.go b/dns/server.go new file mode 100644 index 0000000..db90334 --- /dev/null +++ b/dns/server.go @@ -0,0 +1,106 @@ +package dns + +import ( + "errors" + "net" + + "github.com/Dreamacro/clash/common/sockopt" + "github.com/Dreamacro/clash/context" + "github.com/Dreamacro/clash/log" + + D "github.com/miekg/dns" +) + +var ( + address string + server = &Server{} + + dnsDefaultTTL uint32 = 600 +) + +type Server struct { + *D.Server + handler handler +} + +// ServeDNS implement D.Handler ServeDNS +func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) { + msg, err := handlerWithContext(s.handler, r) + if err != nil { + D.HandleFailed(w, r) + return + } + msg.Compress = true + w.WriteMsg(msg) +} + +func handlerWithContext(handler handler, msg *D.Msg) (*D.Msg, error) { + if len(msg.Question) == 0 { + return nil, errors.New("at least one question is required") + } + + ctx := context.NewDNSContext(msg) + return handler(ctx, msg) +} + +func (s *Server) setHandler(handler handler) { + s.handler = handler +} + +func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { + if addr == address && resolver != nil { + handler := newHandler(resolver, mapper) + server.setHandler(handler) + return + } + + if server.Server != nil { + server.Shutdown() + server = &Server{} + address = "" + } + + if addr == "" { + return + } + + var err error + defer func() { + if err != nil { + log.Errorln("Start DNS server error: %s", err.Error()) + } + }() + + _, port, err := net.SplitHostPort(addr) + if port == "0" || port == "" || err != nil { + return + } + + udpAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return + } + + p, err := net.ListenUDP("udp", udpAddr) + if err != nil { + return + } + + err = sockopt.UDPReuseaddr(p) + if err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + + err = nil + } + + address = addr + handler := newHandler(resolver, mapper) + server = &Server{handler: handler} + server.Server = &D.Server{Addr: addr, PacketConn: p, Handler: server} + + go func() { + server.ActivateAndServe() + }() + + log.Infoln("DNS server listening at: %s", p.LocalAddr().String()) +} diff --git a/dns/util.go b/dns/util.go new file mode 100644 index 0000000..fea2f65 --- /dev/null +++ b/dns/util.go @@ -0,0 +1,152 @@ +package dns + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "time" + + "github.com/Dreamacro/clash/common/cache" + "github.com/Dreamacro/clash/common/picker" + + D "github.com/miekg/dns" + "github.com/samber/lo" +) + +func minimalTTL(records []D.RR) uint32 { + if len(records) == 0 { + return 0 + } + return lo.MinBy(records, func(r1 D.RR, r2 D.RR) bool { + return r1.Header().Ttl < r2.Header().Ttl + }).Header().Ttl +} + +func updateTTL(records []D.RR, ttl uint32) { + if len(records) == 0 { + return + } + delta := minimalTTL(records) - ttl + for i := range records { + records[i].Header().Ttl = lo.Clamp(records[i].Header().Ttl-delta, 1, records[i].Header().Ttl) + } +} + +func putMsgToCache(c *cache.LruCache, key string, q D.Question, msg *D.Msg) { + ttl := minimalTTL(msg.Answer) + if ttl == 0 { + return + } + c.SetWithExpire(key, msg.Copy(), time.Now().Add(time.Duration(ttl)*time.Second)) +} + +func setMsgTTL(msg *D.Msg, ttl uint32) { + for _, answer := range msg.Answer { + answer.Header().Ttl = ttl + } + + for _, ns := range msg.Ns { + ns.Header().Ttl = ttl + } + + for _, extra := range msg.Extra { + extra.Header().Ttl = ttl + } +} + +func updateMsgTTL(msg *D.Msg, ttl uint32) { + updateTTL(msg.Answer, ttl) + updateTTL(msg.Ns, ttl) + updateTTL(msg.Extra, ttl) +} + +func isIPRequest(q D.Question) bool { + return q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA) +} + +func transform(servers []NameServer, resolver *Resolver) []dnsClient { + ret := []dnsClient{} + for _, s := range servers { + switch s.Net { + case "https": + ret = append(ret, newDoHClient(s.Addr, s.Interface, resolver)) + continue + case "dhcp": + ret = append(ret, newDHCPClient(s.Addr)) + continue + } + + host, port, _ := net.SplitHostPort(s.Addr) + ret = append(ret, &client{ + Client: &D.Client{ + Net: s.Net, + TLSConfig: &tls.Config{ + ServerName: host, + }, + UDPSize: 4096, + Timeout: 5 * time.Second, + }, + port: port, + host: host, + iface: s.Interface, + r: resolver, + }) + } + return ret +} + +func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg { + msg := &D.Msg{} + msg.Answer = []D.RR{} + + msg.SetRcode(r, D.RcodeSuccess) + msg.Authoritative = true + msg.RecursionAvailable = true + + return msg +} + +func msgToIP(msg *D.Msg) []net.IP { + ips := []net.IP{} + + for _, answer := range msg.Answer { + switch ans := answer.(type) { + case *D.AAAA: + ips = append(ips, ans.AAAA) + case *D.A: + ips = append(ips, ans.A) + } + } + + return ips +} + +func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) { + fast, ctx := picker.WithContext(ctx) + for _, client := range clients { + r := client + fast.Go(func() (any, error) { + m, err := r.ExchangeContext(ctx, m) + if err != nil { + return nil, err + } else if m.Rcode == D.RcodeServerFailure || m.Rcode == D.RcodeRefused { + return nil, errors.New("server failure") + } + return m, nil + }) + } + + elm := fast.Wait() + if elm == nil { + err := errors.New("all DNS requests failed") + if fErr := fast.Error(); fErr != nil { + err = fmt.Errorf("%w, first error: %s", err, fErr.Error()) + } + return nil, err + } + + msg = elm.(*D.Msg) + return +} diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 0000000..d6d5af6 --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,47 @@ +import { defineConfig } from 'vitepress' +import locales from './locales' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: 'Clash', + + base: '/clash/', + + head: [ + [ + 'link', + { rel: 'icon', type: "image/x-icon", href: '/clash/logo.png' } + ], + ], + + locales: locales.locales, + + lastUpdated: true, + + themeConfig: { + search: { + provider: 'local', + options: { + locales: { + zh_CN: { + translations: { + button: { + buttonText: '搜索文档', + buttonAriaLabel: '搜索文档' + }, + modal: { + noResultsText: '无法找到相关结果', + resetButtonTitle: '清除查询条件', + footer: { + selectText: '选择', + navigateText: '切换' + } + } + } + } + }, + + } + } + }, +}) diff --git a/docs/.vitepress/locales/en_US.ts b/docs/.vitepress/locales/en_US.ts new file mode 100644 index 0000000..ad27538 --- /dev/null +++ b/docs/.vitepress/locales/en_US.ts @@ -0,0 +1,60 @@ +import { createRequire } from 'module' +import { defineConfig } from 'vitepress' +import { generateSidebarChapter } from './side_bar.js' + +const require = createRequire(import.meta.url) + +const chapters = generateSidebarChapter('en_US', new Map([ + ['introduction', 'Introduction'], + ['configuration', 'Configuration'], + ['premium', 'Premium'], + ['runtime', 'Runtime'], + ['advanced-usages', 'Advanced Usages'], +])) + +export default defineConfig({ + lang: 'en-US', + + description: 'A rule-based tunnel in Go.', + + themeConfig: { + nav: nav(), + + logo: '/logo.png', + + lastUpdatedText: 'Last updated at', + + sidebar: chapters, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/Dreamacro/clash' }, + ], + + editLink: { + pattern: 'https://github.com/Dreamacro/clash/edit/master/docs/:path', + text: 'Edit this page on GitHub' + }, + + outline: { + level: 'deep', + label: 'On this page', + }, + + } +}) + +function nav() { + return [ + { text: 'Home', link: '/' }, + { text: 'Configuration', link: '/configuration/configuration-reference' }, + { + text: 'Download', + items: [ + { text: 'Open-source Edition', link: 'https://github.com/Dreamacro/clash/releases/' }, + { text: 'Premium Edition', link: 'https://github.com/Dreamacro/clash/releases/tag/premium' }, + ] + } + ] +} + + diff --git a/docs/.vitepress/locales/index.ts b/docs/.vitepress/locales/index.ts new file mode 100644 index 0000000..baf0171 --- /dev/null +++ b/docs/.vitepress/locales/index.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitepress' +import en_US from './en_US' +import zh_CN from './zh_CN' + +export default defineConfig({ + locales: { + root: { + label: 'English', + lang: en_US.lang, + themeConfig: en_US.themeConfig, + description: en_US.description + }, + zh_CN: { + label: '简体中文', + lang: zh_CN.lang, + themeConfig: zh_CN.themeConfig, + description: zh_CN.description + } + } +}) \ No newline at end of file diff --git a/docs/.vitepress/locales/side_bar.ts b/docs/.vitepress/locales/side_bar.ts new file mode 100644 index 0000000..71fa57e --- /dev/null +++ b/docs/.vitepress/locales/side_bar.ts @@ -0,0 +1,78 @@ +import directoryTree from 'directory-tree' +import fs from 'fs' +import metadataParser from 'markdown-yaml-metadata-parser' + +function getMetadataFromDoc(path: string): { sidebarTitle?: string, sidebarOrder?: number } { + const fileContents = fs.readFileSync(path, 'utf8') + + return metadataParser(fileContents).metadata +} + +export function generateSidebarChapter(locale:string, chapterDirName: Map<string,string>): any[] { + if (chapterDirName.size < 1) { + console.error(chapterDirName) + throw new Error(`Could not genereate sidebar: chapterDirName is empty`) + } + + var chapterPath = '' + var sidebar: any[] = [] + + for (const chapterDirKey of chapterDirName.keys()) { + if (locale !== 'en_US') { + chapterPath = `./${locale}/${chapterDirKey}` + } else { + chapterPath = `./${chapterDirKey}` + } + + const tree = directoryTree(chapterPath) + + if (!tree || !tree.children) { + console.error(tree) + throw new Error(`Could not genereate sidebar: invalid chapter at ${chapterPath}`) + } + + let items: { sidebarOrder: number, text: string, link: string }[] = [] + + // Look into files in the chapter + for (const doc of tree.children) { + // make sure it's a .md file + if (doc.children || !doc.name.endsWith('.md')) + continue + + const { sidebarOrder, sidebarTitle } = getMetadataFromDoc(doc.path) + + if (!sidebarOrder) + throw new Error('Cannot find sidebarOrder in doc metadata: ' + doc.path) + + if (!sidebarTitle) + throw new Error('Cannot find sidebarTitle in doc metadata: ' + doc.path) + + if (chapterDirKey === 'introduction' && doc.name === '_dummy-index.md') { + // Override index page link + items.push({ + sidebarOrder, + text: sidebarTitle, + link: '/' + (locale === 'en_US' ? '' : locale + '/') + }) + } else { + items.push({ + sidebarOrder, + text: sidebarTitle, + link: "/" + doc.path + }) + } + } + + items = items.sort((a, b) => a.sidebarOrder - b.sidebarOrder) + + // remove dash and capitalize first character of each word as chapter title + const text = chapterDirName.get(chapterDirKey) || chapterDirKey.split('-').join(' ').replace(/\b\w/g, l => l.toUpperCase()) + sidebar.push({ + text, + collapsed: false, + items, + }) + } + + return sidebar +} \ No newline at end of file diff --git a/docs/.vitepress/locales/zh_CN.ts b/docs/.vitepress/locales/zh_CN.ts new file mode 100644 index 0000000..65fedf7 --- /dev/null +++ b/docs/.vitepress/locales/zh_CN.ts @@ -0,0 +1,60 @@ +import { createRequire } from 'module' +import { defineConfig } from 'vitepress' +import { generateSidebarChapter } from './side_bar.js' + +const require = createRequire(import.meta.url) + +const chapters = generateSidebarChapter('zh_CN', new Map([ + ['introduction', '简介'], + ['configuration', '配置'], + ['premium', 'Premium 版本'], + ['runtime', '运行时'], + ['advanced-usages', '高级用法'], +])) + +export default defineConfig({ + lang: 'zh-CN', + + description: '基于规则的 Go 网络隧道. ', + + themeConfig: { + nav: nav(), + + logo: '/logo.png', + + lastUpdatedText: '最后更新于', + + sidebar: chapters, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/Dreamacro/clash' }, + ], + + editLink: { + pattern: 'https://github.com/Dreamacro/clash/edit/master/docs/:path', + text: '在 GitHub 中编辑此页面' + }, + + docFooter: { prev: '上一篇', next: '下一篇' }, + + outline: { + level: 'deep', + label: '页面导航', + }, + + } +}) + +function nav() { + return [ + { text: '主页', link: '/zh_CN/' }, + { text: '配置', link: '/zh_CN/configuration/configuration-reference' }, + { + text: '下载', + items: [ + { text: 'GitHub 开源版', link: 'https://github.com/Dreamacro/clash/releases/' }, + { text: 'Premium 版本', link: 'https://github.com/Dreamacro/clash/releases/tag/premium' }, + ] + } + ] +} diff --git a/docs/advanced-usages/golang-api.md b/docs/advanced-usages/golang-api.md new file mode 100644 index 0000000..db7a927 --- /dev/null +++ b/docs/advanced-usages/golang-api.md @@ -0,0 +1,59 @@ +--- +sidebarTitle: Integrating Clash in Golang Programs +sidebarOrder: 3 +--- + +# Integrating Clash in Golang Programs + +If clash does not fit your own usage, you can use Clash in your own Golang code. + +There is already basic support: + +```go +package main + +import ( + "context" + "fmt" + "io" + "net" + + "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/socks" +) + +func main() { + in := make(chan constant.ConnContext, 100) + defer close(in) + + l, err := socks.New("127.0.0.1:10000", in) + if err != nil { + panic(err) + } + defer l.Close() + + println("listen at:", l.Address()) + + direct := outbound.NewDirect() + + for c := range in { + conn := c + metadata := conn.Metadata() + fmt.Printf("request incoming from %s to %s\n", metadata.SourceAddress(), metadata.RemoteAddress()) + go func () { + remote, err := direct.DialContext(context.Background(), metadata) + if err != nil { + fmt.Printf("dial error: %s\n", err.Error()) + return + } + relay(remote, conn.Conn()) + }() + } +} + +func relay(l, r net.Conn) { + go io.Copy(l, r) + io.Copy(r, l) +} +``` diff --git a/docs/advanced-usages/openconnect.md b/docs/advanced-usages/openconnect.md new file mode 100644 index 0000000..11a6f49 --- /dev/null +++ b/docs/advanced-usages/openconnect.md @@ -0,0 +1,93 @@ +--- +sidebarTitle: Rule-based OpenConnect +sidebarOrder: 2 +--- + +# Rule-based OpenConnect + +OpenConnect supports Cisco AnyConnect SSL VPN, Juniper Network Connect, Palo Alto Networks (PAN) GlobalProtect SSL VPN, Pulse Connect Secure SSL VPN, F5 BIG-IP SSL VPN, FortiGate SSL VPN and Array Networks SSL VPN. + +For example, there would be a use case where your company uses Cisco AnyConnect for internal network access. Here I'll show you how you can use OpenConnect with policy routing powered by Clash. + +First, [install vpn-slice](https://github.com/dlenski/vpn-slice#requirements). This tool overrides default routing table behaviour of OpenConnect. Simply saying, it stops the VPN from overriding your default routes. + +Next you would have a script (let's say `tun0.sh`) similar to this: + +```sh +#!/bin/bash +ANYCONNECT_HOST="vpn.example.com" +ANYCONNECT_USER="john" +ANYCONNECT_PASSWORD="foobar" +ROUTING_TABLE_ID="6667" +TUN_INTERFACE="tun0" + +# Add --no-dtls if the server is in mainland China. UDP in China is choppy. +echo "$ANYCONNECT_PASSWORD" | \ + openconnect \ + --non-inter \ + --passwd-on-stdin \ + --protocol=anyconnect \ + --interface $TUN_INTERFACE \ + --script "vpn-slice +if [ \"\$reason\" = 'connect' ]; then + ip rule add from \$INTERNAL_IP4_ADDRESS table $ROUTING_TABLE_ID + ip route add default dev \$TUNDEV scope link table $ROUTING_TABLE_ID +elif [ \"\$reason\" = 'disconnect' ]; then + ip rule del from \$INTERNAL_IP4_ADDRESS table $ROUTING_TABLE_ID + ip route del default dev \$TUNDEV scope link table $ROUTING_TABLE_ID +fi" \ + --user $ANYCONNECT_USER \ + https://$ANYCONNECT_HOST +``` + +After that, we configure it as a systemd service. Create `/etc/systemd/system/tun0.service`: + +```ini +[Unit] +Description=Cisco AnyConnect VPN +After=network-online.target +Conflicts=shutdown.target sleep.target + +[Service] +Type=simple +ExecStart=/path/to/tun0.sh +KillSignal=SIGINT +Restart=always +RestartSec=3 +StartLimitIntervalSec=0 + +[Install] +WantedBy=multi-user.target +``` + +Then we enable & start the service. + +```shell +chmod +x /path/to/tun0.sh +systemctl daemon-reload +systemctl enable tun0 +systemctl start tun0 +``` + +From here you can look at the logs to see if it's running properly. Simple way is to look at if `tun0` interface has been created. + +Similar to the Wireguard one, having an outbound to a TUN device is simple as adding a proxy group: + +```yaml +proxy-groups: + - name: Cisco AnyConnect VPN + type: select + interface-name: tun0 + proxies: + - DIRECT +``` + +... and it's ready to use! Add the desired rules: + +```yaml +rules: + - DOMAIN-SUFFIX,internal.company.com,Cisco AnyConnect VPN +``` + +You should look at the debug level logs when something does not seem right. + diff --git a/docs/advanced-usages/wireguard.md b/docs/advanced-usages/wireguard.md new file mode 100644 index 0000000..e4ba144 --- /dev/null +++ b/docs/advanced-usages/wireguard.md @@ -0,0 +1,40 @@ +--- +sidebarTitle: Rule-based Wireguard +sidebarOrder: 1 +--- + +# Rule-based Wireguard + +Suppose your kernel supports Wireguard and you have it enabled. The `Table` option stops _wg-quick_ from overriding default routes. + +Example `wg0.conf`: + +```ini +[Interface] +PrivateKey = ... +Address = 172.16.0.1/32 +MTU = ... +Table = off +PostUp = ip rule add from 172.16.0.1/32 table 6666 + +[Peer] +AllowedIPs = 0.0.0.0/0 +AllowedIPs = ::/0 +PublicKey = ... +Endpoint = ... +``` + +Then in Clash you would only need to have a DIRECT proxy group that has a specific outbound interface: + +```yaml +proxy-groups: + - name: Wireguard + type: select + interface-name: wg0 + proxies: + - DIRECT +rules: + - DOMAIN,google.com,Wireguard +``` + +This should perform better than whereas if Clash implemented its own userspace Wireguard client. Wireguard is supported in the kernel. diff --git a/docs/assets/connection-flow.png b/docs/assets/connection-flow.png new file mode 100644 index 0000000..7bb6203 Binary files /dev/null and b/docs/assets/connection-flow.png differ diff --git a/docs/configuration/configuration-reference.md b/docs/configuration/configuration-reference.md new file mode 100644 index 0000000..acdb491 --- /dev/null +++ b/docs/configuration/configuration-reference.md @@ -0,0 +1,480 @@ +--- +sidebarTitle: Configuration Reference +sidebarOrder: 7 +--- + +# Configuration Reference + +```yaml +# Port of HTTP(S) proxy server on the local end +port: 7890 + +# Port of SOCKS5 proxy server on the local end +socks-port: 7891 + +# Transparent proxy server port for Linux and macOS (Redirect TCP and TProxy UDP) +# redir-port: 7892 + +# Transparent proxy server port for Linux (TProxy TCP and TProxy UDP) +# tproxy-port: 7893 + +# HTTP(S) and SOCKS4(A)/SOCKS5 server on the same port +# mixed-port: 7890 + +# authentication of local SOCKS5/HTTP(S) server +# authentication: +# - "user1:pass1" +# - "user2:pass2" + +# Set to true to allow connections to the local-end server from +# other LAN IP addresses +# allow-lan: false + +# This is only applicable when `allow-lan` is `true` +# '*': bind all IP addresses +# 192.168.122.11: bind a single IPv4 address +# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address +# bind-address: '*' + +# Clash router working mode +# rule: rule-based packet routing +# global: all packets will be forwarded to a single endpoint +# direct: directly forward the packets to the Internet +mode: rule + +# Clash by default prints logs to STDOUT +# info / warning / error / debug / silent +# log-level: info + +# When set to false, resolver won't translate hostnames to IPv6 addresses +# ipv6: false + +# RESTful web API listening address +external-controller: 127.0.0.1:9090 + +# A relative path to the configuration directory or an absolute path to a +# directory in which you put some static web resource. Clash core will then +# serve it at `http://{{external-controller}}/ui`. +# external-ui: folder + +# Secret for the RESTful API (optional) +# Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` +# ALWAYS set a secret if RESTful API is listening on 0.0.0.0 +# secret: "" + +# Outbound interface name +# interface-name: en0 + +# fwmark on Linux only +# routing-mark: 6666 + +# Static hosts for DNS server and connection establishment (like /etc/hosts) +# +# Wildcard hostnames are supported (e.g. *.clash.dev, *.foo.*.example.com) +# Non-wildcard domain names have a higher priority than wildcard domain names +# e.g. foo.example.com > *.example.com > .example.com +# P.S. +.foo.com equals to .foo.com and foo.com +# hosts: + # '*.clash.dev': 127.0.0.1 + # '.dev': 127.0.0.1 + # 'alpha.clash.dev': '::1' + +# profile: + # Store the `select` results in $HOME/.config/clash/.cache + # set false If you don't want this behavior + # when two different configurations have groups with the same name, the selected values are shared + # store-selected: true + + # persistence fakeip + # store-fake-ip: false + +# DNS server settings +# This section is optional. When not present, the DNS server will be disabled. +dns: + enable: false + listen: 0.0.0.0:53 + # ipv6: false # when the false, response to AAAA questions will be empty + + # These nameservers are used to resolve the DNS nameserver hostnames below. + # Specify IP addresses only + default-nameserver: + - 114.114.114.114 + - 8.8.8.8 + # enhanced-mode: fake-ip + fake-ip-range: 198.18.0.1/16 # Fake IP addresses pool CIDR + # use-hosts: true # lookup hosts and return IP record + + # search-domains: [local] # search domains for A/AAAA record + + # Hostnames in this list will not be resolved with fake IPs + # i.e. questions to these domain names will always be answered with their + # real IP addresses + # fake-ip-filter: + # - '*.lan' + # - localhost.ptlogin2.qq.com + + # Supports UDP, TCP, DoT, DoH. You can specify the port to connect to. + # All DNS questions are sent directly to the nameserver, without proxies + # involved. Clash answers the DNS question with the first result gathered. + nameserver: + - 114.114.114.114 # default value + - 8.8.8.8 # default value + - tls://dns.rubyfish.cn:853 # DNS over TLS + - https://1.1.1.1/dns-query # DNS over HTTPS + - dhcp://en0 # dns from dhcp + # - '8.8.8.8#en0' + + # When `fallback` is present, the DNS server will send concurrent requests + # to the servers in this section along with servers in `nameservers`. + # The answers from fallback servers are used when the GEOIP country + # is not `CN`. + # fallback: + # - tcp://1.1.1.1 + # - 'tcp://1.1.1.1#en0' + + # If IP addresses resolved with servers in `nameservers` are in the specified + # subnets below, they are considered invalid and results from `fallback` + # servers are used instead. + # + # IP address resolved with servers in `nameserver` is used when + # `fallback-filter.geoip` is true and when GEOIP of the IP address is `CN`. + # + # If `fallback-filter.geoip` is false, results from `nameserver` nameservers + # are always used if not match `fallback-filter.ipcidr`. + # + # This is a countermeasure against DNS pollution attacks. + # fallback-filter: + # geoip: true + # geoip-code: CN + # ipcidr: + # - 240.0.0.0/4 + # domain: + # - '+.google.com' + # - '+.facebook.com' + # - '+.youtube.com' + + # Lookup domains via specific nameservers + # nameserver-policy: + # 'www.baidu.com': '114.114.114.114' + # '+.internal.crop.com': '10.0.0.1' + +proxies: + # Shadowsocks + # The supported ciphers (encryption methods): + # aes-128-gcm aes-192-gcm aes-256-gcm + # aes-128-cfb aes-192-cfb aes-256-cfb + # aes-128-ctr aes-192-ctr aes-256-ctr + # rc4-md5 chacha20-ietf xchacha20 + # chacha20-ietf-poly1305 xchacha20-ietf-poly1305 + - name: "ss1" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + # udp: true + + - name: "ss2" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: obfs + plugin-opts: + mode: tls # or http + # host: bing.com + + - name: "ss3" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: v2ray-plugin + plugin-opts: + mode: websocket # no QUIC now + # tls: true # wss + # skip-cert-verify: true + # host: bing.com + # path: "/" + # mux: true + # headers: + # custom: value + + # vmess + # cipher support auto/aes-128-gcm/chacha20-poly1305/none + - name: "vmess" + type: vmess + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + # udp: true + # tls: true + # skip-cert-verify: true + # servername: example.com # priority over wss host + # network: ws + # ws-opts: + # path: /path + # headers: + # Host: v2ray.com + # max-early-data: 2048 + # early-data-header-name: Sec-WebSocket-Protocol + + - name: "vmess-h2" + type: vmess + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + network: h2 + tls: true + h2-opts: + host: + - http.example.com + - http-alt.example.com + path: / + + - name: "vmess-http" + type: vmess + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + # udp: true + # network: http + # http-opts: + # # method: "GET" + # # path: + # # - '/' + # # - '/video' + # # headers: + # # Connection: + # # - keep-alive + + - name: vmess-grpc + server: server + port: 443 + type: vmess + uuid: uuid + alterId: 32 + cipher: auto + network: grpc + tls: true + servername: example.com + # skip-cert-verify: true + grpc-opts: + grpc-service-name: "example" + + # socks5 + - name: "socks" + type: socks5 + server: server + port: 443 + # username: username + # password: password + # tls: true + # skip-cert-verify: true + # udp: true + + # http + - name: "http" + type: http + server: server + port: 443 + # username: username + # password: password + # tls: true # https + # skip-cert-verify: true + # sni: custom.com + + # Snell + # Beware that there's currently no UDP support yet + - name: "snell" + type: snell + server: server + port: 44046 + psk: yourpsk + # version: 2 + # obfs-opts: + # mode: http # or tls + # host: bing.com + + # Trojan + - name: "trojan" + type: trojan + server: server + port: 443 + password: yourpsk + # udp: true + # sni: example.com # aka server name + # alpn: + # - h2 + # - http/1.1 + # skip-cert-verify: true + + - name: trojan-grpc + server: server + port: 443 + type: trojan + password: "example" + network: grpc + sni: example.com + # skip-cert-verify: true + udp: true + grpc-opts: + grpc-service-name: "example" + + - name: trojan-ws + server: server + port: 443 + type: trojan + password: "example" + network: ws + sni: example.com + # skip-cert-verify: true + udp: true + # ws-opts: + # path: /path + # headers: + # Host: example.com + + # ShadowsocksR + # The supported ciphers (encryption methods): all stream ciphers in ss + # The supported obfses: + # plain http_simple http_post + # random_head tls1.2_ticket_auth tls1.2_ticket_fastauth + # The supported supported protocols: + # origin auth_sha1_v4 auth_aes128_md5 + # auth_aes128_sha1 auth_chain_a auth_chain_b + - name: "ssr" + type: ssr + server: server + port: 443 + cipher: chacha20-ietf + password: "password" + obfs: tls1.2_ticket_auth + protocol: auth_sha1_v4 + # obfs-param: domain.tld + # protocol-param: "#" + # udp: true + +proxy-groups: + # relay chains the proxies. proxies shall not contain a relay. No UDP support. + # Traffic: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet + - name: "relay" + type: relay + proxies: + - http + - vmess + - ss1 + - ss2 + + # url-test select which proxy will be used by benchmarking speed to a URL. + - name: "auto" + type: url-test + proxies: + - ss1 + - ss2 + - vmess1 + # tolerance: 150 + # lazy: true + url: 'http://www.gstatic.com/generate_204' + interval: 300 + + # fallback selects an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group. + - name: "fallback-auto" + type: fallback + proxies: + - ss1 + - ss2 + - vmess1 + url: 'http://www.gstatic.com/generate_204' + interval: 300 + + # load-balance: The request of the same eTLD+1 will be dial to the same proxy. + - name: "load-balance" + type: load-balance + proxies: + - ss1 + - ss2 + - vmess1 + url: 'http://www.gstatic.com/generate_204' + interval: 300 + # strategy: consistent-hashing # or round-robin + + # select is used for selecting proxy or proxy group + # you can use RESTful API to switch proxy is recommended for use in GUI. + - name: Proxy + type: select + # disable-udp: true + # filter: 'someregex' + proxies: + - ss1 + - ss2 + - vmess1 + - auto + + # direct to another interfacename or fwmark, also supported on proxy + - name: en1 + type: select + interface-name: en1 + routing-mark: 6667 + proxies: + - DIRECT + + - name: UseProvider + type: select + use: + - provider1 + proxies: + - Proxy + - DIRECT + +proxy-providers: + provider1: + type: http + url: "url" + interval: 3600 + path: ./provider1.yaml + health-check: + enable: true + interval: 600 + # lazy: true + url: http://www.gstatic.com/generate_204 + test: + type: file + path: /test.yaml + health-check: + enable: true + interval: 36000 + url: http://www.gstatic.com/generate_204 + +tunnels: + # one line config + - tcp/udp,127.0.0.1:6553,114.114.114.114:53,proxy + - tcp,127.0.0.1:6666,rds.mysql.com:3306,vpn + # full yaml config + - network: [tcp, udp] + address: 127.0.0.1:7777 + target: target.com + proxy: proxy + +rules: + - DOMAIN-SUFFIX,google.com,auto + - DOMAIN-KEYWORD,google,auto + - DOMAIN,google.com,auto + - DOMAIN-SUFFIX,ad.com,REJECT + - SRC-IP-CIDR,192.168.1.201/32,DIRECT + # optional param "no-resolve" for IP rules (GEOIP, IP-CIDR, IP-CIDR6) + - IP-CIDR,127.0.0.0/8,DIRECT + - GEOIP,CN,DIRECT + - DST-PORT,80,DIRECT + - SRC-PORT,7777,DIRECT + - RULE-SET,apple,REJECT # Premium only + - MATCH,auto +``` diff --git a/docs/configuration/dns.md b/docs/configuration/dns.md new file mode 100644 index 0000000..d3520bf --- /dev/null +++ b/docs/configuration/dns.md @@ -0,0 +1,72 @@ +--- +sidebarTitle: Clash DNS +sidebarOrder: 6 +--- + +# Clash DNS + +Since some parts of Clash run on the Layer 3 (Network Layer), they would've been impossible to obtain domain names of the packets for rule-based routing. + +*Enter fake-ip*. It enables rule-based routing, minimises the impact of DNS pollution attack and improves network performance, sometimes drastically. + +## fake-ip + +The concept of "fake IP" addresses is originated from [RFC 3089](https://tools.ietf.org/rfc/rfc3089): + +> A "fake IP" address is used as a key to look up the corresponding "FQDN" information. + +The default CIDR for the fake-ip pool is `198.18.0.1/16`, a reserved IPv4 address space, which can be changed in `dns.fake-ip-range`. + +When a DNS request is sent to the Clash DNS, the core allocates a *free* fake-ip address from the pool, by managing an internal mapping of domain names and their fake-ip addresses. + +Take an example of accessing `http://google.com` with your browser. + +1. The browser asks Clash DNS for the IP address of `google.com` +2. Clash checks the internal mapping and returned `198.18.1.5` +3. The browser sends an HTTP request to `198.18.1.5` on `80/tcp` +4. When receiving the inbound packet for `198.18.1.5`, Clash looks up the internal mapping and realises the client is actually sending a packet to `google.com` +5. Depending on the rules: + + 1. Clash may just send the domain name to an outbound proxy like SOCKS5 or shadowsocks and establish the connection with the proxy server + + 2. or Clash might look for the real IP address of `google.com`, in the case of encountering a `SCRIPT`, `GEOIP`, `IP-CIDR` rule, or the case of DIRECT outbound + +Being a confusing concept, I'll take another example of accessing `http://google.com` with the cURL utility: + +```txt{2,3,5,6,8,9} +$ curl -v http://google.com +<---- cURL asks your system DNS (Clash) about the IP address of google.com +----> Clash decided 198.18.1.70 should be used as google.com and remembers it +* Trying 198.18.1.70:80... +<---- cURL connects to 198.18.1.70 tcp/80 +----> Clash will accept the connection immediately, and.. +* Connected to google.com (198.18.1.70) port 80 (#0) +----> Clash looks up in its memory and found 198.18.1.70 being google.com +----> Clash looks up in the rules and sends the packet via the matching outbound +> GET / HTTP/1.1 +> Host: google.com +> User-Agent: curl/8.0.1 +> Accept: */* +> +< HTTP/1.1 301 Moved Permanently +< Location: http://www.google.com/ +< Content-Type: text/html; charset=UTF-8 +< Content-Security-Policy-Report-Only: object-src 'none';base-uri 'self';script-src 'nonce-ahELFt78xOoxhySY2lQ34A' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp +< Date: Thu, 11 May 2023 06:52:19 GMT +< Expires: Sat, 10 Jun 2023 06:52:19 GMT +< Cache-Control: public, max-age=2592000 +< Server: gws +< Content-Length: 219 +< X-XSS-Protection: 0 +< X-Frame-Options: SAMEORIGIN +< +<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8"> +<TITLE>301 Moved +

301 Moved

+The document has moved +here. + +* Connection #0 to host google.com left intact +``` + + diff --git a/docs/configuration/getting-started.md b/docs/configuration/getting-started.md new file mode 100644 index 0000000..aa91270 --- /dev/null +++ b/docs/configuration/getting-started.md @@ -0,0 +1,76 @@ +--- +sidebarTitle: Getting Started +sidebarOrder: 2 +--- + +# Getting Started + +It's recommended that you read the [Introduction](/configuration/introduction) before proceeding. After you have a brief understanding of how Clash works, you can start writing your own configuration. + +## Configuration Files + +The main configuration file is called `config.yaml`. By default, Clash reads the configuration files at `$HOME/.config/clash`. If it doesn't exist, Clash will generate a minimal configuration file at that location. + +If you want to place your configurations elsewhere (e.g. `/etc/clash`), you can use command-line option `-d` to specify a configuration directory: + +```shell +clash -d . # current directory +clash -d /etc/clash +``` + +Or, you can use option `-f` to specify a configuration file: + +```shell +clash -f ./config.yaml +clash -f /etc/clash/config.yaml +``` + +## Special Syntaxes + +There are some special syntaxes in Clash configuration files, of which you might want to be aware: + +### IPv6 Addresses + +You should wrap IPv6 addresses in square brackets, for example: + +```txt +[aaaa::a8aa:ff:fe09:57d8] +``` + +### DNS Wildcard Domain Matching + +In some cases, you will need to match against wildcard domains. For example, when you're setting up [Clash DNS](/configuration/dns), you might want to match against all subdomains of `localdomain`. + +Clash do offer support on matching different levels of wildcard domains in the DNS configuration, while the syntaxes defined below: + +::: tip +Any domain with these characters should be wrapped with single quotes (`'`). For example, `'*.google.com'`. +Static domain has a higher priority than wildcard domain (foo.example.com > *.example.com > .example.com). +::: + +Use an asterisk (`*`) to match against a single-level wildcard subdomain. + +| Expression | Matches | Does Not Match | +| ---------- | ------- | -------------- | +| `*.google.com` | `www.google.com` | `google.com` | +| `*.bar.google.com` | `foo.bar.google.com` | `bar.google.com` | +| `*.*.google.com` | `thoughtful.sandbox.google.com` | `one.two.three.google.com` | + +Use a dot sign (`.`) to match against multi-level wildcard subdomains. + +| Expression | Matches | Does Not Match | +| ---------- | ------- | -------------- | +| `.google.com` | `www.google.com` | `google.com` | +| `.google.com` | `thoughtful.sandbox.google.com` | `google.com` | +| `.google.com` | `one.two.three.google.com` | `google.com` | + +Use a plus sign (`+`) to match against multi-level wildcard subdomains. + +`+` wildcard works like DOMAIN-SUFFIX, you can quickly match multi level at a time. + +| Expression | Matches | +| ---------- | ------- | +| `+.google.com` | `google.com` | +| `+.google.com` | `www.google.com` | +| `+.google.com` | `thoughtful.sandbox.google.com` | +| `+.google.com` | `one.two.three.google.com` | diff --git a/docs/configuration/inbound.md b/docs/configuration/inbound.md new file mode 100644 index 0000000..fc4d593 --- /dev/null +++ b/docs/configuration/inbound.md @@ -0,0 +1,69 @@ +--- +sidebarTitle: Inbound +sidebarOrder: 3 +--- + +# Inbound + +Clash supports multiple inbound protocols, including: + +- SOCKS5 +- HTTP(S) +- Redirect TCP +- TProxy TCP +- TProxy UDP +- Linux TUN device (Premium only) + +Connections to any inbound protocol listed above will be handled by the same internal rule-matching engine. That is to say, Clash does not (currently) support different rule sets for different inbounds. + +## Configuration + +```yaml +# Port of HTTP(S) proxy server on the local end +# port: 7890 + +# Port of SOCKS5 proxy server on the local end +# socks-port: 7891 + +# HTTP(S) and SOCKS4(A)/SOCKS5 server on the same port +mixed-port: 7890 + +# Transparent proxy server port for Linux and macOS (Redirect TCP and TProxy UDP) +# redir-port: 7892 + +# Transparent proxy server port for Linux (TProxy TCP and TProxy UDP) +# tproxy-port: 7893 + +# Allow clients other than 127.0.0.1 to connect to the inbounds +allow-lan: false +``` + +## The Mixed Port + +The mixed port is a special port that supports both HTTP(S) and SOCKS5 protocols. You can have any programs that support either HTTP or SOCKS proxy to connect to this port, for example: + +```shell +$ curl -x socks5h://127.0.0.1:7890 -v http://connect.rom.miui.com/generate_204 +* Trying 127.0.0.1:7890... +* SOCKS5 connect to connect.rom.miui.com:80 (remotely resolved) +* SOCKS5 request granted. +* Connected to (nil) (127.0.0.1) port 7890 (#0) +> GET /generate_204 HTTP/1.1 +> Host: connect.rom.miui.com +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 204 No Content +< Date: Thu, 11 May 2023 06:18:22 GMT +< Connection: keep-alive +< Content-Type: text/plain +< +* Connection #0 to host (nil) left intact +``` + +## Redirect and TProxy + +Redirect and TProxy are two different ways of implementing transparent proxying. They are both supported by Clash. + +However, you most likely don't need to mess with these two inbounds - we recommend using [Clash Premium](/premium/introduction) if you want to use transparent proxying, as it has built-in support of the automatic management of the route table, rules and nftables. diff --git a/docs/configuration/introduction.md b/docs/configuration/introduction.md new file mode 100644 index 0000000..72450f3 --- /dev/null +++ b/docs/configuration/introduction.md @@ -0,0 +1,38 @@ +--- +sidebarTitle: Introduction +sidebarOrder: 1 +--- + +# Introduction + +In this chapter, we'll cover the common features of Clash and how they should be used and configured. + +Clash uses [YAML](https://yaml.org), _YAML Ain't Markup Language_, for configuration files. YAML is designed to be easy to be read, be written, and be interpreted by computers, and is commonly used for exact configuration files. + +## Understanding how Clash works + +Before proceeding, it's important to understand how Clash works, in which there are two critical components: + +![](/assets/connection-flow.png) + + + +### Inbound + +Inbound is the component that listens on the local end. It works by opening a local port and listening for incoming connections. When a connection comes in, Clash looks up the rules that are configured in the configuration file, and decides which outbound that the connection should go next. + +### Outbound + +Outbound is the component that connects to the remote end. Depending on the configuration, it can be a specific network interface, a proxy server, or a [proxy group](./outbound#proxy-groups). + +## Rule-based Routing + +Clash supports rule-based routing, which means you can route packets to different outbounds based on the a variety of contraints. The rules can be defined in the `rules` section of the configuration file. + +There's a number of available rule types, and each rule type has its own syntax. The general syntax of a rule is: + +```txt +TYPE,ARGUMENT,POLICY(,no-resolve) +``` + +In the upcoming guides, you will learn more about how rules can be configured. diff --git a/docs/configuration/outbound.md b/docs/configuration/outbound.md new file mode 100644 index 0000000..c6ef236 --- /dev/null +++ b/docs/configuration/outbound.md @@ -0,0 +1,437 @@ +--- +sidebarTitle: Outbound +sidebarOrder: 4 +--- + +# Outbound + +There are several types of outbound targets in Clash. Each type has its own features and usage scenarios. In this page, we'll cover the common features of each type and how they should be used and configured. + +[[toc]] + +## Proxies + +Proxies are some outbound targets that you can configure. Like proxy servers, you define destinations for the packets here. + +### Shadowsocks + +Clash supports the following ciphers (encryption methods) for Shadowsocks: + +| Family | Ciphers | +| ------ | ------- | +| AEAD | aes-128-gcm, aes-192-gcm, aes-256-gcm, chacha20-ietf-poly1305, xchacha20-ietf-poly1305 | +| Stream | aes-128-cfb, aes-192-cfb, aes-256-cfb, rc4-md5, chacha20-ietf, xchacha20 | +| Block | aes-128-ctr, aes-192-ctr, aes-256-ctr | + +In addition, Clash also supports popular Shadowsocks plugins `obfs` and `v2ray-plugin`. + +::: code-group + +```yaml [basic] +- name: "ss1" + type: ss + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + # udp: true +``` + +```yaml [obfs] +- name: "ss2" + type: ss + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: obfs + plugin-opts: + mode: tls # or http + # host: bing.com +``` + +```yaml [ws (websocket)] +- name: "ss3" + type: ss + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: v2ray-plugin + plugin-opts: + mode: websocket # no QUIC now + # tls: true # wss + # skip-cert-verify: true + # host: bing.com + # path: "/" + # mux: true + # headers: + # custom: value +``` + +::: + +### ShadowsocksR + +Clash supports the infamous anti-censorship protocol ShadowsocksR as well. The supported ciphers: + +| Family | Ciphers | +| ------ | ------- | +| Stream | aes-128-cfb, aes-192-cfb, aes-256-cfb, rc4-md5, chacha20-ietf, xchacha20 | + +Supported obfuscation methods: + +- plain +- http_simple +- http_post +- random_head +- tls1.2_ticket_auth +- tls1.2_ticket_fastauth + +Supported protocols: + +- origin +- auth_sha1_v4 +- auth_aes128_md5 +- auth_aes128_sha1 +- auth_chain_a +- auth_chain_b + +```yaml +- name: "ssr" + type: ssr + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + cipher: chacha20-ietf + password: "password" + obfs: tls1.2_ticket_auth + protocol: auth_sha1_v4 + # obfs-param: domain.tld + # protocol-param: "#" + # udp: true +``` + +### Vmess + +Clash supports the following ciphers (encryption methods) for Vmess: + +- auto +- aes-128-gcm +- chacha20-poly1305 +- none + +::: code-group + +```yaml [basic] +- name: "vmess" + type: vmess + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + # udp: true + # tls: true + # skip-cert-verify: true + # servername: example.com # priority over wss host + # network: ws + # ws-opts: + # path: /path + # headers: + # Host: v2ray.com + # max-early-data: 2048 + # early-data-header-name: Sec-WebSocket-Protocol +``` + +```yaml [HTTP] +- name: "vmess-http" + type: vmess + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + # udp: true + # network: http + # http-opts: + # # method: "GET" + # # path: + # # - '/' + # # - '/video' + # # headers: + # # Connection: + # # - keep-alive +``` + +```yaml [HTTP/2] +- name: "vmess-h2" + type: vmess + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + network: h2 + tls: true + h2-opts: + host: + - http.example.com + - http-alt.example.com + path: / +``` + +```yaml [gRPC] +- name: vmess-grpc + type: vmess + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + network: grpc + tls: true + servername: example.com + # skip-cert-verify: true + grpc-opts: + grpc-service-name: "example" +``` + +::: + +### SOCKS5 + +In addition, Clash supports SOCKS5 outbound as well: + +```yaml +- name: "socks" + type: socks5 + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + # username: username + # password: password + # tls: true + # skip-cert-verify: true + # udp: true +``` + +### HTTP + +Clash also supports HTTP outbound: + +::: code-group + +```yaml [HTTP] +- name: "http" + type: http + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + # username: username + # password: password +``` + +```yaml [HTTPS] +- name: "http" + type: http + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + tls: true + # skip-cert-verify: true + # sni: custom.com + # username: username + # password: password +``` + +::: + +### Snell + +Being an alternative protocol for anti-censorship, Clash has integrated support for Snell as well. + +::: tip +Clash does not support Snell v4. ([#2466](https://github.com/Dreamacro/clash/issues/2466)) +::: + +```yaml +# No UDP support yet +- name: "snell" + type: snell + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 44046 + psk: yourpsk + # version: 2 + # obfs-opts: + # mode: http # or tls + # host: bing.com +``` + +### Trojan + +Clash has built support for the popular protocol Trojan: + +::: code-group + +```yaml [basic] +- name: "trojan" + type: trojan + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + password: yourpsk + # udp: true + # sni: example.com # aka server name + # alpn: + # - h2 + # - http/1.1 + # skip-cert-verify: true +``` + +```yaml [gRPC] +- name: trojan-grpc + type: trojan + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + password: "example" + network: grpc + sni: example.com + # skip-cert-verify: true + udp: true + grpc-opts: + grpc-service-name: "example" +``` + +```yaml [ws (websocket)] +- name: trojan-ws + type: trojan + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + password: "example" + network: ws + sni: example.com + # skip-cert-verify: true + udp: true + # ws-opts: + # path: /path + # headers: + # Host: example.com +``` + +::: + +## Proxy Groups + +Proxy Groups are groups of proxies that you can use directly as a rule policy. + +### relay + +The request sent to this proxy group will be relayed through the specified proxy servers sequently. There's currently no UDP support on this. The specified proxy servers should not contain another relay. + +### url-test + +Clash benchmarks each proxy servers in the list, by sending HTTP HEAD requests to a specified URL through these servers periodically. It's possible to set a maximum tolerance value, benchmarking interval, and the target URL. + +### fallback + +Clash periodically tests the availability of servers in the list with the same mechanism of `url-test`. The first available server will be used. + +### load-balance + +The request to the same eTLD+1 will be dialed with the same proxy. + +### select + +The first server is by default used when Clash starts up. Users can choose the server to use with the RESTful API. In this mode, you can hardcode servers in the config or use [Proxy Providers](#proxy-providers). + +Either way, sometimes you might as well just route packets with a direct connection. In this case, you can use the `DIRECT` outbound. + +To use a different network interface, you will need to use a Proxy Group that contains a `DIRECT` outbound with the `interface-name` option set. + +```yaml +- name: "My Wireguard Outbound" + type: select + interface-name: wg0 + proxies: [ 'DIRECT' ] +``` + +## Proxy Providers + +Proxy Providers give users the power to load proxy server lists dynamically, instead of hardcoding them in the configuration file. There are currently two sources for a proxy provider to load server list from: + +- `http`: Clash loads the server list from a specified URL on startup. Clash periodically pulls the server list from remote if the `interval` option is set. +- `file`: Clash loads the server list from a specified location on the filesystem on startup. + +Health check is available for both modes, and works exactly like `fallback` in Proxy Groups. The configuration format for the server list files is also exactly the same in the main configuration file: + +::: code-group + +```yaml [config.yaml] +proxy-providers: + provider1: + type: http + url: "url" + interval: 3600 + path: ./provider1.yaml + # filter: 'a|b' # golang regex string + health-check: + enable: true + interval: 600 + # lazy: true + url: http://www.gstatic.com/generate_204 + test: + type: file + path: /test.yaml + health-check: + enable: true + interval: 36000 + url: http://www.gstatic.com/generate_204 +``` + +```yaml [test.yaml] +proxies: + - name: "ss1" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + + - name: "ss2" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: obfs + plugin-opts: + mode: tls +``` + +::: diff --git a/docs/configuration/rules.md b/docs/configuration/rules.md new file mode 100644 index 0000000..8fdfb02 --- /dev/null +++ b/docs/configuration/rules.md @@ -0,0 +1,159 @@ +--- +sidebarTitle: Rules +sidebarOrder: 5 +--- + +# Rules + +In the Getting Started guide, we covered the basics of rule-based matching in Clash. In this chapter, we'll cover all available rule types in the latest version of Clash. + +```txt +TYPE,ARGUMENT,POLICY(,no-resolve) +``` + +The `no-resolve` option is optional, and it's used to skip DNS resolution for the rule. It's useful when you want to use `GEOIP`, `IP-CIDR`, `IP-CIDR6`, `SCRIPT` rules, but don't want to resolve the domain name to an IP address just yet. + +[[toc]] + +## Policy + +There are four types of POLICY for now, in which: + +- DIRECT: directly connects to the target through `interface-name` (does not lookup system route table) +- REJECT: drops the packet +- Proxy: routes the packet to the specified proxy server +- Proxy Group: routes the packet to the specified proxy group + +## Types of rules + +There are a number of rules where one might find useful. The following section covers each rule type and how they should be used. + +### DOMAIN + +`DOMAIN,www.google.com,policy` routes only `www.google.com` to `policy`. + +### DOMAIN-SUFFIX + +`DOMAIN-SUFFIX,youtube.com,policy` routes any domain names that ends with `youtube.com`. + +In this case, `www.youtube.com` and `foo.bar.youtube.com` will be routed to `policy`. + +### DOMAIN-KEYWORD + +`DOMAIN-KEYWORD,google,policy` routes any domain names to policy that contains `google`. + +In this case, `www.google.com` or `googleapis.com` are routed to `policy`. + +### GEOIP + +GEOIP rules are used to route packets based on the **country code** of the target IP address. Clash uses [MaxMind GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) database for this feature. + +::: warning +When encountering this rule, Clash will resolve the domain name to an IP address and then look up the country code of the IP address. If you want to skip the DNS resolution, use `no-resolve` option. +::: + +`GEOIP,CN,policy` routes any packets destined to a China IP address to `policy`. + +### IP-CIDR + +IP-CIDR rules are used to route packets based on the **destination IPv4 address** of the packet. + +::: warning +When encountering this rule, Clash will resolve the domain name to an IP address. If you want to skip the DNS resolution, use `no-resolve` option. +::: + +`IP-CIDR,127.0.0.0/8,DIRECT` routes any packets destined to `127.0.0.0/8` to the `DIRECT` outbound. + +### IP-CIDR6 + +IP-CIDR6 rules are used to route packets based on the **destination IPv6 address** of the packet. + +::: warning +When encountering this rule, Clash will resolve the domain name to an IP address. If you want to skip the DNS resolution, use `no-resolve` option. +::: + +`IP-CIDR6,2620:0:2d0:200::7/32,policy` routes any packets destined to `2620:0:2d0:200::7/32` to `policy`. + +### SRC-IP-CIDR + +SRC-IP-CIDR rules are used to route packets based on the **source IPv4 address** of the packet. + +`SRC-IP-CIDR,192.168.1.201/32,DIRECT` routes any packets **from** `192.168.1.201/32` to the `DIRECT` policy. + +### SRC-PORT + +SRC-PORT rules are used to route packets based on the **source port** of the packet. + +`SRC-PORT,80,policy` routes any packets **from** the port 80 to `policy`. + +### DST-PORT + +DST-PORT rules are used to route packets based on the **destination port** of the packet. + +`DST-PORT,80,policy` routes any packets **to** the port 80 to `policy`. + +### PROCESS-NAME + +PROCESS-NAME rules are used to route packets based on the name of process that is sending the packet. + +::: warning +Currently, only macOS, Linux, FreeBSD and Windows are supported. +::: + +`PROCESS-NAME,nc,DIRECT` routes all packets from the process `nc` to the `DIRECT` outbound. + +### PROCESS-PATH + +PROCESS-PATH rules are used to route packets based on the PATH of process that is sending the packet. + +::: warning +Currently, only macOS, Linux, FreeBSD and Windows are supported. +::: + +`PROCESS-PATH,/bin/sh,DIRECT` routes all packets from the process `/bin/sh` to the `DIRECT` outbound. + +### IPSET + +IPSET rules are used to match against an IP set and route packets based on the result. According to the [official website of IPSET](https://ipset.netfilter.org/): + +> IP sets are a framework inside the Linux kernel, which can be administered by the ipset utility. Depending on the type, an IP set may store IP addresses, networks, (TCP/UDP) port numbers, MAC addresses, interface names or combinations of them in a way, which ensures lightning speed when matching an entry against a set. + +Therefore, this feature only works on Linux and requires `ipset` to be installed. + +::: warning +When encountering this rule, Clash will resolve the domain name to an IP address. If you want to skip the DNS resolution, use `no-resolve` option. +::: + +`IPSET,chinaip,DIRECT` routes all packets with destination IPs matching the `chinaip` IPSET to DIRECT outbound. + +### RULE-SET + +::: info +This feature is only available in the [Premium](/premium/introduction) edtion. +::: + +RULE-SET rules are used to route packets based on the result of a [rule provider](/premium/rule-providers). When Clash encounters this rule, it loads the rules from the specified rule provider and then matches the packet against the rules. If the packet matches any of the rules, the packet will be routed to the specified policy, otherwise the rule is skipped. + +::: warning +When encountering RULE-SET, Clash will resolve the domain name to an IP address **when the ruleset is of type IPCIDR**. If you want to skip the DNS resolution, use `no-resolve` option for the RULE-SET entry. +::: + +`RULE-SET,my-rule-provider,DIRECT` loads all rules from `my-rule-provider` and sends the matched packets to the `DIRECT` outbound. + +### SCRIPT + +::: info +This feature is only available in the [Premium](/premium/introduction) edtion. +::: + +SCRIPT rules are special rules that are used to route packets based on the result of a [script shortcut](/premium/script-shortcuts). When Clash encounters this rule, it evaluates the expression. If it returns `true`, the packet will be routed to the specified policy, otherwise the rule is skipped. + +::: warning +When encountering this rule, Clash will resolve the domain name to an IP address. If you want to skip the DNS resolution, use `no-resolve` option. +::: + +`SCRIPT,SHORTCUT-NAME,policy` routes any packets to `policy` if they have the shortcut evaluated `true`. + +### MATCH + +`MATCH,policy` routes the rest of the packets to `policy`. This rule is **required** and is usually used as the last rule. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c6f2c5a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,38 @@ + +# What is Clash? + +Welcome to the official knowledge base of the Clash core project ("Clash"). + +Clash is a cross-platform rule-based proxy utility that runs on the network and application layer, supporting various proxy and anti-censorship protocols out-of-the-box. + +It has been adopted widely by the Internet users in some countries and regions where the Internet is heavily censored or blocked. Either way, Clash can be used by anyone who wants to improve their Internet experience. + +There are currently two editions of Clash: + +- [Clash](https://github.com/Dreamacro/clash): the open-source version released at [github.com/Dreamacro/clash](https://github.com/Dreamacro/clash) +- [Clash Premium](https://github.com/Dreamacro/clash/releases/tag/premium): proprietary core with [TUN support and more](/premium/introduction) (free of charge) + +While this wiki covers both, however, the use of Clash could be challenging for the average users. Those might want to consider using a GUI client instead, and we do have some recommendations: + +- [Clash for Windows](https://github.com/Fndroid/clash_for_windows_pkg/releases) (Windows and macOS) +- [Clash for Android](https://github.com/Kr328/ClashForAndroid) +- [ClashX](https://github.com/yichengchen/clashX) or [ClashX Pro](https://install.appcenter.ms/users/clashx/apps/clashx-pro/distribution_groups/public) (macOS) + +## Feature Overview + +- 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 + + +\*: Only available in the free-of-charge Premium edition. + + +## License + +Clash is released under the [GPL-3.0](https://github.com/Dreamacro/clash/blob/master/LICENSE) open-source license. Prior to [v0.16.0](https://github.com/Dreamacro/clash/releases/tag/v0.16.0) or commit [e5284c](https://github.com/Dreamacro/clash/commit/e5284cf647717a8087a185d88d15a01096274bc2), it was licensed under the MIT license. diff --git a/docs/introduction/_dummy-index.md b/docs/introduction/_dummy-index.md new file mode 100644 index 0000000..d9d4f6d --- /dev/null +++ b/docs/introduction/_dummy-index.md @@ -0,0 +1,6 @@ +--- +sidebarTitle: What is Clash? +sidebarOrder: 1 +--- + + diff --git a/docs/introduction/faq.md b/docs/introduction/faq.md new file mode 100644 index 0000000..ab04047 --- /dev/null +++ b/docs/introduction/faq.md @@ -0,0 +1,95 @@ +--- +sidebarTitle: Frequently Asked Questions +sidebarOrder: 4 +--- + +# Frequently Asked Questions + +Here we have some common questions people ask. If you have any questions not listed here, feel free to [open an issue](https://github.com/Dreamacro/clash/issues/new/choose). + +[[toc]] + +## What is the difference between amd64 and amd64-v3? + +Quoting from [golang/go](https://github.com/golang/go/wiki/MinimumRequirements#amd64): + +> Until Go 1.17, the Go compiler always generated x86 binaries that could be executed by any 64-bit x86 processor. +> +> Go 1.18 introduced [4 architectural levels](https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels) for AMD64. +> Each level differs in the set of x86 instructions that the compiler can include in the generated binaries: +> +> * GOAMD64=v1 (default): The baseline. Exclusively generates instructions that all 64-bit x86 processors can execute. +> * GOAMD64=v2: all v1 instructions, plus CMPXCHG16B, LAHF, SAHF, POPCNT, SSE3, SSE4.1, SSE4.2, SSSE3. +> * GOAMD64=v3: all v2 instructions, plus AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE, OSXSAVE. +> * GOAMD64=v4: all v3 instructions, plus AVX512F, AVX512BW, AVX512CD, AVX512DQ, AVX512VL. +> +> Setting, for example, GOAMD64=v3, will allow the Go compiler to use AVX2 instructions in the generated binaries (which may improve performance in some cases); but these binaries will not run on older x86 processors that don't support AVX2. +> +> The Go toolchain may also generate newer instructions, but guarded by dynamic checks to ensure they're only executed on capable processors. For example, with GOAMD64=v1, [math/bits.OnesCount](https://pkg.go.dev/math/bits#OnesCount) will still use the [POPCNT](https://www.felixcloutier.com/x86/popcnt) instruction if [CPUID](https://www.felixcloutier.com/x86/cpuid) reports that it's available. Otherwise, it falls back to a generic implementation. +> +> The Go toolchain does not currently generate any AVX512 instructions. +> +> Note that *processor* is a simplification in this context. In practice, support from the entire system (firmware, hypervisor, kernel) is needed. + +## Which release should I use for my system? + +Here are some common systems that people use Clash on, and the recommended release for each of them: + +- NETGEAR WNDR3700v2: mips-hardfloat [#846](https://github.com/Dreamacro/clash/issues/846) +- NETGEAR WNDR3800: mips-softfloat [#579](https://github.com/Dreamacro/clash/issues/579) +- ASUS RT-AC5300: armv5 [#2356](https://github.com/Dreamacro/clash/issues/2356) +- MediaTek MT7620A, MT7621A: mipsle-softfloat ([#136](https://github.com/Dreamacro/clash/issues/136)) +- mips_24kc: [#192](https://github.com/Dreamacro/clash/issues/192) + +If your device is not listed here, you can check the CPU architecture of your device with `uname -m` and find the corresponding release in the release page. + +## List of wontfix + +The official Clash core project will not implement/fix these things: + +- [Snell](https://github.com/Dreamacro/clash/issues/2466) +- [Custom CA](https://github.com/Dreamacro/clash/issues/2333) +- [VMess Mux](https://github.com/Dreamacro/clash/issues/450) +- [VLess](https://github.com/Dreamacro/clash/issues/1185) +- [KCP](https://github.com/Dreamacro/clash/issues/16) +- [mKCP](https://github.com/Dreamacro/clash/issues/2308) +- [TLS Encrypted Client Hello](https://github.com/Dreamacro/clash/issues/2295) +- [TCP support for Clash DNS server](https://github.com/Dreamacro/clash/issues/368) +- [MITM](https://github.com/Dreamacro/clash/issues/227#issuecomment-508693628) + +The following will be considered implementing when the official Go QUIC library releases. + +- [TUIC](https://github.com/Dreamacro/clash/issues/2222) +- [Hysteria](https://github.com/Dreamacro/clash/issues/1863) + +## Proxies work on my local machine, but not on my router or in a container + +Your system might be out of sync in time. Refer to your platform documentations about time synchronisation - things will break if time is not in sync. + +## Time complexity of rule matching + +Refer to this discussion: [#422](https://github.com/Dreamacro/clash/issues/422) + +## Clash Premium unable to access Internet + +You can refer to these relevant discussions: + +- [#432](https://github.com/Dreamacro/clash/issues/432#issuecomment-571634905) +- [#2480](https://github.com/Dreamacro/clash/issues/2480) + +## error: unsupported rule type RULE-SET + +If you stumbled on this error message: + +```txt +FATA[0000] Parse config error: Rules[0] [RULE-SET,apple,REJECT] error: unsupported rule type RULE-SET +``` + +You're using Clash open-source edition. Rule Providers is currently only available in the [Premium core](https://github.com/Dreamacro/clash/releases/tag/premium). (it's free) + +## DNS Hijack does not work + +Since `tun.auto-route` does not intercept LAN traffic, if your system DNS is set to servers in private subnets, DNS hijack will not work. You can either: + +1. Use a non-private DNS server as your system DNS like `1.1.1.1` +2. Or manually set up your system DNS to the Clash DNS (by default, `198.18.0.1`) diff --git a/docs/introduction/getting-started.md b/docs/introduction/getting-started.md new file mode 100644 index 0000000..4f2ecef --- /dev/null +++ b/docs/introduction/getting-started.md @@ -0,0 +1,50 @@ +--- +sidebarTitle: Getting Started +sidebarOrder: 2 +--- + +# Getting Started + +To get started with Clash, you can either build it from source or download pre-built binaries. + +## Using pre-built binaries + +You can download Clash core binaries here: [https://github.com/Dreamacro/clash/releases](https://github.com/Dreamacro/clash/releases) + +## Install from source + +You can build Clash on your own device with Golang 1.19+: + +```shell +$ go install github.com/Dreamacro/clash@latest +go: downloading github.com/Dreamacro/clash v1.15.1 +``` + +The binary is built under `$GOPATH/bin`: + +```shell +$ $GOPATH/bin/clash -v +Clash unknown version darwin arm64 with go1.20.3 unknown time +``` + +## Build for a different arch/os + +Golang supports cross-compilation, so you can build for a device on a different architecture or operating system. You can use _make_ to build them easily - for example: + +```shell +$ git clone --depth 1 https://github.com/Dreamacro/clash +Cloning into 'clash'... +remote: Enumerating objects: 359, done. +remote: Counting objects: 100% (359/359), done. +remote: Compressing objects: 100% (325/325), done. +remote: Total 359 (delta 25), reused 232 (delta 17), pack-reused 0 +Receiving objects: 100% (359/359), 248.99 KiB | 1.63 MiB/s, done. +Resolving deltas: 100% (25/25), done. +$ cd clash && make darwin-arm64 +fatal: No names found, cannot describe anything. +GOARCH=arm64 GOOS=darwin CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=unknown version" -X "github.com/Dreamacro/clash/constant.BuildTime=Mon May 8 16:47:10 UTC 2023" -w -s -buildid=' -o bin/clash-darwin-arm64 +$ file bin/clash-darwin-arm64 +bin/clash-darwin-arm64: Mach-O 64-bit executable arm64 +``` + +For other build targets, check out the [Makefile](https://github.com/Dreamacro/clash/blob/master/Makefile). diff --git a/docs/introduction/service.md b/docs/introduction/service.md new file mode 100644 index 0000000..7a0e9a8 --- /dev/null +++ b/docs/introduction/service.md @@ -0,0 +1,132 @@ +--- +sidebarTitle: Clash as a Service +sidebarOrder: 3 +--- + +# Clash as a Service + +While Clash is meant to be run in the background, there's currently no elegant way to implement daemons with Golang, hence we recommend you to daemonize Clash with third-party tools. + +## systemd + +Copy Clash binary to `/usr/local/bin` and configuration files to `/etc/clash`: + +```shell +cp clash /usr/local/bin +cp config.yaml /etc/clash/ +cp Country.mmdb /etc/clash/ +``` + +Create the systemd configuration file at `/etc/systemd/system/clash.service`: + +```ini +[Unit] +Description=Clash daemon, A rule-based proxy in Go. +After=network-online.target + +[Service] +Type=simple +Restart=always +ExecStart=/usr/local/bin/clash -d /etc/clash + +[Install] +WantedBy=multi-user.target +``` + +After that you're supposed to reload systemd: + +```shell +systemctl daemon-reload +``` + +Launch clashd on system startup with: + +```shell +systemctl enable clash +``` + +Launch clashd immediately with: + +```shell +systemctl start clash +``` + +Check the health and logs of Clash with: + +```shell +systemctl status clash +journalctl -xe +``` + +Credits to [ktechmidas](https://github.com/ktechmidas) for this guide. ([#754](https://github.com/Dreamacro/clash/issues/754)) + +## Docker + +We provide pre-built images of Clash and Clash Premium. Therefore you can deploy Clash with [Docker Compose](https://docs.docker.com/compose/) if you're on Linux. However, you should be advised that it's [not recommended](https://github.com/Dreamacro/clash/issues/2249#issuecomment-1203494599) to run **Clash Premium** in a container. + +::: warning +This setup will not work on macOS systems due to the lack of [host networking and TUN support](https://github.com/Dreamacro/clash/issues/770#issuecomment-650951876) in Docker for Mac. +::: + + +::: code-group + +```yaml [Clash] +services: + clash: + image: ghcr.io/dreamacro/clash + restart: always + volumes: + - ./config.yaml:/root/.config/clash/config.yaml:ro + # - ./ui:/ui:ro # dashboard volume + ports: + - "7890:7890" + - "7891:7891" + # - "8080:8080" # The External Controller (RESTful API) + network_mode: "bridge" +``` + +```yaml [Clash Premium] +services: + clash: + image: ghcr.io/dreamacro/clash-premium + restart: always + volumes: + - ./config.yaml:/root/.config/clash/config.yaml:ro + # - ./ui:/ui:ro # dashboard volume + ports: + - "7890:7890" + - "7891:7891" + # - "8080:8080" # The External Controller (RESTful API) + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun + network_mode: "host" +``` + +::: + +Save as `docker-compose.yaml` and place your `config.yaml` in the same directory. + +::: tip +Before proceeding, refer to your platform documentations about time synchronisation - things will break if time is not in sync. +::: + +When you're ready, run the following commands to bring up Clash: + +```shell +docker-compose up -d +``` + +You can view the logs with: + +```shell +docker-compose logs +``` + +Stop Clash with: + +```shell +docker-compose stop +``` diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..c624b94 Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..d8deb7d --- /dev/null +++ b/docs/package.json @@ -0,0 +1,13 @@ +{ + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + }, + "devDependencies": { + "@types/node": "^20.5.0", + "directory-tree": "^3.5.1", + "markdown-yaml-metadata-parser": "^3.0.0", + "vitepress": "1.0.0-rc.4" + } +} diff --git a/docs/premium/ebpf.md b/docs/premium/ebpf.md new file mode 100644 index 0000000..eec20f8 --- /dev/null +++ b/docs/premium/ebpf.md @@ -0,0 +1,26 @@ +--- +sidebarTitle: "Feature: eBPF Redirect to TUN" +sidebarOrder: 3 +--- + +# eBPF Redirect to TUN + +eBPF redirect to TUN is a feature that intercepts all network traffic on a specific network interface and redirects it to the TUN interface. [Support from your kernel](https://github.com/iovisor/bcc/blob/master/INSTALL.md#kernel-configuration) is required. + +::: warning +This feature conflicts with `tun.auto-route`. +::: + +While it usually brings better performance compared to `tun.auto-redir` and `tun.auto-route`, it's less tested compared to `auto-route`. Therefore, you should proceed with caution. + +## Configuration + +```yaml +ebpf: + redirect-to-tun: + - eth0 +``` + +## Known Issues + +- This feature breaks Tailscaled, so you should use `tun.auto-route` instead. diff --git a/docs/premium/experimental-features.md b/docs/premium/experimental-features.md new file mode 100644 index 0000000..89c8d1e --- /dev/null +++ b/docs/premium/experimental-features.md @@ -0,0 +1,19 @@ +--- +sidebarTitle: Experimental Features +sidebarOrder: 9 +--- + +# Experimental Features + +Occasionally we make new features that would require a fair amount of testing before having it in the main release. These features are marked as experimental and are disabled by default. + +::: warning +Some features listed here can be unstable, and might get removed in any future version - we do not recommend using them unless you have a specific reason to do so. +::: + +## Sniff TLS SNI + +```yaml +experimental: + sniff-tls-sni: true +``` diff --git a/docs/premium/introduction.md b/docs/premium/introduction.md new file mode 100644 index 0000000..2d3aecb --- /dev/null +++ b/docs/premium/introduction.md @@ -0,0 +1,26 @@ +--- +sidebarTitle: Introduction +sidebarOrder: 1 +--- + +# Introduction + +In the past, there was only one open-source version of Clash, until some [improper uses and redistributions](https://github.com/Dreamacro/clash/issues/541#issuecomment-672029110) of Clash arose. From that, we decided to fork Clash and develop the more advanced features in a private GitHub repository. + +Don't worry just yet - the Premium core will stay free of charge, and the security is enforced with peer reviews from multiple credible developers. + +## What's the difference? + +The Premium core is a fork of the open-source Clash core with the addition of the following features: + +- [TUN Device](/premium/tun-device) with the support of `auto-redir` and `auto-route` +- [eBPF Redirect to TUN](/premium/ebpf) +- [Rule Providers](/premium/rule-providers) +- [Script](/premium/script) +- [Script Shortcuts](/premium/script-shortcuts) +- [Userspace Wireguard](/premium/userspace-wireguard) +- [The Profiling Engine](/premium/the-profiling-engine) + +## Obtaining a Copy + +You can download the latest Clash Premium binaries from [GitHub Releases](https://github.com/Dreamacro/clash/releases/tag/premium). diff --git a/docs/premium/rule-providers.md b/docs/premium/rule-providers.md new file mode 100644 index 0000000..300085e --- /dev/null +++ b/docs/premium/rule-providers.md @@ -0,0 +1,100 @@ +--- +sidebarTitle: "Feature: Rule Providers" +sidebarOrder: 4 +--- + +# Rule Providers + +Rule Providers are pretty much the same compared to Proxy Providers. It enables users to load rules from external sources and overall cleaner configuration. This feature is currently Premium core only. + +To define a Rule Provider, add the `rule-providers` field to the main configuration: + +```yaml +rule-providers: + apple: + behavior: "domain" # domain, ipcidr or classical (premium core only) + type: http + url: "url" + # format: 'yaml' # or 'text' + interval: 3600 + path: ./apple.yaml + microsoft: + behavior: "domain" + type: file + path: /microsoft.yaml + +rules: + - RULE-SET,apple,REJECT + - RULE-SET,microsoft,policy +``` + +There are three behavior types available: + +## `domain` + +yaml: + +```yaml +payload: + - '.blogger.com' + - '*.*.microsoft.com' + - 'books.itunes.apple.com' +``` + +text: + +```txt +# comment +.blogger.com +*.*.microsoft.com +books.itunes.apple.com +``` + +## `ipcidr` + +yaml + +```yaml +payload: + - '192.168.1.0/24' + - '10.0.0.0.1/32' +``` + +text: + +```txt +# comment +192.168.1.0/24 +10.0.0.0.1/32 +``` + +## `classical` + +yaml: + +```yaml +payload: + - DOMAIN-SUFFIX,google.com + - DOMAIN-KEYWORD,google + - DOMAIN,ad.com + - SRC-IP-CIDR,192.168.1.201/32 + - IP-CIDR,127.0.0.0/8 + - GEOIP,CN + - DST-PORT,80 + - SRC-PORT,7777 + # MATCH is not necessary here +``` + +text: + +```txt +# comment +DOMAIN-SUFFIX,google.com +DOMAIN-KEYWORD,google +DOMAIN,ad.com +SRC-IP-CIDR,192.168.1.201/32 +IP-CIDR,127.0.0.0/8 +GEOIP,CN +DST-PORT,80 +SRC-PORT,7777 +``` diff --git a/docs/premium/script-shortcuts.md b/docs/premium/script-shortcuts.md new file mode 100644 index 0000000..e6ea0aa --- /dev/null +++ b/docs/premium/script-shortcuts.md @@ -0,0 +1,60 @@ +--- +sidebarTitle: "Feature: Script Shortcuts" +sidebarOrder: 6 +--- + +# Script Shortcuts + +Clash Premium implements the Scripting feature powered by Python3, enableing users to programmatically select policies for the packets with dynamic flexibility. + +You can either controll the entire rule-matching engine with a single Python script, or define a number of shortcuts and use them in companion with the regular rules. This page refers to the latter feature. For the former, see [Script](./script.md). + +This feature enables the use of script in `rules` mode. By default, DNS resolution takes place for SCRIPT rules. `no-resolve` can be appended to the rule to prevent the resolution. (i.e.: `SCRIPT,quic,DIRECT,no-resolve`) + +```yaml +mode: Rule + +script: + engine: expr # or starlark (10x to 20x slower) + shortcuts: + quic: network == 'udp' and dst_port == 443 + curl: resolve_process_name() == 'curl' + # curl: resolve_process_path() == '/usr/bin/curl' + +rules: + - SCRIPT,quic,REJECT +``` + +## Evaluation Engines + +[Expr](https://expr.medv.io/) is used as the default engine for Script Shortcuts, offering 10x to 20x performance boost compared to Starlark. + +[Starlark](https://github.com/google/starlark-go) is a Python-like langauge for configuration purposes, you can also use it for Script Shortcuts. + +## Variables + +- network: string +- type: string +- src_ip: string +- dst_ip: string +- src_port: uint16 +- dst_port: uint16 +- inbound_port: uint16 +- host: string +- process_path: string + +::: warning +Starlark is missing `process_path` variable for now. +::: + +## Functions + +```ts +type resolve_ip = (host: string) => string // ip string +type in_cidr = (ip: string, cidr: string) => boolean // ip in cidr +type in_ipset = (name: string, ip: string) => boolean // ip in ipset +type geoip = (ip: string) => string // country code +type match_provider = (name: string) => boolean // in rule provider +type resolve_process_name = () => string // find process name (curl .e.g) +type resolve_process_path = () => string // find process path (/usr/bin/curl .e.g) +``` diff --git a/docs/premium/script.md b/docs/premium/script.md new file mode 100644 index 0000000..79b3e6b --- /dev/null +++ b/docs/premium/script.md @@ -0,0 +1,72 @@ +--- +sidebarTitle: "Feature: Script" +sidebarOrder: 5 +--- + +# Script + +Clash Premium implements the Scripting feature powered by Python3, enableing users to programmatically select policies for the packets with dynamic flexibility. + +You can either control the entire rule-matching engine with a single Python script, or define a number of shortcuts and use them in companion with the regular rules. This page refers to the first feature, for the latter, see [Script Shortcuts](./script-shortcuts.md). + +## Scripting the entire rule-matching engine + +```yaml +mode: Script + +# https://lancellc.gitbook.io/clash/clash-config-file/script +script: + code: | + def main(ctx, metadata): + ip = ctx.resolve_ip(metadata["host"]) + if ip == "": + return "DIRECT" + metadata["dst_ip"] = ip + + code = ctx.geoip(ip) + if code == "LAN" or code == "CN": + return "DIRECT" + + return "Proxy" # default policy for requests which are not matched by any other script +``` + +If you want to use ip rules (i.e.: IP-CIDR, GEOIP, etc), you will first need to manually resolve IP addresses and assign them to metadata: + +```python +def main(ctx, metadata): + # ctx.rule_providers["geoip"].match(metadata) return false + + ip = ctx.resolve_ip(metadata["host"]) + if ip == "": + return "DIRECT" + metadata["dst_ip"] = ip + + # ctx.rule_providers["iprule"].match(metadata) return true + + return "Proxy" +``` + +Interface definition for Metadata and Context: + +```ts +interface Metadata { + type: string // socks5、http + network: string // tcp + host: string + src_ip: string + src_port: string + dst_ip: string + dst_port: string + inbound_port: number +} + +interface Context { + resolve_ip: (host: string) => string // ip string + resolve_process_name: (metadata: Metadata) => string + resolve_process_path: (metadata: Metadata) => string + geoip: (ip: string) => string // country code + log: (log: string) => void + proxy_providers: Record> + rule_providers: Record boolean }> +} +``` diff --git a/docs/premium/the-profiling-engine.md b/docs/premium/the-profiling-engine.md new file mode 100644 index 0000000..87adaae --- /dev/null +++ b/docs/premium/the-profiling-engine.md @@ -0,0 +1,13 @@ +--- +sidebarTitle: "Feature: The Profiling Engine" +sidebarOrder: 8 +--- + +# The Profiling Engine + +https://github.com/Dreamacro/clash-tracing + +```yaml +profile: + tracing: true +``` diff --git a/docs/premium/tun-device.md b/docs/premium/tun-device.md new file mode 100644 index 0000000..58c77dc --- /dev/null +++ b/docs/premium/tun-device.md @@ -0,0 +1,65 @@ +--- +sidebarTitle: "Feature: TUN Device" +sidebarOrder: 2 +--- + +# TUN Device + +The Premium core has out-of-the-box support of TUN device. Being a Network layer device, it can be used to handle TCP, UDP, ICMP traffic. It has been extensively tested and used in production environments - you can even play competitive games with it. + +One of the biggest advantage of using Clash TUN is the built-in support of the *automagic* management of the route table, routing rules and nftable. You can enable it with the options `tun.auto-route` and `tun.auto-redir`. It's a drop-in replacement of the ancient configuration option `redir-port` (TCP) for the sake of easier configuration and better stability. + +::: tip +`tun.auto-route` is only available on macOS, Windows, Linux and Android, and only receives IPv4 traffic. `tun.auto-redir` is only available on Linux(needs netlink support in the kernel). +::: + +There are two options of TCP/IP stack available: `system` or `gvisor`. In order to get the best performance available, we recommend that you always use `system` stack unless you have a specific reason or compatibility issue to use `gvisor`. If that's the case, do not hesitate to [submit an issue](https://github.com/Dreamacro/clash/issues/new/choose). + +## Technical Limitations + +* For Android, the control device is at `/dev/tun` instead of `/dev/net/tun`, you will need to create a symbolic link first (i.e. `ln -sf /dev/tun /dev/net/tun`) + +* DNS hijacking might result in a failure, if the system DNS is at a private IP address (since `auto-route` does not capture private network traffic). + +## Linux, macOS or Android + +This is an example configuration of the TUN feature: + +```yaml +interface-name: en0 # conflict with `tun.auto-detect-interface` + +tun: + enable: true + stack: system # or gvisor + # dns-hijack: + # - 8.8.8.8:53 + # - tcp://8.8.8.8:53 + # - any:53 + # - tcp://any:53 + auto-route: true # manage `ip route` and `ip rules` + auto-redir: true # manage nftable REDIRECT + auto-detect-interface: true # conflict with `interface-name` +``` + +Be advised, since the use of TUN device and manipulation of system route/nft settings, Clash will need superuser privileges to run. + +```shell +sudo ./clash +``` + +If your device already has some TUN device, Clash TUN might not work - you will have to check the route table and routing rules manually. In this case, `fake-ip-filter` may helpful as well. + +## Windows + +You will need to visit the [WinTUN website](https://www.wintun.net) and download the latest release. After that, copy `wintun.dll` into Clash home directory. Example configuration: + +```yaml +tun: + enable: true + stack: gvisor # or system + dns-hijack: + - 198.18.0.2:53 # when `fake-ip-range` is 198.18.0.1/16, should hijack 198.18.0.2:53 + auto-route: true # auto set global route for Windows + # It is recommended to use `interface-name` + auto-detect-interface: true # auto detect interface, conflict with `interface-name` +``` diff --git a/docs/premium/userspace-wireguard.md b/docs/premium/userspace-wireguard.md new file mode 100644 index 0000000..3c93e95 --- /dev/null +++ b/docs/premium/userspace-wireguard.md @@ -0,0 +1,25 @@ +--- +sidebarTitle: "Feature: Userspace Wireguard" +sidebarOrder: 7 +--- + +# Userspace Wireguard + +Due to the dependency on gvisor TCP/IP stack, Wireguard outbound is currently only available in the Premium core. + +```yaml +proxies: + - name: "wg" + type: wireguard + server: 127.0.0.1 + port: 443 + ip: 172.16.0.2 + # ipv6: your_ipv6 + private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= + public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= + # preshared-key: base64 + # remote-dns-resolve: true # remote resolve DNS with `dns` field, default is true + # dns: [1.1.1.1, 8.8.8.8] + # mtu: 1420 + udp: true +``` diff --git a/docs/public/logo.png b/docs/public/logo.png new file mode 100644 index 0000000..c624b94 Binary files /dev/null and b/docs/public/logo.png differ diff --git a/docs/runtime/external-controller.md b/docs/runtime/external-controller.md new file mode 100644 index 0000000..ebf4dc9 --- /dev/null +++ b/docs/runtime/external-controller.md @@ -0,0 +1,130 @@ +--- +sidebarTitle: The External Controller +sidebarOrder: 1 +--- + +# The External Controller + +## Introduction + +External Controller enables users to control Clash programmatically with the HTTP RESTful API. The third-party Clash GUIs are heavily based on this feature. Enable this feature by specifying an address in `external-controller`. + +## Authentication + +- External Controllers Accept `Bearer Tokens` as access authentication method. + - Use `Authorization: Bearer ` as your request header in order to pass credentials. + +## RESTful API Documentation + +### Logs + +- `/logs` + - Method: `GET` + - Full Path: `GET /logs` + - Description: Get real-time logs + +### Traffic + +- `/traffic` + - Method: `GET` + - Full Path: `GET /traffic` + - Description: Get real-time traffic data + +### Version + +- `/version` + - Method: `GET` + - Full Path: `GET /version` + - Description: Get clash version + +### Configs + +- `/configs` + - Method: `GET` + - Full Path: `GET /configs` + - Description: Get base configs + + - Method: `PUT` + - Full Path: `PUT /configs` + - Description: Reloading base configs + + - Method: `PATCH` + - Full Path: `PATCH /configs` + - Description: Update base configs + +### Proxies + +- `/proxies` + - Method: `GET` + - Full Path: `GET /proxies` + - Description: Get proxies information + +- `/proxies/:name` + - Method: `GET` + - Full Path: `GET /proxies/:name` + - Description: Get specific proxy information + + - Method: `PUT` + - Full Path: `PUT /proxies/:name` + - Description: Select specific proxy + +- `/proxies/:name/delay` + - Method: `GET` + - Full Path: `GET /proxies/:name/delay` + - Description: Get specific proxy delay test information + +### Rules + +- `/rules` + - Method: `GET` + - Full Path: `GET /rules` + - Description: Get rules information + +### Connections + +- `/connections` + - Method: `GET` + - Full Path: `GET /connections` + - Description: Get connections information + + - Method: `DELETE` + - Full Path: `DELETE /connections` + - Description: Close all connections + +- `/connections/:id` + - Method: `DELETE` + - Full Path: `DELETE /connections/:id` + - Description: Close specific connection + +### Providers + +- `/providers/proxies` + - Method: `GET` + - Full Path: `GET /providers/proxies` + - Description: Get all proxies information for all proxy-providers + +- `/providers/proxies/:name` + - Method: `GET` + - Full Path: `GET /providers/proxies/:name` + - Description: Get proxies information for specific proxy-provider + + - Method: `PUT` + - Full Path: `PUT /providers/proxies/:name` + - Description: Select specific proxy-provider + +- `/providers/proxies/:name/healthcheck` + - Method: `GET` + - Full Path: `GET /providers/proxies/:name/healthcheck` + - Description: Get proxies information for specific proxy-provider + +### DNS Query + +- `/dns/query` + - Method: `GET` + - Full Path: `GET /dns/query?name={name}[&type={type}]` + - Description: Get DNS query data for a specified name and type. + - Parameters: + - `name` (required): The domain name to query. + - `type` (optional): The DNS record type to query (e.g., A, MX, CNAME, etc.). Defaults to `A` if not provided. + + - Example: `GET /dns/query?name=example.com&type=A` diff --git a/docs/zh_CN/advanced-usages/golang-api.md b/docs/zh_CN/advanced-usages/golang-api.md new file mode 100644 index 0000000..73c810b --- /dev/null +++ b/docs/zh_CN/advanced-usages/golang-api.md @@ -0,0 +1,59 @@ +--- +sidebarTitle: 在 Golang 程序中集成 Clash +sidebarOrder: 3 +--- + +# 在 Golang 程序中集成 Clash + +如果 Clash 不能满足您的需求, 您可以在自己的 Golang 代码中使用 Clash. + +目前已经有基本的支持: + +```go +package main + +import ( + "context" + "fmt" + "io" + "net" + + "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/socks" +) + +func main() { + in := make(chan constant.ConnContext, 100) + defer close(in) + + l, err := socks.New("127.0.0.1:10000", in) + if err != nil { + panic(err) + } + defer l.Close() + + println("listen at:", l.Address()) + + direct := outbound.NewDirect() + + for c := range in { + conn := c + metadata := conn.Metadata() + fmt.Printf("请求从 %s 传入到 %s\n", metadata.SourceAddress(), metadata.RemoteAddress()) + go func () { + remote, err := direct.DialContext(context.Background(), metadata) + if err != nil { + fmt.Printf("Dial 错误: %s\n", err.Error()) + return + } + relay(remote, conn.Conn()) + }() + } +} + +func relay(l, r net.Conn) { + go io.Copy(l, r) + io.Copy(r, l) +} +``` diff --git a/docs/zh_CN/advanced-usages/openconnect.md b/docs/zh_CN/advanced-usages/openconnect.md new file mode 100644 index 0000000..d8710d2 --- /dev/null +++ b/docs/zh_CN/advanced-usages/openconnect.md @@ -0,0 +1,102 @@ +--- +sidebarTitle: 基于规则的 OpenConnect +sidebarOrder: 2 +--- + +# 基于规则的 OpenConnect + +支持以下 OpenConnect: + +- Cisco AnyConnect SSL VPN +- Juniper Network Connect +- Palo Alto Networks (PAN) GlobalProtect SSL VPN +- Pulse Connect Secure SSL VPN +- F5 BIG-IP SSL VPN +- FortiGate SSL VPN +- Array Networks SSL VPN + +例如, 您的公司使用 Cisco AnyConnect 作为内部网络访问的方式. 这里我将向您展示如何使用 Clash 提供的策略路由来使用 OpenConnect. + +首先, [安装 vpn-slice](https://github.com/dlenski/vpn-slice#requirements). 这个工具会覆写 OpenConnect 的默认路由表行为. 简单来说, 它会阻止 VPN 覆写您的默认路由. + +接下来您需要一个脚本 (比如 `tun0.sh`) 类似于这样: + +```sh +#!/bin/bash +ANYCONNECT_HOST="vpn.example.com" +ANYCONNECT_USER="john" +ANYCONNECT_PASSWORD="foobar" +ROUTING_TABLE_ID="6667" +TUN_INTERFACE="tun0" + +# 如果服务器在中国大陆, 请添加 --no-dtls. 中国大陆的 UDP 会很卡. +echo "$ANYCONNECT_PASSWORD" | \ + openconnect \ + --non-inter \ + --passwd-on-stdin \ + --protocol=anyconnect \ + --interface $TUN_INTERFACE \ + --script "vpn-slice +if [ \"\$reason\" = 'connect' ]; then + ip rule add from \$INTERNAL_IP4_ADDRESS table $ROUTING_TABLE_ID + ip route add default dev \$TUNDEV scope link table $ROUTING_TABLE_ID +elif [ \"\$reason\" = 'disconnect' ]; then + ip rule del from \$INTERNAL_IP4_ADDRESS table $ROUTING_TABLE_ID + ip route del default dev \$TUNDEV scope link table $ROUTING_TABLE_ID +fi" \ + --user $ANYCONNECT_USER \ + https://$ANYCONNECT_HOST +``` + +之后, 我们将其配置成一个 systemd 服务. 创建 `/etc/systemd/system/tun0.service`: + +```ini +[Unit] +Description=Cisco AnyConnect VPN +After=network-online.target +Conflicts=shutdown.target sleep.target + +[Service] +Type=simple +ExecStart=/path/to/tun0.sh +KillSignal=SIGINT +Restart=always +RestartSec=3 +StartLimitIntervalSec=0 + +[Install] +WantedBy=multi-user.target +``` + +然后我们启用并启动服务. + +```shell +chmod +x /path/to/tun0.sh +systemctl daemon-reload +systemctl enable tun0 +systemctl start tun0 +``` + +这里您可以查看日志来查看它是否正常运行. 简单的方法是查看 `tun0` 接口是否已经创建. + +和 Wireguard 类似, 将 TUN 设备作为出站很简单, 只需要添加一个策略组: + +```yaml +proxy-groups: + - name: Cisco AnyConnect VPN + type: select + interface-name: tun0 + proxies: + - DIRECT +``` + +... 然后就可以使用了! + +添加您想要的规则: + +```yaml +rules: + - DOMAIN-SUFFIX,internal.company.com,Cisco AnyConnect VPN +``` + +当您发现有问题时, 您应该查看 debug 级别的日志. diff --git a/docs/zh_CN/advanced-usages/wireguard.md b/docs/zh_CN/advanced-usages/wireguard.md new file mode 100644 index 0000000..11f12fb --- /dev/null +++ b/docs/zh_CN/advanced-usages/wireguard.md @@ -0,0 +1,40 @@ +--- +sidebarTitle: 基于规则的 Wireguard +sidebarOrder: 1 +--- + +# 基于规则的 Wireguard + +假设您的内核支持 Wireguard 并且您已经启用了它. `Table` 选项可以阻止 _wg-quick_ 覆写默认路由. + +例如 `wg0.conf`: + +```ini +[Interface] +PrivateKey = ... +Address = 172.16.0.1/32 +MTU = ... +Table = off +PostUp = ip rule add from 172.16.0.1/32 table 6666 + +[Peer] +AllowedIPs = 0.0.0.0/0 +AllowedIPs = ::/0 +PublicKey = ... +Endpoint = ... +``` + +然后在 Clash 中您只需要有一个 DIRECT 策略组, 它包含一个指定的出站接口: + +```yaml +proxy-groups: + - name: Wireguard + type: select + interface-name: wg0 + proxies: + - DIRECT +rules: + - DOMAIN,google.com,Wireguard +``` + +这通常比 Clash 自己实现的用户空间 Wireguard 客户端性能更好. Wireguard 在内核中支持. diff --git a/docs/zh_CN/configuration/configuration-reference.md b/docs/zh_CN/configuration/configuration-reference.md new file mode 100644 index 0000000..2cd719e --- /dev/null +++ b/docs/zh_CN/configuration/configuration-reference.md @@ -0,0 +1,476 @@ +--- +sidebarTitle: 参考配置 +sidebarOrder: 7 +--- + +# 参考配置 + +```yaml +# HTTP(S) 代理服务端口 +port: 7890 + +# SOCKS5 代理服务端口 +socks-port: 7891 + +# Linux 和 macOS 的透明代理服务端口 (TCP 和 TProxy UDP 重定向) +# redir-port: 7892 + +# Linux 的透明代理服务端口 (TProxy TCP 和 TProxy UDP) +# tproxy-port: 7893 + +# HTTP(S) 和 SOCKS4(A)/SOCKS5 代理服务共用一个端口 +# mixed-port: 7890 + +# 本地 SOCKS5/HTTP(S) 代理服务的认证 +# authentication: +# - "user1:pass1" +# - "user2:pass2" + +# 设置为 true 以允许来自其他 LAN IP 地址的连接 +# allow-lan: false + +# 仅当 `allow-lan` 为 `true` 时有效 +# '*': 绑定所有 IP 地址 +# 192.168.122.11: 绑定单个 IPv4 地址 +# "[aaaa::a8aa:ff:fe09:57d8]": 绑定单个 IPv6 地址 +# bind-address: '*' + +# Clash 路由工作模式 +# rule: 基于规则的数据包路由 +# global: 所有数据包将被转发到单个节点 +# direct: 直接将数据包转发到互联网 +mode: rule + +# 默认情况下, Clash 将日志打印到 STDOUT +# 日志级别: info / warning / error / debug / silent +# log-level: info + +# 当设置为 false 时, 解析器不会将主机名解析为 IPv6 地址 +# ipv6: false + +# RESTful Web API 监听地址 +external-controller: 127.0.0.1:9090 + +# 配置目录的相对路径或静态 Web 资源目录的绝对路径. Clash core 将在 +# `http://{{external-controller}}/ui` 中提供服务. +# external-ui: folder + +# RESTful API 密钥 (可选) +# 通过指定 HTTP 头 `Authorization: Bearer ${secret}` 进行身份验证 +# 如果RESTful API在 0.0.0.0 上监听, 务必设置一个 secret 密钥. +# secret: "" + +# 出站接口名称 +# interface-name: en0 + +# fwmark (仅在 Linux 上有效) +# routing-mark: 6666 + +# 用于DNS服务器和连接建立的静态主机 (如/etc/hosts) . +# +# 支持通配符主机名 (例如 *.clash.dev, *.foo.*.example.com) +# 非通配符域名优先级高于通配符域名 +# 例如 foo.example.com > *.example.com > .example.com +# P.S. +.foo.com 等于 .foo.com 和 foo.com +# hosts: + # '*.clash.dev': 127.0.0.1 + # '.dev': 127.0.0.1 + # 'alpha.clash.dev': '::1' + +# profile: + # 将 `select` 手动选择 结果存储在 $HOME/.config/clash/.cache 中 + # 如果不需要此行为, 请设置为 false + # 当两个不同的配置具有同名的组时, 将共享所选值 + # store-selected: true + + # 持久化 fakeip + # store-fake-ip: false + +# DNS 服务设置 +# 此部分是可选的. 当不存在时, DNS 服务将被禁用. +dns: + enable: false + listen: 0.0.0.0:53 + # ipv6: false # 当为 false 时, AAAA 查询的响应将为空 + + # 这些 名称服务器(nameservers) 用于解析下列 DNS 名称服务器主机名. + # 仅指定 IP 地址 + default-nameserver: + - 114.114.114.114 + - 8.8.8.8 + # enhanced-mode: fake-ip + fake-ip-range: 198.18.0.1/16 # Fake IP 地址池 CIDR + # use-hosts: true # 查找 hosts 并返回 IP 记录 + + # search-domains: [local] # A/AAAA 记录的搜索域 + + # 此列表中的主机名将不会使用 Fake IP 解析 + # 即, 对这些域名的请求将始终使用其真实 IP 地址进行响应 + # fake-ip-filter: + # - '*.lan' + # - localhost.ptlogin2.qq.com + + # 支持 UDP、TCP、DoT、DoH. 您可以指定要连接的端口. + # 所有 DNS 查询都直接发送到名称服务器, 无需代理 + # Clash 使用第一个收到的响应作为 DNS 查询的结果. + nameserver: + - 114.114.114.114 # 默认值 + - 8.8.8.8 # 默认值 + - tls://dns.rubyfish.cn:853 # DNS over TLS + - https://1.1.1.1/dns-query # DNS over HTTPS + - dhcp://en0 # 来自 dhcp 的 dns + # - '8.8.8.8#en0' + + # 当 `fallback` 存在时, DNS 服务器将向此部分中的服务器 + # 与 `nameservers` 中的服务器发送并发请求 + # 当 GEOIP 国家不是 `CN` 时, 将使用 fallback 服务器的响应 + # fallback: + # - tcp://1.1.1.1 + # - 'tcp://1.1.1.1#en0' + + # 如果使用 `nameservers` 解析的 IP 地址在下面指定的子网中, + # 则认为它们无效, 并使用 `fallback` 服务器的结果. + # + # 当 `fallback-filter.geoip` 为 true 且 IP 地址的 GEOIP 为 `CN` 时, + # 将使用 `nameservers` 服务器解析的 IP 地址. + # + # 如果 `fallback-filter.geoip` 为 false, 且不匹配 `fallback-filter.ipcidr`, + # 则始终使用 `nameservers` 服务器的结果 + # + # 这是对抗 DNS 污染攻击的一种措施. + # fallback-filter: + # geoip: true + # geoip-code: CN + # ipcidr: + # - 240.0.0.0/4 + # domain: + # - '+.google.com' + # - '+.facebook.com' + # - '+.youtube.com' + + # 通过特定的名称服务器查找域名 + # nameserver-policy: + # 'www.baidu.com': '114.114.114.114' + # '+.internal.crop.com': '10.0.0.1' + +proxies: + # Shadowsocks + # 支持的加密方法: + # aes-128-gcm aes-192-gcm aes-256-gcm + # aes-128-cfb aes-192-cfb aes-256-cfb + # aes-128-ctr aes-192-ctr aes-256-ctr + # rc4-md5 chacha20-ietf xchacha20 + # chacha20-ietf-poly1305 xchacha20-ietf-poly1305 + - name: "ss1" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + # udp: true + + - name: "ss2" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: obfs + plugin-opts: + mode: tls # or http + # host: bing.com + + - name: "ss3" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: v2ray-plugin + plugin-opts: + mode: websocket # 暂不支持 QUIC + # tls: true # wss + # skip-cert-verify: true + # host: bing.com + # path: "/" + # mux: true + # headers: + # custom: value + + # vmess + # 支持的加密方法: + # auto/aes-128-gcm/chacha20-poly1305/none + - name: "vmess" + type: vmess + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + # udp: true + # tls: true + # skip-cert-verify: true + # servername: example.com # 优先于 wss 主机 + # network: ws + # ws-opts: + # path: /path + # headers: + # Host: v2ray.com + # max-early-data: 2048 + # early-data-header-name: Sec-WebSocket-Protocol + + - name: "vmess-h2" + type: vmess + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + network: h2 + tls: true + h2-opts: + host: + - http.example.com + - http-alt.example.com + path: / + + - name: "vmess-http" + type: vmess + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + # udp: true + # network: http + # http-opts: + # # method: "GET" + # # path: + # # - '/' + # # - '/video' + # # headers: + # # Connection: + # # - keep-alive + + - name: vmess-grpc + server: server + port: 443 + type: vmess + uuid: uuid + alterId: 32 + cipher: auto + network: grpc + tls: true + servername: example.com + # skip-cert-verify: true + grpc-opts: + grpc-service-name: "example" + + # socks5 + - name: "socks" + type: socks5 + server: server + port: 443 + # username: username + # password: password + # tls: true + # skip-cert-verify: true + # udp: true + + # http + - name: "http" + type: http + server: server + port: 443 + # username: username + # password: password + # tls: true # https + # skip-cert-verify: true + # sni: custom.com + + # Snell + # 请注意, 目前还没有UDP支持. + - name: "snell" + type: snell + server: server + port: 44046 + psk: yourpsk + # version: 2 + # obfs-opts: + # mode: http # or tls + # host: bing.com + + # Trojan + - name: "trojan" + type: trojan + server: server + port: 443 + password: yourpsk + # udp: true + # sni: example.com # aka 服务器名称 + # alpn: + # - h2 + # - http/1.1 + # skip-cert-verify: true + + - name: trojan-grpc + server: server + port: 443 + type: trojan + password: "example" + network: grpc + sni: example.com + # skip-cert-verify: true + udp: true + grpc-opts: + grpc-service-name: "example" + + - name: trojan-ws + server: server + port: 443 + type: trojan + password: "example" + network: ws + sni: example.com + # skip-cert-verify: true + udp: true + # ws-opts: + # path: /path + # headers: + # Host: example.com + + # ShadowsocksR + # 支持的加密方法: ss 中的所有流加密方法 + # 支持的混淆方式: + # plain http_simple http_post + # random_head tls1.2_ticket_auth tls1.2_ticket_fastauth + # 支持的协议: + # origin auth_sha1_v4 auth_aes128_md5 + # auth_aes128_sha1 auth_chain_a auth_chain_b + - name: "ssr" + type: ssr + server: server + port: 443 + cipher: chacha20-ietf + password: "password" + obfs: tls1.2_ticket_auth + protocol: auth_sha1_v4 + # obfs-param: domain.tld + # protocol-param: "#" + # udp: true + +proxy-groups: + # 中继链路代理节点. 节点不应包含中继. 不支持 UDP. + # 流量节点链路: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet + - name: "relay" + type: relay + proxies: + - http + - vmess + - ss1 + - ss2 + + # url-test 通过对 指定URL 进行基准速度测试来选择将使用哪个代理. + - name: "auto" + type: url-test + proxies: + - ss1 + - ss2 + - vmess1 + # tolerance: 150 + # lazy: true + url: 'http://www.gstatic.com/generate_204' + interval: 300 + + # fallback-auto 基于优先级选择可用策略. 可用性通过访问 指定URL 来测试, 就像自动 url-test 组一样. + - name: "fallback-auto" + type: fallback + proxies: + - ss1 + - ss2 + - vmess1 + url: 'http://www.gstatic.com/generate_204' + interval: 300 + + # 负载均衡: 同一 eTLD+1 的请求将拨号到同一代理. + - name: "load-balance" + type: load-balance + proxies: + - ss1 + - ss2 + - vmess1 + url: 'http://www.gstatic.com/generate_204' + interval: 300 + # strategy: consistent-hashing # or round-robin + + # select 手动选择, 用于选择代理或策略组 + # 您可以使用 RESTful API 来切换代理, 建议在GUI中切换. + - name: Proxy + type: select + # disable-udp: true + # filter: 'someregex' + proxies: + - ss1 + - ss2 + - vmess1 + - auto + + # 直接连接到另一个接口名称或 fwmark, 也支持代理 + - name: en1 + type: select + interface-name: en1 + routing-mark: 6667 + proxies: + - DIRECT + + - name: UseProvider + type: select + use: + - provider1 + proxies: + - Proxy + - DIRECT + +proxy-providers: + provider1: + type: http + url: "url" + interval: 3600 + path: ./provider1.yaml + health-check: + enable: true + interval: 600 + # lazy: true + url: http://www.gstatic.com/generate_204 + test: + type: file + path: /test.yaml + health-check: + enable: true + interval: 36000 + url: http://www.gstatic.com/generate_204 + +tunnels: + # 单行配置 + - tcp/udp,127.0.0.1:6553,114.114.114.114:53,proxy + - tcp,127.0.0.1:6666,rds.mysql.com:3306,vpn + # 全 yaml 配置 + - network: [tcp, udp] + address: 127.0.0.1:7777 + target: target.com + proxy: proxy + +rules: + - DOMAIN-SUFFIX,google.com,auto + - DOMAIN-KEYWORD,google,auto + - DOMAIN,google.com,auto + - DOMAIN-SUFFIX,ad.com,REJECT + - SRC-IP-CIDR,192.168.1.201/32,DIRECT + # 用于 IP 规则 (GEOIP, IP-CIDR, IP-CIDR6) 的可选参数 "no-resolve" + - IP-CIDR,127.0.0.0/8,DIRECT + - GEOIP,CN,DIRECT + - DST-PORT,80,DIRECT + - SRC-PORT,7777,DIRECT + - RULE-SET,apple,REJECT # 仅 Premium 版本支持 + - MATCH,auto +``` diff --git a/docs/zh_CN/configuration/dns.md b/docs/zh_CN/configuration/dns.md new file mode 100644 index 0000000..267da6c --- /dev/null +++ b/docs/zh_CN/configuration/dns.md @@ -0,0 +1,72 @@ +--- +sidebarTitle: Clash DNS +sidebarOrder: 6 +--- + +# Clash DNS + +由于 Clash 的某些部分运行在第 3 层 (网络层) , 因此其数据包的域名是无法获取的, 也就无法进行基于规则的路由. + +*Enter fake-ip*: 它支持基于规则的路由, 最大程度地减少了 DNS 污染攻击的影响, 并且提高了网络性能, 有时甚至是显著的. + +## fake-ip + +"fake IP" 的概念源自 [RFC 3089](https://tools.ietf.org/rfc/rfc3089): + +> 一个 "fake IP" 地址被用于查询相应的 "FQDN" 信息的关键字. + +fake-ip 池的默认 CIDR 是 `198.18.0.1/16` (一个保留的 IPv4 地址空间, 可以在 `dns.fake-ip-range` 中进行更改). + +当 DNS 请求被发送到 Clash DNS 时, Clash 内核会通过管理内部的域名和其 fake-ip 地址的映射, 从池中分配一个 *空闲* 的 fake-ip 地址. + +以使用浏览器访问 `http://google.com` 为例. + +1. 浏览器向 Clash DNS 请求 `google.com` 的 IP 地址 +2. Clash 检查内部映射并返回 `198.18.1.5` +3. 浏览器向 `198.18.1.5` 的 `80/tcp` 端口发送 HTTP 请求 +4. 当收到 `198.18.1.5` 的入站数据包时, Clash 查询内部映射, 发现客户端实际上是在向 `google.com` 发送数据包 +5. 根据规则的不同: + + 1. Clash 可能仅将域名发送到 SOCKS5 或 shadowsocks 等出站代理, 并与代理服务器建立连接 + + 2. 或者 Clash 可能会基于 `SCRIPT`、`GEOIP`、`IP-CIDR` 规则或者使用 DIRECT 直连出口查询 `google.com` 的真实 IP 地址 + +由于这是一个令人困惑的概念, 我将以使用 cURL 程序访问 `http://google.com` 为例: + +```txt{2,3,5,6,8,9} +$ curl -v http://google.com +<---- cURL 向您的系统 DNS (Clash) 询问 google.com 的 IP 地址 +----> Clash 决定使用 198.18.1.70 作为 google.com 的 IP 地址, 并记住它 +* Trying 198.18.1.70:80... +<---- cURL 连接到 198.18.1.70 tcp/80 +----> Clash 将立即接受连接, 并且.. +* Connected to google.com (198.18.1.70) port 80 (#0) +----> Clash 在其内存中查找到 198.18.1.70 对应于 google.com +----> Clash 查询对应的规则, 并通过匹配的出口发送数据包 +> GET / HTTP/1.1 +> Host: google.com +> User-Agent: curl/8.0.1 +> Accept: */* +> +< HTTP/1.1 301 Moved Permanently +< Location: http://www.google.com/ +< Content-Type: text/html; charset=UTF-8 +< Content-Security-Policy-Report-Only: object-src 'none';base-uri 'self';script-src 'nonce-ahELFt78xOoxhySY2lQ34A' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp +< Date: Thu, 11 May 2023 06:52:19 GMT +< Expires: Sat, 10 Jun 2023 06:52:19 GMT +< Cache-Control: public, max-age=2592000 +< Server: gws +< Content-Length: 219 +< X-XSS-Protection: 0 +< X-Frame-Options: SAMEORIGIN +< + +301 Moved +

301 Moved

+The document has moved +here. + +* Connection #0 to host google.com left intact +``` + + diff --git a/docs/zh_CN/configuration/getting-started.md b/docs/zh_CN/configuration/getting-started.md new file mode 100644 index 0000000..919692d --- /dev/null +++ b/docs/zh_CN/configuration/getting-started.md @@ -0,0 +1,76 @@ +--- +sidebarTitle: 快速入手 +sidebarOrder: 2 +--- + +# 快速入手 + +建议您在继续阅读本节之前, 先阅读[介绍](/zh_CN/configuration/introduction). 在您对Clash的工作原理有了简单的了解后, 您可以开始编写您自己的配置. + +## 配置文件 + +主配置文件名为 `config.yaml`. 默认情况下, Clash会在 `$HOME/.config/clash` 目录读取配置文件. 如果该目录不存在, Clash会在该位置生成一个最小的配置文件. + +如果您想将配置文件放在其他地方 (例如 `/etc/clash`) , 您可以使用命令行选项 `-d` 来指定配置目录: + +```shell +clash -d . # current directory +clash -d /etc/clash +``` + +或者, 您可以使用选项 `-f` 来指定配置文件: + +```shell +clash -f ./config.yaml +clash -f /etc/clash/config.yaml +``` + +## 特殊语法 + +Clash 配置文件中有一些特殊的语法, 您可能需要了解: + +### IPv6 地址 + +您应该使用方括号 (`[]`) 来包裹 IPv6 地址, 例如: + +```txt +[aaaa::a8aa:ff:fe09:57d8] +``` + +### DNS 通配符域名匹配 + +在某些情况下, 您需要匹配通配符域名. 例如, 当您设置 [Clash DNS](/zh_CN/configuration/dns) 时, 您可能想要匹配 `localdomain` 的所有子域名. + +Clash 在 DNS 配置中提供了匹配不同级别通配符域名的支持, 其语法如下: + +::: tip +任何包含这些字符的域名都应该用单引号 (`'`) 包裹. 例如, `'*.google.com'`. +静态域名的优先级高于通配符域名 (foo.example.com > *.example.com > .example.com) . +::: + +使用星号 (`*`) 来匹配单级通配符子域名. + +| 表达式 | 匹配 | 不匹配 | +| ---------- | ------- | -------------- | +| `*.google.com` | `www.google.com` | `google.com` | +| `*.bar.google.com` | `foo.bar.google.com` | `bar.google.com` | +| `*.*.google.com` | `thoughtful.sandbox.google.com` | `one.two.three.google.com` | + +使用点号 (`.`) 来匹配多级通配符子域名. + +| 表达式 | 匹配 | 不匹配 | +| ---------- | ------- | -------------- | +| `.google.com` | `www.google.com` | `google.com` | +| `.google.com` | `thoughtful.sandbox.google.com` | `google.com` | +| `.google.com` | `one.two.three.google.com` | `google.com` | + +使用加号 (`+`) 来匹配多级通配符子域名. + +`+` 通配符的工作方式类似于 `DOMAIN-SUFFIX`, 您可以一次进行多级的快速匹配. + +| 表达式 | 匹配 | +| ---------- | ------- | +| `+.google.com` | `google.com` | +| `+.google.com` | `www.google.com` | +| `+.google.com` | `thoughtful.sandbox.google.com` | +| `+.google.com` | `one.two.three.google.com` | diff --git a/docs/zh_CN/configuration/inbound.md b/docs/zh_CN/configuration/inbound.md new file mode 100644 index 0000000..393af28 --- /dev/null +++ b/docs/zh_CN/configuration/inbound.md @@ -0,0 +1,69 @@ +--- +sidebarTitle: Inbound 入站 +sidebarOrder: 3 +--- + +# Inbound 入站 + +Clash 支持多种入站协议, 包括: + +- SOCKS5 +- HTTP(S) +- Redirect TCP +- TProxy TCP +- TProxy UDP +- Linux TUN 设备 (仅 Premium 版本) + +任何入站协议的连接都将由同一个内部规则匹配引擎处理. 也就是说, Clash **目前**不支持为不同的入站协议设置不同的规则集. + +## 配置 + +```yaml +# HTTP(S) 代理服务端口 +# port: 7890 + +# SOCKS5 代理服务端口 +socks-port: 7891 + +# HTTP(S) 和 SOCKS4(A)/SOCKS5 代理服务共用一个端口 +mixed-port: 7890 + +# Linux 和 macOS 的透明代理服务端口 (TCP 和 TProxy UDP 重定向) +# redir-port: 7892 + +# Linux 的透明代理服务端口 (TProxy TCP 和 TProxy UDP) +# tproxy-port: 7893 + +# 设置为 true 以允许来自其他 LAN IP 地址的连接 +# allow-lan: false +``` + +## Mixed 混合端口 + +混合端口是一个特殊的端口, 它同时支持 HTTP(S) 和 SOCKS5 协议. 您可以使用任何支持 HTTP 或 SOCKS 代理的程序连接到这个端口, 例如: + +```shell +$ curl -x socks5h://127.0.0.1:7890 -v http://connect.rom.miui.com/generate_204 +* Trying 127.0.0.1:7890... +* SOCKS5 connect to connect.rom.miui.com:80 (remotely resolved) +* SOCKS5 request granted. +* Connected to (nil) (127.0.0.1) port 7890 (#0) +> GET /generate_204 HTTP/1.1 +> Host: connect.rom.miui.com +> User-Agent: curl/7.81.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 204 No Content +< Date: Thu, 11 May 2023 06:18:22 GMT +< Connection: keep-alive +< Content-Type: text/plain +< +* Connection #0 to host (nil) left intact +``` + +## Redirect 和 TProxy + +Redirect 和 TProxy 是两种实现透明代理的不同方式, 均被 Clash 所支持. + +然而, 您不一定需要手动设置这两个功能 - 我们建议您使用 [Clash Premium 版本](/zh_CN/premium/introduction) 来配置透明代理, 因为它内置了对操作系统路由表、规则和 nftables 的自动管理. diff --git a/docs/zh_CN/configuration/introduction.md b/docs/zh_CN/configuration/introduction.md new file mode 100644 index 0000000..56c59d2 --- /dev/null +++ b/docs/zh_CN/configuration/introduction.md @@ -0,0 +1,39 @@ +--- +sidebarTitle: 介绍 +sidebarOrder: 1 +--- + +# 介绍 + +在本章中, 我们将介绍 Clash 的常见功能以及如何使用和配置它们. + +Clash 使用 [YAML](https://yaml.org) (YAML Ain't Markup Language) 作为配置文件格式. YAML 旨在易于阅读、编写和解析, 通常用于配置文件. + +## 了解 Clash 的工作原理 + +在继续之前, 有必要了解 Clash 的工作原理, 其中有两个关键部分: + +![](/assets/connection-flow.png) + + + +### Inbound 入站 + +Inbound 入站是在本地端监听的部分, 它通过打开一个本地端口并监听传入的连接来工作. 当连接进来时, Clash 会查询配置文件中配置的规则, 并决定连接应该去哪个 Outbound 出站. + +### Outbound 出站 + +Outbound 出站是连接到远程端的部分. 根据配置的不同, 它可以是一个特定的网络接口、一个代理服务器或一个[策略组](/zh_CN/configuration/outbound#proxy-groups-策略组). + +## 基于规则的路由 + +Clash 支持基于规则的路由, 这意味着您可以根据各种规则将数据包路由到不同的出站. 规则可以在配置文件的 `rules` 部分中定义. + +有许多可用的规则类型, 每种规则类型都有自己的语法. 规则的一般语法是: + +```txt +# 类型,参数,策略(,no-resolve) +TYPE,ARGUMENT,POLICY(,no-resolve) +``` + +在下一步指南中, 您将了解有关如何配置规则的更多信息. diff --git a/docs/zh_CN/configuration/outbound.md b/docs/zh_CN/configuration/outbound.md new file mode 100644 index 0000000..6a928c5 --- /dev/null +++ b/docs/zh_CN/configuration/outbound.md @@ -0,0 +1,434 @@ +--- +sidebarTitle: Outbound 出站 +sidebarOrder: 4 +--- + +# Outbound 出站 + +Clash 中有几种类型的出站. 每种类型都有自己的特点和使用场景. 在本页中, 我们将介绍每种类型的通用特点以及如何使用和配置它们. + +[[toc]] + +## Proxies 代理节点 + +Proxies 代理节点是您可以配置的一些出站目标. 就像代理服务器一样, 您在这里为数据包定义目的地. + +### Shadowsocks + +Clash 支持以下 Shadowsocks 的加密方法: + +| 系列 | 加密方法 | +| ------ | ------- | +| AEAD | aes-128-gcm, aes-192-gcm, aes-256-gcm, chacha20-ietf-poly1305, xchacha20-ietf-poly1305 | +| 流式 | aes-128-cfb, aes-192-cfb, aes-256-cfb, rc4-md5, chacha20-ietf, xchacha20 | +| 块式 | aes-128-ctr, aes-192-ctr, aes-256-ctr | + +此外, Clash 还支持流行的 Shadowsocks 插件 `obfs` 和 `v2ray-plugin`. + +::: code-group + +```yaml [basic] +- name: "ss1" + type: ss + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + # udp: true +``` + +```yaml [obfs] +- name: "ss2" + type: ss + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: obfs + plugin-opts: + mode: tls # or http + # host: bing.com +``` + +```yaml [ws (websocket)] +- name: "ss3" + type: ss + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: v2ray-plugin + plugin-opts: + mode: websocket # 暂不支持 QUIC + # tls: true # wss + # skip-cert-verify: true + # host: bing.com + # path: "/" + # mux: true + # headers: + # custom: value +``` + +::: + +### ShadowsocksR + +Clash 也支持声名狼藉的反审查协议 ShadowsocksR. + +支持以下 ShadowsocksR 的加密方法: + +| 系列 | 加密方法 | +| ------ | ------- | +| 流式 | aes-128-cfb, aes-192-cfb, aes-256-cfb, rc4-md5, chacha20-ietf, xchacha20 | + +支持的混淆方法: + +- plain +- http_simple +- http_post +- random_head +- tls1.2_ticket_auth +- tls1.2_ticket_fastauth + +支持的协议: + +- origin +- auth_sha1_v4 +- auth_aes128_md5 +- auth_aes128_sha1 +- auth_chain_a +- auth_chain_b + +```yaml +- name: "ssr" + type: ssr + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + cipher: chacha20-ietf + password: "password" + obfs: tls1.2_ticket_auth + protocol: auth_sha1_v4 + # obfs-param: domain.tld + # protocol-param: "#" + # udp: true +``` + +### Vmess + +Clash 支持以下 Vmess 的加密方法: + +- auto +- aes-128-gcm +- chacha20-poly1305 +- none + +::: code-group + +```yaml [basic] +- name: "vmess" + type: vmess + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + # udp: true + # tls: true + # skip-cert-verify: true + # servername: example.com # 优先于 wss 主机 + # network: ws + # ws-opts: + # path: /path + # headers: + # Host: v2ray.com + # max-early-data: 2048 + # early-data-header-name: Sec-WebSocket-Protocol +``` + +```yaml [HTTP] +- name: "vmess-http" + type: vmess + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + # udp: true + # network: http + # http-opts: + # # method: "GET" + # # path: + # # - '/' + # # - '/video' + # # headers: + # # Connection: + # # - keep-alive +``` + +```yaml [HTTP/2] +- name: "vmess-h2" + type: vmess + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + network: h2 + tls: true + h2-opts: + host: + - http.example.com + - http-alt.example.com + path: / +``` + +```yaml [gRPC] +- name: vmess-grpc + type: vmess + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + uuid: uuid + alterId: 32 + cipher: auto + network: grpc + tls: true + servername: example.com + # skip-cert-verify: true + grpc-opts: + grpc-service-name: "example" +``` + +::: + +### Socks5 + +此外, Clash 还支持 Socks5 代理. + +```yaml +- name: "socks" + type: socks5 + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + # username: username + # password: password + # tls: true + # skip-cert-verify: true + # udp: true +``` + +### HTTP + +Clash 也支持 HTTP 代理: + +::: code-group + +```yaml [HTTP] +- name: "http" + type: http + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + # username: username + # password: password +``` + +```yaml [HTTPS] +- name: "http" + type: http + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + # username: username + # password: password + tls: true + skip-cert-verify: true +``` + +::: + +### Snell + +作为可选的反审查协议, Clash也集成了对Snell的支持. + +```yaml +# 暂不支持 UDP +- name: "snell" + type: snell + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 44046 + psk: yourpsk + # version: 2 + # obfs-opts: + # mode: http # or tls + # host: bing.com +``` + +### Trojan + +Clash 内置了对流行协议 Trojan 的支持: + +::: code-group + +```yaml [basic] +- name: "trojan" + type: trojan + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + password: yourpsk + # udp: true + # sni: example.com # aka server name + # alpn: + # - h2 + # - http/1.1 + # skip-cert-verify: true +``` + +```yaml [gRPC] +- name: trojan-grpc + type: trojan + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + password: "example" + network: grpc + sni: example.com + # skip-cert-verify: true + udp: true + grpc-opts: + grpc-service-name: "example" +``` + +```yaml [ws (websocket)] +- name: trojan-ws + type: trojan + # interface-name: eth0 + # routing-mark: 1234 + server: server + port: 443 + password: "example" + network: ws + sni: example.com + # skip-cert-verify: true + udp: true + # ws-opts: + # path: /path + # headers: + # Host: example.com +``` + +::: + +## Proxy Groups 策略组 + +Proxy Groups 策略组用于根据不同策略分发规则传递过来的请求, 其可以直接被规则引用, 也可以被其他策略组引用, 而最上级策略组被规则引用. + +### relay 中继 + +请求将依次通过指定的代理服务器进行中继, 目前不支持 UDP. 指定的代理服务器不应包含另一个 relay 中继. + +### url-test 延迟测试 + +Clash 会周期性地通过指定的 URL 向列表中的代理服务器发送 HTTP HEAD 请求来测试每个代理服务器的**延迟**. 可以设置最大容忍值、测试间隔和目标 URL. + +### fallback 可用性测试 + +Clash 会周期性地通过指定的 URL 向列表中的代理服务器发送 HTTP HEAD 请求来测试每个代理服务器的**可用性**. 第一个可用的服务器将被使用. + +### load-balance 负载均衡 + +相同 eTLD+1 的请求将使用同一个代理服务器. + +### select 手动选择 + +Clash 启动时默认使用策略组中的第一个代理服务器. 用户可以使用 RESTful API 选择要使用的代理服务器. 在此模式下, 您可以在配置中硬编码服务器或使用 [Proxy Providers 代理集](#proxy-providers-代理集) 动态添加服务器. + +无论哪种方式, 有时您也可以使用直接连接来路由数据包. 在这种情况下, 您可以使用 `DIRECT` 直连出站. + +要使用不同的网络接口, 您需要使用包含 `DIRECT` 直连出站的策略组, 并设置 `interface-name` 选项. + +```yaml +- name: "My Wireguard Outbound" + type: select + interface-name: wg0 + proxies: [ 'DIRECT' ] +``` + +## Proxy Providers 代理集 + +代理集使用户可以动态加载代理服务器列表, 而不是在配置文件中硬编码. 目前有两种代理集可以加载服务器列表: + +- `http`: Clash 会在启动时从指定的 URL 加载服务器列表. 如果设置了 `interval` 选项, Clash 会定期从远程拉取服务器列表. +- `file`: Clash 会在启动时从指定的文件位置加载服务器列表. + +健康检查对两种模式都可用, 并且与策略组中的 `fallback` 完全相同. 服务器列表文件的配置格式在主配置文件中也完全相同: + +::: code-group + +```yaml [config.yaml] +proxy-providers: + provider1: + type: http + url: "url" + interval: 3600 + path: ./provider1.yaml + # filter: 'a|b' # golang regex 正则表达式 + health-check: + enable: true + interval: 600 + # lazy: true + url: http://www.gstatic.com/generate_204 + test: + type: file + path: /test.yaml + health-check: + enable: true + interval: 36000 + url: http://www.gstatic.com/generate_204 +``` + +```yaml [test.yaml] +proxies: + - name: "ss1" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + + - name: "ss2" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + plugin: obfs + plugin-opts: + mode: tls +``` + +::: diff --git a/docs/zh_CN/configuration/rules.md b/docs/zh_CN/configuration/rules.md new file mode 100644 index 0000000..8a17a99 --- /dev/null +++ b/docs/zh_CN/configuration/rules.md @@ -0,0 +1,168 @@ +--- +sidebarTitle: Rules 规则 +sidebarOrder: 5 +--- + +# Rules 规则 + +在[快速入手](/zh_CN/configuration/getting-started)中, 我们介绍了Clash中基于规则的匹配的基本知识. 在本章中, 我们将介绍最新版本的 Clash 中所有可用的规则类型. + +```txt +# 类型,参数,策略(,no-resolve) +TYPE,ARGUMENT,POLICY(,no-resolve) +``` + +`no-resolve` 选项是可选的, 它用于跳过规则的 DNS 解析. 当您想要使用 `GEOIP`、`IP-CIDR`、`IP-CIDR6`、`SCRIPT` 规则, 但又不想立即将域名解析为 IP 地址时, 这个选项就很有用了. + +[[toc]] + +## 策略 + +目前有四种策略类型, 其中: + +- DIRECT: 通过 `interface-name` 直接连接到目标 (不查找系统路由表) +- REJECT: 丢弃数据包 +- Proxy: 将数据包路由到指定的代理服务器 +- Proxy Group: 将数据包路由到指定的策略组 + +## 规则类型 + +以下部分介绍了每种规则类型及其使用方法: + +### DOMAIN 域名 + +`DOMAIN,www.google.com,policy` 将 `www.google.com` 路由到 `policy`. + +### DOMAIN-SUFFIX 域名后缀 + +`DOMAIN-SUFFIX,youtube.com,policy` 将任何以 `youtube.com` 结尾的域名路由到 `policy`. + +在这种情况下, `www.youtube.com` 和 `foo.bar.youtube.com` 都将路由到 `policy`. + +### DOMAIN-KEYWORD 域名关键字 + +`DOMAIN-KEYWORD,google,policy` 将任何包含 `google` 关键字的域名路由到 `policy`. + +在这种情况下, `www.google.com` 或 `googleapis.com` 都将路由到 `policy`. + +### GEOIP IP地理位置 (国家代码) + +GEOIP 规则用于根据数据包的目标 IP 地址的**国家代码**路由数据包. Clash 使用 [MaxMind GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) 数据库来实现这一功能. + +::: warning +使用这种规则时, Clash 将域名解析为 IP 地址, 然后查找 IP 地址的国家代码. +如果要跳过 DNS 解析, 请使用 `no-resolve` 选项. +::: + +`GEOIP,CN,policy` 将任何目标 IP 地址为中国的数据包路由到 `policy`. + +### IP-CIDR IPv4地址段 + +IP-CIDR 规则用于根据数据包的**目标 IPv4 地址**路由数据包. + +::: warning +使用这种规则时, Clash 将域名解析为 IPv4 地址. +如果要跳过 DNS 解析, 请使用 `no-resolve` 选项. +::: + +`IP-CIDR,127.0.0.0/8,DIRECT` 将任何目标 IP 地址为 `127.0.0.0/8` 的数据包路由到 `DIRECT`. + +### IP-CIDR6 IPv6地址段 + +IP-CIDR6 规则用于根据数据包的**目标 IPv6 地址**路由数据包. + +::: warning +使用这种规则时, Clash 将域名解析为 IPv6 地址. +如果要跳过 DNS 解析, 请使用 `no-resolve` 选项. +::: + +`IP-CIDR6,2620:0:2d0:200::7/32,policy` 将任何目标 IP 地址为 `2620:0:2d0:200::7/32` 的数据包路由到 `policy`. + +### SRC-IP-CIDR 源IP段地址 + +SRC-IP-CIDR 规则用于根据数据包的**源 IPv4 地址**路由数据包. + +`SRC-IP-CIDR,192.168.1.201/32,DIRECT` 将任何源 IP 地址为 `192.168.1.201/32` 的数据包路由到 `DIRECT`. + +### SRC-PORT 源端口 + +SRC-PORT 规则用于根据数据包的**源端口**路由数据包. + +`SRC-PORT,80,policy` 将任何源端口为 `80` 的数据包路由到 `policy`. + +### DST-PORT 目标端口 + +DST-PORT 规则用于根据数据包的**目标端口**路由数据包. + +`DST-PORT,80,policy` 将任何目标端口为 `80` 的数据包路由到 `policy`. + +### PROCESS-NAME 源进程名 + +PROCESS-NAME 规则用于根据发送数据包的进程名称路由数据包. + +::: warning +目前, 仅支持 macOS、Linux、FreeBSD 和 Windows. +::: + +`PROCESS-NAME,nc,DIRECT` 将任何来自进程 `nc` 的数据包路由到 `DIRECT`. + +### PROCESS-PATH 源进程路径 + +PROCESS-PATH 规则用于根据发送数据包的进程路径路由数据包. + +::: warning +目前, 仅支持 macOS、Linux、FreeBSD 和 Windows. +::: + +`PROCESS-PATH,/usr/local/bin/nc,DIRECT` 将任何来自路径为 `/usr/local/bin/nc` 的进程的数据包路由到 `DIRECT`. + +### IPSET IP集 + +IPSET 规则用于根据 IP 集匹配并路由数据包. 根据 [IPSET 的官方网站](https://ipset.netfilter.org/) 的介绍: + +> IP 集是 Linux 内核中的一个框架, 可以通过 ipset 程序进行管理. 根据类型, IP 集可以存储 IP 地址、网络、 (TCP/UDP) 端口号、MAC 地址、接口名称或它们以某种方式的组合, 以确保在集合中匹配条目时具有闪电般的速度. + +因此, 此功能仅在 Linux 上工作, 并且需要安装 `ipset`. + +::: warning +使用此规则时, Clash 将解析域名以获取 IP 地址, 然后查找 IP 地址是否在 IP 集中. +如果要跳过 DNS 解析, 请使用 `no-resolve` 选项. +::: + +`IPSET,chnroute,policy` 将任何目标 IP 地址在 IP 集 `chnroute` 中的数据包路由到 `policy`. + +### RULE-SET 规则集 + +::: info +此功能仅在 [Premium 版本](/zh_CN/premium/introduction) 中可用. +::: + +RULE-SET 规则用于根据 [Rule Providers 规则集](/zh_CN/premium/rule-providers) 的结果路由数据包. 当 Clash 使用此规则时, 它会从指定的 Rule Providers 规则集中加载规则, 然后将数据包与规则进行匹配. 如果数据包与任何规则匹配, 则将数据包路由到指定的策略, 否则跳过此规则. + +::: warning +使用 RULE-SET 时, 当规则集的类型为 IPCIDR , Clash 将解析域名以获取 IP 地址. +如果要跳过 DNS 解析, 请使用 `no-resolve` 选项. +::: + +`RULE-SET,my-rule-provider,DIRECT` 从 `my-rule-provider` 加载所有规则 + +### SCRIPT 脚本 + +::: info +此功能仅在 [Premium 版本](/zh_CN/premium/introduction) 中可用. +::: + +SCRIPT 规则用于根据脚本的结果路由数据包. 当 Clash 使用此规则时, 它会执行指定的脚本, 然后将数据包路由到脚本的输出. + +::: warning +使用 SCRIPT 时, Clash 将解析域名以获取 IP 地址. +如果要跳过 DNS 解析, 请使用 `no-resolve` 选项. +::: + +`SCRIPT,script-path,DIRECT` 将数据包路由到脚本 `script-path` 的输出. + +### MATCH 全匹配 + +MATCH 规则用于路由剩余的数据包. 该规则是**必需**的, 通常用作最后一条规则. + +`MATCH,policy` 将剩余的数据包路由到 `policy`. diff --git a/docs/zh_CN/index.md b/docs/zh_CN/index.md new file mode 100644 index 0000000..b2c1113 --- /dev/null +++ b/docs/zh_CN/index.md @@ -0,0 +1,38 @@ + +# 什么是 Clash? + +欢迎访问 Clash 内核项目的官方说明文档. + +Clash是一个跨平台的基于规则的代理工具, 在网络和应用层运行, 支持各种代理和反审查协议的开箱即用. + +在一些互联网受到严格审查或封锁的国家和地区, 它已被互联网用户广泛采用. 无论如何, 任何想要改善其 Internet 体验的人都可以使用 Clash. + +目前, Clash 包含两个版本: + +- [Clash](https://github.com/Dreamacro/clash): 发布于[github.com/Dreamacro/clash](https://github.com/Dreamacro/clash)的开源版本 +- [Clash Premium 版本](https://github.com/Dreamacro/clash/releases/tag/premium): 具有[TUN 和更多支持](/zh_CN/premium/introduction) 的专有内核 (免费) + +虽然这个 Wiki 涵盖了上述两个版本的内容, 然而对于普通用户来说, Clash 的使用可能仍是一种挑战. 而对于考虑使用 GUI 客户端的用户, 我们确实有一些建议: + +- [Clash for Windows](https://github.com/Fndroid/clash_for_windows_pkg/releases) (Windows 和 macOS) +- [Clash for Android](https://github.com/Kr328/ClashForAndroid) +- [ClashX](https://github.com/yichengchen/clashX) 或 [ClashX Pro](https://install.appcenter.ms/users/clashx/apps/clashx-pro/distribution_groups/public) (macOS) + +## 特点概述 + +- 入站连接支持: HTTP, HTTPS, SOCKS5 服务端, TUN 设备* +- 出站连接支持: Shadowsocks(R), VMess, Trojan, Snell, SOCKS5, HTTP(S), Wireguard* +- 基于规则的路由: 动态脚本、域名、IP地址、进程名称和更多* +- Fake-IP DNS: 尽量减少 DNS 污染的影响, 提高网络性能 +- 透明代理: 使用自动路由表/规则管理 Redirect TCP 和 TProxy TCP/UDP* +- Proxy Groups 策略组: 自动化的可用性测试 (fallback)、负载均衡 (load balance) 或 延迟测试 (url-test) +- 远程 Providers: 动态加载远程代理列表 +- RESTful API: 通过一个全面的 API 就地更新配置 + + +\*: 只在免费的 Premium 版本中提供. + + +## License + +Clash 是根据 [GPL-3.0](https://github.com/Dreamacro/clash/blob/master/LICENSE) 开源许可证发布的. 在 [v0.16.0](https://github.com/Dreamacro/clash/releases/tag/v0.16.0) 或 [e5284c](https://github.com/Dreamacro/clash/commit/e5284cf647717a8087a185d88d15a01096274bc2) 提交之前, 其基于 MIT 许可证授权. diff --git a/docs/zh_CN/introduction/_dummy-index.md b/docs/zh_CN/introduction/_dummy-index.md new file mode 100644 index 0000000..4c76269 --- /dev/null +++ b/docs/zh_CN/introduction/_dummy-index.md @@ -0,0 +1,6 @@ +--- +sidebarTitle: 什么是 Clash? +sidebarOrder: 1 +--- + + diff --git a/docs/zh_CN/introduction/faq.md b/docs/zh_CN/introduction/faq.md new file mode 100644 index 0000000..3f9d557 --- /dev/null +++ b/docs/zh_CN/introduction/faq.md @@ -0,0 +1,95 @@ +--- +sidebarTitle: 常见问题 +sidebarOrder: 4 +--- + +# 常见问题 + +这里是一些大家遇到的常见问题. 如果您有任何此处未列出的问题, 请随时[提交一个 issue](https://github.com/Dreamacro/clash/issues/new/choose). + +[[toc]] + +## amd64 和 amd64-v3 有什么区别? + +引用自 [golang/go](https://github.com/golang/go/wiki/MinimumRequirements#amd64): + +> 在 Go 1.17 之前, Go 编译器总是生成任何 64 位 x86 处理器都可以执行的 x86 二进制文件. +> +> Go 1.18 引入了 AMD64 的 [4 个架构级别](https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels). +> 每个级别都有不同的x86指令集, 编译器可以在生成的二进制文件中包含这些指令: +> +> - GOAMD64=v1 (默认) : 基线. 仅生成所有 64 位 x86 处理器都可以执行的指令. +> - GOAMD64=v2: 所有 v1 指令, 加上 CMPXCHG16B、LAHF、SAHF、POPCNT、SSE3、SSE4.1、SSE4.2、SSSE3. +> - GOAMD64=v3: 所有 v2 指令, 加上 AVX、AVX2、BMI1、BMI2、F16C、FMA、LZCNT、MOVBE、OSXSAVE. +> - GOAMD64=v4: 所有 v3 指令, 加上 AVX512F、AVX512BW、AVX512CD、AVX512DQ、AVX512VL. +> +> 例如, 设置 `GOAMD64=v3` 将允许 Go 编译器在生成的二进制文件中使用 AVX2 指令 (这可能会在某些情况下提高性能) ;但是这些二进制文件将无法在不支持 AVX2 的旧 x86 处理器上运行. +> +> Go工具链也可能生成较新的指令, 但会存在动态检查保护, 确保它们只在有能力的处理器上执行. 例如在 `GOAMD64=v1` 的情况下, 如果 [CPUID](https://www.felixcloutier.com/x86/cpuid) 报告说 [POPCNT](https://www.felixcloutier.com/x86/popcnt) 指令可用, [math/bits.OnesCount](https://pkg.go.dev/math/bits#OnesCount) 仍将使用该指令. 否则, 它就会退回到一个通用的实现. +> +> Go 工具链目前不会生成任何 AVX512 指令. +> +> 请注意, 在这种情况下, *处理器*是一个简化. 实际上, 整个系统 (固件、hypervisor、内核) 都需要支持. + +## 我的系统应该使用哪个版本? + +这里是一些人们在 Clash 上使用的常见系统, 以及每个系统的推荐版本: + +- NETGEAR WNDR3700v2: mips-hardfloat [#846](https://github.com/Dreamacro/clash/issues/846) +- NETGEAR WNDR3800: mips-softfloat [#579](https://github.com/Dreamacro/clash/issues/579) +- 华硕RT-AC5300: armv5 [#2356](https://github.com/Dreamacro/clash/issues/2356) +- 联发科MT7620A, MT7621A: mipsle-softfloat ([#136](https://github.com/Dreamacro/clash/issues/136)) +- mips_24kc: [#192](https://github.com/Dreamacro/clash/issues/192) + +如果您的设备未在此处列出, 您可以使用 `uname -m` 检查设备的 CPU 架构, 并在发布页面中找到相应的版本. + +## 不会修复的问题 + +官方 Clash 内核项目不会实现/修复以下内容: + +- [Snell](https://github.com/Dreamacro/clash/issues/2466) +- [Custom CA](https://github.com/Dreamacro/clash/issues/2333) +- [VMess Mux](https://github.com/Dreamacro/clash/issues/450) +- [VLess](https://github.com/Dreamacro/clash/issues/1185) +- [KCP](https://github.com/Dreamacro/clash/issues/16) +- [mKCP](https://github.com/Dreamacro/clash/issues/2308) +- [TLS Encrypted Client Hello](https://github.com/Dreamacro/clash/issues/2295) +- [TCP support for Clash DNS server](https://github.com/Dreamacro/clash/issues/368) +- [MITM](https://github.com/Dreamacro/clash/issues/227#issuecomment-508693628) + +当官方Go QUIC库发布时, 以下内容将被考虑实施: + +- [TUIC](https://github.com/Dreamacro/clash/issues/2222) +- [Hysteria](https://github.com/Dreamacro/clash/issues/1863) + +## 在本地机器上节点正常工作, 但在路由器或容器中不起作用 + +您的系统可能未与世界时间同步. 请参考您的平台关于时间同步的文件 - 如果时间不同步, 某些协议可能无法正常工作. + +## 规则匹配的时间复杂度 + +请参考这个讨论: [#422](https://github.com/Dreamacro/clash/issues/422) + +## Clash Premium 无法访问互联网 + +您可以参考这些相关讨论: + +- [#432](https://github.com/Dreamacro/clash/issues/432#issuecomment-571634905) +- [#2480](https://github.com/Dreamacro/clash/issues/2480) + +## 错误: 不支持的 RULE-SET 规则类型 + +如果您遇到了这个错误信息: + +```txt +FATA[0000] Parse config error: Rules[0] [RULE-SET,apple,REJECT] error: unsupported rule type RULE-SET +``` + +您正在使用 Clash 开源版. 规则 Providers 目前仅在 [免费 Premium 内核](https://github.com/Dreamacro/clash/releases/tag/premium) 中可用. + +## DNS 劫持不起作用 + +由于 `tun.auto-route` 不会拦截局域网流量, 如果您的系统 DNS 设置为私有子网中的服务器, 则 DNS 劫持将不起作用. 您可以: + +1. 使用非私有 DNS 服务器作为系统 DNS, 如 `1.1.1.1` +2. 或者手动将系统 DNS 设置为 Clash DNS (默认为 `198.18.0.1`) diff --git a/docs/zh_CN/introduction/getting-started.md b/docs/zh_CN/introduction/getting-started.md new file mode 100644 index 0000000..64accfa --- /dev/null +++ b/docs/zh_CN/introduction/getting-started.md @@ -0,0 +1,50 @@ +--- +sidebarTitle: 快速开始 +sidebarOrder: 2 +--- + +# 快速开始 + +为了开始使用 Clash, 您可以从源码编译或者下载预编译的二进制文件. + +## 使用预编译的二进制文件 + +您可以在这里下载 Clash 的内核二进制文件: [https://github.com/Dreamacro/clash/releases](https://github.com/Dreamacro/clash/releases) + +## 从源码编译 + +您可以使用 Golang 1.19+ 在您的设备上编译 Clash: + +```shell +$ go install github.com/Dreamacro/clash@latest +go: downloading github.com/Dreamacro/clash v1.15.1 +``` + +二进制文件将会被编译到 `$GOPATH/bin` 目录下: + +```shell +$ $GOPATH/bin/clash -v +Clash unknown version darwin arm64 with go1.20.3 unknown time +``` + +## 跨平台/操作系统编译 + +Golang 支持交叉编译, 所以您可以为不同架构或操作系统的设备编译 Clash. 您可以使用 _make_ 来轻松地编译它们, 例如: + +```shell +$ git clone --depth 1 https://github.com/Dreamacro/clash +Cloning into 'clash'... +remote: Enumerating objects: 359, done. +remote: Counting objects: 100% (359/359), done. +remote: Compressing objects: 100% (325/325), done. +remote: Total 359 (delta 25), reused 232 (delta 17), pack-reused 0 +Receiving objects: 100% (359/359), 248.99 KiB | 1.63 MiB/s, done. +Resolving deltas: 100% (25/25), done. +$ cd clash && make darwin-arm64 +fatal: No names found, cannot describe anything. +GOARCH=arm64 GOOS=darwin CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=unknown version" -X "github.com/Dreamacro/clash/constant.BuildTime=Mon May 8 16:47:10 UTC 2023" -w -s -buildid=' -o bin/clash-darwin-arm64 +$ file bin/clash-darwin-arm64 +bin/clash-darwin-arm64: Mach-O 64-bit executable arm64 +``` + +对于其他构建目标, 请查看 [Makefile](https://github.com/Dreamacro/clash/blob/master/Makefile). \ No newline at end of file diff --git a/docs/zh_CN/introduction/service.md b/docs/zh_CN/introduction/service.md new file mode 100644 index 0000000..16b8c87 --- /dev/null +++ b/docs/zh_CN/introduction/service.md @@ -0,0 +1,131 @@ +--- +sidebarTitle: Clash 服务运行 +sidebarOrder: 3 +--- + +# Clash 服务运行 + +Clash 需要在后台运行, 但是目前 Golang 还没有很好的守护进程实现, 因此我们推荐使用第三方工具来创建 Clash 的守护进程. + +## systemd + +使用以下命令将 Clash 二进制文件复制到 `/usr/local/bin`, 配置文件复制到 `/etc/clash`: + +```shell +cp clash /usr/local/bin +cp config.yaml /etc/clash/ +cp Country.mmdb /etc/clash/ +``` + +创建 systemd 配置文件 `/etc/systemd/system/clash.service`: + +```ini +[Unit] +Description=Clash 守护进程, Go 语言实现的基于规则的代理. +After=network-online.target + +[Service] +Type=simple +Restart=always +ExecStart=/usr/local/bin/clash -d /etc/clash + +[Install] +WantedBy=multi-user.target +``` + +之后, 您应该使用以下命令重新加载 systemd: + +```shell +systemctl daemon-reload +``` + +使用以下命令在系统启动时启动 Clash: + +```shell +systemctl enable clash +``` + +使用以下命令立即启动 Clash: + +```shell +systemctl start clash +``` + +使用以下命令检查 Clash 的运行状况和日志: + +```shell +systemctl status clash +journalctl -xe +``` + +本指南贡献者为 [ktechmidas](https://github.com/ktechmidas). ([#754](https://github.com/Dreamacro/clash/issues/754)) + +## Docker + +本项目提供了预构建的 Clash 和 Clash Premium Docker 镜像. 因此, 在 Linux 上您可以使用 [Docker Compose](https://docs.docker.com/compose/) 部署 Clash. 但是, 您应该知道在容器中运行 **Clash Premium** 是[不被推荐的](https://github.com/Dreamacro/clash/issues/2249#issuecomment-1203494599) + +::: warning +由于 Mac 版 Docker 中缺少[主机网络和 TUN 支持](https://github.com/Dreamacro/clash/issues/770#issuecomment-650951876), 此设置将无法在 macOS 系统上运行. +::: + +::: code-group + +```yaml [Clash] +services: + clash: + image: ghcr.io/dreamacro/clash + restart: always + volumes: + - ./config.yaml:/root/.config/clash/config.yaml:ro + # - ./ui:/ui:ro # 仪表盘 Volume 映射 + ports: + - "7890:7890" + - "7891:7891" + # - "8080:8080" # 外部控制 (RESTful API) + network_mode: "bridge" +``` + +```yaml [Clash Premium] +services: + clash: + image: ghcr.io/dreamacro/clash-premium + restart: always + volumes: + - ./config.yaml:/root/.config/clash/config.yaml:ro + # - ./ui:/ui:ro # 仪表盘 Volume 映射 + ports: + - "7890:7890" + - "7891:7891" + # - "8080:8080" # 外部控制 (RESTful API) + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun + network_mode: "host" +``` + +::: + +保存为 `docker-compose.yaml`, 并将您的 `config.yaml` 放在同一目录下. + +::: tip +在继续操作之前, 请参考您的平台关于时间同步的文件 - 如果时间不同步, 某些协议可能无法正常工作. +::: + +准备就绪后, 运行以下命令以启动 Clash: + +```shell +docker-compose up -d +``` + +您可以使用以下命令查看日志: + +```shell +docker-compose logs +``` + +Stop Clash with: + +```shell +docker-compose stop +``` diff --git a/docs/zh_CN/premium/ebpf.md b/docs/zh_CN/premium/ebpf.md new file mode 100644 index 0000000..c353b17 --- /dev/null +++ b/docs/zh_CN/premium/ebpf.md @@ -0,0 +1,26 @@ +--- +sidebarTitle: "功能: eBPF 重定向到 TUN" +sidebarOrder: 3 +--- + +# 功能: eBPF 重定向到 TUN + +eBPF 重定向到 TUN 是一项拦截特定网络接口上的所有网络流量, 并将其重定向到 TUN 接口的功能. 该功能需要[内核支持](https://github.com/iovisor/bcc/blob/master/INSTALL.md#kernel-configuration). + +::: warning +此功能与 `tun.auto-route` 冲突. +::: + +虽然它通常与 `tun.auto-redir` 和 `tun.auto-route` 相比具有更好的性能, 但与 `auto-route` 相比, 它并不够成熟. 因此, 您应该谨慎使用. + +## 配置 + +```yaml +ebpf: + redirect-to-tun: + - eth0 +``` + +## 已知问题 + +- 此功能与 Tailscaled 冲突, 因此您应该使用 `tun.auto-route` 作为替代. diff --git a/docs/zh_CN/premium/experimental-features.md b/docs/zh_CN/premium/experimental-features.md new file mode 100644 index 0000000..fc25e7d --- /dev/null +++ b/docs/zh_CN/premium/experimental-features.md @@ -0,0 +1,19 @@ +--- +sidebarTitle: 实验功能 +sidebarOrder: 9 +--- + +# 实验功能 + +偶尔我们会做一些新的功能, 这些功能需要大量的测试才能在主要版本中使用. 这些功能被标记为实验性的, 并且默认是禁用的. + +::: warning +这里列出的一些功能可能不稳定, 并且可能在任何未来版本中被删除 - 我们不建议使用它们, 除非您有特定的原因. +::: + +## 嗅探 TLS SNI + +```yaml +experimental: + sniff-tls-sni: true +``` diff --git a/docs/zh_CN/premium/introduction.md b/docs/zh_CN/premium/introduction.md new file mode 100644 index 0000000..d26dc6f --- /dev/null +++ b/docs/zh_CN/premium/introduction.md @@ -0,0 +1,26 @@ +--- +sidebarTitle: 简介 +sidebarOrder: 1 +--- + +# 简介 + +在过去, 只有一个开源版本的 Clash, 直到一些 [不当使用和再分发](https://github.com/Dreamacro/clash/issues/541#issuecomment-672029110) 的 Clash 出现. 从那时起, 我们决定分叉 Clash 并在私有 GitHub 存储库中开发更高级的功能. + +不要担心 - Premium 内核将保持免费, 并且其源代码的安全性通过多个可信的开发人员相互审查以保证. + +## 有什么区别? + +Premium 内核是开源 Clash 内核的 Fork 分支, 增加了以下功能: + +- [TUN 设备](/zh_CN/premium/tun-device) 支持 `auto-redir` 和 `auto-route` +- [eBPF 重定向到 TUN](/zh_CN/premium/ebpf) +- [Rule Providers 规则集](/zh_CN/premium/rule-providers) +- [Script 脚本](/zh_CN/premium/script) +- [Script Shotcuts 脚本捷径](/zh_CN/premium/script-shortcuts) +- [用户空间 Wireguard](/zh_CN/premium/userspace-wireguard) +- [性能分析引擎](/zh_CN/premium/the-profiling-engine) + +## 获取副本 + +您可以从 [GitHub Releases](https://github.com/Dreamacro/clash/releases/tag/premium) 下载最新的 Clash Premium 二进制文件. diff --git a/docs/zh_CN/premium/rule-providers.md b/docs/zh_CN/premium/rule-providers.md new file mode 100644 index 0000000..f6e16e8 --- /dev/null +++ b/docs/zh_CN/premium/rule-providers.md @@ -0,0 +1,100 @@ +--- +sidebarTitle: "功能: Rule Providers 规则集" +sidebarOrder: 4 +--- + +# Rule Providers 规则集 + +Rule Providers 规则集和 [Proxy Providers 代理集](/zh_CN/configuration/outbound#proxy-providers-代理集) 基本相同. 它允许用户从外部源加载规则, 从而使配置更加简洁. 该功能目前仅适用于 Clash Premium 内核. + +要定义 Rule Providers 规则集, 请将 `rule-providers` 规则集字段添加到主配置中: + +```yaml +rule-providers: + apple: + behavior: "domain" # domain, ipcidr or classical (仅限 Clash Premium 内核) + type: http + url: "url" + # format: 'yaml' # or 'text' + interval: 3600 + path: ./apple.yaml + microsoft: + behavior: "domain" + type: file + path: /microsoft.yaml + +rules: + - RULE-SET,apple,REJECT + - RULE-SET,microsoft,policy +``` + +有三种行为类型可用: + +## `domain` + +yaml: + +```yaml +payload: + - '.blogger.com' + - '*.*.microsoft.com' + - 'books.itunes.apple.com' +``` + +text: + +```txt +# comment +.blogger.com +*.*.microsoft.com +books.itunes.apple.com +``` + +## `ipcidr` + +yaml + +```yaml +payload: + - '192.168.1.0/24' + - '10.0.0.0.1/32' +``` + +text: + +```txt +# comment +192.168.1.0/24 +10.0.0.0.1/32 +``` + +## `classical` + +yaml: + +```yaml +payload: + - DOMAIN-SUFFIX,google.com + - DOMAIN-KEYWORD,google + - DOMAIN,ad.com + - SRC-IP-CIDR,192.168.1.201/32 + - IP-CIDR,127.0.0.0/8 + - GEOIP,CN + - DST-PORT,80 + - SRC-PORT,7777 + # MATCH 在这里并不是必须的 +``` + +text: + +```txt +# comment +DOMAIN-SUFFIX,google.com +DOMAIN-KEYWORD,google +DOMAIN,ad.com +SRC-IP-CIDR,192.168.1.201/32 +IP-CIDR,127.0.0.0/8 +GEOIP,CN +DST-PORT,80 +SRC-PORT,7777 +``` diff --git a/docs/zh_CN/premium/script-shortcuts.md b/docs/zh_CN/premium/script-shortcuts.md new file mode 100644 index 0000000..fa9c410 --- /dev/null +++ b/docs/zh_CN/premium/script-shortcuts.md @@ -0,0 +1,60 @@ +--- +sidebarTitle: "功能: Script Shortcuts 脚本捷径" +sidebarOrder: 6 +--- + +# Script Shortcuts 脚本捷径 + +Clash Premium 实现了基于 Python3 的脚本功能, 允许用户以动态灵活的方式为数据包选择策略. + +您可以使用单个 Python 脚本控制整个规则匹配引擎, 也可以定义一些 Shortcuts 捷径并将它们与常规规则一起使用. 本页参考后者功能. 有关前者, 请参见 [脚本](./script.md). + +此功能使得在 `rules` 模式下使用脚本成为可能. 默认情况下, DNS 解析将在 SCRIPT 规则中进行. 可以在规则后面添加 `no-resolve` 来阻止解析. (例如: `SCRIPT,quic,DIRECT,no-resolve`) + +```yaml +mode: Rule + +script: + engine: expr # or starlark (10x to 20x slower) + shortcuts: + quic: network == 'udp' and dst_port == 443 + curl: resolve_process_name() == 'curl' + # curl: resolve_process_path() == '/usr/bin/curl' + +rules: + - SCRIPT,quic,REJECT +``` + +## 评估引擎 + +[Expr](https://expr.medv.io/) 作为 Script Shortcuts 的默认引擎, 相比 Starlark 提供了 10 倍到 20 倍的性能提升. + +[Starlark](https://github.com/google/starlark-go) 是一种类似 Python 的配置语言, 您也可以将其用于 Script Shortcuts. + +## 变量 + +- network: string +- type: string +- src_ip: string +- dst_ip: string +- src_port: uint16 +- dst_port: uint16 +- inbound_port: uint16 +- host: string +- process_path: string + +::: warning +Starlark 目前不包含 `process_path` 变量. +::: + +## 函数 + +```ts +type resolve_ip = (host: string) => string // ip string +type in_cidr = (ip: string, cidr: string) => boolean // ip in cidr +type in_ipset = (name: string, ip: string) => boolean // ip in ipset +type geoip = (ip: string) => string // country code +type match_provider = (name: string) => boolean // in rule provider +type resolve_process_name = () => string // find process name (curl .e.g) +type resolve_process_path = () => string // find process path (/usr/bin/curl .e.g) +``` diff --git a/docs/zh_CN/premium/script.md b/docs/zh_CN/premium/script.md new file mode 100644 index 0000000..d5c8507 --- /dev/null +++ b/docs/zh_CN/premium/script.md @@ -0,0 +1,71 @@ +--- +sidebarTitle: "功能: Script 脚本" +sidebarOrder: 5 +--- + +# Script 脚本 + +Clash Premium 实现了基于 Python3 的脚本功能, 使用户能够以动态灵活的方式为数据包选择策略. + +您可以使用单个 Python 脚本控制整个规则匹配引擎, 也可以定义一些快捷方式, 并与常规规则一起使用. 本页介绍了第一种功能, 有关后者, 请参见[Script Shortcuts 脚本捷径](./script-shortcuts.md). + +## 控制整个规则匹配引擎 + +```yaml +mode: Script + +# https://lancellc.gitbook.io/clash/clash-config-file/script +script: + code: | + def main(ctx, metadata): + ip = metadata["dst_ip"] = ctx.resolve_ip(metadata["host"]) + if ip == "": + return "DIRECT" + + code = ctx.geoip(ip) + if code == "LAN" or code == "CN": + return "DIRECT" + + return "Proxy" # default policy for requests which are not matched by any other script +``` + +如果您想使用 IP 规则 (即: IP-CIDR、GEOIP 等) , 您首先需要手动解析 IP 地址并将其分配给 metadata: + +```python +def main(ctx, metadata): + # ctx.rule_providers["geoip"].match(metadata) return false + + ip = ctx.resolve_ip(metadata["host"]) + if ip == "": + return "DIRECT" + metadata["dst_ip"] = ip + + # ctx.rule_providers["iprule"].match(metadata) return true + + return "Proxy" +``` + +Metadata 和 Context 的接口定义: + +```ts +interface Metadata { + type: string // socks5、http + network: string // tcp + host: string + src_ip: string + src_port: string + dst_ip: string + dst_port: string + inbound_port: number +} + +interface Context { + resolve_ip: (host: string) => string // ip string + resolve_process_name: (metadata: Metadata) => string + resolve_process_path: (metadata: Metadata) => string + geoip: (ip: string) => string // country code + log: (log: string) => void + proxy_providers: Record> + rule_providers: Record boolean }> +} +``` diff --git a/docs/zh_CN/premium/the-profiling-engine.md b/docs/zh_CN/premium/the-profiling-engine.md new file mode 100644 index 0000000..31a4d0e --- /dev/null +++ b/docs/zh_CN/premium/the-profiling-engine.md @@ -0,0 +1,13 @@ +--- +sidebarTitle: "功能: 性能分析引擎" +sidebarOrder: 8 +--- + +# 性能分析引擎 + +https://github.com/Dreamacro/clash-tracing + +```yaml +profile: + tracing: true +``` diff --git a/docs/zh_CN/premium/tun-device.md b/docs/zh_CN/premium/tun-device.md new file mode 100644 index 0000000..803fc66 --- /dev/null +++ b/docs/zh_CN/premium/tun-device.md @@ -0,0 +1,65 @@ +--- +sidebarTitle: "功能: TUN 设备" +sidebarOrder: 2 +--- + +# TUN 设备 + +Premium 内核支持 TUN 设备. 作为网络层设备, 它可以用来处理 TCP、UDP、ICMP 流量. 它已经在生产环境中进行了广泛的测试和使用 - 您甚至可以用它来玩竞技游戏. + +使用 Clash TUN 的最大优势之一是内置支持对操作系统路由表、路由规则和 nftable 的自动管理. 您可以通过选项 `tun.auto-route` 和 `tun.auto-redir` 来启用它. 这个功能替换了古老的配置选项 `redir-port`(TCP), 以方便配置和提高稳定性. + +::: tip +`tun.auto-route` 仅在 macOS、Windows、Linux 和 Android 上可用, 并且仅接收 IPv4 流量。`tun.auto-redir` 仅在 Linux 上可用(需要内核 netlink 支持)。 +::: + +Clash 有两种可供选择的 TCP/IP 协议栈: `system` or `gvisor`. 为了获得最好的性能, 我们建议您优先使用 `system` 栈, 只有遇到兼容性问题时才使用 `gvisor`. 并且如果你遇到这样的情况, 请立即[提交 Issue](https://github.com/Dreamacro/clash/issues/new/choose). + +## 技术限制 + +* 对于 Android, 控制设备位于 `/dev/tun` 而不是 `/dev/net/tun`, 您需要先创建一个软链接 (i.e. `ln -sf /dev/tun /dev/net/tun`) + +* 如果系统 DNS 位于私有 IP 地址上, DNS 劫持可能会失败 (因为 `auto-route` 不会捕获私有网络流量). + +## Linux, macOS 和 Windows + +这是 TUN 功能的示例配置: + +```yaml +interface-name: en0 # 与 `tun.auto-detect-interface` 冲突 + +tun: + enable: true + stack: system # or gvisor + # dns-hijack: + # - 8.8.8.8:53 + # - tcp://8.8.8.8:53 + # - any:53 + # - tcp://any:53 + auto-route: true # manage `ip route` and `ip rules` + auto-redir: true # manage nftable REDIRECT + auto-detect-interface: true # 与 `interface-name` 冲突 +``` + +请注意, 由于使用了 TUN 设备和对系统路由表、nftable 的操作, Clash 在此处将需要超级用户权限来运行. + +```shell +sudo ./clash +``` + +如果您的设备已经有一些 TUN 设备, Clash TUN 可能无法工作 - 您必须手动检查路由表和路由规则. 在这种情况下, `fake-ip-filter` 也许也有帮助. + +## Windows + +您需要访问 [WinTUN 网站](https://www.wintun.net) 并下载最新版本. 之后, 将 `wintun.dll` 复制到 Clash 主目录. 示例配置: + +```yaml +tun: + enable: true + stack: gvisor # or system + dns-hijack: + - 198.18.0.2:53 # 当 `fake-ip-range` 是 198.18.0.1/16, 应该劫持 198.18.0.2:53 + auto-route: true # 为 Windows 自动设置全局路由 + # 推荐使用 `interface-name` + auto-detect-interface: true # 自动检测接口, 与 `interface-name` 冲突 +``` diff --git a/docs/zh_CN/premium/userspace-wireguard.md b/docs/zh_CN/premium/userspace-wireguard.md new file mode 100644 index 0000000..59d8d5f --- /dev/null +++ b/docs/zh_CN/premium/userspace-wireguard.md @@ -0,0 +1,25 @@ +--- +sidebarTitle: "功能: 用户空间 Wireguard" +sidebarOrder: 7 +--- + +# 用户空间 Wireguard + +由于依赖 gvisor TCP/IP 栈, 用户空间 Wireguard 目前仅在 Premium 内核中可用. + +```yaml +proxies: + - name: "wg" + type: wireguard + server: 127.0.0.1 + port: 443 + ip: 172.16.0.2 + # ipv6: your_ipv6 + private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= + public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= + # preshared-key: base64 + # remote-dns-resolve: true # 远程解析 DNS, 使用 `dns` 字段, 默认为 true + # dns: [1.1.1.1, 8.8.8.8] + # mtu: 1420 + udp: true +``` diff --git a/docs/zh_CN/runtime/external-controller.md b/docs/zh_CN/runtime/external-controller.md new file mode 100644 index 0000000..d0cd436 --- /dev/null +++ b/docs/zh_CN/runtime/external-controller.md @@ -0,0 +1,130 @@ +--- +sidebarTitle: 外部控制设置 +sidebarOrder: 1 +--- + +# 外部控制设置 + +## 简介 + +外部控制允许用户通过 HTTP RESTful API 来控制 Clash. 第三方 Clash GUI 就是基于这个功能的. 通过在 `external-controller` 中指定地址来启用这个功能. + +## 认证 + +- 外部控制器接受 `Bearer Tokens` 作为访问认证方式. + - 使用 `Authorization: Bearer ` 作为请求头来传递凭证. + +## RESTful API 文档 + +### 日志 + +- `/logs` + - 方法: `GET` + - 完整路径: `GET /logs` + - 描述: 获取实时日志 + +### 流量 + +- `/traffic` + - 方法: `GET` + - 完整路径: `GET /traffic` + - 描述: 获取实时流量数据 + +### 版本 + +- `/version` + - 方法: `GET` + - 完整路径: `GET /version` + - 描述: 获取 Clash 版本 + +### 配置 + +- `/configs` + - 方法: `GET` + - 完整路径: `GET /configs` + - 描述: 获取基础配置 + + - 方法: `PUT` + - 完整路径: `PUT /configs` + - 描述: 重新加载配置文件 + + - 方法: `PATCH` + - 完整路径: `PATCH /configs` + - 描述: 增量修改配置 + +### 节点 + +- `/proxies` + - 方法: `GET` + - 完整路径: `GET /proxies` + - 描述: 获取所有节点信息 + +- `/proxies/:name` + - 方法: `GET` + - 完整路径: `GET /proxies/:name` + - 描述: 获取指定节点信息 + + - 方法: `PUT` + - 完整路径: `PUT /proxies/:name` + - 描述: 切换 Selector 中选中的节点 + +- `/proxies/:name/delay` + - 方法: `GET` + - 完整路径: `GET /proxies/:name/delay` + - 描述: 获取指定节点的延迟测试信息 + +### 规则 + +- `/rules` + - 方法: `GET` + - 完整路径: `GET /rules` + - 描述: 获取规则信息 + +### 连接 + +- `/connections` + - 方法: `GET` + - 完整路径: `GET /connections` + - 描述: 获取连接信息 + + - 方法: `DELETE` + - 完整路径: `DELETE /connections` + - 描述: 关闭所有连接 + +- `/connections/:id` + - 方法: `DELETE` + - 完整路径: `DELETE /connections/:id` + - 描述: 关闭指定连接 + +### 代理集 + +- `/providers/proxies` + - 方法: `GET` + - 完整路径: `GET /providers/proxies` + - 描述: 获取所有代理集的代理信息 + +- `/providers/proxies/:name` + - 方法: `GET` + - 完整路径: `GET /providers/proxies/:name` + - 描述: 获取指定代理集的代理信息 + + - 方法: `PUT` + - 完整路径: `PUT /providers/proxies/:name` + - 描述: 切换指定代理集 + +- `/providers/proxies/:name/healthcheck` + - 方法: `GET` + - 完整路径: `GET /providers/proxies/:name/healthcheck` + - 描述: 获取指定代理集的代理信息 + +### DNS 查询 + +- `/dns/query` + - 方法: `GET` + - 完整路径: `GET /dns/query?name={name}[&type={type}]` + - 描述: 获取指定域名和类型的 DNS 查询数据 + - 参数: + - `name` (必填): 要查询的域名 + - `type` (可选): 要查询的 DNS 记录类型 (例如, A, MX, CNAME 等). 如果未提供, 则默认为 `A`. + + - 示例: `GET /dns/query?name=example.com&type=A` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fd9fbe2 --- /dev/null +++ b/go.mod @@ -0,0 +1,47 @@ +module github.com/Dreamacro/clash + +go 1.21 + +require ( + github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 + github.com/dlclark/regexp2 v1.10.0 + github.com/go-chi/chi/v5 v5.0.10 + github.com/go-chi/cors v1.2.1 + github.com/go-chi/render v1.0.3 + github.com/gofrs/uuid/v5 v5.0.0 + github.com/gorilla/websocket v1.5.0 + github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d + github.com/mdlayher/netlink v1.7.2 + github.com/miekg/dns v1.1.55 + github.com/oschwald/geoip2-golang v1.9.0 + github.com/samber/lo v1.38.1 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.4 + github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01 + go.etcd.io/bbolt v1.3.7 + go.uber.org/atomic v1.11.0 + go.uber.org/automaxprocs v1.5.3 + golang.org/x/crypto v0.12.0 + golang.org/x/net v0.14.0 + golang.org/x/sync v0.3.0 + golang.org/x/sys v0.11.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/ajg/form v1.5.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/oschwald/maxminddb-golang v1.11.0 // indirect + github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect + github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/text v0.12.0 // indirect + golang.org/x/tools v0.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d1ccbc9 --- /dev/null +++ b/go.sum @@ -0,0 +1,93 @@ +github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 h1:JFnwKplz9hj8ubqYjm8HkgZS1Rvz9yW+u/XCNNTxr0k= +github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158/go.mod h1:QvmEZ/h6KXszPOr2wUFl7Zn3hfFNYdfbXwPVDTyZs6k= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= +github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d h1:Ka64cclWedOkGzm9M2/XYuwJUdmWRUozmsxW0PyKA3A= +github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc= +github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y= +github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0= +github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg= +github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01 h1:F9xjJm4IH8VjcqG4ujciOF+GIM4mjPkHhWLLzOghPtM= +github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01/go.mod h1:cAAsePK2e15YDAMJNyOpGYEWNe4sIghTY7gpz4cX/Ik= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hub/executor/executor.go b/hub/executor/executor.go new file mode 100644 index 0000000..99ebc20 --- /dev/null +++ b/hub/executor/executor.go @@ -0,0 +1,249 @@ +package executor + +import ( + "fmt" + "os" + "sync" + + "github.com/Dreamacro/clash/adapter" + "github.com/Dreamacro/clash/adapter/outboundgroup" + "github.com/Dreamacro/clash/component/auth" + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/iface" + "github.com/Dreamacro/clash/component/profile" + "github.com/Dreamacro/clash/component/profile/cachefile" + "github.com/Dreamacro/clash/component/resolver" + "github.com/Dreamacro/clash/component/trie" + "github.com/Dreamacro/clash/config" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" + "github.com/Dreamacro/clash/dns" + "github.com/Dreamacro/clash/listener" + authStore "github.com/Dreamacro/clash/listener/auth" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/tunnel" +) + +var mux sync.Mutex + +func readConfig(path string) ([]byte, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, err + } + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + if len(data) == 0 { + return nil, fmt.Errorf("configuration file %s is empty", path) + } + + return data, err +} + +// Parse config with default config path +func Parse() (*config.Config, error) { + return ParseWithPath(C.Path.Config()) +} + +// ParseWithPath parse config with custom config path +func ParseWithPath(path string) (*config.Config, error) { + buf, err := readConfig(path) + if err != nil { + return nil, err + } + + return ParseWithBytes(buf) +} + +// ParseWithBytes config with buffer +func ParseWithBytes(buf []byte) (*config.Config, error) { + return config.Parse(buf) +} + +// ApplyConfig dispatch configure to all parts +func ApplyConfig(cfg *config.Config, force bool) { + mux.Lock() + defer mux.Unlock() + + updateUsers(cfg.Users) + updateProxies(cfg.Proxies, cfg.Providers) + updateRules(cfg.Rules) + updateHosts(cfg.Hosts) + updateProfile(cfg) + updateGeneral(cfg.General, force) + updateInbounds(cfg.Inbounds, force) + updateDNS(cfg.DNS) + updateExperimental(cfg) + updateTunnels(cfg.Tunnels) +} + +func GetGeneral() *config.General { + ports := listener.GetPorts() + authenticator := []string{} + if auth := authStore.Authenticator(); auth != nil { + authenticator = auth.Users() + } + + general := &config.General{ + LegacyInbound: config.LegacyInbound{ + Port: ports.Port, + SocksPort: ports.SocksPort, + RedirPort: ports.RedirPort, + TProxyPort: ports.TProxyPort, + MixedPort: ports.MixedPort, + AllowLan: listener.AllowLan(), + BindAddress: listener.BindAddress(), + }, + Authentication: authenticator, + Mode: tunnel.Mode(), + LogLevel: log.Level(), + IPv6: !resolver.DisableIPv6, + } + + return general +} + +func updateExperimental(c *config.Config) { + tunnel.UDPFallbackMatch.Store(c.Experimental.UDPFallbackMatch) +} + +func updateDNS(c *config.DNS) { + if !c.Enable { + resolver.DefaultResolver = nil + resolver.DefaultHostMapper = nil + dns.ReCreateServer("", nil, nil) + return + } + + cfg := dns.Config{ + Main: c.NameServer, + Fallback: c.Fallback, + IPv6: c.IPv6, + EnhancedMode: c.EnhancedMode, + Pool: c.FakeIPRange, + Hosts: c.Hosts, + FallbackFilter: dns.FallbackFilter{ + GeoIP: c.FallbackFilter.GeoIP, + GeoIPCode: c.FallbackFilter.GeoIPCode, + IPCIDR: c.FallbackFilter.IPCIDR, + Domain: c.FallbackFilter.Domain, + }, + Default: c.DefaultNameserver, + Policy: c.NameServerPolicy, + SearchDomains: c.SearchDomains, + } + + r := dns.NewResolver(cfg) + m := dns.NewEnhancer(cfg) + + // reuse cache of old host mapper + if old := resolver.DefaultHostMapper; old != nil { + m.PatchFrom(old.(*dns.ResolverEnhancer)) + } + + resolver.DefaultResolver = r + resolver.DefaultHostMapper = m + + dns.ReCreateServer(c.Listen, r, m) +} + +func updateHosts(tree *trie.DomainTrie) { + resolver.DefaultHosts = tree +} + +func updateProxies(proxies map[string]C.Proxy, providers map[string]provider.ProxyProvider) { + tunnel.UpdateProxies(proxies, providers) +} + +func updateRules(rules []C.Rule) { + tunnel.UpdateRules(rules) +} + +func updateTunnels(tunnels []config.Tunnel) { + listener.PatchTunnel(tunnels, tunnel.TCPIn(), tunnel.UDPIn()) +} + +func updateInbounds(inbounds []C.Inbound, force bool) { + if !force { + return + } + tcpIn := tunnel.TCPIn() + udpIn := tunnel.UDPIn() + + listener.ReCreateListeners(inbounds, tcpIn, udpIn) +} + +func updateGeneral(general *config.General, force bool) { + log.SetLevel(general.LogLevel) + tunnel.SetMode(general.Mode) + resolver.DisableIPv6 = !general.IPv6 + + dialer.DefaultInterface.Store(general.Interface) + dialer.DefaultRoutingMark.Store(int32(general.RoutingMark)) + + iface.FlushCache() + + if !force { + return + } + + allowLan := general.AllowLan + listener.SetAllowLan(allowLan) + + bindAddress := general.BindAddress + listener.SetBindAddress(bindAddress) + + ports := listener.Ports{ + Port: general.Port, + SocksPort: general.SocksPort, + RedirPort: general.RedirPort, + TProxyPort: general.TProxyPort, + MixedPort: general.MixedPort, + } + listener.ReCreatePortsListeners(ports, tunnel.TCPIn(), tunnel.UDPIn()) +} + +func updateUsers(users []auth.AuthUser) { + authenticator := auth.NewAuthenticator(users) + authStore.SetAuthenticator(authenticator) + if authenticator != nil { + log.Infoln("Authentication of local server updated") + } +} + +func updateProfile(cfg *config.Config) { + profileCfg := cfg.Profile + + profile.StoreSelected.Store(profileCfg.StoreSelected) + if profileCfg.StoreSelected { + patchSelectGroup(cfg.Proxies) + } +} + +func patchSelectGroup(proxies map[string]C.Proxy) { + mapping := cachefile.Cache().SelectedMap() + if mapping == nil { + return + } + + for name, proxy := range proxies { + outbound, ok := proxy.(*adapter.Proxy) + if !ok { + continue + } + + selector, ok := outbound.ProxyAdapter.(*outboundgroup.Selector) + if !ok { + continue + } + + selected, exist := mapping[name] + if !exist { + continue + } + + selector.Set(selected) + } +} diff --git a/hub/hub.go b/hub/hub.go new file mode 100644 index 0000000..471fdb5 --- /dev/null +++ b/hub/hub.go @@ -0,0 +1,50 @@ +package hub + +import ( + "github.com/Dreamacro/clash/config" + "github.com/Dreamacro/clash/hub/executor" + "github.com/Dreamacro/clash/hub/route" +) + +type Option func(*config.Config) + +func WithExternalUI(externalUI string) Option { + return func(cfg *config.Config) { + cfg.General.ExternalUI = externalUI + } +} + +func WithExternalController(externalController string) Option { + return func(cfg *config.Config) { + cfg.General.ExternalController = externalController + } +} + +func WithSecret(secret string) Option { + return func(cfg *config.Config) { + cfg.General.Secret = secret + } +} + +// Parse call at the beginning of clash +func Parse(options ...Option) error { + cfg, err := executor.Parse() + if err != nil { + return err + } + + for _, option := range options { + option(cfg) + } + + if cfg.General.ExternalUI != "" { + route.SetUIPath(cfg.General.ExternalUI) + } + + if cfg.General.ExternalController != "" { + go route.Start(cfg.General.ExternalController, cfg.General.Secret) + } + + executor.ApplyConfig(cfg, true) + return nil +} diff --git a/hub/route/common.go b/hub/route/common.go new file mode 100644 index 0000000..d0053e6 --- /dev/null +++ b/hub/route/common.go @@ -0,0 +1,17 @@ +package route + +import ( + "net/http" + "net/url" + + "github.com/go-chi/chi/v5" +) + +// When name is composed of a partial escape string, Golang does not unescape it +func getEscapeParam(r *http.Request, paramName string) string { + param := chi.URLParam(r, paramName) + if newParam, err := url.PathUnescape(param); err == nil { + param = newParam + } + return param +} diff --git a/hub/route/configs.go b/hub/route/configs.go new file mode 100644 index 0000000..3841ec4 --- /dev/null +++ b/hub/route/configs.go @@ -0,0 +1,126 @@ +package route + +import ( + "net/http" + "path/filepath" + + "github.com/Dreamacro/clash/component/resolver" + "github.com/Dreamacro/clash/config" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/hub/executor" + "github.com/Dreamacro/clash/listener" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/tunnel" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/samber/lo" +) + +func configRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getConfigs) + r.Put("/", updateConfigs) + r.Patch("/", patchConfigs) + return r +} + +func getConfigs(w http.ResponseWriter, r *http.Request) { + general := executor.GetGeneral() + render.JSON(w, r, general) +} + +func patchConfigs(w http.ResponseWriter, r *http.Request) { + general := struct { + Port *int `json:"port"` + SocksPort *int `json:"socks-port"` + RedirPort *int `json:"redir-port"` + TProxyPort *int `json:"tproxy-port"` + MixedPort *int `json:"mixed-port"` + AllowLan *bool `json:"allow-lan"` + BindAddress *string `json:"bind-address"` + Mode *tunnel.TunnelMode `json:"mode"` + LogLevel *log.LogLevel `json:"log-level"` + IPv6 *bool `json:"ipv6"` + }{} + if err := render.DecodeJSON(r.Body, &general); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + if general.Mode != nil { + tunnel.SetMode(*general.Mode) + } + + if general.LogLevel != nil { + log.SetLevel(*general.LogLevel) + } + + if general.IPv6 != nil { + resolver.DisableIPv6 = !*general.IPv6 + } + + if general.AllowLan != nil { + listener.SetAllowLan(*general.AllowLan) + } + + if general.BindAddress != nil { + listener.SetBindAddress(*general.BindAddress) + } + + ports := listener.GetPorts() + ports.Port = lo.FromPtrOr(general.Port, ports.Port) + ports.SocksPort = lo.FromPtrOr(general.SocksPort, ports.SocksPort) + ports.RedirPort = lo.FromPtrOr(general.RedirPort, ports.RedirPort) + ports.TProxyPort = lo.FromPtrOr(general.TProxyPort, ports.TProxyPort) + ports.MixedPort = lo.FromPtrOr(general.MixedPort, ports.MixedPort) + + listener.ReCreatePortsListeners(*ports, tunnel.TCPIn(), tunnel.UDPIn()) + + render.NoContent(w, r) +} + +func updateConfigs(w http.ResponseWriter, r *http.Request) { + req := struct { + Path string `json:"path"` + Payload string `json:"payload"` + }{} + if err := render.DecodeJSON(r.Body, &req); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + force := r.URL.Query().Get("force") == "true" + var cfg *config.Config + var err error + + if req.Payload != "" { + cfg, err = executor.ParseWithBytes([]byte(req.Payload)) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(err.Error())) + return + } + } else { + if req.Path == "" { + req.Path = C.Path.Config() + } + if !filepath.IsAbs(req.Path) { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("path is not a absolute path")) + return + } + + cfg, err = executor.ParseWithPath(req.Path) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(err.Error())) + return + } + } + + executor.ApplyConfig(cfg, force) + render.NoContent(w, r) +} diff --git a/hub/route/connections.go b/hub/route/connections.go new file mode 100644 index 0000000..b033555 --- /dev/null +++ b/hub/route/connections.go @@ -0,0 +1,92 @@ +package route + +import ( + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/Dreamacro/clash/tunnel/statistic" + + "github.com/Dreamacro/protobytes" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/gorilla/websocket" +) + +func connectionRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getConnections) + r.Delete("/", closeAllConnections) + r.Delete("/{id}", closeConnection) + return r +} + +func getConnections(w http.ResponseWriter, r *http.Request) { + if !websocket.IsWebSocketUpgrade(r) { + snapshot := statistic.DefaultManager.Snapshot() + render.JSON(w, r, snapshot) + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + + intervalStr := r.URL.Query().Get("interval") + interval := 1000 + if intervalStr != "" { + t, err := strconv.Atoi(intervalStr) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + interval = t + } + + buf := protobytes.BytesWriter{} + sendSnapshot := func() error { + buf.Reset() + snapshot := statistic.DefaultManager.Snapshot() + if err := json.NewEncoder(&buf).Encode(snapshot); err != nil { + return err + } + + return conn.WriteMessage(websocket.TextMessage, buf.Bytes()) + } + + if err := sendSnapshot(); err != nil { + return + } + + tick := time.NewTicker(time.Millisecond * time.Duration(interval)) + defer tick.Stop() + for range tick.C { + if err := sendSnapshot(); err != nil { + break + } + } +} + +func closeConnection(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + snapshot := statistic.DefaultManager.Snapshot() + for _, c := range snapshot.Connections { + if id == c.ID() { + c.Close() + break + } + } + render.NoContent(w, r) +} + +func closeAllConnections(w http.ResponseWriter, r *http.Request) { + snapshot := statistic.DefaultManager.Snapshot() + for _, c := range snapshot.Connections { + c.Close() + } + render.NoContent(w, r) +} diff --git a/hub/route/ctxkeys.go b/hub/route/ctxkeys.go new file mode 100644 index 0000000..5637019 --- /dev/null +++ b/hub/route/ctxkeys.go @@ -0,0 +1,14 @@ +package route + +var ( + CtxKeyProxyName = contextKey("proxy name") + CtxKeyProviderName = contextKey("provider name") + CtxKeyProxy = contextKey("proxy") + CtxKeyProvider = contextKey("provider") +) + +type contextKey string + +func (c contextKey) String() string { + return "clash context key " + string(c) +} diff --git a/hub/route/dns.go b/hub/route/dns.go new file mode 100644 index 0000000..2918b05 --- /dev/null +++ b/hub/route/dns.go @@ -0,0 +1,82 @@ +package route + +import ( + "context" + "math" + "net/http" + + "github.com/Dreamacro/clash/component/resolver" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/miekg/dns" + "github.com/samber/lo" +) + +func dnsRouter() http.Handler { + r := chi.NewRouter() + r.Get("/query", queryDNS) + return r +} + +func queryDNS(w http.ResponseWriter, r *http.Request) { + if resolver.DefaultResolver == nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError("DNS section is disabled")) + return + } + + name := r.URL.Query().Get("name") + qTypeStr, _ := lo.Coalesce(r.URL.Query().Get("type"), "A") + + qType, exist := dns.StringToType[qTypeStr] + if !exist { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("invalid query type")) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) + defer cancel() + + msg := dns.Msg{} + msg.SetQuestion(dns.Fqdn(name), qType) + resp, err := resolver.DefaultResolver.ExchangeContext(ctx, &msg) + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(err.Error())) + return + } + + responseData := render.M{ + "Status": resp.Rcode, + "Question": resp.Question, + "TC": resp.Truncated, + "RD": resp.RecursionDesired, + "RA": resp.RecursionAvailable, + "AD": resp.AuthenticatedData, + "CD": resp.CheckingDisabled, + } + + rr2Json := func(rr dns.RR, _ int) render.M { + header := rr.Header() + return render.M{ + "name": header.Name, + "type": header.Rrtype, + "TTL": header.Ttl, + "data": lo.Substring(rr.String(), len(header.String()), math.MaxUint), + } + } + + if len(resp.Answer) > 0 { + responseData["Answer"] = lo.Map(resp.Answer, rr2Json) + } + if len(resp.Ns) > 0 { + responseData["Authority"] = lo.Map(resp.Ns, rr2Json) + } + if len(resp.Extra) > 0 { + responseData["Additional"] = lo.Map(resp.Extra, rr2Json) + } + + render.JSON(w, r, responseData) +} diff --git a/hub/route/errors.go b/hub/route/errors.go new file mode 100644 index 0000000..b469e10 --- /dev/null +++ b/hub/route/errors.go @@ -0,0 +1,22 @@ +package route + +var ( + ErrUnauthorized = newError("Unauthorized") + ErrBadRequest = newError("Body invalid") + ErrForbidden = newError("Forbidden") + ErrNotFound = newError("Resource not found") + ErrRequestTimeout = newError("Timeout") +) + +// HTTPError is custom HTTP error for API +type HTTPError struct { + Message string `json:"message"` +} + +func (e *HTTPError) Error() string { + return e.Message +} + +func newError(msg string) *HTTPError { + return &HTTPError{Message: msg} +} diff --git a/hub/route/inbounds.go b/hub/route/inbounds.go new file mode 100644 index 0000000..8e53bd1 --- /dev/null +++ b/hub/route/inbounds.go @@ -0,0 +1,39 @@ +package route + +import ( + "net/http" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener" + "github.com/Dreamacro/clash/tunnel" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func inboundRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getInbounds) + r.Put("/", updateInbounds) + return r +} + +func getInbounds(w http.ResponseWriter, r *http.Request) { + inbounds := listener.GetInbounds() + render.JSON(w, r, render.M{ + "inbounds": inbounds, + }) +} + +func updateInbounds(w http.ResponseWriter, r *http.Request) { + var req []C.Inbound + if err := render.DecodeJSON(r.Body, &req); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + tcpIn := tunnel.TCPIn() + udpIn := tunnel.UDPIn() + listener.ReCreateListeners(req, tcpIn, udpIn) + render.NoContent(w, r) +} diff --git a/hub/route/provider.go b/hub/route/provider.go new file mode 100644 index 0000000..9e64eea --- /dev/null +++ b/hub/route/provider.go @@ -0,0 +1,111 @@ +package route + +import ( + "context" + "net/http" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" + "github.com/Dreamacro/clash/tunnel" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/samber/lo" +) + +func proxyProviderRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getProviders) + + r.Route("/{providerName}", func(r chi.Router) { + r.Use(parseProviderName, findProviderByName) + r.Get("/", getProvider) + r.Put("/", updateProvider) + r.Get("/healthcheck", healthCheckProvider) + r.Mount("/", proxyProviderProxyRouter()) + }) + return r +} + +func proxyProviderProxyRouter() http.Handler { + r := chi.NewRouter() + r.Route("/{name}", func(r chi.Router) { + r.Use(parseProxyName, findProviderProxyByName) + r.Get("/", getProxy) + r.Get("/healthcheck", getProxyDelay) + }) + return r +} + +func getProviders(w http.ResponseWriter, r *http.Request) { + providers := tunnel.Providers() + render.JSON(w, r, render.M{ + "providers": providers, + }) +} + +func getProvider(w http.ResponseWriter, r *http.Request) { + provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) + render.JSON(w, r, provider) +} + +func updateProvider(w http.ResponseWriter, r *http.Request) { + provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) + if err := provider.Update(); err != nil { + render.Status(r, http.StatusServiceUnavailable) + render.JSON(w, r, newError(err.Error())) + return + } + render.NoContent(w, r) +} + +func healthCheckProvider(w http.ResponseWriter, r *http.Request) { + provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) + provider.HealthCheck() + render.NoContent(w, r) +} + +func parseProviderName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := getEscapeParam(r, "providerName") + ctx := context.WithValue(r.Context(), CtxKeyProviderName, name) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func findProviderByName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := r.Context().Value(CtxKeyProviderName).(string) + providers := tunnel.Providers() + provider, exist := providers[name] + if !exist { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + return + } + + ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func findProviderProxyByName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var ( + name = r.Context().Value(CtxKeyProxyName).(string) + pd = r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) + ) + proxy, exist := lo.Find(pd.Proxies(), func(proxy C.Proxy) bool { + return proxy.Name() == name + }) + + if !exist { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + return + } + + ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/hub/route/proxies.go b/hub/route/proxies.go new file mode 100644 index 0000000..6b07d88 --- /dev/null +++ b/hub/route/proxies.go @@ -0,0 +1,129 @@ +package route + +import ( + "context" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/Dreamacro/clash/adapter" + "github.com/Dreamacro/clash/adapter/outboundgroup" + "github.com/Dreamacro/clash/component/profile/cachefile" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/tunnel" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func proxyRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getProxies) + + r.Route("/{name}", func(r chi.Router) { + r.Use(parseProxyName, findProxyByName) + r.Get("/", getProxy) + r.Get("/delay", getProxyDelay) + r.Put("/", updateProxy) + }) + return r +} + +func parseProxyName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := getEscapeParam(r, "name") + ctx := context.WithValue(r.Context(), CtxKeyProxyName, name) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func findProxyByName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := r.Context().Value(CtxKeyProxyName).(string) + proxies := tunnel.Proxies() + proxy, exist := proxies[name] + if !exist { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, ErrNotFound) + return + } + + ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getProxies(w http.ResponseWriter, r *http.Request) { + proxies := tunnel.Proxies() + render.JSON(w, r, render.M{ + "proxies": proxies, + }) +} + +func getProxy(w http.ResponseWriter, r *http.Request) { + proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) + render.JSON(w, r, proxy) +} + +func updateProxy(w http.ResponseWriter, r *http.Request) { + req := struct { + Name string `json:"name"` + }{} + if err := render.DecodeJSON(r.Body, &req); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + proxy := r.Context().Value(CtxKeyProxy).(*adapter.Proxy) + selector, ok := proxy.ProxyAdapter.(*outboundgroup.Selector) + if !ok { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("Must be a Selector")) + return + } + + if err := selector.Set(req.Name); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error()))) + return + } + + cachefile.Cache().SetSelected(proxy.Name(), req.Name) + render.NoContent(w, r) +} + +func getProxyDelay(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + url := query.Get("url") + timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout)) + defer cancel() + + delay, meanDelay, err := proxy.URLTest(ctx, url) + if ctx.Err() != nil { + render.Status(r, http.StatusGatewayTimeout) + render.JSON(w, r, ErrRequestTimeout) + return + } + + if err != nil || delay == 0 { + render.Status(r, http.StatusServiceUnavailable) + render.JSON(w, r, newError("An error occurred in the delay test")) + return + } + + render.JSON(w, r, render.M{ + "delay": delay, + "meanDelay": meanDelay, + }) +} diff --git a/hub/route/rules.go b/hub/route/rules.go new file mode 100644 index 0000000..ea819b6 --- /dev/null +++ b/hub/route/rules.go @@ -0,0 +1,39 @@ +package route + +import ( + "net/http" + + "github.com/Dreamacro/clash/tunnel" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func ruleRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getRules) + return r +} + +type Rule struct { + Type string `json:"type"` + Payload string `json:"payload"` + Proxy string `json:"proxy"` +} + +func getRules(w http.ResponseWriter, r *http.Request) { + rawRules := tunnel.Rules() + + rules := []Rule{} + for _, rule := range rawRules { + rules = append(rules, Rule{ + Type: rule.RuleType().String(), + Payload: rule.Payload(), + Proxy: rule.Adapter(), + }) + } + + render.JSON(w, r, render.M{ + "rules": rules, + }) +} diff --git a/hub/route/server.go b/hub/route/server.go new file mode 100644 index 0000000..0cd3ef5 --- /dev/null +++ b/hub/route/server.go @@ -0,0 +1,266 @@ +package route + +import ( + "bytes" + "crypto/subtle" + "encoding/json" + "net" + "net/http" + "strings" + "time" + "unsafe" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/tunnel/statistic" + + "github.com/Dreamacro/protobytes" + "github.com/go-chi/chi/v5" + "github.com/go-chi/cors" + "github.com/go-chi/render" + "github.com/gorilla/websocket" +) + +var ( + serverSecret = "" + serverAddr = "" + + uiPath = "" + + upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + } +) + +type Traffic struct { + Up int64 `json:"up"` + Down int64 `json:"down"` +} + +func SetUIPath(path string) { + uiPath = C.Path.Resolve(path) +} + +func Start(addr string, secret string) { + if serverAddr != "" { + return + } + + serverAddr = addr + serverSecret = secret + + r := chi.NewRouter() + + cors := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + MaxAge: 300, + }) + + r.Use(cors.Handler) + r.Group(func(r chi.Router) { + r.Use(authentication) + + r.Get("/", hello) + r.Get("/logs", getLogs) + r.Get("/traffic", traffic) + r.Get("/version", version) + r.Mount("/configs", configRouter()) + r.Mount("/inbounds", inboundRouter()) + r.Mount("/proxies", proxyRouter()) + r.Mount("/rules", ruleRouter()) + r.Mount("/connections", connectionRouter()) + r.Mount("/providers/proxies", proxyProviderRouter()) + r.Mount("/dns", dnsRouter()) + }) + + if uiPath != "" { + r.Group(func(r chi.Router) { + fs := http.StripPrefix("/ui", http.FileServer(http.Dir(uiPath))) + r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP) + r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) { + fs.ServeHTTP(w, r) + }) + }) + } + + l, err := net.Listen("tcp", addr) + if err != nil { + log.Errorln("External controller listen error: %s", err) + return + } + serverAddr = l.Addr().String() + log.Infoln("RESTful API listening at: %s", serverAddr) + if err = http.Serve(l, r); err != nil { + log.Errorln("External controller serve error: %s", err) + } +} + +func safeEuqal(a, b string) bool { + aBuf := unsafe.Slice(unsafe.StringData(a), len(a)) + bBuf := unsafe.Slice(unsafe.StringData(b), len(b)) + return subtle.ConstantTimeCompare(aBuf, bBuf) == 1 +} + +func authentication(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + if serverSecret == "" { + next.ServeHTTP(w, r) + return + } + + // Browser websocket not support custom header + if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" { + token := r.URL.Query().Get("token") + if !safeEuqal(token, serverSecret) { + render.Status(r, http.StatusUnauthorized) + render.JSON(w, r, ErrUnauthorized) + return + } + next.ServeHTTP(w, r) + return + } + + header := r.Header.Get("Authorization") + bearer, token, found := strings.Cut(header, " ") + + hasInvalidHeader := bearer != "Bearer" + hasInvalidSecret := !found || !safeEuqal(token, serverSecret) + if hasInvalidHeader || hasInvalidSecret { + render.Status(r, http.StatusUnauthorized) + render.JSON(w, r, ErrUnauthorized) + return + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} + +func hello(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, render.M{"hello": "clash"}) +} + +func traffic(w http.ResponseWriter, r *http.Request) { + var wsConn *websocket.Conn + if websocket.IsWebSocketUpgrade(r) { + var err error + wsConn, err = upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + } + + if wsConn == nil { + w.Header().Set("Content-Type", "application/json") + render.Status(r, http.StatusOK) + } + + tick := time.NewTicker(time.Second) + defer tick.Stop() + t := statistic.DefaultManager + buf := protobytes.BytesWriter{} + var err error + for range tick.C { + buf.Reset() + up, down := t.Now() + if err := json.NewEncoder(&buf).Encode(Traffic{ + Up: up, + Down: down, + }); err != nil { + break + } + + if wsConn == nil { + _, err = w.Write(buf.Bytes()) + w.(http.Flusher).Flush() + } else { + err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) + } + + if err != nil { + break + } + } +} + +type Log struct { + Type string `json:"type"` + Payload string `json:"payload"` +} + +func getLogs(w http.ResponseWriter, r *http.Request) { + levelText := r.URL.Query().Get("level") + if levelText == "" { + levelText = "info" + } + + level, ok := log.LogLevelMapping[levelText] + if !ok { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + var wsConn *websocket.Conn + if websocket.IsWebSocketUpgrade(r) { + var err error + wsConn, err = upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + } + + if wsConn == nil { + w.Header().Set("Content-Type", "application/json") + render.Status(r, http.StatusOK) + } + + ch := make(chan log.Event, 1024) + sub := log.Subscribe() + defer log.UnSubscribe(sub) + buf := &bytes.Buffer{} + + go func() { + for elm := range sub { + log := elm.(log.Event) + select { + case ch <- log: + default: + } + } + close(ch) + }() + + for log := range ch { + if log.LogLevel < level { + continue + } + buf.Reset() + + if err := json.NewEncoder(buf).Encode(Log{ + Type: log.Type(), + Payload: log.Payload, + }); err != nil { + break + } + + var err error + if wsConn == nil { + _, err = w.Write(buf.Bytes()) + w.(http.Flusher).Flush() + } else { + err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) + } + + if err != nil { + break + } + } +} + +func version(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, render.M{"version": C.Version}) +} diff --git a/listener/auth/auth.go b/listener/auth/auth.go new file mode 100644 index 0000000..7047311 --- /dev/null +++ b/listener/auth/auth.go @@ -0,0 +1,15 @@ +package auth + +import ( + "github.com/Dreamacro/clash/component/auth" +) + +var authenticator auth.Authenticator + +func Authenticator() auth.Authenticator { + return authenticator +} + +func SetAuthenticator(au auth.Authenticator) { + authenticator = au +} diff --git a/listener/http/client.go b/listener/http/client.go new file mode 100644 index 0000000..eb7b7fd --- /dev/null +++ b/listener/http/client.go @@ -0,0 +1,44 @@ +package http + +import ( + "context" + "errors" + "net" + "net/http" + "time" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +func newClient(source net.Addr, originTarget net.Addr, in chan<- C.ConnContext) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + // from http.DefaultTransport + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + DialContext: func(context context.Context, network, address string) (net.Conn, error) { + if network != "tcp" && network != "tcp4" && network != "tcp6" { + return nil, errors.New("unsupported network " + network) + } + + dstAddr := socks5.ParseAddr(address) + if dstAddr == nil { + return nil, socks5.ErrAddressNotSupported + } + + left, right := net.Pipe() + + in <- inbound.NewHTTP(dstAddr, source, originTarget, right) + + return left, nil + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} diff --git a/listener/http/hack.go b/listener/http/hack.go new file mode 100644 index 0000000..c33eb6f --- /dev/null +++ b/listener/http/hack.go @@ -0,0 +1,10 @@ +package http + +import ( + "bufio" + "net/http" + _ "unsafe" +) + +//go:linkname ReadRequest net/http.readRequest +func ReadRequest(b *bufio.Reader) (req *http.Request, err error) diff --git a/listener/http/proxy.go b/listener/http/proxy.go new file mode 100644 index 0000000..59d0831 --- /dev/null +++ b/listener/http/proxy.go @@ -0,0 +1,136 @@ +package http + +import ( + "fmt" + "net" + "net/http" + "strings" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/cache" + N "github.com/Dreamacro/clash/common/net" + C "github.com/Dreamacro/clash/constant" + authStore "github.com/Dreamacro/clash/listener/auth" + "github.com/Dreamacro/clash/log" +) + +func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache) { + client := newClient(c.RemoteAddr(), c.LocalAddr(), in) + defer client.CloseIdleConnections() + + conn := N.NewBufferedConn(c) + + keepAlive := true + trusted := cache == nil // disable authenticate if cache is nil + + for keepAlive { + request, err := ReadRequest(conn.Reader()) + if err != nil { + break + } + + request.RemoteAddr = conn.RemoteAddr().String() + + keepAlive = strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive" + + var resp *http.Response + + if !trusted { + resp = authenticate(request, cache) + + trusted = resp == nil + } + + if trusted { + if request.Method == http.MethodConnect { + // Manual writing to support CONNECT for http 1.0 (workaround for uplay client) + if _, err = fmt.Fprintf(conn, "HTTP/%d.%d %03d %s\r\n\r\n", request.ProtoMajor, request.ProtoMinor, http.StatusOK, "Connection established"); err != nil { + break // close connection + } + + in <- inbound.NewHTTPS(request, conn) + + return // hijack connection + } + + host := request.Header.Get("Host") + if host != "" { + request.Host = host + } + + request.RequestURI = "" + + if isUpgradeRequest(request) { + handleUpgrade(conn, request, in) + + return // hijack connection + } + + removeHopByHopHeaders(request.Header) + removeExtraHTTPHostPort(request) + + if request.URL.Scheme == "" || request.URL.Host == "" { + resp = responseWith(request, http.StatusBadRequest) + } else { + resp, err = client.Do(request) + if err != nil { + resp = responseWith(request, http.StatusBadGateway) + } + } + + removeHopByHopHeaders(resp.Header) + } + + if keepAlive { + resp.Header.Set("Proxy-Connection", "keep-alive") + resp.Header.Set("Connection", "keep-alive") + resp.Header.Set("Keep-Alive", "timeout=4") + } + + resp.Close = !keepAlive + + err = resp.Write(conn) + if err != nil { + break // close connection + } + } + + conn.Close() +} + +func authenticate(request *http.Request, cache *cache.LruCache) *http.Response { + authenticator := authStore.Authenticator() + if authenticator != nil { + credential := parseBasicProxyAuthorization(request) + if credential == "" { + resp := responseWith(request, http.StatusProxyAuthRequired) + resp.Header.Set("Proxy-Authenticate", "Basic") + return resp + } + + authed, exist := cache.Get(credential) + if !exist { + user, pass, err := decodeBasicProxyAuthorization(credential) + authed = err == nil && authenticator.Verify(user, pass) + cache.Set(credential, authed) + } + if !authed.(bool) { + log.Infoln("Auth failed from %s", request.RemoteAddr) + + return responseWith(request, http.StatusForbidden) + } + } + + return nil +} + +func responseWith(request *http.Request, statusCode int) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Status: http.StatusText(statusCode), + Proto: request.Proto, + ProtoMajor: request.ProtoMajor, + ProtoMinor: request.ProtoMinor, + Header: http.Header{}, + } +} diff --git a/listener/http/server.go b/listener/http/server.go new file mode 100644 index 0000000..85f6e36 --- /dev/null +++ b/listener/http/server.go @@ -0,0 +1,65 @@ +package http + +import ( + "net" + + "github.com/Dreamacro/clash/common/cache" + C "github.com/Dreamacro/clash/constant" +) + +type Listener struct { + listener net.Listener + addr string + closed bool +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + return l.listener.Close() +} + +func New(addr string, in chan<- C.ConnContext) (C.Listener, error) { + return NewWithAuthenticate(addr, in, true) +} + +func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool) (C.Listener, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + var c *cache.LruCache + if authenticate { + c = cache.New(cache.WithAge(30)) + } + + hl := &Listener{ + listener: l, + addr: addr, + } + go func() { + for { + conn, err := hl.listener.Accept() + if err != nil { + if hl.closed { + break + } + continue + } + go HandleConn(conn, in, c) + } + }() + + return hl, nil +} diff --git a/listener/http/upgrade.go b/listener/http/upgrade.go new file mode 100644 index 0000000..c6870d9 --- /dev/null +++ b/listener/http/upgrade.go @@ -0,0 +1,69 @@ +package http + +import ( + "net" + "net/http" + "strings" + + "github.com/Dreamacro/clash/adapter/inbound" + N "github.com/Dreamacro/clash/common/net" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +func isUpgradeRequest(req *http.Request) bool { + for _, header := range req.Header["Connection"] { + for _, elm := range strings.Split(header, ",") { + if strings.EqualFold(strings.TrimSpace(elm), "Upgrade") { + return true + } + } + } + + return false +} + +func handleUpgrade(conn net.Conn, request *http.Request, in chan<- C.ConnContext) { + defer conn.Close() + + removeProxyHeaders(request.Header) + removeExtraHTTPHostPort(request) + + address := request.Host + if _, _, err := net.SplitHostPort(address); err != nil { + address = net.JoinHostPort(address, "80") + } + + dstAddr := socks5.ParseAddr(address) + if dstAddr == nil { + return + } + + left, right := net.Pipe() + + in <- inbound.NewHTTP(dstAddr, conn.RemoteAddr(), conn.LocalAddr(), right) + + bufferedLeft := N.NewBufferedConn(left) + defer bufferedLeft.Close() + + err := request.Write(bufferedLeft) + if err != nil { + return + } + + resp, err := http.ReadResponse(bufferedLeft.Reader(), request) + if err != nil { + return + } + + removeProxyHeaders(resp.Header) + + err = resp.Write(conn) + if err != nil { + return + } + + if resp.StatusCode == http.StatusSwitchingProtocols { + N.Relay(bufferedLeft, conn) + } +} diff --git a/listener/http/utils.go b/listener/http/utils.go new file mode 100644 index 0000000..37cca79 --- /dev/null +++ b/listener/http/utils.go @@ -0,0 +1,80 @@ +package http + +import ( + "encoding/base64" + "errors" + "net" + "net/http" + "strings" +) + +// removeHopByHopHeaders remove Proxy-* headers +func removeProxyHeaders(header http.Header) { + header.Del("Proxy-Connection") + header.Del("Proxy-Authenticate") + header.Del("Proxy-Authorization") +} + +// removeHopByHopHeaders remove hop-by-hop header +func removeHopByHopHeaders(header http.Header) { + // Strip hop-by-hop header based on RFC: + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 + // https://www.mnot.net/blog/2011/07/11/what_proxies_must_do + + removeProxyHeaders(header) + + header.Del("TE") + header.Del("Trailers") + header.Del("Transfer-Encoding") + header.Del("Upgrade") + + connections := header.Get("Connection") + header.Del("Connection") + if len(connections) == 0 { + return + } + for _, h := range strings.Split(connections, ",") { + header.Del(strings.TrimSpace(h)) + } +} + +// removeExtraHTTPHostPort remove extra host port (example.com:80 --> example.com) +// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com) +func removeExtraHTTPHostPort(req *http.Request) { + host := req.Host + if host == "" { + host = req.URL.Host + } + + if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" { + host = pHost + } + + req.Host = host + req.URL.Host = host +} + +// parseBasicProxyAuthorization parse header Proxy-Authorization and return base64-encoded credential +func parseBasicProxyAuthorization(request *http.Request) string { + value := request.Header.Get("Proxy-Authorization") + if !strings.HasPrefix(value, "Basic ") { + return "" + } + + return value[6:] // value[len("Basic "):] +} + +// decodeBasicProxyAuthorization decode base64-encoded credential +func decodeBasicProxyAuthorization(credential string) (string, string, error) { + plain, err := base64.StdEncoding.DecodeString(credential) + if err != nil { + return "", "", err + } + + user, pass, found := strings.Cut(string(plain), ":") + if !found { + return "", "", errors.New("invalid login") + } + + return user, pass, nil +} diff --git a/listener/listener.go b/listener/listener.go new file mode 100644 index 0000000..f6b1963 --- /dev/null +++ b/listener/listener.go @@ -0,0 +1,365 @@ +package listener + +import ( + "fmt" + "net" + "strconv" + "strings" + "sync" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/config" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/http" + "github.com/Dreamacro/clash/listener/mixed" + "github.com/Dreamacro/clash/listener/redir" + "github.com/Dreamacro/clash/listener/socks" + "github.com/Dreamacro/clash/listener/tproxy" + "github.com/Dreamacro/clash/listener/tunnel" + "github.com/Dreamacro/clash/log" + + "github.com/samber/lo" +) + +var ( + allowLan = false + bindAddress = "*" + + tcpListeners = map[C.Inbound]C.Listener{} + udpListeners = map[C.Inbound]C.Listener{} + + tunnelTCPListeners = map[string]*tunnel.Listener{} + tunnelUDPListeners = map[string]*tunnel.PacketConn{} + + // lock for recreate function + recreateMux sync.Mutex + tunnelMux sync.Mutex +) + +type Ports struct { + Port int `json:"port"` + SocksPort int `json:"socks-port"` + RedirPort int `json:"redir-port"` + TProxyPort int `json:"tproxy-port"` + MixedPort int `json:"mixed-port"` +} + +var tcpListenerCreators = map[C.InboundType]tcpListenerCreator{ + C.InboundTypeHTTP: http.New, + C.InboundTypeSocks: socks.New, + C.InboundTypeRedir: redir.New, + C.InboundTypeTproxy: tproxy.New, + C.InboundTypeMixed: mixed.New, +} + +var udpListenerCreators = map[C.InboundType]udpListenerCreator{ + C.InboundTypeSocks: socks.NewUDP, + C.InboundTypeRedir: tproxy.NewUDP, + C.InboundTypeTproxy: tproxy.NewUDP, + C.InboundTypeMixed: socks.NewUDP, +} + +type ( + tcpListenerCreator func(addr string, tcpIn chan<- C.ConnContext) (C.Listener, error) + udpListenerCreator func(addr string, udpIn chan<- *inbound.PacketAdapter) (C.Listener, error) +) + +func AllowLan() bool { + return allowLan +} + +func BindAddress() string { + return bindAddress +} + +func SetAllowLan(al bool) { + allowLan = al +} + +func SetBindAddress(host string) { + bindAddress = host +} + +func createListener(inbound C.Inbound, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + addr := inbound.BindAddress + if portIsZero(addr) { + return + } + tcpCreator := tcpListenerCreators[inbound.Type] + udpCreator := udpListenerCreators[inbound.Type] + if tcpCreator == nil && udpCreator == nil { + log.Errorln("inbound type %s not support.", inbound.Type) + return + } + if tcpCreator != nil { + tcpListener, err := tcpCreator(addr, tcpIn) + if err != nil { + log.Errorln("create addr %s tcp listener error. err:%v", addr, err) + return + } + tcpListeners[inbound] = tcpListener + } + if udpCreator != nil { + udpListener, err := udpCreator(addr, udpIn) + if err != nil { + log.Errorln("create addr %s udp listener error. err:%v", addr, err) + return + } + udpListeners[inbound] = udpListener + } + log.Infoln("inbound %s create success.", inbound.ToAlias()) +} + +func closeListener(inbound C.Inbound) { + listener := tcpListeners[inbound] + if listener != nil { + if err := listener.Close(); err != nil { + log.Errorln("close tcp address `%s` error. err:%s", inbound.ToAlias(), err.Error()) + } + delete(tcpListeners, inbound) + } + listener = udpListeners[inbound] + if listener != nil { + if err := listener.Close(); err != nil { + log.Errorln("close udp address `%s` error. err:%s", inbound.ToAlias(), err.Error()) + } + delete(udpListeners, inbound) + } +} + +func getNeedCloseAndCreateInbound(originInbounds []C.Inbound, newInbounds []C.Inbound) ([]C.Inbound, []C.Inbound) { + needCloseMap := map[C.Inbound]bool{} + needClose := []C.Inbound{} + needCreate := []C.Inbound{} + + for _, inbound := range originInbounds { + needCloseMap[inbound] = true + } + for _, inbound := range newInbounds { + if needCloseMap[inbound] { + delete(needCloseMap, inbound) + } else { + needCreate = append(needCreate, inbound) + } + } + for inbound := range needCloseMap { + needClose = append(needClose, inbound) + } + return needClose, needCreate +} + +// only recreate inbound config listener +func ReCreateListeners(inbounds []C.Inbound, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + newInbounds := []C.Inbound{} + newInbounds = append(newInbounds, inbounds...) + for _, inbound := range getInbounds() { + if inbound.IsFromPortCfg { + newInbounds = append(newInbounds, inbound) + } + } + reCreateListeners(newInbounds, tcpIn, udpIn) +} + +// only recreate ports config listener +func ReCreatePortsListeners(ports Ports, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + newInbounds := []C.Inbound{} + newInbounds = append(newInbounds, GetInbounds()...) + newInbounds = addPortInbound(newInbounds, C.InboundTypeHTTP, ports.Port) + newInbounds = addPortInbound(newInbounds, C.InboundTypeSocks, ports.SocksPort) + newInbounds = addPortInbound(newInbounds, C.InboundTypeRedir, ports.RedirPort) + newInbounds = addPortInbound(newInbounds, C.InboundTypeTproxy, ports.TProxyPort) + newInbounds = addPortInbound(newInbounds, C.InboundTypeMixed, ports.MixedPort) + reCreateListeners(newInbounds, tcpIn, udpIn) +} + +func addPortInbound(inbounds []C.Inbound, inboundType C.InboundType, port int) []C.Inbound { + if port != 0 { + inbounds = append(inbounds, C.Inbound{ + Type: inboundType, + BindAddress: genAddr(bindAddress, port, allowLan), + IsFromPortCfg: true, + }) + } + return inbounds +} + +func reCreateListeners(inbounds []C.Inbound, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + recreateMux.Lock() + defer recreateMux.Unlock() + needClose, needCreate := getNeedCloseAndCreateInbound(getInbounds(), inbounds) + for _, inbound := range needClose { + closeListener(inbound) + } + for _, inbound := range needCreate { + createListener(inbound, tcpIn, udpIn) + } +} + +func PatchTunnel(tunnels []config.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + tunnelMux.Lock() + defer tunnelMux.Unlock() + + type addrProxy struct { + network string + addr string + target string + proxy string + } + + tcpOld := lo.Map( + lo.Keys(tunnelTCPListeners), + func(key string, _ int) addrProxy { + parts := strings.Split(key, "/") + return addrProxy{ + network: "tcp", + addr: parts[0], + target: parts[1], + proxy: parts[2], + } + }, + ) + udpOld := lo.Map( + lo.Keys(tunnelUDPListeners), + func(key string, _ int) addrProxy { + parts := strings.Split(key, "/") + return addrProxy{ + network: "udp", + addr: parts[0], + target: parts[1], + proxy: parts[2], + } + }, + ) + oldElm := lo.Union(tcpOld, udpOld) + + newElm := lo.FlatMap( + tunnels, + func(tunnel config.Tunnel, _ int) []addrProxy { + return lo.Map( + tunnel.Network, + func(network string, _ int) addrProxy { + return addrProxy{ + network: network, + addr: tunnel.Address, + target: tunnel.Target, + proxy: tunnel.Proxy, + } + }, + ) + }, + ) + + needClose, needCreate := lo.Difference(oldElm, newElm) + + for _, elm := range needClose { + key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy) + if elm.network == "tcp" { + tunnelTCPListeners[key].Close() + delete(tunnelTCPListeners, key) + } else { + tunnelUDPListeners[key].Close() + delete(tunnelUDPListeners, key) + } + } + + for _, elm := range needCreate { + key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy) + if elm.network == "tcp" { + l, err := tunnel.New(elm.addr, elm.target, elm.proxy, tcpIn) + if err != nil { + log.Errorln("Start tunnel %s error: %s", elm.target, err.Error()) + continue + } + tunnelTCPListeners[key] = l + log.Infoln("Tunnel(tcp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelTCPListeners[key].Address()) + } else { + l, err := tunnel.NewUDP(elm.addr, elm.target, elm.proxy, udpIn) + if err != nil { + log.Errorln("Start tunnel %s error: %s", elm.target, err.Error()) + continue + } + tunnelUDPListeners[key] = l + log.Infoln("Tunnel(udp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelUDPListeners[key].Address()) + } + } +} + +func GetInbounds() []C.Inbound { + return lo.Filter(getInbounds(), func(inbound C.Inbound, idx int) bool { + return !inbound.IsFromPortCfg + }) +} + +// GetInbounds return the inbounds of proxy servers +func getInbounds() []C.Inbound { + var inbounds []C.Inbound + for inbound := range tcpListeners { + inbounds = append(inbounds, inbound) + } + for inbound := range udpListeners { + if _, ok := tcpListeners[inbound]; !ok { + inbounds = append(inbounds, inbound) + } + } + return inbounds +} + +// GetPorts return the ports of proxy servers +func GetPorts() *Ports { + ports := &Ports{} + for _, inbound := range getInbounds() { + fillPort(inbound, ports) + } + return ports +} + +func fillPort(inbound C.Inbound, ports *Ports) { + if inbound.IsFromPortCfg { + port := getPort(inbound.BindAddress) + switch inbound.Type { + case C.InboundTypeHTTP: + ports.Port = port + case C.InboundTypeSocks: + ports.SocksPort = port + case C.InboundTypeTproxy: + ports.TProxyPort = port + case C.InboundTypeRedir: + ports.RedirPort = port + case C.InboundTypeMixed: + ports.MixedPort = port + default: + // do nothing + } + } +} + +func portIsZero(addr string) bool { + _, port, err := net.SplitHostPort(addr) + if port == "0" || port == "" || err != nil { + return true + } + return false +} + +func genAddr(host string, port int, allowLan bool) string { + if allowLan { + if host == "*" { + return fmt.Sprintf(":%d", port) + } + return fmt.Sprintf("%s:%d", host, port) + } + + return fmt.Sprintf("127.0.0.1:%d", port) +} + +func getPort(addr string) int { + _, portStr, err := net.SplitHostPort(addr) + if err != nil { + return 0 + } + port, err := strconv.Atoi(portStr) + if err != nil { + return 0 + } + return port +} diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go new file mode 100644 index 0000000..c8ff9e9 --- /dev/null +++ b/listener/mixed/mixed.go @@ -0,0 +1,82 @@ +package mixed + +import ( + "net" + + "github.com/Dreamacro/clash/common/cache" + N "github.com/Dreamacro/clash/common/net" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/http" + "github.com/Dreamacro/clash/listener/socks" + "github.com/Dreamacro/clash/transport/socks4" + "github.com/Dreamacro/clash/transport/socks5" +) + +type Listener struct { + listener net.Listener + addr string + cache *cache.LruCache + closed bool +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + return l.listener.Close() +} + +func New(addr string, in chan<- C.ConnContext) (C.Listener, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + ml := &Listener{ + listener: l, + addr: addr, + cache: cache.New(cache.WithAge(30)), + } + go func() { + for { + c, err := ml.listener.Accept() + if err != nil { + if ml.closed { + break + } + continue + } + go handleConn(c, in, ml.cache) + } + }() + + return ml, nil +} + +func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.LruCache) { + conn.(*net.TCPConn).SetKeepAlive(true) + + bufConn := N.NewBufferedConn(conn) + head, err := bufConn.Peek(1) + if err != nil { + return + } + + switch head[0] { + case socks4.Version: + socks.HandleSocks4(bufConn, in) + case socks5.Version: + socks.HandleSocks5(bufConn, in) + default: + http.HandleConn(bufConn, in, cache) + } +} diff --git a/listener/redir/tcp.go b/listener/redir/tcp.go new file mode 100644 index 0000000..5f4b903 --- /dev/null +++ b/listener/redir/tcp.go @@ -0,0 +1,66 @@ +package redir + +import ( + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" +) + +type Listener struct { + listener net.Listener + addr string + closed bool +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + return l.listener.Close() +} + +func New(addr string, in chan<- C.ConnContext) (C.Listener, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + rl := &Listener{ + listener: l, + addr: addr, + } + + go func() { + for { + c, err := l.Accept() + if err != nil { + if rl.closed { + break + } + continue + } + go handleRedir(c, in) + } + }() + + return rl, nil +} + +func handleRedir(conn net.Conn, in chan<- C.ConnContext) { + target, err := parserPacket(conn) + if err != nil { + conn.Close() + return + } + conn.(*net.TCPConn).SetKeepAlive(true) + in <- inbound.NewSocket(target, conn, C.REDIR) +} diff --git a/listener/redir/tcp_darwin.go b/listener/redir/tcp_darwin.go new file mode 100644 index 0000000..5a2f331 --- /dev/null +++ b/listener/redir/tcp_darwin.go @@ -0,0 +1,58 @@ +package redir + +import ( + "net" + "syscall" + "unsafe" + + "github.com/Dreamacro/clash/transport/socks5" +) + +func parserPacket(c net.Conn) (socks5.Addr, error) { + const ( + PfInout = 0 + PfIn = 1 + PfOut = 2 + IOCOut = 0x40000000 + IOCIn = 0x80000000 + IOCInOut = IOCIn | IOCOut + IOCPARMMask = 0x1FFF + LEN = 4*16 + 4*4 + 4*1 + // #define _IOC(inout,group,num,len) (inout | ((len & IOCPARMMask) << 16) | ((group) << 8) | (num)) + // #define _IOWR(g,n,t) _IOC(IOCInOut, (g), (n), sizeof(t)) + // #define DIOCNATLOOK _IOWR('D', 23, struct pfioc_natlook) + DIOCNATLOOK = IOCInOut | ((LEN & IOCPARMMask) << 16) | ('D' << 8) | 23 + ) + + fd, err := syscall.Open("/dev/pf", 0, syscall.O_RDONLY) + if err != nil { + return nil, err + } + defer syscall.Close(fd) + + nl := struct { // struct pfioc_natlook + saddr, daddr, rsaddr, rdaddr [16]byte + sxport, dxport, rsxport, rdxport [4]byte + af, proto, protoVariant, direction uint8 + }{ + af: syscall.AF_INET, + proto: syscall.IPPROTO_TCP, + direction: PfOut, + } + saddr := c.RemoteAddr().(*net.TCPAddr) + daddr := c.LocalAddr().(*net.TCPAddr) + copy(nl.saddr[:], saddr.IP) + copy(nl.daddr[:], daddr.IP) + nl.sxport[0], nl.sxport[1] = byte(saddr.Port>>8), byte(saddr.Port) + nl.dxport[0], nl.dxport[1] = byte(daddr.Port>>8), byte(daddr.Port) + + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 { + return nil, errno + } + + addr := make([]byte, 1+net.IPv4len+2) + addr[0] = socks5.AtypIPv4 + copy(addr[1:1+net.IPv4len], nl.rdaddr[:4]) + copy(addr[1+net.IPv4len:], nl.rdxport[:2]) + return addr, nil +} diff --git a/listener/redir/tcp_freebsd.go b/listener/redir/tcp_freebsd.go new file mode 100644 index 0000000..6ecb249 --- /dev/null +++ b/listener/redir/tcp_freebsd.go @@ -0,0 +1,66 @@ +package redir + +import ( + "encoding/binary" + "errors" + "net" + "net/netip" + "syscall" + "unsafe" + + "github.com/Dreamacro/clash/transport/socks5" + + "golang.org/x/sys/unix" +) + +const ( + SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h + IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h +) + +func parserPacket(conn net.Conn) (socks5.Addr, error) { + c, ok := conn.(*net.TCPConn) + if !ok { + return nil, errors.New("only work with TCP connection") + } + + rc, err := c.SyscallConn() + if err != nil { + return nil, err + } + + var addr netip.AddrPort + + rc.Control(func(fd uintptr) { + if ip4 := c.LocalAddr().(*net.TCPAddr).IP.To4(); ip4 != nil { + addr, err = getorigdst(fd) + } else { + addr, err = getorigdst6(fd) + } + }) + + return socks5.AddrFromStdAddrPort(addr), err +} + +// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c +func getorigdst(fd uintptr) (netip.AddrPort, error) { + addr := unix.RawSockaddrInet4{} + size := uint32(unsafe.Sizeof(addr)) + _, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0) + if err != 0 { + return netip.AddrPort{}, err + } + port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) + return netip.AddrPortFrom(netip.AddrFrom4(addr.Addr), port), nil +} + +func getorigdst6(fd uintptr) (netip.AddrPort, error) { + addr := unix.RawSockaddrInet6{} + size := uint32(unsafe.Sizeof(addr)) + _, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0) + if err != 0 { + return netip.AddrPort{}, err + } + port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) + return netip.AddrPortFrom(netip.AddrFrom16(addr.Addr), port), nil +} diff --git a/listener/redir/tcp_linux.go b/listener/redir/tcp_linux.go new file mode 100644 index 0000000..b65c34e --- /dev/null +++ b/listener/redir/tcp_linux.go @@ -0,0 +1,64 @@ +package redir + +import ( + "encoding/binary" + "errors" + "net" + "net/netip" + "syscall" + "unsafe" + + "github.com/Dreamacro/clash/transport/socks5" + + "golang.org/x/sys/unix" +) + +const ( + SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h + IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h +) + +func parserPacket(conn net.Conn) (socks5.Addr, error) { + c, ok := conn.(*net.TCPConn) + if !ok { + return nil, errors.New("only work with TCP connection") + } + + rc, err := c.SyscallConn() + if err != nil { + return nil, err + } + + var addr netip.AddrPort + + rc.Control(func(fd uintptr) { + if ip4 := c.LocalAddr().(*net.TCPAddr).IP.To4(); ip4 != nil { + addr, err = getorigdst(fd) + } else { + addr, err = getorigdst6(fd) + } + }) + + return socks5.AddrFromStdAddrPort(addr), err +} + +// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c +func getorigdst(fd uintptr) (netip.AddrPort, error) { + addr := unix.RawSockaddrInet4{} + size := uint32(unsafe.Sizeof(addr)) + if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0); err != nil { + return netip.AddrPort{}, err + } + port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) + return netip.AddrPortFrom(netip.AddrFrom4(addr.Addr), port), nil +} + +func getorigdst6(fd uintptr) (netip.AddrPort, error) { + addr := unix.RawSockaddrInet6{} + size := uint32(unsafe.Sizeof(addr)) + if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), 0); err != nil { + return netip.AddrPort{}, err + } + port := binary.BigEndian.Uint16((*(*[2]byte)(unsafe.Pointer(&addr.Port)))[:]) + return netip.AddrPortFrom(netip.AddrFrom16(addr.Addr), port), nil +} diff --git a/listener/redir/tcp_linux_386.go b/listener/redir/tcp_linux_386.go new file mode 100644 index 0000000..32f692d --- /dev/null +++ b/listener/redir/tcp_linux_386.go @@ -0,0 +1,17 @@ +package redir + +import ( + "syscall" + "unsafe" +) + +const GETSOCKOPT = 15 // https://golang.org/src/syscall/syscall_linux_386.go#L183 + +func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error { + var a [6]uintptr + a[0], a[1], a[2], a[3], a[4], a[5] = a0, a1, a2, a3, a4, a5 + if _, _, errno := syscall.Syscall6(syscall.SYS_SOCKETCALL, call, uintptr(unsafe.Pointer(&a)), 0, 0, 0, 0); errno != 0 { + return errno + } + return nil +} diff --git a/listener/redir/tcp_linux_other.go b/listener/redir/tcp_linux_other.go new file mode 100644 index 0000000..b8c7de8 --- /dev/null +++ b/listener/redir/tcp_linux_other.go @@ -0,0 +1,14 @@ +//go:build linux && !386 + +package redir + +import "syscall" + +const GETSOCKOPT = syscall.SYS_GETSOCKOPT + +func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error { + if _, _, errno := syscall.Syscall6(call, a0, a1, a2, a3, a4, a5); errno != 0 { + return errno + } + return nil +} diff --git a/listener/redir/tcp_other.go b/listener/redir/tcp_other.go new file mode 100644 index 0000000..a01550c --- /dev/null +++ b/listener/redir/tcp_other.go @@ -0,0 +1,14 @@ +//go:build !darwin && !linux && !freebsd + +package redir + +import ( + "errors" + "net" + + "github.com/Dreamacro/clash/transport/socks5" +) + +func parserPacket(conn net.Conn) (socks5.Addr, error) { + return nil, errors.New("system not support yet") +} diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go new file mode 100644 index 0000000..bdf7501 --- /dev/null +++ b/listener/socks/tcp.go @@ -0,0 +1,103 @@ +package socks + +import ( + "io" + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + N "github.com/Dreamacro/clash/common/net" + C "github.com/Dreamacro/clash/constant" + authStore "github.com/Dreamacro/clash/listener/auth" + "github.com/Dreamacro/clash/transport/socks4" + "github.com/Dreamacro/clash/transport/socks5" +) + +type Listener struct { + listener net.Listener + addr string + closed bool +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + return l.listener.Close() +} + +func New(addr string, in chan<- C.ConnContext) (C.Listener, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + sl := &Listener{ + listener: l, + addr: addr, + } + go func() { + for { + c, err := l.Accept() + if err != nil { + if sl.closed { + break + } + continue + } + go handleSocks(c, in) + } + }() + + return sl, nil +} + +func handleSocks(conn net.Conn, in chan<- C.ConnContext) { + conn.(*net.TCPConn).SetKeepAlive(true) + bufConn := N.NewBufferedConn(conn) + head, err := bufConn.Peek(1) + if err != nil { + conn.Close() + return + } + + switch head[0] { + case socks4.Version: + HandleSocks4(bufConn, in) + case socks5.Version: + HandleSocks5(bufConn, in) + default: + conn.Close() + } +} + +func HandleSocks4(conn net.Conn, in chan<- C.ConnContext) { + addr, _, err := socks4.ServerHandshake(conn, authStore.Authenticator()) + if err != nil { + conn.Close() + return + } + in <- inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4) +} + +func HandleSocks5(conn net.Conn, in chan<- C.ConnContext) { + target, command, err := socks5.ServerHandshake(conn, authStore.Authenticator()) + if err != nil { + conn.Close() + return + } + if command == socks5.CmdUDPAssociate { + defer conn.Close() + io.Copy(io.Discard, conn) + return + } + in <- inbound.NewSocket(target, conn, C.SOCKS5) +} diff --git a/listener/socks/udp.go b/listener/socks/udp.go new file mode 100644 index 0000000..dd230f8 --- /dev/null +++ b/listener/socks/udp.go @@ -0,0 +1,85 @@ +package socks + +import ( + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/common/sockopt" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/socks5" +) + +type UDPListener struct { + packetConn net.PacketConn + addr string + closed bool +} + +// RawAddress implements C.Listener +func (l *UDPListener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *UDPListener) Address() string { + return l.packetConn.LocalAddr().String() +} + +// Close implements C.Listener +func (l *UDPListener) Close() error { + l.closed = true + return l.packetConn.Close() +} + +func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (C.Listener, error) { + l, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + if err := sockopt.UDPReuseaddr(l.(*net.UDPConn)); err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + } + + sl := &UDPListener{ + packetConn: l, + addr: addr, + } + go func() { + for { + buf := pool.Get(pool.UDPBufferSize) + n, remoteAddr, err := l.ReadFrom(buf) + if err != nil { + pool.Put(buf) + if sl.closed { + break + } + continue + } + handleSocksUDP(l, in, buf[:n], remoteAddr) + } + }() + + return sl, nil +} + +func handleSocksUDP(pc net.PacketConn, in chan<- *inbound.PacketAdapter, buf []byte, addr net.Addr) { + target, payload, err := socks5.DecodeUDPPacket(buf) + if err != nil { + // Unresolved UDP packet, return buffer to the pool + pool.Put(buf) + return + } + packet := &packet{ + pc: pc, + rAddr: addr, + payload: payload, + bufRef: buf, + } + select { + case in <- inbound.NewPacket(target, pc.LocalAddr(), packet, C.SOCKS5): + default: + } +} diff --git a/listener/socks/utils.go b/listener/socks/utils.go new file mode 100644 index 0000000..28dfef7 --- /dev/null +++ b/listener/socks/utils.go @@ -0,0 +1,37 @@ +package socks + +import ( + "net" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/transport/socks5" +) + +type packet struct { + pc net.PacketConn + rAddr net.Addr + payload []byte + bufRef []byte +} + +func (c *packet) Data() []byte { + return c.payload +} + +// WriteBack write UDP packet with source(ip, port) = `addr` +func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { + packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) + if err != nil { + return + } + return c.pc.WriteTo(packet, c.rAddr) +} + +// LocalAddr returns the source IP/Port of UDP Packet +func (c *packet) LocalAddr() net.Addr { + return c.rAddr +} + +func (c *packet) Drop() { + pool.Put(c.bufRef) +} diff --git a/listener/tproxy/packet.go b/listener/tproxy/packet.go new file mode 100644 index 0000000..9299df9 --- /dev/null +++ b/listener/tproxy/packet.go @@ -0,0 +1,38 @@ +package tproxy + +import ( + "net" + "net/netip" + + "github.com/Dreamacro/clash/common/pool" +) + +type packet struct { + lAddr netip.AddrPort + buf []byte +} + +func (c *packet) Data() []byte { + return c.buf +} + +// WriteBack opens a new socket binding `addr` to write UDP packet back +func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { + tc, err := dialUDP("udp", addr.(*net.UDPAddr).AddrPort(), c.lAddr) + if err != nil { + n = 0 + return + } + n, err = tc.Write(b) + tc.Close() + return +} + +// LocalAddr returns the source IP/Port of UDP Packet +func (c *packet) LocalAddr() net.Addr { + return &net.UDPAddr{IP: c.lAddr.Addr().AsSlice(), Port: int(c.lAddr.Port()), Zone: c.lAddr.Addr().Zone()} +} + +func (c *packet) Drop() { + pool.Put(c.buf) +} diff --git a/listener/tproxy/setsockopt_linux.go b/listener/tproxy/setsockopt_linux.go new file mode 100644 index 0000000..06f3e1c --- /dev/null +++ b/listener/tproxy/setsockopt_linux.go @@ -0,0 +1,40 @@ +//go:build linux + +package tproxy + +import ( + "net" + "syscall" +) + +func setsockopt(rc syscall.RawConn, addr string) error { + isIPv6 := true + host, _, err := net.SplitHostPort(addr) + if err != nil { + return err + } + ip := net.ParseIP(host) + if ip != nil && ip.To4() != nil { + isIPv6 = false + } + + rc.Control(func(fd uintptr) { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + + if err == nil { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) + } + if err == nil && isIPv6 { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, IPV6_TRANSPARENT, 1) + } + + if err == nil { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) + } + if err == nil && isIPv6 { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) + } + }) + + return err +} diff --git a/listener/tproxy/setsockopt_other.go b/listener/tproxy/setsockopt_other.go new file mode 100644 index 0000000..9ba06f9 --- /dev/null +++ b/listener/tproxy/setsockopt_other.go @@ -0,0 +1,12 @@ +//go:build !linux + +package tproxy + +import ( + "errors" + "syscall" +) + +func setsockopt(rc syscall.RawConn, addr string) error { + return errors.New("not supported on current platform") +} diff --git a/listener/tproxy/tcp.go b/listener/tproxy/tcp.go new file mode 100644 index 0000000..c6365e6 --- /dev/null +++ b/listener/tproxy/tcp.go @@ -0,0 +1,75 @@ +package tproxy + +import ( + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type Listener struct { + listener net.Listener + addr string + closed bool +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + return l.listener.Close() +} + +func (l *Listener) handleTProxy(conn net.Conn, in chan<- C.ConnContext) { + target := socks5.ParseAddrToSocksAddr(conn.LocalAddr()) + conn.(*net.TCPConn).SetKeepAlive(true) + in <- inbound.NewSocket(target, conn, C.TPROXY) +} + +func New(addr string, in chan<- C.ConnContext) (C.Listener, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + tl := l.(*net.TCPListener) + rc, err := tl.SyscallConn() + if err != nil { + return nil, err + } + + err = setsockopt(rc, addr) + if err != nil { + return nil, err + } + + rl := &Listener{ + listener: l, + addr: addr, + } + + go func() { + for { + c, err := l.Accept() + if err != nil { + if rl.closed { + break + } + continue + } + go rl.handleTProxy(c, in) + } + }() + + return rl, nil +} diff --git a/listener/tproxy/udp.go b/listener/tproxy/udp.go new file mode 100644 index 0000000..3c40360 --- /dev/null +++ b/listener/tproxy/udp.go @@ -0,0 +1,97 @@ +package tproxy + +import ( + "net" + "net/netip" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/pool" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type UDPListener struct { + packetConn net.PacketConn + addr string + closed bool +} + +// RawAddress implements C.Listener +func (l *UDPListener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *UDPListener) Address() string { + return l.packetConn.LocalAddr().String() +} + +// Close implements C.Listener +func (l *UDPListener) Close() error { + l.closed = true + return l.packetConn.Close() +} + +func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (C.Listener, error) { + l, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + rl := &UDPListener{ + packetConn: l, + addr: addr, + } + + c := l.(*net.UDPConn) + + rc, err := c.SyscallConn() + if err != nil { + return nil, err + } + + err = setsockopt(rc, addr) + if err != nil { + return nil, err + } + + go func() { + oob := make([]byte, 1024) + for { + buf := pool.Get(pool.UDPBufferSize) + n, oobn, _, lAddr, err := c.ReadMsgUDPAddrPort(buf, oob) + if err != nil { + pool.Put(buf) + if rl.closed { + break + } + continue + } + + rAddr, err := getOrigDst(oob[:oobn]) + if err != nil { + continue + } + + if rAddr.Addr().Is4() { + // try to unmap 4in6 address + lAddr = netip.AddrPortFrom(lAddr.Addr().Unmap(), lAddr.Port()) + } + handlePacketConn(in, buf[:n], lAddr, rAddr) + } + }() + + return rl, nil +} + +func handlePacketConn(in chan<- *inbound.PacketAdapter, buf []byte, lAddr, rAddr netip.AddrPort) { + target := socks5.AddrFromStdAddrPort(rAddr) + pkt := &packet{ + lAddr: lAddr, + buf: buf, + } + select { + case in <- inbound.NewPacket(target, target.UDPAddr(), pkt, C.TPROXY): + default: + } +} diff --git a/listener/tproxy/udp_linux.go b/listener/tproxy/udp_linux.go new file mode 100644 index 0000000..472a23d --- /dev/null +++ b/listener/tproxy/udp_linux.go @@ -0,0 +1,124 @@ +//go:build linux + +package tproxy + +import ( + "fmt" + "net" + "net/netip" + "os" + "strconv" + "syscall" + + "golang.org/x/sys/unix" +) + +const ( + IPV6_TRANSPARENT = 0x4b + IPV6_RECVORIGDSTADDR = 0x4a +) + +// dialUDP acts like net.DialUDP for transparent proxy. +// It binds to a non-local address(`lAddr`). +func dialUDP(network string, lAddr, rAddr netip.AddrPort) (uc *net.UDPConn, err error) { + rSockAddr, err := udpAddrToSockAddr(rAddr) + if err != nil { + return nil, err + } + + lSockAddr, err := udpAddrToSockAddr(lAddr) + if err != nil { + return nil, err + } + + fd, err := syscall.Socket(udpAddrFamily(network, lAddr, rAddr), syscall.SOCK_DGRAM, 0) + if err != nil { + return nil, err + } + + defer func() { + if err != nil { + syscall.Close(fd) + } + }() + + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + return nil, err + } + + if err = syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { + return nil, err + } + + if err = syscall.Bind(fd, lSockAddr); err != nil { + return nil, err + } + + if err = syscall.Connect(fd, rSockAddr); err != nil { + return nil, err + } + + fdFile := os.NewFile(uintptr(fd), fmt.Sprintf("net-udp-dial-%s", rAddr.String())) + defer fdFile.Close() + + c, err := net.FileConn(fdFile) + if err != nil { + return nil, err + } + + return c.(*net.UDPConn), nil +} + +func udpAddrToSockAddr(addr netip.AddrPort) (syscall.Sockaddr, error) { + if addr.Addr().Is4() { + return &syscall.SockaddrInet4{Addr: addr.Addr().As4(), Port: int(addr.Port())}, nil + } + + zoneID, err := strconv.ParseUint(addr.Addr().Zone(), 10, 32) + if err != nil { + zoneID = 0 + } + + return &syscall.SockaddrInet6{Addr: addr.Addr().As16(), Port: int(addr.Port()), ZoneId: uint32(zoneID)}, nil +} + +func udpAddrFamily(net string, lAddr, rAddr netip.AddrPort) int { + switch net[len(net)-1] { + case '4': + return syscall.AF_INET + case '6': + return syscall.AF_INET6 + } + + if lAddr.Addr().Is4() && rAddr.Addr().Is4() { + return syscall.AF_INET + } + return syscall.AF_INET6 +} + +func getOrigDst(oob []byte) (netip.AddrPort, error) { + // oob contains socket control messages which we need to parse. + scms, err := unix.ParseSocketControlMessage(oob) + if err != nil { + return netip.AddrPort{}, fmt.Errorf("parse control message: %w", err) + } + + // retrieve the destination address from the SCM. + sa, err := unix.ParseOrigDstAddr(&scms[0]) + if err != nil { + return netip.AddrPort{}, fmt.Errorf("retrieve destination: %w", err) + } + + // encode the destination address into a cmsg. + var rAddr netip.AddrPort + switch v := sa.(type) { + case *unix.SockaddrInet4: + rAddr = netip.AddrPortFrom(netip.AddrFrom4(v.Addr), uint16(v.Port)) + case *unix.SockaddrInet6: + rAddr = netip.AddrPortFrom(netip.AddrFrom16(v.Addr), uint16(v.Port)) + default: + return netip.AddrPort{}, fmt.Errorf("unsupported address type: %T", v) + } + + return rAddr, nil +} diff --git a/listener/tproxy/udp_other.go b/listener/tproxy/udp_other.go new file mode 100644 index 0000000..b35b07d --- /dev/null +++ b/listener/tproxy/udp_other.go @@ -0,0 +1,17 @@ +//go:build !linux + +package tproxy + +import ( + "errors" + "net" + "net/netip" +) + +func getOrigDst(oob []byte) (netip.AddrPort, error) { + return netip.AddrPort{}, errors.New("UDP redir not supported on current platform") +} + +func dialUDP(network string, lAddr, rAddr netip.AddrPort) (*net.UDPConn, error) { + return nil, errors.New("UDP redir not supported on current platform") +} diff --git a/listener/tunnel/packet.go b/listener/tunnel/packet.go new file mode 100644 index 0000000..0ade972 --- /dev/null +++ b/listener/tunnel/packet.go @@ -0,0 +1,31 @@ +package tunnel + +import ( + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +type packet struct { + pc net.PacketConn + rAddr net.Addr + payload []byte +} + +func (c *packet) Data() []byte { + return c.payload +} + +// WriteBack write UDP packet with source(ip, port) = `addr` +func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { + return c.pc.WriteTo(b, c.rAddr) +} + +// LocalAddr returns the source IP/Port of UDP Packet +func (c *packet) LocalAddr() net.Addr { + return c.rAddr +} + +func (c *packet) Drop() { + pool.Put(c.payload) +} diff --git a/listener/tunnel/tcp.go b/listener/tunnel/tcp.go new file mode 100644 index 0000000..4ae5865 --- /dev/null +++ b/listener/tunnel/tcp.go @@ -0,0 +1,75 @@ +package tunnel + +import ( + "fmt" + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type Listener struct { + listener net.Listener + addr string + target socks5.Addr + proxy string + closed bool +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + return l.listener.Close() +} + +func (l *Listener) handleTCP(conn net.Conn, in chan<- C.ConnContext) { + conn.(*net.TCPConn).SetKeepAlive(true) + ctx := inbound.NewSocket(l.target, conn, C.TUNNEL) + ctx.Metadata().SpecialProxy = l.proxy + in <- ctx +} + +func New(addr, target, proxy string, in chan<- C.ConnContext) (*Listener, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + targetAddr := socks5.ParseAddr(target) + if targetAddr == nil { + return nil, fmt.Errorf("invalid target address %s", target) + } + + rl := &Listener{ + listener: l, + target: targetAddr, + proxy: proxy, + addr: addr, + } + + go func() { + for { + c, err := l.Accept() + if err != nil { + if rl.closed { + break + } + continue + } + go rl.handleTCP(c, in) + } + }() + + return rl, nil +} diff --git a/listener/tunnel/udp.go b/listener/tunnel/udp.go new file mode 100644 index 0000000..1a658ba --- /dev/null +++ b/listener/tunnel/udp.go @@ -0,0 +1,85 @@ +package tunnel + +import ( + "fmt" + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/pool" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type PacketConn struct { + conn net.PacketConn + addr string + target socks5.Addr + proxy string + closed bool +} + +// RawAddress implements C.Listener +func (l *PacketConn) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *PacketConn) Address() string { + return l.conn.LocalAddr().String() +} + +// Close implements C.Listener +func (l *PacketConn) Close() error { + l.closed = true + return l.conn.Close() +} + +func NewUDP(addr, target, proxy string, in chan<- *inbound.PacketAdapter) (*PacketConn, error) { + l, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + targetAddr := socks5.ParseAddr(target) + if targetAddr == nil { + return nil, fmt.Errorf("invalid target address %s", target) + } + + sl := &PacketConn{ + conn: l, + target: targetAddr, + proxy: proxy, + addr: addr, + } + go func() { + for { + buf := pool.Get(pool.UDPBufferSize) + n, remoteAddr, err := l.ReadFrom(buf) + if err != nil { + pool.Put(buf) + if sl.closed { + break + } + continue + } + sl.handleUDP(l, in, buf[:n], remoteAddr) + } + }() + + return sl, nil +} + +func (l *PacketConn) handleUDP(pc net.PacketConn, in chan<- *inbound.PacketAdapter, buf []byte, addr net.Addr) { + packet := &packet{ + pc: pc, + rAddr: addr, + payload: buf, + } + + ctx := inbound.NewPacket(l.target, pc.LocalAddr(), packet, C.TUNNEL) + ctx.Metadata().SpecialProxy = l.proxy + select { + case in <- ctx: + default: + } +} diff --git a/log/level.go b/log/level.go new file mode 100644 index 0000000..ea06ee4 --- /dev/null +++ b/log/level.go @@ -0,0 +1,76 @@ +package log + +import ( + "encoding/json" + "errors" +) + +// LogLevelMapping is a mapping for LogLevel enum +var LogLevelMapping = map[string]LogLevel{ + ERROR.String(): ERROR, + WARNING.String(): WARNING, + INFO.String(): INFO, + DEBUG.String(): DEBUG, + SILENT.String(): SILENT, +} + +const ( + DEBUG LogLevel = iota + INFO + WARNING + ERROR + SILENT +) + +type LogLevel int + +// UnmarshalYAML unserialize LogLevel with yaml +func (l *LogLevel) UnmarshalYAML(unmarshal func(any) error) error { + var tp string + unmarshal(&tp) + level, exist := LogLevelMapping[tp] + if !exist { + return errors.New("invalid mode") + } + *l = level + return nil +} + +// UnmarshalJSON unserialize LogLevel with json +func (l *LogLevel) UnmarshalJSON(data []byte) error { + var tp string + json.Unmarshal(data, &tp) + level, exist := LogLevelMapping[tp] + if !exist { + return errors.New("invalid mode") + } + *l = level + return nil +} + +// MarshalJSON serialize LogLevel with json +func (l LogLevel) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +// MarshalYAML serialize LogLevel with yaml +func (l LogLevel) MarshalYAML() (any, error) { + return l.String(), nil +} + +func (l LogLevel) String() string { + switch l { + case INFO: + return "info" + case WARNING: + return "warning" + case ERROR: + return "error" + case DEBUG: + return "debug" + case SILENT: + return "silent" + default: + return "unknown" + } +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..64c69a2 --- /dev/null +++ b/log/log.go @@ -0,0 +1,101 @@ +package log + +import ( + "fmt" + "os" + + "github.com/Dreamacro/clash/common/observable" + + log "github.com/sirupsen/logrus" +) + +var ( + logCh = make(chan any) + source = observable.NewObservable(logCh) + level = INFO +) + +func init() { + log.SetOutput(os.Stdout) + log.SetLevel(log.DebugLevel) +} + +type Event struct { + LogLevel LogLevel + Payload string +} + +func (e *Event) Type() string { + return e.LogLevel.String() +} + +func Infoln(format string, v ...any) { + event := newLog(INFO, format, v...) + logCh <- event + print(event) +} + +func Warnln(format string, v ...any) { + event := newLog(WARNING, format, v...) + logCh <- event + print(event) +} + +func Errorln(format string, v ...any) { + event := newLog(ERROR, format, v...) + logCh <- event + print(event) +} + +func Debugln(format string, v ...any) { + event := newLog(DEBUG, format, v...) + logCh <- event + print(event) +} + +func Fatalln(format string, v ...any) { + log.Fatalf(format, v...) +} + +func Subscribe() observable.Subscription { + sub, _ := source.Subscribe() + return sub +} + +func UnSubscribe(sub observable.Subscription) { + source.UnSubscribe(sub) +} + +func Level() LogLevel { + return level +} + +func SetLevel(newLevel LogLevel) { + level = newLevel +} + +func print(data Event) { + if data.LogLevel < level { + return + } + + switch data.LogLevel { + case INFO: + log.Infoln(data.Payload) + case WARNING: + log.Warnln(data.Payload) + case ERROR: + log.Errorln(data.Payload) + case DEBUG: + log.Debugln(data.Payload) + case SILENT: + return + } +} + +func newLog(logLevel LogLevel, format string, v ...any) Event { + return Event{ + LogLevel: logLevel, + Payload: fmt.Sprintf(format, v...), + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3adfe3d --- /dev/null +++ b/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "path/filepath" + "runtime" + "syscall" + + "github.com/Dreamacro/clash/config" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/hub" + "github.com/Dreamacro/clash/hub/executor" + "github.com/Dreamacro/clash/log" + + "go.uber.org/automaxprocs/maxprocs" +) + +var ( + flagset map[string]bool + version bool + testConfig bool + homeDir string + configFile string + externalUI string + externalController string + secret string +) + +func init() { + flag.StringVar(&homeDir, "d", "", "set configuration directory") + flag.StringVar(&configFile, "f", "", "specify configuration file") + flag.StringVar(&externalUI, "ext-ui", "", "override external ui directory") + flag.StringVar(&externalController, "ext-ctl", "", "override external controller address") + flag.StringVar(&secret, "secret", "", "override secret for RESTful API") + flag.BoolVar(&version, "v", false, "show current version of clash") + flag.BoolVar(&testConfig, "t", false, "test configuration and exit") + flag.Parse() + + flagset = map[string]bool{} + flag.Visit(func(f *flag.Flag) { + flagset[f.Name] = true + }) +} + +func main() { + maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) + if version { + fmt.Printf("Clash %s %s %s with %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) + return + } + + if homeDir != "" { + if !filepath.IsAbs(homeDir) { + currentDir, _ := os.Getwd() + homeDir = filepath.Join(currentDir, homeDir) + } + C.SetHomeDir(homeDir) + } + + if configFile != "" { + if !filepath.IsAbs(configFile) { + currentDir, _ := os.Getwd() + configFile = filepath.Join(currentDir, configFile) + } + C.SetConfig(configFile) + } else { + configFile := filepath.Join(C.Path.HomeDir(), C.Path.Config()) + C.SetConfig(configFile) + } + + if err := config.Init(C.Path.HomeDir()); err != nil { + log.Fatalln("Initial configuration directory error: %s", err.Error()) + } + + if testConfig { + if _, err := executor.Parse(); err != nil { + log.Errorln(err.Error()) + fmt.Printf("configuration file %s test failed\n", C.Path.Config()) + os.Exit(1) + } + fmt.Printf("configuration file %s test is successful\n", C.Path.Config()) + return + } + + var options []hub.Option + if flagset["ext-ui"] { + options = append(options, hub.WithExternalUI(externalUI)) + } + if flagset["ext-ctl"] { + options = append(options, hub.WithExternalController(externalController)) + } + if flagset["secret"] { + options = append(options, hub.WithSecret(secret)) + } + + if err := hub.Parse(options...); err != nil { + log.Fatalln("Parse config error: %s", err.Error()) + } + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh +} diff --git a/rule/base.go b/rule/base.go new file mode 100644 index 0000000..0f2d9f2 --- /dev/null +++ b/rule/base.go @@ -0,0 +1,20 @@ +package rules + +import ( + "errors" +) + +var ( + errPayload = errors.New("payload error") + + noResolve = "no-resolve" +) + +func HasNoResolve(params []string) bool { + for _, p := range params { + if p == noResolve { + return true + } + } + return false +} diff --git a/rule/domain.go b/rule/domain.go new file mode 100644 index 0000000..1a07875 --- /dev/null +++ b/rule/domain.go @@ -0,0 +1,46 @@ +package rules + +import ( + "strings" + + C "github.com/Dreamacro/clash/constant" +) + +// Implements C.Rule +var _ C.Rule = (*Domain)(nil) + +type Domain struct { + domain string + adapter string +} + +func (d *Domain) RuleType() C.RuleType { + return C.Domain +} + +func (d *Domain) Match(metadata *C.Metadata) bool { + return metadata.Host == d.domain +} + +func (d *Domain) Adapter() string { + return d.adapter +} + +func (d *Domain) Payload() string { + return d.domain +} + +func (d *Domain) ShouldResolveIP() bool { + return false +} + +func (d *Domain) ShouldFindProcess() bool { + return false +} + +func NewDomain(domain string, adapter string) *Domain { + return &Domain{ + domain: strings.ToLower(domain), + adapter: adapter, + } +} diff --git a/rule/domain_keyword.go b/rule/domain_keyword.go new file mode 100644 index 0000000..0f2c29a --- /dev/null +++ b/rule/domain_keyword.go @@ -0,0 +1,46 @@ +package rules + +import ( + "strings" + + C "github.com/Dreamacro/clash/constant" +) + +// Implements C.Rule +var _ C.Rule = (*DomainKeyword)(nil) + +type DomainKeyword struct { + keyword string + adapter string +} + +func (dk *DomainKeyword) RuleType() C.RuleType { + return C.DomainKeyword +} + +func (dk *DomainKeyword) Match(metadata *C.Metadata) bool { + return strings.Contains(metadata.Host, dk.keyword) +} + +func (dk *DomainKeyword) Adapter() string { + return dk.adapter +} + +func (dk *DomainKeyword) Payload() string { + return dk.keyword +} + +func (dk *DomainKeyword) ShouldResolveIP() bool { + return false +} + +func (dk *DomainKeyword) ShouldFindProcess() bool { + return false +} + +func NewDomainKeyword(keyword string, adapter string) *DomainKeyword { + return &DomainKeyword{ + keyword: strings.ToLower(keyword), + adapter: adapter, + } +} diff --git a/rule/domain_suffix.go b/rule/domain_suffix.go new file mode 100644 index 0000000..9b1dc45 --- /dev/null +++ b/rule/domain_suffix.go @@ -0,0 +1,47 @@ +package rules + +import ( + "strings" + + C "github.com/Dreamacro/clash/constant" +) + +// Implements C.Rule +var _ C.Rule = (*DomainSuffix)(nil) + +type DomainSuffix struct { + suffix string + adapter string +} + +func (ds *DomainSuffix) RuleType() C.RuleType { + return C.DomainSuffix +} + +func (ds *DomainSuffix) Match(metadata *C.Metadata) bool { + domain := metadata.Host + return strings.HasSuffix(domain, "."+ds.suffix) || domain == ds.suffix +} + +func (ds *DomainSuffix) Adapter() string { + return ds.adapter +} + +func (ds *DomainSuffix) Payload() string { + return ds.suffix +} + +func (ds *DomainSuffix) ShouldResolveIP() bool { + return false +} + +func (ds *DomainSuffix) ShouldFindProcess() bool { + return false +} + +func NewDomainSuffix(suffix string, adapter string) *DomainSuffix { + return &DomainSuffix{ + suffix: strings.ToLower(suffix), + adapter: adapter, + } +} diff --git a/rule/final.go b/rule/final.go new file mode 100644 index 0000000..c4d800c --- /dev/null +++ b/rule/final.go @@ -0,0 +1,42 @@ +package rules + +import ( + C "github.com/Dreamacro/clash/constant" +) + +// Implements C.Rule +var _ C.Rule = (*Match)(nil) + +type Match struct { + adapter string +} + +func (f *Match) RuleType() C.RuleType { + return C.MATCH +} + +func (f *Match) Match(metadata *C.Metadata) bool { + return true +} + +func (f *Match) Adapter() string { + return f.adapter +} + +func (f *Match) Payload() string { + return "" +} + +func (f *Match) ShouldResolveIP() bool { + return false +} + +func (f *Match) ShouldFindProcess() bool { + return false +} + +func NewMatch(adapter string) *Match { + return &Match{ + adapter: adapter, + } +} diff --git a/rule/geoip.go b/rule/geoip.go new file mode 100644 index 0000000..1451373 --- /dev/null +++ b/rule/geoip.go @@ -0,0 +1,60 @@ +package rules + +import ( + "strings" + + "github.com/Dreamacro/clash/component/mmdb" + C "github.com/Dreamacro/clash/constant" +) + +// Implements C.Rule +var _ C.Rule = (*GEOIP)(nil) + +type GEOIP struct { + country string + adapter string + noResolveIP bool +} + +func (g *GEOIP) RuleType() C.RuleType { + return C.GEOIP +} + +func (g *GEOIP) Match(metadata *C.Metadata) bool { + ip := metadata.DstIP + if ip == nil { + return false + } + + if strings.EqualFold(g.country, "LAN") { + return ip.IsPrivate() + } + record, _ := mmdb.Instance().Country(ip) + return strings.EqualFold(record.Country.IsoCode, g.country) +} + +func (g *GEOIP) Adapter() string { + return g.adapter +} + +func (g *GEOIP) Payload() string { + return g.country +} + +func (g *GEOIP) ShouldResolveIP() bool { + return !g.noResolveIP +} + +func (g *GEOIP) ShouldFindProcess() bool { + return false +} + +func NewGEOIP(country string, adapter string, noResolveIP bool) *GEOIP { + geoip := &GEOIP{ + country: country, + adapter: adapter, + noResolveIP: noResolveIP, + } + + return geoip +} diff --git a/rule/ipcidr.go b/rule/ipcidr.go new file mode 100644 index 0000000..c26c264 --- /dev/null +++ b/rule/ipcidr.go @@ -0,0 +1,80 @@ +package rules + +import ( + "net" + + C "github.com/Dreamacro/clash/constant" +) + +type IPCIDROption func(*IPCIDR) + +func WithIPCIDRSourceIP(b bool) IPCIDROption { + return func(i *IPCIDR) { + i.isSourceIP = b + } +} + +func WithIPCIDRNoResolve(noResolve bool) IPCIDROption { + return func(i *IPCIDR) { + i.noResolveIP = noResolve + } +} + +// Implements C.Rule +var _ C.Rule = (*IPCIDR)(nil) + +type IPCIDR struct { + ipnet *net.IPNet + adapter string + isSourceIP bool + noResolveIP bool +} + +func (i *IPCIDR) RuleType() C.RuleType { + if i.isSourceIP { + return C.SrcIPCIDR + } + return C.IPCIDR +} + +func (i *IPCIDR) Match(metadata *C.Metadata) bool { + ip := metadata.DstIP + if i.isSourceIP { + ip = metadata.SrcIP + } + return ip != nil && i.ipnet.Contains(ip) +} + +func (i *IPCIDR) Adapter() string { + return i.adapter +} + +func (i *IPCIDR) Payload() string { + return i.ipnet.String() +} + +func (i *IPCIDR) ShouldResolveIP() bool { + return !i.noResolveIP +} + +func (i *IPCIDR) ShouldFindProcess() bool { + return false +} + +func NewIPCIDR(s string, adapter string, opts ...IPCIDROption) (*IPCIDR, error) { + _, ipnet, err := net.ParseCIDR(s) + if err != nil { + return nil, errPayload + } + + ipcidr := &IPCIDR{ + ipnet: ipnet, + adapter: adapter, + } + + for _, o := range opts { + o(ipcidr) + } + + return ipcidr, nil +} diff --git a/rule/ipset.go b/rule/ipset.go new file mode 100644 index 0000000..7c596da --- /dev/null +++ b/rule/ipset.go @@ -0,0 +1,57 @@ +package rules + +import ( + "github.com/Dreamacro/clash/component/ipset" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" +) + +// Implements C.Rule +var _ C.Rule = (*IPSet)(nil) + +type IPSet struct { + name string + adapter string + noResolveIP bool +} + +func (f *IPSet) RuleType() C.RuleType { + return C.IPSet +} + +func (f *IPSet) Match(metadata *C.Metadata) bool { + exist, err := ipset.Test(f.name, metadata.DstIP) + if err != nil { + log.Warnln("check ipset '%s' failed: %s", f.name, err.Error()) + return false + } + return exist +} + +func (f *IPSet) Adapter() string { + return f.adapter +} + +func (f *IPSet) Payload() string { + return f.name +} + +func (f *IPSet) ShouldResolveIP() bool { + return !f.noResolveIP +} + +func (f *IPSet) ShouldFindProcess() bool { + return false +} + +func NewIPSet(name string, adapter string, noResolveIP bool) (*IPSet, error) { + if err := ipset.Verify(name); err != nil { + return nil, err + } + + return &IPSet{ + name: name, + adapter: adapter, + noResolveIP: noResolveIP, + }, nil +} diff --git a/rule/parser.go b/rule/parser.go new file mode 100644 index 0000000..9743007 --- /dev/null +++ b/rule/parser.go @@ -0,0 +1,54 @@ +package rules + +import ( + "fmt" + + C "github.com/Dreamacro/clash/constant" +) + +func ParseRule(tp, payload, target string, params []string) (C.Rule, error) { + var ( + parseErr error + parsed C.Rule + ) + + ruleConfigType := C.RuleConfig(tp) + + switch ruleConfigType { + case C.RuleConfigDomain: + parsed = NewDomain(payload, target) + case C.RuleConfigDomainSuffix: + parsed = NewDomainSuffix(payload, target) + case C.RuleConfigDomainKeyword: + parsed = NewDomainKeyword(payload, target) + case C.RuleConfigGeoIP: + noResolve := HasNoResolve(params) + parsed = NewGEOIP(payload, target, noResolve) + case C.RuleConfigIPCIDR, C.RuleConfigIPCIDR6: + noResolve := HasNoResolve(params) + parsed, parseErr = NewIPCIDR(payload, target, WithIPCIDRNoResolve(noResolve)) + case C.RuleConfigSrcIPCIDR: + parsed, parseErr = NewIPCIDR(payload, target, WithIPCIDRSourceIP(true), WithIPCIDRNoResolve(true)) + case C.RuleConfigSrcPort: + parsed, parseErr = NewPort(payload, target, PortTypeSrc) + case C.RuleConfigDstPort: + parsed, parseErr = NewPort(payload, target, PortTypeDest) + case C.RuleConfigInboundPort: + parsed, parseErr = NewPort(payload, target, PortTypeInbound) + case C.RuleConfigProcessName: + parsed, parseErr = NewProcess(payload, target, true) + case C.RuleConfigProcessPath: + parsed, parseErr = NewProcess(payload, target, false) + case C.RuleConfigIPSet: + noResolve := HasNoResolve(params) + parsed, parseErr = NewIPSet(payload, target, noResolve) + case C.RuleConfigMatch: + parsed = NewMatch(target) + case C.RuleConfigRuleSet, C.RuleConfigScript: + parseErr = fmt.Errorf("unsupported rule type %s", tp) + default: + parseErr = fmt.Errorf("unsupported rule type %s", tp) + } + + return parsed, parseErr +} diff --git a/rule/parser_test.go b/rule/parser_test.go new file mode 100644 index 0000000..bccd0a1 --- /dev/null +++ b/rule/parser_test.go @@ -0,0 +1,173 @@ +package rules + +import ( + "errors" + "fmt" + "testing" + + C "github.com/Dreamacro/clash/constant" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseRule(t *testing.T) { + type testCase struct { + tp C.RuleConfig + payload string + target string + params []string + expectedRule C.Rule + expectedError error + } + + policy := "DIRECT" + + testCases := []testCase{ + { + tp: C.RuleConfigDomain, + payload: "example.com", + target: policy, + expectedRule: NewDomain("example.com", policy), + }, + { + tp: C.RuleConfigDomainSuffix, + payload: "example.com", + target: policy, + expectedRule: NewDomainSuffix("example.com", policy), + }, + { + tp: C.RuleConfigDomainKeyword, + payload: "example.com", + target: policy, + expectedRule: NewDomainKeyword("example.com", policy), + }, + { + tp: C.RuleConfigGeoIP, + payload: "CN", + target: policy, params: []string{noResolve}, + expectedRule: NewGEOIP("CN", policy, true), + }, + { + tp: C.RuleConfigIPCIDR, + payload: "127.0.0.0/8", + target: policy, + expectedRule: lo.Must(NewIPCIDR("127.0.0.0/8", policy, WithIPCIDRNoResolve(false))), + }, + { + tp: C.RuleConfigIPCIDR, + payload: "127.0.0.0/8", + target: policy, params: []string{noResolve}, + expectedRule: lo.Must(NewIPCIDR("127.0.0.0/8", policy, WithIPCIDRNoResolve(true))), + }, + { + tp: C.RuleConfigIPCIDR6, + payload: "2001:db8::/32", + target: policy, + expectedRule: lo.Must(NewIPCIDR("2001:db8::/32", policy, WithIPCIDRNoResolve(false))), + }, + { + tp: C.RuleConfigIPCIDR6, + payload: "2001:db8::/32", + target: policy, params: []string{noResolve}, + expectedRule: lo.Must(NewIPCIDR("2001:db8::/32", policy, WithIPCIDRNoResolve(true))), + }, + { + tp: C.RuleConfigSrcIPCIDR, + payload: "192.168.1.201/32", + target: policy, + expectedRule: lo.Must(NewIPCIDR("192.168.1.201/32", policy, WithIPCIDRSourceIP(true), WithIPCIDRNoResolve(true))), + }, + { + tp: C.RuleConfigSrcPort, + payload: "80", + target: policy, + expectedRule: lo.Must(NewPort("80", policy, PortTypeSrc)), + }, + { + tp: C.RuleConfigDstPort, + payload: "80", + target: policy, + expectedRule: lo.Must(NewPort("80", policy, PortTypeDest)), + }, + { + tp: C.RuleConfigInboundPort, + payload: "80", + target: policy, + expectedRule: lo.Must(NewPort("80", policy, PortTypeInbound)), + }, + { + tp: C.RuleConfigProcessName, + payload: "example.exe", + target: policy, + expectedRule: lo.Must(NewProcess("example.exe", policy, true)), + }, + { + tp: C.RuleConfigProcessPath, + payload: "C:\\Program Files\\example.exe", + target: policy, + expectedRule: lo.Must(NewProcess("C:\\Program Files\\example.exe", policy, false)), + }, + { + tp: C.RuleConfigProcessPath, + payload: "/opt/example/example", + target: policy, + expectedRule: lo.Must(NewProcess("/opt/example/example", policy, false)), + }, + { + tp: C.RuleConfigIPSet, + payload: "example", + target: policy, + // unit test runs on Linux machine and NewIPSet(...) won't be available + expectedError: errors.New("operation not permitted"), + }, + { + tp: C.RuleConfigIPSet, + payload: "example", + target: policy, params: []string{noResolve}, + // unit test runs on Linux machine and NewIPSet(...) won't be available + expectedError: errors.New("operation not permitted"), + }, + { + tp: C.RuleConfigMatch, + payload: "example", + target: policy, + expectedRule: NewMatch(policy), + }, + { + tp: C.RuleConfigRuleSet, + payload: "example", + target: policy, + expectedError: fmt.Errorf("unsupported rule type %s", C.RuleConfigRuleSet), + }, + { + tp: C.RuleConfigScript, + payload: "example", + target: policy, + expectedError: fmt.Errorf("unsupported rule type %s", C.RuleConfigScript), + }, + { + tp: "UNKNOWN", + payload: "example", + target: policy, + expectedError: errors.New("unsupported rule type UNKNOWN"), + }, + { + tp: "ABCD", + payload: "example", + target: policy, + expectedError: errors.New("unsupported rule type ABCD"), + }, + } + + for _, tc := range testCases { + _, err := ParseRule(string(tc.tp), tc.payload, tc.target, tc.params) + if tc.expectedError != nil { + require.Error(t, err) + assert.EqualError(t, err, tc.expectedError.Error()) + } else { + require.NoError(t, err) + } + } +} diff --git a/rule/port.go b/rule/port.go new file mode 100644 index 0000000..6076136 --- /dev/null +++ b/rule/port.go @@ -0,0 +1,79 @@ +package rules + +import ( + "fmt" + "strconv" + + C "github.com/Dreamacro/clash/constant" +) + +type PortType int + +const ( + PortTypeSrc PortType = iota + PortTypeDest + PortTypeInbound +) + +// Implements C.Rule +var _ C.Rule = (*Port)(nil) + +type Port struct { + adapter string + port C.Port + portType PortType +} + +func (p *Port) RuleType() C.RuleType { + switch p.portType { + case PortTypeSrc: + return C.SrcPort + case PortTypeDest: + return C.DstPort + case PortTypeInbound: + return C.InboundPort + default: + panic(fmt.Errorf("unknown port type: %v", p.portType)) + } +} + +func (p *Port) Match(metadata *C.Metadata) bool { + switch p.portType { + case PortTypeSrc: + return metadata.SrcPort == p.port + case PortTypeDest: + return metadata.DstPort == p.port + case PortTypeInbound: + return metadata.OriginDst.Port() == uint16(p.port) + default: + panic(fmt.Errorf("unknown port type: %v", p.portType)) + } +} + +func (p *Port) Adapter() string { + return p.adapter +} + +func (p *Port) Payload() string { + return p.port.String() +} + +func (p *Port) ShouldResolveIP() bool { + return false +} + +func (p *Port) ShouldFindProcess() bool { + return false +} + +func NewPort(port string, adapter string, portType PortType) (*Port, error) { + p, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return nil, errPayload + } + return &Port{ + adapter: adapter, + port: C.Port(p), + portType: portType, + }, nil +} diff --git a/rule/process.go b/rule/process.go new file mode 100644 index 0000000..bf3af86 --- /dev/null +++ b/rule/process.go @@ -0,0 +1,57 @@ +package rules + +import ( + "path/filepath" + "strings" + + C "github.com/Dreamacro/clash/constant" +) + +// Implements C.Rule +var _ C.Rule = (*Process)(nil) + +type Process struct { + adapter string + process string + nameOnly bool +} + +func (ps *Process) RuleType() C.RuleType { + if ps.nameOnly { + return C.Process + } + + return C.ProcessPath +} + +func (ps *Process) Match(metadata *C.Metadata) bool { + if ps.nameOnly { + return strings.EqualFold(filepath.Base(metadata.ProcessPath), ps.process) + } + + return strings.EqualFold(metadata.ProcessPath, ps.process) +} + +func (ps *Process) Adapter() string { + return ps.adapter +} + +func (ps *Process) Payload() string { + return ps.process +} + +func (ps *Process) ShouldResolveIP() bool { + return false +} + +func (ps *Process) ShouldFindProcess() bool { + return true +} + +func NewProcess(process string, adapter string, nameOnly bool) (*Process, error) { + return &Process{ + adapter: adapter, + process: process, + nameOnly: nameOnly, + }, nil +} diff --git a/test/.golangci.yaml b/test/.golangci.yaml new file mode 100644 index 0000000..de68d3a --- /dev/null +++ b/test/.golangci.yaml @@ -0,0 +1,16 @@ +linters: + disable-all: true + enable: + - gofumpt + - govet + - gci + - staticcheck + +linters-settings: + gci: + sections: + - standard + - default + - prefix(github.com/Dreamacro/clash) + staticcheck: + go: '1.21' diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000..827585c --- /dev/null +++ b/test/Makefile @@ -0,0 +1,9 @@ +lint: + GOOS=darwin golangci-lint run --fix ./... + GOOS=linux golangci-lint run --fix ./... + +test: + go test -p 1 -v ./... + +benchmark: + go test -benchmem -run=^$$ -bench . diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..a95f3ae --- /dev/null +++ b/test/README.md @@ -0,0 +1,59 @@ +## Clash testing suit + +### Protocol testing suit + +* TCP pingpong test +* UDP pingpong test +* TCP large data test +* UDP large data test + +### Protocols + +- [x] Shadowsocks + - [x] Normal + - [x] ObfsHTTP + - [x] ObfsTLS + - [x] ObfsV2rayPlugin +- [x] Vmess + - [x] Normal + - [x] AEAD + - [x] HTTP + - [x] HTTP2 + - [x] TLS + - [x] Websocket + - [x] Websocket TLS + - [x] gRPC +- [x] Trojan + - [x] Normal + - [x] gRPC +- [x] Snell + - [x] Normal + - [x] ObfsHTTP + - [x] ObfsTLS + +### Features + +- [ ] DNS + - [x] DNS Server + - [x] FakeIP + - [x] Host + +### Command + +Prerequisite + +* docker (support Linux and macOS) + +``` +$ make test +``` + +benchmark (Linux) + +> Cannot represent the throughput of the protocol on your machine +> but you can compare the corresponding throughput of the protocol on clash +> (change chunkSize to measure the maximum throughput of clash on your machine) + +``` +$ make benchmark +``` diff --git a/test/clash_test.go b/test/clash_test.go new file mode 100644 index 0000000..e08d17e --- /dev/null +++ b/test/clash_test.go @@ -0,0 +1,671 @@ +package main + +import ( + "context" + "crypto/md5" + "crypto/rand" + "errors" + "fmt" + "io" + "net" + "os" + "path/filepath" + "runtime" + "sync" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Dreamacro/clash/adapter/outbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/hub/executor" + "github.com/Dreamacro/clash/transport/socks5" +) + +const ( + ImageShadowsocks = "mritd/shadowsocks:latest" + ImageShadowsocksRust = "ghcr.io/shadowsocks/ssserver-rust:latest" + ImageVmess = "v2fly/v2fly-core:latest" + ImageTrojan = "trojangfw/trojan:latest" + ImageTrojanGo = "p4gefau1t/trojan-go:latest" + ImageSnell = "ghcr.io/icpz/snell-server:latest" + ImageXray = "teddysun/xray:latest" +) + +var ( + waitTime = time.Second + localIP = net.ParseIP("127.0.0.1") + + defaultExposedPorts = nat.PortSet{ + "10002/tcp": struct{}{}, + "10002/udp": struct{}{}, + } + defaultPortBindings = nat.PortMap{ + "10002/tcp": []nat.PortBinding{ + {HostPort: "10002", HostIP: "0.0.0.0"}, + }, + "10002/udp": []nat.PortBinding{ + {HostPort: "10002", HostIP: "0.0.0.0"}, + }, + } + isDarwin = runtime.GOOS == "darwin" +) + +func init() { + currentDir, err := os.Getwd() + if err != nil { + panic(err) + } + homeDir := filepath.Join(currentDir, "config") + C.SetHomeDir(homeDir) + + if isDarwin { + localIP, err = defaultRouteIP() + if err != nil { + panic(err) + } + } + + c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer c.Close() + + list, err := c.ImageList(context.Background(), types.ImageListOptions{All: true}) + if err != nil { + panic(err) + } + + imageExist := func(image string) bool { + for _, item := range list { + for _, tag := range item.RepoTags { + if image == tag { + return true + } + } + } + return false + } + + images := []string{ + ImageShadowsocks, + ImageShadowsocksRust, + ImageVmess, + ImageTrojan, + ImageTrojanGo, + ImageSnell, + ImageXray, + } + + for _, image := range images { + if imageExist(image) { + continue + } + + println("pulling image:", image) + imageStream, err := c.ImagePull(context.Background(), image, types.ImagePullOptions{}) + if err != nil { + panic(err) + } + + io.Copy(io.Discard, imageStream) + } +} + +var clean = ` +port: 0 +socks-port: 0 +mixed-port: 0 +redir-port: 0 +tproxy-port: 0 +dns: + enable: false +` + +func cleanup() { + parseAndApply(clean) +} + +func parseAndApply(cfgStr string) error { + cfg, err := executor.ParseWithBytes([]byte(cfgStr)) + if err != nil { + return err + } + + executor.ApplyConfig(cfg, true) + return nil +} + +func newPingPongPair() (chan []byte, chan []byte, func(t *testing.T) error) { + pingCh := make(chan []byte) + pongCh := make(chan []byte) + test := func(t *testing.T) error { + defer close(pingCh) + defer close(pongCh) + pingOpen := false + pongOpen := false + var recv []byte + + for { + if pingOpen && pongOpen { + break + } + + select { + case recv, pingOpen = <-pingCh: + assert.True(t, pingOpen) + assert.Equal(t, []byte("ping"), recv) + case recv, pongOpen = <-pongCh: + assert.True(t, pongOpen) + assert.Equal(t, []byte("pong"), recv) + case <-time.After(10 * time.Second): + return errors.New("timeout") + } + } + return nil + } + + return pingCh, pongCh, test +} + +func newLargeDataPair() (chan hashPair, chan hashPair, func(t *testing.T) error) { + pingCh := make(chan hashPair) + pongCh := make(chan hashPair) + test := func(t *testing.T) error { + defer close(pingCh) + defer close(pongCh) + pingOpen := false + pongOpen := false + var serverPair hashPair + var clientPair hashPair + + for { + if pingOpen && pongOpen { + break + } + + select { + case serverPair, pingOpen = <-pingCh: + assert.True(t, pingOpen) + case clientPair, pongOpen = <-pongCh: + assert.True(t, pongOpen) + case <-time.After(10 * time.Second): + return errors.New("timeout") + } + } + + assert.Equal(t, serverPair.recvHash, clientPair.sendHash) + assert.Equal(t, serverPair.sendHash, clientPair.recvHash) + + return nil + } + + return pingCh, pongCh, test +} + +func testPingPongWithSocksPort(t *testing.T, port int) error { + l, err := Listen("tcp", ":10001") + require.NoError(t, err) + defer l.Close() + + pingCh, pongCh, test := newPingPongPair() + go func() { + c, err := l.Accept() + if err != nil { + return + } + + buf := make([]byte, 4) + if _, err = io.ReadFull(c, buf); err != nil { + return + } + + pingCh <- buf + if _, err = c.Write([]byte("pong")); err != nil { + return + } + }() + + go func() { + c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer c.Close() + + if _, err = socks5.ClientHandshake(c, socks5.ParseAddr("127.0.0.1:10001"), socks5.CmdConnect, nil); err != nil { + return + } + + if _, err = c.Write([]byte("ping")); err != nil { + return + } + + buf := make([]byte, 4) + if _, err = io.ReadFull(c, buf); err != nil { + return + } + + pongCh <- buf + }() + + return test(t) +} + +func testPingPongWithConn(t *testing.T, c net.Conn) error { + l, err := Listen("tcp", ":10001") + if err != nil { + return err + } + defer l.Close() + + pingCh, pongCh, test := newPingPongPair() + go func() { + c, err := l.Accept() + if err != nil { + return + } + + buf := make([]byte, 4) + if _, err := io.ReadFull(c, buf); err != nil { + return + } + + pingCh <- buf + if _, err := c.Write([]byte("pong")); err != nil { + return + } + }() + + go func() { + if _, err := c.Write([]byte("ping")); err != nil { + return + } + + buf := make([]byte, 4) + if _, err := io.ReadFull(c, buf); err != nil { + return + } + + pongCh <- buf + }() + + return test(t) +} + +func testPingPongWithPacketConn(t *testing.T, pc net.PacketConn) error { + l, err := ListenPacket("udp", ":10001") + require.NoError(t, err) + defer l.Close() + + rAddr := &net.UDPAddr{IP: localIP, Port: 10001} + + pingCh, pongCh, test := newPingPongPair() + go func() { + buf := make([]byte, 1024) + n, rAddr, err := l.ReadFrom(buf) + if err != nil { + return + } + + pingCh <- buf[:n] + if _, err := l.WriteTo([]byte("pong"), rAddr); err != nil { + return + } + }() + + go func() { + if _, err := pc.WriteTo([]byte("ping"), rAddr); err != nil { + return + } + + buf := make([]byte, 1024) + n, _, err := pc.ReadFrom(buf) + if err != nil { + return + } + + pongCh <- buf[:n] + }() + + return test(t) +} + +type hashPair struct { + sendHash map[int][]byte + recvHash map[int][]byte +} + +func testLargeDataWithConn(t *testing.T, c net.Conn) error { + l, err := Listen("tcp", ":10001") + require.NoError(t, err) + defer l.Close() + + times := 100 + chunkSize := int64(64 * 1024) + + pingCh, pongCh, test := newLargeDataPair() + writeRandData := func(conn net.Conn) (map[int][]byte, error) { + buf := make([]byte, chunkSize) + hashMap := map[int][]byte{} + for i := 0; i < times; i++ { + if _, err := rand.Read(buf[1:]); err != nil { + return nil, err + } + buf[0] = byte(i) + + hash := md5.Sum(buf) + hashMap[i] = hash[:] + + if _, err := conn.Write(buf); err != nil { + return nil, err + } + } + + return hashMap, nil + } + + go func() { + c, err := l.Accept() + if err != nil { + return + } + defer c.Close() + + hashMap := map[int][]byte{} + buf := make([]byte, chunkSize) + + for i := 0; i < times; i++ { + _, err := io.ReadFull(c, buf) + if err != nil { + t.Log(err.Error()) + return + } + + hash := md5.Sum(buf) + hashMap[int(buf[0])] = hash[:] + } + + sendHash, err := writeRandData(c) + if err != nil { + t.Log(err.Error()) + return + } + + pingCh <- hashPair{ + sendHash: sendHash, + recvHash: hashMap, + } + }() + + go func() { + sendHash, err := writeRandData(c) + if err != nil { + t.Log(err.Error()) + return + } + + hashMap := map[int][]byte{} + buf := make([]byte, chunkSize) + + for i := 0; i < times; i++ { + _, err := io.ReadFull(c, buf) + if err != nil { + t.Log(err.Error()) + return + } + + hash := md5.Sum(buf) + hashMap[int(buf[0])] = hash[:] + } + + pongCh <- hashPair{ + sendHash: sendHash, + recvHash: hashMap, + } + }() + + return test(t) +} + +func testLargeDataWithPacketConn(t *testing.T, pc net.PacketConn) error { + l, err := ListenPacket("udp", ":10001") + require.NoError(t, err) + defer l.Close() + + rAddr := &net.UDPAddr{IP: localIP, Port: 10001} + + times := 50 + chunkSize := int64(1024) + + pingCh, pongCh, test := newLargeDataPair() + writeRandData := func(pc net.PacketConn, addr net.Addr) (map[int][]byte, error) { + hashMap := map[int][]byte{} + mux := sync.Mutex{} + for i := 0; i < times; i++ { + go func(idx int) { + buf := make([]byte, chunkSize) + if _, err := rand.Read(buf[1:]); err != nil { + t.Log(err.Error()) + return + } + buf[0] = byte(idx) + + hash := md5.Sum(buf) + mux.Lock() + hashMap[idx] = hash[:] + mux.Unlock() + + if _, err := pc.WriteTo(buf, addr); err != nil { + t.Log(err.Error()) + return + } + }(i) + } + + return hashMap, nil + } + + go func() { + var rAddr net.Addr + hashMap := map[int][]byte{} + buf := make([]byte, 64*1024) + + for i := 0; i < times; i++ { + _, rAddr, err = l.ReadFrom(buf) + if err != nil { + t.Log(err.Error()) + return + } + + hash := md5.Sum(buf[:chunkSize]) + hashMap[int(buf[0])] = hash[:] + } + + sendHash, err := writeRandData(l, rAddr) + if err != nil { + t.Log(err.Error()) + return + } + + pingCh <- hashPair{ + sendHash: sendHash, + recvHash: hashMap, + } + }() + + go func() { + sendHash, err := writeRandData(pc, rAddr) + if err != nil { + t.Log(err.Error()) + return + } + + hashMap := map[int][]byte{} + buf := make([]byte, 64*1024) + + for i := 0; i < times; i++ { + _, _, err := pc.ReadFrom(buf) + if err != nil { + t.Log(err.Error()) + return + } + + hash := md5.Sum(buf[:chunkSize]) + hashMap[int(buf[0])] = hash[:] + } + + pongCh <- hashPair{ + sendHash: sendHash, + recvHash: hashMap, + } + }() + + return test(t) +} + +func testPacketConnTimeout(t *testing.T, pc net.PacketConn) error { + err := pc.SetReadDeadline(time.Now().Add(time.Millisecond * 300)) + require.NoError(t, err) + + errCh := make(chan error, 1) + go func() { + buf := make([]byte, 1024) + _, _, err := pc.ReadFrom(buf) + errCh <- err + }() + + select { + case <-errCh: + return nil + case <-time.After(time.Second * 10): + return errors.New("timeout") + } +} + +func testSuit(t *testing.T, proxy C.ProxyAdapter) { + conn, err := proxy.DialContext(context.Background(), &C.Metadata{ + Host: localIP.String(), + DstPort: "10001", + }) + require.NoError(t, err) + defer conn.Close() + assert.NoError(t, testPingPongWithConn(t, conn)) + + conn, err = proxy.DialContext(context.Background(), &C.Metadata{ + Host: localIP.String(), + DstPort: "10001", + }) + require.NoError(t, err) + defer conn.Close() + assert.NoError(t, testLargeDataWithConn(t, conn)) + + if !proxy.SupportUDP() { + return + } + + pc, err := proxy.ListenPacketContext(context.Background(), &C.Metadata{ + NetWork: C.UDP, + DstIP: localIP, + DstPort: "10001", + }) + require.NoError(t, err) + defer pc.Close() + + assert.NoError(t, testPingPongWithPacketConn(t, pc)) + + pc, err = proxy.ListenPacketContext(context.Background(), &C.Metadata{ + NetWork: C.UDP, + DstIP: localIP, + DstPort: "10001", + }) + require.NoError(t, err) + defer pc.Close() + + assert.NoError(t, testLargeDataWithPacketConn(t, pc)) + + pc, err = proxy.ListenPacketContext(context.Background(), &C.Metadata{ + NetWork: C.UDP, + DstIP: localIP, + DstPort: "10001", + }) + require.NoError(t, err) + defer pc.Close() + + assert.NoError(t, testPacketConnTimeout(t, pc)) +} + +func benchmarkProxy(b *testing.B, proxy C.ProxyAdapter) { + l, err := Listen("tcp", ":10001") + require.NoError(b, err) + defer l.Close() + + chunkSize := int64(16 * 1024) + chunk := make([]byte, chunkSize) + rand.Read(chunk) + + go func() { + c, err := l.Accept() + if err != nil { + return + } + defer c.Close() + + go func() { + for { + _, err := c.Write(chunk) + if err != nil { + return + } + } + }() + io.Copy(io.Discard, c) + }() + + conn, err := proxy.DialContext(context.Background(), &C.Metadata{ + Host: localIP.String(), + DstPort: "10001", + }) + require.NoError(b, err) + + _, err = conn.Write([]byte("skip protocol handshake")) + require.NoError(b, err) + + b.Run("Write", func(b *testing.B) { + b.SetBytes(chunkSize) + for i := 0; i < b.N; i++ { + conn.Write(chunk) + } + }) + + b.Run("Read", func(b *testing.B) { + b.SetBytes(chunkSize) + buf := make([]byte, chunkSize) + for i := 0; i < b.N; i++ { + io.ReadFull(conn, buf) + } + }) +} + +func TestClash_Basic(t *testing.T) { + basic := ` +mixed-port: 10000 +log-level: silent +` + + err := parseAndApply(basic) + require.NoError(t, err) + defer cleanup() + + require.True(t, TCPing(net.JoinHostPort("127.0.0.1", "10000"))) + require.NoError(t, testPingPongWithSocksPort(t, 10000)) +} + +func Benchmark_Direct(b *testing.B) { + proxy := outbound.NewDirect() + benchmarkProxy(b, proxy) +} diff --git a/test/config/example.org-key.pem b/test/config/example.org-key.pem new file mode 100644 index 0000000..dbe9a3d --- /dev/null +++ b/test/config/example.org-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDQ+c++LkDTdaw5 +5spCu9MWMcvVdrYBZZ5qZy7DskphSUSQp25cIu34GJXVPNxtbWx1CQCmdLlwqXvo +PfUt5/pz9qsfhdAbzFduZQgGd7GTQOTJBDrAhm2+iVsQyGHHhF68muN+SgT+AtRE +sJyZoHNYtjjWEIHQ++FHEDqwUVnj6Ut99LHlyfCjOZ5+WyBiKCjyMNots/gDep7R +i4X2kMTqNMIIqPUcAaP5EQk41bJbFhKe915qN9b1dRISKFKmiWeOsxgTB/O/EaL5 +LsBYwZ/BiIMDk30aZvzRJeloasIR3z4hrKQqBfB0lfeIdiPpJIs5rXJQEiWH89ge +gplsLbfrAgMBAAECggEBAKpMGaZzDPMF/v8Ee6lcZM2+cMyZPALxa+JsCakCvyh+ +y7hSKVY+RM0cQ+YM/djTBkJtvrDniEMuasI803PAitI7nwJGSuyMXmehP6P9oKFO +jeLeZn6ETiSqzKJlmYE89vMeCevdqCnT5mW/wy5Smg0eGj0gIJpM2S3PJPSQpv9Z +ots0JXkwooJcpGWzlwPkjSouY2gDbE4Coi+jmYLNjA1k5RbggcutnUCZZkJ6yMNv +H52VjnkffpAFHRouK/YgF+5nbMyyw5YTLOyTWBq7qfBMsXynkWLU73GC/xDZa3yG +o/Ph2knXCjgLmCRessTOObdOXedjnGWIjiqF8fVboDECgYEA6x5CteYiwthDBULZ +CG5nE9VKkRHJYdArm+VjmGbzK51tKli112avmU4r3ol907+mEa4tWLkPqdZrrL49 +aHltuHizZJixJcw0rcI302ot/Ov0gkF9V55gnAQS/Kemvx9FHWm5NHdYvbObzj33 +bYRLJBtJWzYg9M8Bw9ZrUnegc/MCgYEA44kq5OSYCbyu3eaX8XHTtFhuQHNFjwl7 +Xk/Oel6PVZzmt+oOlDHnOfGSB/KpR3YXxFRngiiPZzbrOwFyPGe7HIfg03HAXiJh +ivEfrPHbQqQUI/4b44GpDy6bhNtz777ivFGYEt21vpwd89rFiye+RkqF8eL/evxO +pUayDZYvwikCgYEA07wFoZ/lkAiHmpZPsxsRcrfzFd+pto9splEWtumHdbCo3ajT +4W5VFr9iHF8/VFDT8jokFjFaXL1/bCpKTOqFl8oC68XiSkKy8gPkmFyXm5y2LhNi +GGTFZdr5alRkgttbN5i9M/WCkhvMZRhC2Xp43MRB9IUzeqNtWHqhXbvjYGcCgYEA +vTMOztviLJ6PjYa0K5lp31l0+/SeD21j/y0/VPOSHi9kjeN7EfFZAw6DTkaSShDB +fIhutYVCkSHSgfMW6XGb3gKCiW/Z9KyEDYOowicuGgDTmoYu7IOhbzVjLhtJET7Z +zJvQZ0eiW4f3RBFTF/4JMuu+6z7FD6ADSV06qx+KQNkCgYBw26iQxmT5e/4kVv8X +DzBJ1HuliKBnnzZA1YRjB4H8F6Yrq+9qur1Lurez4YlbkGV8yPFt+Iu82ViUWL28 +9T7Jgp3TOpf8qOqsWFv8HldpEZbE0Tcib4x6s+zOg/aw0ac/xOPY1sCVFB81VODP +XCar+uxMBXI1zbXqd9QdEwy4Ig== +-----END PRIVATE KEY----- diff --git a/test/config/example.org.pem b/test/config/example.org.pem new file mode 100644 index 0000000..9b99259 --- /dev/null +++ b/test/config/example.org.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIESzCCArOgAwIBAgIQIi5xRZvFZaSweWU9Y5mExjANBgkqhkiG9w0BAQsFADCB +hzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMS4wLAYDVQQLDCVkcmVh +bWFjcm9ARHJlYW1hY3JvLmxvY2FsIChEcmVhbWFjcm8pMTUwMwYDVQQDDCxta2Nl +cnQgZHJlYW1hY3JvQERyZWFtYWNyby5sb2NhbCAoRHJlYW1hY3JvKTAeFw0yMTAz +MTcxNDQwMzZaFw0yMzA2MTcxNDQwMzZaMFkxJzAlBgNVBAoTHm1rY2VydCBkZXZl +bG9wbWVudCBjZXJ0aWZpY2F0ZTEuMCwGA1UECwwlZHJlYW1hY3JvQERyZWFtYWNy +by5sb2NhbCAoRHJlYW1hY3JvKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAND5z74uQNN1rDnmykK70xYxy9V2tgFlnmpnLsOySmFJRJCnblwi7fgYldU8 +3G1tbHUJAKZ0uXCpe+g99S3n+nP2qx+F0BvMV25lCAZ3sZNA5MkEOsCGbb6JWxDI +YceEXrya435KBP4C1ESwnJmgc1i2ONYQgdD74UcQOrBRWePpS330seXJ8KM5nn5b +IGIoKPIw2i2z+AN6ntGLhfaQxOo0wgio9RwBo/kRCTjVslsWEp73Xmo31vV1EhIo +UqaJZ46zGBMH878RovkuwFjBn8GIgwOTfRpm/NEl6WhqwhHfPiGspCoF8HSV94h2 +I+kkizmtclASJYfz2B6CmWwtt+sCAwEAAaNgMF4wDgYDVR0PAQH/BAQDAgWgMBMG +A1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFO800LQ6Pa85RH4EbMmFH6ln +F150MBYGA1UdEQQPMA2CC2V4YW1wbGUub3JnMA0GCSqGSIb3DQEBCwUAA4IBgQAP +TsF53h7bvJcUXT3Y9yZ2vnW6xr9r92tNnM1Gfo3D2Yyn9oLf2YrfJng6WZ04Fhqa +Wh0HOvE0n6yPNpm/Q7mh64DrgolZ8Ce5H4RTJDAabHU9XhEzfGSVtzRSFsz+szu1 +Y30IV+08DxxqMmNPspYdpAET2Lwyk2WhnARGiGw11CRkQCEkVEe6d702vS9UGBUz +Du6lmCYCm0SbFrZ0CGgmHSHoTcCtf3EjVam7dPg3yWiPbWjvhXxgip6hz9sCqkhG +WA5f+fPgSZ1I9U4i+uYnqjfrzwgC08RwUYordm15F6gPvXw+KVwDO8yUYQoEH0b6 +AFJtbzoAXDysvBC6kWYFFOr62EaisaEkELTS/NrPD9ux1eKbxcxHCwEtVjgC0CL6 +gAxEAQ+9maJMbrAFhsOBbGGFC+mMCGg4eEyx6+iMB0oQe0W7QFeRUAFi7Ptc/ocS +tZ9lbrfX1/wrcTTWIYWE+xH6oeb4fhs29kxjHcf2l+tQzmpl0aP3Z/bMW4BSB+w= +-----END CERTIFICATE----- diff --git a/test/config/snell-http.conf b/test/config/snell-http.conf new file mode 100644 index 0000000..f5130dc --- /dev/null +++ b/test/config/snell-http.conf @@ -0,0 +1,4 @@ +[snell-server] +listen = 0.0.0.0:10002 +psk = password +obfs = http diff --git a/test/config/snell-tls.conf b/test/config/snell-tls.conf new file mode 100644 index 0000000..bf8b513 --- /dev/null +++ b/test/config/snell-tls.conf @@ -0,0 +1,4 @@ +[snell-server] +listen = 0.0.0.0:10002 +psk = password +obfs = tls diff --git a/test/config/snell.conf b/test/config/snell.conf new file mode 100644 index 0000000..4026639 --- /dev/null +++ b/test/config/snell.conf @@ -0,0 +1,3 @@ +[snell-server] +listen = 0.0.0.0:10002 +psk = password diff --git a/test/config/trojan-grpc.json b/test/config/trojan-grpc.json new file mode 100644 index 0000000..eb0dcc9 --- /dev/null +++ b/test/config/trojan-grpc.json @@ -0,0 +1,40 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "trojan", + "settings": { + "clients": [ + { + "password": "example", + "email": "grpc@example.com" + } + ] + }, + "streamSettings": { + "network": "grpc", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + }, + "grpcSettings": { + "serviceName": "example" + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} \ No newline at end of file diff --git a/test/config/trojan-ws.json b/test/config/trojan-ws.json new file mode 100644 index 0000000..efc0acb --- /dev/null +++ b/test/config/trojan-ws.json @@ -0,0 +1,20 @@ +{ + "run_type": "server", + "local_addr": "0.0.0.0", + "local_port": 10002, + "disable_http_check": true, + "password": [ + "example" + ], + "websocket": { + "enabled": true, + "path": "/", + "host": "example.org" + }, + "ssl": { + "verify": true, + "cert": "/fullchain.pem", + "key": "/privkey.pem", + "sni": "example.org" + } +} \ No newline at end of file diff --git a/test/config/trojan.json b/test/config/trojan.json new file mode 100644 index 0000000..18e33a9 --- /dev/null +++ b/test/config/trojan.json @@ -0,0 +1,40 @@ +{ + "run_type": "server", + "local_addr": "0.0.0.0", + "local_port": 10002, + "password": [ + "password" + ], + "log_level": 1, + "ssl": { + "cert": "/path/to/certificate.crt", + "key": "/path/to/private.key", + "key_password": "", + "cipher": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384", + "cipher_tls13": "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384", + "prefer_server_cipher": true, + "alpn": [ + "http/1.1" + ], + "alpn_port_override": { + "h2": 81 + }, + "reuse_session": true, + "session_ticket": false, + "session_timeout": 600, + "plain_http_response": "", + "curves": "", + "dhparam": "" + }, + "tcp": { + "prefer_ipv4": false, + "no_delay": true, + "keep_alive": true, + "reuse_port": false, + "fast_open": false, + "fast_open_qlen": 20 + }, + "mysql": { + "enabled": false + } +} \ No newline at end of file diff --git a/test/config/vmess-grpc.json b/test/config/vmess-grpc.json new file mode 100644 index 0000000..178e068 --- /dev/null +++ b/test/config/vmess-grpc.json @@ -0,0 +1,39 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "grpc", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + }, + "grpcSettings": { + "serviceName": "example!" + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} \ No newline at end of file diff --git a/test/config/vmess-http.json b/test/config/vmess-http.json new file mode 100644 index 0000000..90550c3 --- /dev/null +++ b/test/config/vmess-http.json @@ -0,0 +1,54 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "tcp", + "tcpSettings": { + "header": { + "type": "http", + "response": { + "version": "1.1", + "status": "200", + "reason": "OK", + "headers": { + "Content-Type": [ + "application/octet-stream", + "video/mpeg", + "application/x-msdownload", + "text/html", + "application/x-shockwave-flash" + ], + "Transfer-Encoding": [ + "chunked" + ], + "Connection": [ + "keep-alive" + ], + "Pragma": "no-cache" + } + } + } + }, + "security": "none" + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} \ No newline at end of file diff --git a/test/config/vmess-http2.json b/test/config/vmess-http2.json new file mode 100644 index 0000000..c6916a1 --- /dev/null +++ b/test/config/vmess-http2.json @@ -0,0 +1,42 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "http", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + }, + "httpSettings": { + "host": [ + "example.org" + ], + "path": "/test" + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} \ No newline at end of file diff --git a/test/config/vmess-tls.json b/test/config/vmess-tls.json new file mode 100644 index 0000000..17e87d6 --- /dev/null +++ b/test/config/vmess-tls.json @@ -0,0 +1,36 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "tcp", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} \ No newline at end of file diff --git a/test/config/vmess-ws-0rtt.json b/test/config/vmess-ws-0rtt.json new file mode 100644 index 0000000..c22909b --- /dev/null +++ b/test/config/vmess-ws-0rtt.json @@ -0,0 +1,29 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "none", + "wsSettings": { + "maxEarlyData": 128, + "earlyDataHeaderName": "Sec-WebSocket-Protocol" + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-ws-tls-zero.json b/test/config/vmess-ws-tls-zero.json new file mode 100644 index 0000000..4ac6f14 --- /dev/null +++ b/test/config/vmess-ws-tls-zero.json @@ -0,0 +1,35 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811", + "alterId": 0, + "security": "zero" + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-ws-tls.json b/test/config/vmess-ws-tls.json new file mode 100644 index 0000000..14278f3 --- /dev/null +++ b/test/config/vmess-ws-tls.json @@ -0,0 +1,33 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "tlsSettings": { + "certificates": [ + { + "certificateFile": "/etc/ssl/v2ray/fullchain.pem", + "keyFile": "/etc/ssl/v2ray/privkey.pem" + } + ] + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/vmess-ws.json b/test/config/vmess-ws.json new file mode 100644 index 0000000..2bcb604 --- /dev/null +++ b/test/config/vmess-ws.json @@ -0,0 +1,25 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "none" + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ] +} \ No newline at end of file diff --git a/test/config/vmess.json b/test/config/vmess.json new file mode 100644 index 0000000..1a8f935 --- /dev/null +++ b/test/config/vmess.json @@ -0,0 +1,27 @@ +{ + "inbounds": [ + { + "port": 10002, + "listen": "0.0.0.0", + "protocol": "vmess", + "settings": { + "clients": [ + { + "id": "b831381d-6324-4d53-ad4f-8cda48b30811" + } + ] + }, + "streamSettings": { + "network": "tcp" + } + } + ], + "outbounds": [ + { + "protocol": "freedom" + } + ], + "log": { + "loglevel": "debug" + } +} \ No newline at end of file diff --git a/test/dns_test.go b/test/dns_test.go new file mode 100644 index 0000000..c378762 --- /dev/null +++ b/test/dns_test.go @@ -0,0 +1,100 @@ +package main + +import ( + "testing" + "time" + + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func exchange(address, domain string, tp uint16) ([]dns.RR, error) { + client := dns.Client{} + query := &dns.Msg{} + query.SetQuestion(dns.Fqdn(domain), tp) + + r, _, err := client.Exchange(query, address) + if err != nil { + return nil, err + } + return r.Answer, nil +} + +func TestClash_DNS(t *testing.T) { + basic := ` +log-level: silent +dns: + enable: true + listen: 0.0.0.0:8553 + nameserver: + - 119.29.29.29 +` + + err := parseAndApply(basic) + require.NoError(t, err) + defer cleanup() + + time.Sleep(waitTime) + + rr, err := exchange("127.0.0.1:8553", "1.1.1.1.nip.io", dns.TypeA) + assert.NoError(t, err) + assert.NotEmptyf(t, rr, "record empty") + + record := rr[0].(*dns.A) + assert.Equal(t, record.A.String(), "1.1.1.1") + + rr, err = exchange("127.0.0.1:8553", "2606-4700-4700--1111.sslip.io", dns.TypeAAAA) + assert.NoError(t, err) + assert.Empty(t, rr) +} + +func TestClash_DNSHostAndFakeIP(t *testing.T) { + basic := ` +log-level: silent +hosts: + foo.clash.dev: 1.1.1.1 +dns: + enable: true + listen: 0.0.0.0:8553 + ipv6: true + enhanced-mode: fake-ip + fake-ip-range: 198.18.0.1/16 + fake-ip-filter: + - .sslip.io + nameserver: + - 119.29.29.29 +` + + err := parseAndApply(basic) + require.NoError(t, err) + defer cleanup() + + time.Sleep(waitTime) + + type domainPair struct { + domain string + ip string + } + + list := []domainPair{ + {"foo.org", "198.18.0.2"}, + {"bar.org", "198.18.0.3"}, + {"foo.org", "198.18.0.2"}, + {"foo.clash.dev", "1.1.1.1"}, + } + + for _, pair := range list { + rr, err := exchange("127.0.0.1:8553", pair.domain, dns.TypeA) + assert.NoError(t, err) + assert.NotEmpty(t, rr) + + record := rr[0].(*dns.A) + assert.Equal(t, record.A.String(), pair.ip) + } + + rr, err := exchange("127.0.0.1:8553", "2606-4700-4700--1111.sslip.io", dns.TypeAAAA) + assert.NoError(t, err) + assert.NotEmpty(t, rr) + assert.Equal(t, rr[0].(*dns.AAAA).AAAA.String(), "2606:4700:4700::1111") +} diff --git a/test/docker_test.go b/test/docker_test.go new file mode 100644 index 0000000..8513dde --- /dev/null +++ b/test/docker_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" +) + +func startContainer(cfg *container.Config, hostCfg *container.HostConfig, name string) (string, error) { + c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return "", err + } + defer c.Close() + + if !isDarwin { + hostCfg.NetworkMode = "host" + } + + container, err := c.ContainerCreate(context.Background(), cfg, hostCfg, nil, nil, name) + if err != nil { + return "", err + } + + if err = c.ContainerStart(context.Background(), container.ID, types.ContainerStartOptions{}); err != nil { + return "", err + } + + return container.ID, nil +} + +func cleanContainer(id string) error { + c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return err + } + defer c.Close() + + removeOpts := types.ContainerRemoveOptions{Force: true} + return c.ContainerRemove(context.Background(), id, removeOpts) +} diff --git a/test/go.mod b/test/go.mod new file mode 100644 index 0000000..4d3dc7e --- /dev/null +++ b/test/go.mod @@ -0,0 +1,58 @@ +module clash-test + +go 1.21 + +require ( + github.com/Dreamacro/clash v1.12.0 + github.com/docker/docker v24.0.5+incompatible + github.com/docker/go-connections v0.4.0 + github.com/miekg/dns v1.1.55 + github.com/stretchr/testify v1.8.4 + go.uber.org/automaxprocs v1.5.3 + golang.org/x/net v0.14.0 +) + +replace github.com/Dreamacro/clash => ../ + +require ( + github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/gofrs/uuid/v5 v5.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/oschwald/geoip2-golang v1.9.0 // indirect + github.com/oschwald/maxminddb-golang v1.11.0 // indirect + github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/samber/lo v1.38.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect + github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01 // indirect + github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect + go.etcd.io/bbolt v1.3.7 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.6.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.4.0 // indirect +) diff --git a/test/go.sum b/test/go.sum new file mode 100644 index 0000000..ed3c0f7 --- /dev/null +++ b/test/go.sum @@ -0,0 +1,144 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 h1:JFnwKplz9hj8ubqYjm8HkgZS1Rvz9yW+u/XCNNTxr0k= +github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158/go.mod h1:QvmEZ/h6KXszPOr2wUFl7Zn3hfFNYdfbXwPVDTyZs6k= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY= +github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= +github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d h1:Ka64cclWedOkGzm9M2/XYuwJUdmWRUozmsxW0PyKA3A= +github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc= +github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y= +github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0= +github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg= +github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01 h1:F9xjJm4IH8VjcqG4ujciOF+GIM4mjPkHhWLLzOghPtM= +github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01/go.mod h1:cAAsePK2e15YDAMJNyOpGYEWNe4sIghTY7gpz4cX/Ik= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= diff --git a/test/listener_test.go b/test/listener_test.go new file mode 100644 index 0000000..8332a36 --- /dev/null +++ b/test/listener_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "net" + "strconv" + "testing" + "time" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener" + "github.com/Dreamacro/clash/tunnel" + + "github.com/stretchr/testify/require" +) + +func TestClash_Listener(t *testing.T) { + basic := ` +log-level: silent +port: 7890 +socks-port: 7891 +redir-port: 7892 +tproxy-port: 7893 +mixed-port: 7894 +` + + err := parseAndApply(basic) + require.NoError(t, err) + defer cleanup() + + time.Sleep(waitTime) + + for i := 7890; i <= 7894; i++ { + require.True(t, TCPing(net.JoinHostPort("127.0.0.1", strconv.Itoa(i))), "tcp port %d", i) + } +} + +func TestClash_ListenerCreate(t *testing.T) { + basic := ` +log-level: silent +` + err := parseAndApply(basic) + require.NoError(t, err) + defer cleanup() + + time.Sleep(waitTime) + tcpIn := tunnel.TCPIn() + udpIn := tunnel.UDPIn() + + ports := listener.Ports{ + Port: 7890, + } + listener.ReCreatePortsListeners(ports, tcpIn, udpIn) + require.True(t, TCPing("127.0.0.1:7890")) + require.Equal(t, ports, *listener.GetPorts()) + + inbounds := []C.Inbound{ + { + Type: C.InboundTypeHTTP, + BindAddress: "127.0.0.1:7891", + }, + } + listener.ReCreateListeners(inbounds, tcpIn, udpIn) + require.True(t, TCPing("127.0.0.1:7890")) + require.Equal(t, ports, *listener.GetPorts()) + + require.True(t, TCPing("127.0.0.1:7891")) + require.Equal(t, len(inbounds), len(listener.GetInbounds())) + + ports.Port = 0 + ports.SocksPort = 7892 + listener.ReCreatePortsListeners(ports, tcpIn, udpIn) + require.False(t, TCPing("127.0.0.1:7890")) + require.True(t, TCPing("127.0.0.1:7892")) + require.Equal(t, ports, *listener.GetPorts()) + + require.True(t, TCPing("127.0.0.1:7891")) + require.Equal(t, len(inbounds), len(listener.GetInbounds())) +} diff --git a/test/main.go b/test/main.go new file mode 100644 index 0000000..1a99371 --- /dev/null +++ b/test/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + "runtime" + "strconv" + + "go.uber.org/automaxprocs/maxprocs" +) + +func main() { + maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) + os.Stdout.Write([]byte(strconv.FormatInt(int64(runtime.GOMAXPROCS(0)), 10))) +} diff --git a/test/rule_test.go b/test/rule_test.go new file mode 100644 index 0000000..3b108d0 --- /dev/null +++ b/test/rule_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestClash_RuleInbound(t *testing.T) { + basic := ` +socks-port: 7890 +inbounds: + - socks://127.0.0.1:7891 + - type: socks + bind-address: 127.0.0.1:7892 +rules: + - INBOUND-PORT,7891,REJECT +log-level: silent +` + + err := parseAndApply(basic) + require.NoError(t, err) + defer cleanup() + + require.True(t, TCPing(net.JoinHostPort("127.0.0.1", "7890"))) + require.True(t, TCPing(net.JoinHostPort("127.0.0.1", "7891"))) + require.True(t, TCPing(net.JoinHostPort("127.0.0.1", "7892"))) + + require.Error(t, testPingPongWithSocksPort(t, 7891)) + require.NoError(t, testPingPongWithSocksPort(t, 7890)) + require.NoError(t, testPingPongWithSocksPort(t, 7892)) +} diff --git a/test/snell_test.go b/test/snell_test.go new file mode 100644 index 0000000..3731400 --- /dev/null +++ b/test/snell_test.go @@ -0,0 +1,174 @@ +package main + +import ( + "fmt" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/require" + + "github.com/Dreamacro/clash/adapter/outbound" + C "github.com/Dreamacro/clash/constant" +) + +func TestClash_SnellObfsHTTP(t *testing.T) { + cfg := &container.Config{ + Image: ImageSnell, + ExposedPorts: defaultExposedPorts, + Cmd: []string{"-c", "/config.conf"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell-http.conf"))}, + } + + id, err := startContainer(cfg, hostCfg, "snell-http") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewSnell(outbound.SnellOption{ + Name: "snell", + Server: localIP.String(), + Port: 10002, + Psk: "password", + ObfsOpts: map[string]any{ + "mode": "http", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_SnellObfsTLS(t *testing.T) { + cfg := &container.Config{ + Image: ImageSnell, + ExposedPorts: defaultExposedPorts, + Cmd: []string{"-c", "/config.conf"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell-tls.conf"))}, + } + + id, err := startContainer(cfg, hostCfg, "snell-tls") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewSnell(outbound.SnellOption{ + Name: "snell", + Server: localIP.String(), + Port: 10002, + Psk: "password", + ObfsOpts: map[string]any{ + "mode": "tls", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_Snell(t *testing.T) { + cfg := &container.Config{ + Image: ImageSnell, + ExposedPorts: defaultExposedPorts, + Cmd: []string{"-c", "/config.conf"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell.conf"))}, + } + + id, err := startContainer(cfg, hostCfg, "snell") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewSnell(outbound.SnellOption{ + Name: "snell", + Server: localIP.String(), + Port: 10002, + Psk: "password", + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_Snellv3(t *testing.T) { + cfg := &container.Config{ + Image: ImageSnell, + ExposedPorts: defaultExposedPorts, + Cmd: []string{"-c", "/config.conf"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell.conf"))}, + } + + id, err := startContainer(cfg, hostCfg, "snell") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewSnell(outbound.SnellOption{ + Name: "snell", + Server: localIP.String(), + Port: 10002, + Psk: "password", + UDP: true, + Version: 3, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func Benchmark_Snell(b *testing.B) { + cfg := &container.Config{ + Image: ImageSnell, + ExposedPorts: defaultExposedPorts, + Cmd: []string{"-c", "/config.conf"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell-http.conf"))}, + } + + id, err := startContainer(cfg, hostCfg, "snell-bench") + require.NoError(b, err) + + b.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewSnell(outbound.SnellOption{ + Name: "snell", + Server: localIP.String(), + Port: 10002, + Psk: "password", + ObfsOpts: map[string]any{ + "mode": "http", + }, + }) + require.NoError(b, err) + + time.Sleep(waitTime) + benchmarkProxy(b, proxy) +} diff --git a/test/ss_test.go b/test/ss_test.go new file mode 100644 index 0000000..3e93673 --- /dev/null +++ b/test/ss_test.go @@ -0,0 +1,190 @@ +package main + +import ( + "net" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/require" + + "github.com/Dreamacro/clash/adapter/outbound" +) + +func TestClash_Shadowsocks(t *testing.T) { + cfg := &container.Config{ + Image: ImageShadowsocksRust, + Entrypoint: []string{"ssserver"}, + Cmd: []string{"-s", "0.0.0.0:10002", "-m", "chacha20-ietf-poly1305", "-k", "FzcLbKs2dY9mhL", "-U"}, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + } + + id, err := startContainer(cfg, hostCfg, "ss") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ + Name: "ss", + Server: localIP.String(), + Port: 10002, + Password: "FzcLbKs2dY9mhL", + Cipher: "chacha20-ietf-poly1305", + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_ShadowsocksObfsHTTP(t *testing.T) { + cfg := &container.Config{ + Image: ImageShadowsocks, + Env: []string{ + "SS_MODULE=ss-server", + "SS_CONFIG=-s 0.0.0.0 -u -p 10002 -m chacha20-ietf-poly1305 -k FzcLbKs2dY9mhL --plugin obfs-server --plugin-opts obfs=http", + }, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + } + + id, err := startContainer(cfg, hostCfg, "ss-obfs-http") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ + Name: "ss", + Server: localIP.String(), + Port: 10002, + Password: "FzcLbKs2dY9mhL", + Cipher: "chacha20-ietf-poly1305", + UDP: true, + Plugin: "obfs", + PluginOpts: map[string]any{ + "mode": "http", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_ShadowsocksObfsTLS(t *testing.T) { + cfg := &container.Config{ + Image: ImageShadowsocks, + Env: []string{ + "SS_MODULE=ss-server", + "SS_CONFIG=-s 0.0.0.0 -u -p 10002 -m chacha20-ietf-poly1305 -k FzcLbKs2dY9mhL --plugin obfs-server --plugin-opts obfs=tls", + }, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + } + + id, err := startContainer(cfg, hostCfg, "ss-obfs-tls") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ + Name: "ss", + Server: localIP.String(), + Port: 10002, + Password: "FzcLbKs2dY9mhL", + Cipher: "chacha20-ietf-poly1305", + UDP: true, + Plugin: "obfs", + PluginOpts: map[string]any{ + "mode": "tls", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_ShadowsocksV2RayPlugin(t *testing.T) { + cfg := &container.Config{ + Image: ImageShadowsocks, + Env: []string{ + "SS_MODULE=ss-server", + "SS_CONFIG=-s 0.0.0.0 -u -p 10002 -m chacha20-ietf-poly1305 -k FzcLbKs2dY9mhL --plugin v2ray-plugin --plugin-opts=server", + }, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + } + + id, err := startContainer(cfg, hostCfg, "ss-v2ray-plugin") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ + Name: "ss", + Server: localIP.String(), + Port: 10002, + Password: "FzcLbKs2dY9mhL", + Cipher: "chacha20-ietf-poly1305", + UDP: true, + Plugin: "v2ray-plugin", + PluginOpts: map[string]any{ + "mode": "websocket", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func Benchmark_Shadowsocks(b *testing.B) { + cfg := &container.Config{ + Image: ImageShadowsocksRust, + Entrypoint: []string{"ssserver"}, + Cmd: []string{"-s", "0.0.0.0:10002", "-m", "aes-256-gcm", "-k", "FzcLbKs2dY9mhL", "-U"}, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + } + + id, err := startContainer(cfg, hostCfg, "ss-bench") + require.NoError(b, err) + + b.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ + Name: "ss", + Server: localIP.String(), + Port: 10002, + Password: "FzcLbKs2dY9mhL", + Cipher: "aes-256-gcm", + UDP: true, + }) + require.NoError(b, err) + + require.True(b, TCPing(net.JoinHostPort(localIP.String(), "10002"))) + benchmarkProxy(b, proxy) +} diff --git a/test/trojan_test.go b/test/trojan_test.go new file mode 100644 index 0000000..89b2547 --- /dev/null +++ b/test/trojan_test.go @@ -0,0 +1,161 @@ +package main + +import ( + "fmt" + "net" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/require" + + "github.com/Dreamacro/clash/adapter/outbound" + C "github.com/Dreamacro/clash/constant" +) + +func TestClash_Trojan(t *testing.T) { + cfg := &container.Config{ + Image: ImageTrojan, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/config/config.json", C.Path.Resolve("trojan.json")), + fmt.Sprintf("%s:/path/to/certificate.crt", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/path/to/private.key", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "trojan") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewTrojan(outbound.TrojanOption{ + Name: "trojan", + Server: localIP.String(), + Port: 10002, + Password: "password", + SNI: "example.org", + SkipCertVerify: true, + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_TrojanGrpc(t *testing.T) { + cfg := &container.Config{ + Image: ImageXray, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/xray/config.json", C.Path.Resolve("trojan-grpc.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "trojan-grpc") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewTrojan(outbound.TrojanOption{ + Name: "trojan", + Server: localIP.String(), + Port: 10002, + Password: "example", + SNI: "example.org", + SkipCertVerify: true, + UDP: true, + Network: "grpc", + GrpcOpts: outbound.GrpcOptions{ + GrpcServiceName: "example", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_TrojanWebsocket(t *testing.T) { + cfg := &container.Config{ + Image: ImageTrojanGo, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/trojan-go/config.json", C.Path.Resolve("trojan-ws.json")), + fmt.Sprintf("%s:/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "trojan-ws") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewTrojan(outbound.TrojanOption{ + Name: "trojan", + Server: localIP.String(), + Port: 10002, + Password: "example", + SNI: "example.org", + SkipCertVerify: true, + UDP: true, + Network: "ws", + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func Benchmark_Trojan(b *testing.B) { + cfg := &container.Config{ + Image: ImageTrojan, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/config/config.json", C.Path.Resolve("trojan.json")), + fmt.Sprintf("%s:/path/to/certificate.crt", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/path/to/private.key", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "trojan-bench") + require.NoError(b, err) + + b.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewTrojan(outbound.TrojanOption{ + Name: "trojan", + Server: localIP.String(), + Port: 10002, + Password: "password", + SNI: "example.org", + SkipCertVerify: true, + UDP: true, + }) + require.NoError(b, err) + + require.True(b, TCPing(net.JoinHostPort(localIP.String(), "10002"))) + benchmarkProxy(b, proxy) +} diff --git a/test/util.go b/test/util.go new file mode 100644 index 0000000..3d0fce8 --- /dev/null +++ b/test/util.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "net" + "time" +) + +func Listen(network, address string) (net.Listener, error) { + lc := net.ListenConfig{} + + var lastErr error + for i := 0; i < 5; i++ { + l, err := lc.Listen(context.Background(), network, address) + if err == nil { + return l, nil + } + + lastErr = err + time.Sleep(time.Millisecond * 200) + } + return nil, lastErr +} + +func ListenPacket(network, address string) (net.PacketConn, error) { + var lastErr error + for i := 0; i < 5; i++ { + l, err := net.ListenPacket(network, address) + if err == nil { + return l, nil + } + + lastErr = err + time.Sleep(time.Millisecond * 200) + } + return nil, lastErr +} + +func TCPing(addr string) bool { + for i := 0; i < 10; i++ { + conn, err := net.Dial("tcp", addr) + if err == nil { + conn.Close() + return true + } + time.Sleep(time.Millisecond * 500) + } + + return false +} diff --git a/test/util_darwin_test.go b/test/util_darwin_test.go new file mode 100644 index 0000000..ab9b8b2 --- /dev/null +++ b/test/util_darwin_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "errors" + "fmt" + "net" + "syscall" + + "golang.org/x/net/route" +) + +func defaultRouteIP() (net.IP, error) { + idx, err := defaultRouteInterfaceIndex() + if err != nil { + return nil, err + } + iface, err := net.InterfaceByIndex(idx) + if err != nil { + return nil, err + } + addrs, err := iface.Addrs() + if err != nil { + return nil, err + } + for _, addr := range addrs { + ip := addr.(*net.IPNet).IP + if ip.To4() != nil { + return ip, nil + } + } + + return nil, errors.New("no ipv4 addr") +} + +func defaultRouteInterfaceIndex() (int, error) { + rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0) + if err != nil { + return 0, fmt.Errorf("route.FetchRIB: %w", err) + } + msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib) + if err != nil { + return 0, fmt.Errorf("route.ParseRIB: %w", err) + } + for _, message := range msgs { + routeMessage := message.(*route.RouteMessage) + if routeMessage.Flags&(syscall.RTF_UP|syscall.RTF_GATEWAY|syscall.RTF_STATIC) == 0 { + continue + } + + addresses := routeMessage.Addrs + + destination, ok := addresses[0].(*route.Inet4Addr) + if !ok { + continue + } + + if destination.IP != [4]byte{0, 0, 0, 0} { + continue + } + + switch addresses[1].(type) { + case *route.Inet4Addr: + return routeMessage.Index, nil + default: + continue + } + } + + return 0, fmt.Errorf("ambiguous gateway interfaces found") +} diff --git a/test/util_other_test.go b/test/util_other_test.go new file mode 100644 index 0000000..708b609 --- /dev/null +++ b/test/util_other_test.go @@ -0,0 +1,12 @@ +//go:build !darwin + +package main + +import ( + "errors" + "net" +) + +func defaultRouteIP() (net.IP, error) { + return nil, errors.New("not supported") +} diff --git a/test/vmess_test.go b/test/vmess_test.go new file mode 100644 index 0000000..61f9e90 --- /dev/null +++ b/test/vmess_test.go @@ -0,0 +1,452 @@ +package main + +import ( + "fmt" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/require" + + "github.com/Dreamacro/clash/adapter/outbound" + C "github.com/Dreamacro/clash/constant" +) + +func TestClash_Vmess(t *testing.T) { + configPath := C.Path.Resolve("vmess.json") + + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, + } + + id, err := startContainer(cfg, hostCfg, "vmess") + require.NoError(t, err) + + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessTLS(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-tls.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vmess-tls") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + TLS: true, + SkipCertVerify: true, + ServerName: "example.org", + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessHTTP2(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-http2.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vmess-http2") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "h2", + TLS: true, + SkipCertVerify: true, + ServerName: "example.org", + UDP: true, + HTTP2Opts: outbound.HTTP2Options{ + Host: []string{"example.org"}, + Path: "/test", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessHTTP(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-http.json")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vmess-http") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "http", + UDP: true, + HTTPOpts: outbound.HTTPOptions{ + Method: "GET", + Path: []string{"/"}, + Headers: map[string][]string{ + "Host": {"www.amazon.com"}, + "User-Agent": { + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36 Edg/84.0.522.49", + }, + "Accept-Encoding": { + "gzip, deflate", + }, + "Connection": { + "keep-alive", + }, + "Pragma": {"no-cache"}, + }, + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessWebsocket(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-ws.json")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vmess-ws") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "ws", + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessWebsocketTLS(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-ws-tls.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vmess-ws-tls") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "ws", + TLS: true, + SkipCertVerify: true, + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessWebsocketTLSZero(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-ws-tls-zero.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vmess-ws-tls-zero") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "zero", + Network: "ws", + TLS: true, + SkipCertVerify: true, + UDP: true, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessGrpc(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-grpc.json")), + fmt.Sprintf("%s:/etc/ssl/v2ray/fullchain.pem", C.Path.Resolve("example.org.pem")), + fmt.Sprintf("%s:/etc/ssl/v2ray/privkey.pem", C.Path.Resolve("example.org-key.pem")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vmess-grpc") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "grpc", + TLS: true, + SkipCertVerify: true, + UDP: true, + ServerName: "example.org", + GrpcOpts: outbound.GrpcOptions{ + GrpcServiceName: "example!", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessWebsocket0RTT(t *testing.T) { + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vmess-ws-0rtt.json")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vmess-ws-0rtt") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "ws", + UDP: true, + ServerName: "example.org", + WSOpts: outbound.WSOptions{ + MaxEarlyData: 2048, + EarlyDataHeaderName: "Sec-WebSocket-Protocol", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func TestClash_VmessWebsocketXray0RTT(t *testing.T) { + cfg := &container.Config{ + Image: ImageXray, + ExposedPorts: defaultExposedPorts, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{ + fmt.Sprintf("%s:/etc/xray/config.json", C.Path.Resolve("vmess-ws-0rtt.json")), + }, + } + + id, err := startContainer(cfg, hostCfg, "vmess-xray-ws-0rtt") + require.NoError(t, err) + t.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + Network: "ws", + UDP: true, + ServerName: "example.org", + WSOpts: outbound.WSOptions{ + Path: "/?ed=2048", + }, + }) + require.NoError(t, err) + + time.Sleep(waitTime) + testSuit(t, proxy) +} + +func Benchmark_Vmess(b *testing.B) { + configPath := C.Path.Resolve("vmess.json") + + cfg := &container.Config{ + Image: ImageVmess, + ExposedPorts: defaultExposedPorts, + Entrypoint: []string{"/usr/bin/v2ray"}, + Cmd: []string{"run", "-c", "/etc/v2ray/config.json"}, + } + hostCfg := &container.HostConfig{ + PortBindings: defaultPortBindings, + Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)}, + } + + id, err := startContainer(cfg, hostCfg, "vmess-bench") + require.NoError(b, err) + + b.Cleanup(func() { + cleanContainer(id) + }) + + proxy, err := outbound.NewVmess(outbound.VmessOption{ + Name: "vmess", + Server: localIP.String(), + Port: 10002, + UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", + Cipher: "auto", + AlterID: 0, + UDP: true, + }) + require.NoError(b, err) + + time.Sleep(waitTime) + benchmarkProxy(b, proxy) +} diff --git a/transport/gun/gun.go b/transport/gun/gun.go new file mode 100644 index 0000000..d1cc7e6 --- /dev/null +++ b/transport/gun/gun.go @@ -0,0 +1,239 @@ +// Modified from: https://github.com/Qv2ray/gun-lite +// License: MIT + +package gun + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "sync" + "time" + + "github.com/Dreamacro/clash/common/pool" + + "go.uber.org/atomic" + "golang.org/x/net/http2" +) + +var ( + ErrInvalidLength = errors.New("invalid length") + ErrSmallBuffer = errors.New("buffer too small") +) + +var defaultHeader = http.Header{ + "content-type": []string{"application/grpc"}, + "user-agent": []string{"grpc-go/1.36.0"}, +} + +type DialFn = func(network, addr string) (net.Conn, error) + +type Conn struct { + response *http.Response + request *http.Request + transport *http2.Transport + writer *io.PipeWriter + once sync.Once + close *atomic.Bool + err error + remain int + br *bufio.Reader + + // deadlines + deadline *time.Timer +} + +type Config struct { + ServiceName string + Host string +} + +func (g *Conn) initRequest() { + response, err := g.transport.RoundTrip(g.request) + if err != nil { + g.err = err + g.writer.Close() + return + } + + if !g.close.Load() { + g.response = response + g.br = bufio.NewReader(response.Body) + } else { + response.Body.Close() + } +} + +func (g *Conn) Read(b []byte) (n int, err error) { + g.once.Do(g.initRequest) + if g.err != nil { + return 0, g.err + } + + if g.remain > 0 { + size := g.remain + if len(b) < size { + size = len(b) + } + + n, err = io.ReadFull(g.br, b[:size]) + g.remain -= n + return + } else if g.response == nil { + return 0, net.ErrClosed + } + + // 0x00 grpclength(uint32) 0x0A uleb128 payload + _, err = g.br.Discard(6) + if err != nil { + return 0, err + } + + protobufPayloadLen, err := binary.ReadUvarint(g.br) + if err != nil { + return 0, ErrInvalidLength + } + + size := int(protobufPayloadLen) + if len(b) < size { + size = len(b) + } + + n, err = io.ReadFull(g.br, b[:size]) + if err != nil { + return + } + + remain := int(protobufPayloadLen) - n + if remain > 0 { + g.remain = remain + } + + return n, nil +} + +func (g *Conn) Write(b []byte) (n int, err error) { + protobufHeader := [binary.MaxVarintLen64 + 1]byte{0x0A} + varuintSize := binary.PutUvarint(protobufHeader[1:], uint64(len(b))) + grpcHeader := make([]byte, 5) + grpcPayloadLen := uint32(varuintSize + 1 + len(b)) + binary.BigEndian.PutUint32(grpcHeader[1:5], grpcPayloadLen) + + buf := pool.GetBytesBuffer() + defer pool.PutBytesBuffer(buf) + buf.PutSlice(grpcHeader) + buf.PutSlice(protobufHeader[:varuintSize+1]) + buf.PutSlice(b) + + _, err = g.writer.Write(buf.Bytes()) + if err == io.ErrClosedPipe && g.err != nil { + err = g.err + } + + return len(b), err +} + +func (g *Conn) Close() error { + g.close.Store(true) + if r := g.response; r != nil { + r.Body.Close() + } + + return g.writer.Close() +} + +func (g *Conn) LocalAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4zero, Port: 0} } +func (g *Conn) RemoteAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4zero, Port: 0} } +func (g *Conn) SetReadDeadline(t time.Time) error { return g.SetDeadline(t) } +func (g *Conn) SetWriteDeadline(t time.Time) error { return g.SetDeadline(t) } + +func (g *Conn) SetDeadline(t time.Time) error { + d := time.Until(t) + if g.deadline != nil { + g.deadline.Reset(d) + return nil + } + g.deadline = time.AfterFunc(d, func() { + g.Close() + }) + return nil +} + +func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config) *http2.Transport { + dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { + pconn, err := dialFn(network, addr) + if err != nil { + return nil, err + } + + cn := tls.Client(pconn, cfg) + if err := cn.HandshakeContext(ctx); err != nil { + pconn.Close() + return nil, err + } + state := cn.ConnectionState() + if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { + cn.Close() + return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) + } + return cn, nil + } + + return &http2.Transport{ + DialTLSContext: dialFunc, + TLSClientConfig: tlsConfig, + AllowHTTP: false, + DisableCompression: true, + PingTimeout: 0, + } +} + +func StreamGunWithTransport(transport *http2.Transport, cfg *Config) (net.Conn, error) { + serviceName := "GunService" + if cfg.ServiceName != "" { + serviceName = cfg.ServiceName + } + + reader, writer := io.Pipe() + request := &http.Request{ + Method: http.MethodPost, + Body: reader, + URL: &url.URL{ + Scheme: "https", + Host: cfg.Host, + Path: fmt.Sprintf("/%s/Tun", serviceName), + // for unescape path + Opaque: fmt.Sprintf("//%s/%s/Tun", cfg.Host, serviceName), + }, + Proto: "HTTP/2", + ProtoMajor: 2, + ProtoMinor: 0, + Header: defaultHeader, + } + + conn := &Conn{ + request: request, + transport: transport, + writer: writer, + close: atomic.NewBool(false), + } + + go conn.once.Do(conn.initRequest) + return conn, nil +} + +func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config) (net.Conn, error) { + dialFn := func(network, addr string) (net.Conn, error) { + return conn, nil + } + + transport := NewHTTP2Client(dialFn, tlsConfig) + return StreamGunWithTransport(transport, cfg) +} diff --git a/transport/shadowsocks/README.md b/transport/shadowsocks/README.md new file mode 100644 index 0000000..c9fc815 --- /dev/null +++ b/transport/shadowsocks/README.md @@ -0,0 +1,5 @@ +## Embedded go-shadowsocks2 + +from https://github.com/Dreamacro/go-shadowsocks2 + +origin https://github.com/riobard/go-shadowsocks2 diff --git a/transport/shadowsocks/core/cipher.go b/transport/shadowsocks/core/cipher.go new file mode 100644 index 0000000..2f5acf6 --- /dev/null +++ b/transport/shadowsocks/core/cipher.go @@ -0,0 +1,164 @@ +package core + +import ( + "crypto/md5" + "errors" + "net" + "sort" + "strings" + + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowstream" +) + +type Cipher interface { + StreamConnCipher + PacketConnCipher +} + +type StreamConnCipher interface { + StreamConn(net.Conn) net.Conn +} + +type PacketConnCipher interface { + PacketConn(net.PacketConn) net.PacketConn +} + +// ErrCipherNotSupported occurs when a cipher is not supported (likely because of security concerns). +var ErrCipherNotSupported = errors.New("cipher not supported") + +const ( + aeadAes128Gcm = "AEAD_AES_128_GCM" + aeadAes192Gcm = "AEAD_AES_192_GCM" + aeadAes256Gcm = "AEAD_AES_256_GCM" + aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305" + aeadXChacha20Poly1305 = "AEAD_XCHACHA20_POLY1305" +) + +// List of AEAD ciphers: key size in bytes and constructor +var aeadList = map[string]struct { + KeySize int + New func([]byte) (shadowaead.Cipher, error) +}{ + aeadAes128Gcm: {16, shadowaead.AESGCM}, + aeadAes192Gcm: {24, shadowaead.AESGCM}, + aeadAes256Gcm: {32, shadowaead.AESGCM}, + aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305}, + aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305}, +} + +// List of stream ciphers: key size in bytes and constructor +var streamList = map[string]struct { + KeySize int + New func(key []byte) (shadowstream.Cipher, error) +}{ + "RC4-MD5": {16, shadowstream.RC4MD5}, + "AES-128-CTR": {16, shadowstream.AESCTR}, + "AES-192-CTR": {24, shadowstream.AESCTR}, + "AES-256-CTR": {32, shadowstream.AESCTR}, + "AES-128-CFB": {16, shadowstream.AESCFB}, + "AES-192-CFB": {24, shadowstream.AESCFB}, + "AES-256-CFB": {32, shadowstream.AESCFB}, + "CHACHA20-IETF": {32, shadowstream.Chacha20IETF}, + "XCHACHA20": {32, shadowstream.Xchacha20}, +} + +// ListCipher returns a list of available cipher names sorted alphabetically. +func ListCipher() []string { + var l []string + for k := range aeadList { + l = append(l, k) + } + for k := range streamList { + l = append(l, k) + } + sort.Strings(l) + return l +} + +// PickCipher returns a Cipher of the given name. Derive key from password if given key is empty. +func PickCipher(name string, key []byte, password string) (Cipher, error) { + name = strings.ToUpper(name) + + switch name { + case "DUMMY": + return &dummy{}, nil + case "CHACHA20-IETF-POLY1305": + name = aeadChacha20Poly1305 + case "XCHACHA20-IETF-POLY1305": + name = aeadXChacha20Poly1305 + case "AES-128-GCM": + name = aeadAes128Gcm + case "AES-192-GCM": + name = aeadAes192Gcm + case "AES-256-GCM": + name = aeadAes256Gcm + } + + if choice, ok := aeadList[name]; ok { + if len(key) == 0 { + key = Kdf(password, choice.KeySize) + } + if len(key) != choice.KeySize { + return nil, shadowaead.KeySizeError(choice.KeySize) + } + aead, err := choice.New(key) + return &AeadCipher{Cipher: aead, Key: key}, err + } + + if choice, ok := streamList[name]; ok { + if len(key) == 0 { + key = Kdf(password, choice.KeySize) + } + if len(key) != choice.KeySize { + return nil, shadowstream.KeySizeError(choice.KeySize) + } + ciph, err := choice.New(key) + return &StreamCipher{Cipher: ciph, Key: key}, err + } + + return nil, ErrCipherNotSupported +} + +type AeadCipher struct { + shadowaead.Cipher + + Key []byte +} + +func (aead *AeadCipher) StreamConn(c net.Conn) net.Conn { return shadowaead.NewConn(c, aead) } +func (aead *AeadCipher) PacketConn(c net.PacketConn) net.PacketConn { + return shadowaead.NewPacketConn(c, aead) +} + +type StreamCipher struct { + shadowstream.Cipher + + Key []byte +} + +func (ciph *StreamCipher) StreamConn(c net.Conn) net.Conn { return shadowstream.NewConn(c, ciph) } +func (ciph *StreamCipher) PacketConn(c net.PacketConn) net.PacketConn { + return shadowstream.NewPacketConn(c, ciph) +} + +// dummy cipher does not encrypt + +type dummy struct{} + +func (dummy) StreamConn(c net.Conn) net.Conn { return c } +func (dummy) PacketConn(c net.PacketConn) net.PacketConn { return c } + +// key-derivation function from original Shadowsocks +func Kdf(password string, keyLen int) []byte { + var b, prev []byte + h := md5.New() + for len(b) < keyLen { + h.Write(prev) + h.Write([]byte(password)) + b = h.Sum(b) + prev = b[len(b)-h.Size():] + h.Reset() + } + return b[:keyLen] +} diff --git a/transport/shadowsocks/shadowaead/cipher.go b/transport/shadowsocks/shadowaead/cipher.go new file mode 100644 index 0000000..3cf7574 --- /dev/null +++ b/transport/shadowsocks/shadowaead/cipher.go @@ -0,0 +1,94 @@ +package shadowaead + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "io" + "strconv" + + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/hkdf" +) + +type Cipher interface { + KeySize() int + SaltSize() int + Encrypter(salt []byte) (cipher.AEAD, error) + Decrypter(salt []byte) (cipher.AEAD, error) +} + +type KeySizeError int + +func (e KeySizeError) Error() string { + return "key size error: need " + strconv.Itoa(int(e)) + " bytes" +} + +func hkdfSHA1(secret, salt, info, outkey []byte) { + r := hkdf.New(sha1.New, secret, salt, info) + if _, err := io.ReadFull(r, outkey); err != nil { + panic(err) // should never happen + } +} + +type metaCipher struct { + psk []byte + makeAEAD func(key []byte) (cipher.AEAD, error) +} + +func (a *metaCipher) KeySize() int { return len(a.psk) } +func (a *metaCipher) SaltSize() int { + if ks := a.KeySize(); ks > 16 { + return ks + } + return 16 +} + +func (a *metaCipher) Encrypter(salt []byte) (cipher.AEAD, error) { + subkey := make([]byte, a.KeySize()) + hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey) + return a.makeAEAD(subkey) +} + +func (a *metaCipher) Decrypter(salt []byte) (cipher.AEAD, error) { + subkey := make([]byte, a.KeySize()) + hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey) + return a.makeAEAD(subkey) +} + +func aesGCM(key []byte) (cipher.AEAD, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return cipher.NewGCM(blk) +} + +// AESGCM creates a new Cipher with a pre-shared key. len(psk) must be +// one of 16, 24, or 32 to select AES-128/196/256-GCM. +func AESGCM(psk []byte) (Cipher, error) { + switch l := len(psk); l { + case 16, 24, 32: // AES 128/196/256 + default: + return nil, aes.KeySizeError(l) + } + return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil +} + +// Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func Chacha20Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha20poly1305.KeySize { + return nil, KeySizeError(chacha20poly1305.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.New}, nil +} + +// XChacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func XChacha20Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha20poly1305.KeySize { + return nil, KeySizeError(chacha20poly1305.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil +} diff --git a/transport/shadowsocks/shadowaead/packet.go b/transport/shadowsocks/shadowaead/packet.go new file mode 100644 index 0000000..7043ead --- /dev/null +++ b/transport/shadowsocks/shadowaead/packet.go @@ -0,0 +1,95 @@ +package shadowaead + +import ( + "crypto/rand" + "errors" + "io" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +// ErrShortPacket means that the packet is too short for a valid encrypted packet. +var ErrShortPacket = errors.New("short packet") + +var _zerononce [128]byte // read-only. 128 bytes is more than enough. + +// Pack encrypts plaintext using Cipher with a randomly generated salt and +// returns a slice of dst containing the encrypted packet and any error occurred. +// Ensure len(dst) >= ciph.SaltSize() + len(plaintext) + aead.Overhead(). +func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) { + saltSize := ciph.SaltSize() + salt := dst[:saltSize] + if _, err := rand.Read(salt); err != nil { + return nil, err + } + aead, err := ciph.Encrypter(salt) + if err != nil { + return nil, err + } + if len(dst) < saltSize+len(plaintext)+aead.Overhead() { + return nil, io.ErrShortBuffer + } + b := aead.Seal(dst[saltSize:saltSize], _zerononce[:aead.NonceSize()], plaintext, nil) + return dst[:saltSize+len(b)], nil +} + +// Unpack decrypts pkt using Cipher and returns a slice of dst containing the decrypted payload and any error occurred. +// Ensure len(dst) >= len(pkt) - aead.SaltSize() - aead.Overhead(). +func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) { + saltSize := ciph.SaltSize() + if len(pkt) < saltSize { + return nil, ErrShortPacket + } + salt := pkt[:saltSize] + aead, err := ciph.Decrypter(salt) + if err != nil { + return nil, err + } + if len(pkt) < saltSize+aead.Overhead() { + return nil, ErrShortPacket + } + if saltSize+len(dst)+aead.Overhead() < len(pkt) { + return nil, io.ErrShortBuffer + } + b, err := aead.Open(dst[:0], _zerononce[:aead.NonceSize()], pkt[saltSize:], nil) + return b, err +} + +type PacketConn struct { + net.PacketConn + Cipher +} + +const maxPacketSize = 64 * 1024 + +// NewPacketConn wraps a net.PacketConn with cipher +func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn { + return &PacketConn{PacketConn: c, Cipher: ciph} +} + +// WriteTo encrypts b and write to addr using the embedded PacketConn. +func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + buf := pool.Get(maxPacketSize) + defer pool.Put(buf) + buf, err := Pack(buf, b, c) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(buf, addr) + return len(b), err +} + +// ReadFrom reads from the embedded PacketConn and decrypts into b. +func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, addr, err := c.PacketConn.ReadFrom(b) + if err != nil { + return n, addr, err + } + bb, err := Unpack(b[c.Cipher.SaltSize():], b[:n], c) + if err != nil { + return n, addr, err + } + copy(b, bb) + return len(bb), addr, err +} diff --git a/transport/shadowsocks/shadowaead/stream.go b/transport/shadowsocks/shadowaead/stream.go new file mode 100644 index 0000000..e92bdda --- /dev/null +++ b/transport/shadowsocks/shadowaead/stream.go @@ -0,0 +1,285 @@ +package shadowaead + +import ( + "crypto/cipher" + "crypto/rand" + "errors" + "io" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +const ( + // payloadSizeMask is the maximum size of payload in bytes. + payloadSizeMask = 0x3FFF // 16*1024 - 1 + bufSize = 17 * 1024 // >= 2+aead.Overhead()+payloadSizeMask+aead.Overhead() +) + +var ErrZeroChunk = errors.New("zero chunk") + +type Writer struct { + io.Writer + cipher.AEAD + nonce [32]byte // should be sufficient for most nonce sizes +} + +// NewWriter wraps an io.Writer with authenticated encryption. +func NewWriter(w io.Writer, aead cipher.AEAD) *Writer { return &Writer{Writer: w, AEAD: aead} } + +// Write encrypts p and writes to the embedded io.Writer. +func (w *Writer) Write(p []byte) (n int, err error) { + buf := pool.Get(bufSize) + defer pool.Put(buf) + nonce := w.nonce[:w.NonceSize()] + tag := w.Overhead() + off := 2 + tag + + // compatible with snell + if len(p) == 0 { + buf = buf[:off] + buf[0], buf[1] = byte(0), byte(0) + w.Seal(buf[:0], nonce, buf[:2], nil) + increment(nonce) + _, err = w.Writer.Write(buf) + return + } + + for nr := 0; n < len(p) && err == nil; n += nr { + nr = payloadSizeMask + if n+nr > len(p) { + nr = len(p) - n + } + buf = buf[:off+nr+tag] + buf[0], buf[1] = byte(nr>>8), byte(nr) // big-endian payload size + w.Seal(buf[:0], nonce, buf[:2], nil) + increment(nonce) + w.Seal(buf[:off], nonce, p[n:n+nr], nil) + increment(nonce) + _, err = w.Writer.Write(buf) + } + return +} + +// ReadFrom reads from the given io.Reader until EOF or error, encrypts and +// writes to the embedded io.Writer. Returns number of bytes read from r and +// any error encountered. +func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) { + buf := pool.Get(bufSize) + defer pool.Put(buf) + nonce := w.nonce[:w.NonceSize()] + tag := w.Overhead() + off := 2 + tag + for { + nr, er := r.Read(buf[off : off+payloadSizeMask]) + n += int64(nr) + buf[0], buf[1] = byte(nr>>8), byte(nr) + w.Seal(buf[:0], nonce, buf[:2], nil) + increment(nonce) + w.Seal(buf[:off], nonce, buf[off:off+nr], nil) + increment(nonce) + if _, ew := w.Writer.Write(buf[:off+nr+tag]); ew != nil { + err = ew + return + } + if er != nil { + if er != io.EOF { // ignore EOF as per io.ReaderFrom contract + err = er + } + return + } + } +} + +type Reader struct { + io.Reader + cipher.AEAD + nonce [32]byte // should be sufficient for most nonce sizes + buf []byte // to be put back into bufPool + off int // offset to unconsumed part of buf +} + +// NewReader wraps an io.Reader with authenticated decryption. +func NewReader(r io.Reader, aead cipher.AEAD) *Reader { return &Reader{Reader: r, AEAD: aead} } + +// Read and decrypt a record into p. len(p) >= max payload size + AEAD overhead. +func (r *Reader) read(p []byte) (int, error) { + nonce := r.nonce[:r.NonceSize()] + tag := r.Overhead() + + // decrypt payload size + p = p[:2+tag] + if _, err := io.ReadFull(r.Reader, p); err != nil { + return 0, err + } + _, err := r.Open(p[:0], nonce, p, nil) + increment(nonce) + if err != nil { + return 0, err + } + + // decrypt payload + size := (int(p[0])<<8 + int(p[1])) & payloadSizeMask + if size == 0 { + return 0, ErrZeroChunk + } + + p = p[:size+tag] + if _, err := io.ReadFull(r.Reader, p); err != nil { + return 0, err + } + _, err = r.Open(p[:0], nonce, p, nil) + increment(nonce) + if err != nil { + return 0, err + } + return size, nil +} + +// Read reads from the embedded io.Reader, decrypts and writes to p. +func (r *Reader) Read(p []byte) (int, error) { + if r.buf == nil { + if len(p) >= payloadSizeMask+r.Overhead() { + return r.read(p) + } + b := pool.Get(bufSize) + n, err := r.read(b) + if err != nil { + return 0, err + } + r.buf = b[:n] + r.off = 0 + } + + n := copy(p, r.buf[r.off:]) + r.off += n + if r.off == len(r.buf) { + pool.Put(r.buf[:cap(r.buf)]) + r.buf = nil + } + return n, nil +} + +// WriteTo reads from the embedded io.Reader, decrypts and writes to w until +// there's no more data to write or when an error occurs. Return number of +// bytes written to w and any error encountered. +func (r *Reader) WriteTo(w io.Writer) (n int64, err error) { + if r.buf == nil { + r.buf = pool.Get(bufSize) + r.off = len(r.buf) + } + + for { + for r.off < len(r.buf) { + nw, ew := w.Write(r.buf[r.off:]) + r.off += nw + n += int64(nw) + if ew != nil { + if r.off == len(r.buf) { + pool.Put(r.buf[:cap(r.buf)]) + r.buf = nil + } + err = ew + return + } + } + + nr, er := r.read(r.buf) + if er != nil { + if er != io.EOF { + err = er + } + return + } + r.buf = r.buf[:nr] + r.off = 0 + } +} + +// increment little-endian encoded unsigned integer b. Wrap around on overflow. +func increment(b []byte) { + for i := range b { + b[i]++ + if b[i] != 0 { + return + } + } +} + +type Conn struct { + net.Conn + Cipher + r *Reader + w *Writer +} + +// NewConn wraps a stream-oriented net.Conn with cipher. +func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} } + +func (c *Conn) initReader() error { + salt := make([]byte, c.SaltSize()) + if _, err := io.ReadFull(c.Conn, salt); err != nil { + return err + } + + aead, err := c.Decrypter(salt) + if err != nil { + return err + } + + c.r = NewReader(c.Conn, aead) + return nil +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.Read(b) +} + +func (c *Conn) WriteTo(w io.Writer) (int64, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.WriteTo(w) +} + +func (c *Conn) initWriter() error { + salt := make([]byte, c.SaltSize()) + if _, err := rand.Read(salt); err != nil { + return err + } + aead, err := c.Encrypter(salt) + if err != nil { + return err + } + _, err = c.Conn.Write(salt) + if err != nil { + return err + } + c.w = NewWriter(c.Conn, aead) + return nil +} + +func (c *Conn) Write(b []byte) (int, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.Write(b) +} + +func (c *Conn) ReadFrom(r io.Reader) (int64, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.ReadFrom(r) +} diff --git a/transport/shadowsocks/shadowstream/cipher.go b/transport/shadowsocks/shadowstream/cipher.go new file mode 100644 index 0000000..dd39d03 --- /dev/null +++ b/transport/shadowsocks/shadowstream/cipher.go @@ -0,0 +1,116 @@ +package shadowstream + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rc4" + "strconv" + + "golang.org/x/crypto/chacha20" +) + +// Cipher generates a pair of stream ciphers for encryption and decryption. +type Cipher interface { + IVSize() int + Encrypter(iv []byte) cipher.Stream + Decrypter(iv []byte) cipher.Stream +} + +type KeySizeError int + +func (e KeySizeError) Error() string { + return "key size error: need " + strconv.Itoa(int(e)) + " bytes" +} + +// CTR mode +type ctrStream struct{ cipher.Block } + +func (b *ctrStream) IVSize() int { return b.BlockSize() } +func (b *ctrStream) Decrypter(iv []byte) cipher.Stream { return b.Encrypter(iv) } +func (b *ctrStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCTR(b, iv) } + +func AESCTR(key []byte) (Cipher, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return &ctrStream{blk}, nil +} + +// CFB mode +type cfbStream struct{ cipher.Block } + +func (b *cfbStream) IVSize() int { return b.BlockSize() } +func (b *cfbStream) Decrypter(iv []byte) cipher.Stream { return cipher.NewCFBDecrypter(b, iv) } +func (b *cfbStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCFBEncrypter(b, iv) } + +func AESCFB(key []byte) (Cipher, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return &cfbStream{blk}, nil +} + +// IETF-variant of chacha20 +type chacha20ietfkey []byte + +func (k chacha20ietfkey) IVSize() int { return chacha20.NonceSize } +func (k chacha20ietfkey) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k chacha20ietfkey) Encrypter(iv []byte) cipher.Stream { + ciph, err := chacha20.NewUnauthenticatedCipher(k, iv) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func Chacha20IETF(key []byte) (Cipher, error) { + if len(key) != chacha20.KeySize { + return nil, KeySizeError(chacha20.KeySize) + } + return chacha20ietfkey(key), nil +} + +type xchacha20key []byte + +func (k xchacha20key) IVSize() int { return chacha20.NonceSizeX } +func (k xchacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) } +func (k xchacha20key) Encrypter(iv []byte) cipher.Stream { + ciph, err := chacha20.NewUnauthenticatedCipher(k, iv) + if err != nil { + panic(err) // should never happen + } + return ciph +} + +func Xchacha20(key []byte) (Cipher, error) { + if len(key) != chacha20.KeySize { + return nil, KeySizeError(chacha20.KeySize) + } + return xchacha20key(key), nil +} + +type rc4Md5Key []byte + +func (k rc4Md5Key) IVSize() int { + return 16 +} + +func (k rc4Md5Key) Encrypter(iv []byte) cipher.Stream { + h := md5.New() + h.Write([]byte(k)) + h.Write(iv) + rc4key := h.Sum(nil) + c, _ := rc4.NewCipher(rc4key) + return c +} + +func (k rc4Md5Key) Decrypter(iv []byte) cipher.Stream { + return k.Encrypter(iv) +} + +func RC4MD5(key []byte) (Cipher, error) { + return rc4Md5Key(key), nil +} diff --git a/transport/shadowsocks/shadowstream/packet.go b/transport/shadowsocks/shadowstream/packet.go new file mode 100644 index 0000000..0b46dea --- /dev/null +++ b/transport/shadowsocks/shadowstream/packet.go @@ -0,0 +1,79 @@ +package shadowstream + +import ( + "crypto/rand" + "errors" + "io" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +// ErrShortPacket means the packet is too short to be a valid encrypted packet. +var ErrShortPacket = errors.New("short packet") + +// Pack encrypts plaintext using stream cipher s and a random IV. +// Returns a slice of dst containing random IV and ciphertext. +// Ensure len(dst) >= s.IVSize() + len(plaintext). +func Pack(dst, plaintext []byte, s Cipher) ([]byte, error) { + if len(dst) < s.IVSize()+len(plaintext) { + return nil, io.ErrShortBuffer + } + iv := dst[:s.IVSize()] + _, err := rand.Read(iv) + if err != nil { + return nil, err + } + s.Encrypter(iv).XORKeyStream(dst[len(iv):], plaintext) + return dst[:len(iv)+len(plaintext)], nil +} + +// Unpack decrypts pkt using stream cipher s. +// Returns a slice of dst containing decrypted plaintext. +func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) { + if len(pkt) < s.IVSize() { + return nil, ErrShortPacket + } + if len(dst) < len(pkt)-s.IVSize() { + return nil, io.ErrShortBuffer + } + iv := pkt[:s.IVSize()] + s.Decrypter(iv).XORKeyStream(dst, pkt[len(iv):]) + return dst[:len(pkt)-len(iv)], nil +} + +type PacketConn struct { + net.PacketConn + Cipher +} + +// NewPacketConn wraps a net.PacketConn with stream cipher encryption/decryption. +func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn { + return &PacketConn{PacketConn: c, Cipher: ciph} +} + +const maxPacketSize = 64 * 1024 + +func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + buf := pool.Get(maxPacketSize) + defer pool.Put(buf) + buf, err := Pack(buf, b, c.Cipher) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(buf, addr) + return len(b), err +} + +func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, addr, err := c.PacketConn.ReadFrom(b) + if err != nil { + return n, addr, err + } + bb, err := Unpack(b[c.IVSize():], b[:n], c.Cipher) + if err != nil { + return n, addr, err + } + copy(b, bb) + return len(bb), addr, err +} diff --git a/transport/shadowsocks/shadowstream/stream.go b/transport/shadowsocks/shadowstream/stream.go new file mode 100644 index 0000000..6c4b0f6 --- /dev/null +++ b/transport/shadowsocks/shadowstream/stream.go @@ -0,0 +1,197 @@ +package shadowstream + +import ( + "crypto/cipher" + "crypto/rand" + "io" + "net" +) + +const bufSize = 2048 + +type Writer struct { + io.Writer + cipher.Stream + buf [bufSize]byte +} + +// NewWriter wraps an io.Writer with stream cipher encryption. +func NewWriter(w io.Writer, s cipher.Stream) *Writer { return &Writer{Writer: w, Stream: s} } + +func (w *Writer) Write(p []byte) (n int, err error) { + buf := w.buf[:] + for nw := 0; n < len(p) && err == nil; n += nw { + end := n + len(buf) + if end > len(p) { + end = len(p) + } + w.XORKeyStream(buf, p[n:end]) + nw, err = w.Writer.Write(buf[:end-n]) + } + return +} + +func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) { + buf := w.buf[:] + for { + nr, er := r.Read(buf) + n += int64(nr) + b := buf[:nr] + w.XORKeyStream(b, b) + if _, err = w.Writer.Write(b); err != nil { + return + } + if er != nil { + if er != io.EOF { // ignore EOF as per io.ReaderFrom contract + err = er + } + return + } + } +} + +type Reader struct { + io.Reader + cipher.Stream + buf [bufSize]byte +} + +// NewReader wraps an io.Reader with stream cipher decryption. +func NewReader(r io.Reader, s cipher.Stream) *Reader { return &Reader{Reader: r, Stream: s} } + +func (r *Reader) Read(p []byte) (n int, err error) { + n, err = r.Reader.Read(p) + if err != nil { + return 0, err + } + r.XORKeyStream(p, p[:n]) + return +} + +func (r *Reader) WriteTo(w io.Writer) (n int64, err error) { + buf := r.buf[:] + for { + nr, er := r.Reader.Read(buf) + if nr > 0 { + r.XORKeyStream(buf, buf[:nr]) + nw, ew := w.Write(buf[:nr]) + n += int64(nw) + if ew != nil { + err = ew + return + } + } + if er != nil { + if er != io.EOF { // ignore EOF as per io.Copy contract (using src.WriteTo shortcut) + err = er + } + return + } + } +} + +// A Conn represents a Shadowsocks connection. It implements the net.Conn interface. +type Conn struct { + net.Conn + Cipher + r *Reader + w *Writer + readIV []byte + writeIV []byte +} + +// NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption. +func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} } + +func (c *Conn) initReader() error { + if c.r == nil { + iv, err := c.ObtainReadIV() + if err != nil { + return err + } + c.r = NewReader(c.Conn, c.Decrypter(iv)) + } + return nil +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.Read(b) +} + +func (c *Conn) WriteTo(w io.Writer) (int64, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.WriteTo(w) +} + +func (c *Conn) initWriter() error { + if c.w == nil { + iv, err := c.ObtainWriteIV() + if err != nil { + return err + } + if _, err := c.Conn.Write(iv); err != nil { + return err + } + c.w = NewWriter(c.Conn, c.Encrypter(iv)) + } + return nil +} + +func (c *Conn) Write(b []byte) (int, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.Write(b) +} + +func (c *Conn) ReadFrom(r io.Reader) (int64, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.ReadFrom(r) +} + +func (c *Conn) ObtainWriteIV() ([]byte, error) { + if len(c.writeIV) == c.IVSize() { + return c.writeIV, nil + } + + iv := make([]byte, c.IVSize()) + + if _, err := rand.Read(iv); err != nil { + return nil, err + } + + c.writeIV = iv + + return iv, nil +} + +func (c *Conn) ObtainReadIV() ([]byte, error) { + if len(c.readIV) == c.IVSize() { + return c.readIV, nil + } + + iv := make([]byte, c.IVSize()) + + if _, err := io.ReadFull(c.Conn, iv); err != nil { + return nil, err + } + + c.readIV = iv + + return iv, nil +} diff --git a/transport/simple-obfs/http.go b/transport/simple-obfs/http.go new file mode 100644 index 0000000..97e7159 --- /dev/null +++ b/transport/simple-obfs/http.go @@ -0,0 +1,95 @@ +package obfs + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + mathRand "math/rand" + "net" + "net/http" + + "github.com/Dreamacro/clash/common/pool" +) + +// HTTPObfs is shadowsocks http simple-obfs implementation +type HTTPObfs struct { + net.Conn + host string + port string + buf []byte + offset int + firstRequest bool + firstResponse bool +} + +func (ho *HTTPObfs) Read(b []byte) (int, error) { + if ho.buf != nil { + n := copy(b, ho.buf[ho.offset:]) + ho.offset += n + if ho.offset == len(ho.buf) { + pool.Put(ho.buf) + ho.buf = nil + } + return n, nil + } + + if ho.firstResponse { + buf := pool.Get(pool.RelayBufferSize) + n, err := ho.Conn.Read(buf) + if err != nil { + pool.Put(buf) + return 0, err + } + idx := bytes.Index(buf[:n], []byte("\r\n\r\n")) + if idx == -1 { + pool.Put(buf) + return 0, io.EOF + } + ho.firstResponse = false + length := n - (idx + 4) + n = copy(b, buf[idx+4:n]) + if length > n { + ho.buf = buf[:idx+4+length] + ho.offset = idx + 4 + n + } else { + pool.Put(buf) + } + return n, nil + } + return ho.Conn.Read(b) +} + +func (ho *HTTPObfs) Write(b []byte) (int, error) { + if ho.firstRequest { + randBytes := make([]byte, 16) + rand.Read(randBytes) + req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:])) + req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", mathRand.Int()%54, mathRand.Int()%2)) + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Connection", "Upgrade") + req.Host = ho.host + if ho.port != "80" { + req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port) + } + req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes)) + req.ContentLength = int64(len(b)) + err := req.Write(ho.Conn) + ho.firstRequest = false + return len(b), err + } + + return ho.Conn.Write(b) +} + +// NewHTTPObfs return a HTTPObfs +func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn { + return &HTTPObfs{ + Conn: conn, + firstRequest: true, + firstResponse: true, + host: host, + port: port, + } +} diff --git a/transport/simple-obfs/tls.go b/transport/simple-obfs/tls.go new file mode 100644 index 0000000..763be8e --- /dev/null +++ b/transport/simple-obfs/tls.go @@ -0,0 +1,191 @@ +package obfs + +import ( + "crypto/rand" + "encoding/binary" + "io" + "net" + "time" + + "github.com/Dreamacro/clash/common/pool" + + "github.com/Dreamacro/protobytes" +) + +const ( + chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024 +) + +// TLSObfs is shadowsocks tls simple-obfs implementation +type TLSObfs struct { + net.Conn + server string + remain int + firstRequest bool + firstResponse bool +} + +func (to *TLSObfs) read(b []byte, discardN int) (int, error) { + buf := pool.Get(discardN) + _, err := io.ReadFull(to.Conn, buf) + pool.Put(buf) + if err != nil { + return 0, err + } + + sizeBuf := make([]byte, 2) + _, err = io.ReadFull(to.Conn, sizeBuf) + if err != nil { + return 0, nil + } + + length := int(binary.BigEndian.Uint16(sizeBuf)) + if length > len(b) { + n, err := to.Conn.Read(b) + if err != nil { + return n, err + } + to.remain = length - n + return n, nil + } + + return io.ReadFull(to.Conn, b[:length]) +} + +func (to *TLSObfs) Read(b []byte) (int, error) { + if to.remain > 0 { + length := to.remain + if length > len(b) { + length = len(b) + } + + n, err := io.ReadFull(to.Conn, b[:length]) + to.remain -= n + return n, err + } + + if to.firstResponse { + // type + ver + lensize + 91 = 96 + // type + ver + lensize + 1 = 6 + // type + ver = 3 + to.firstResponse = false + return to.read(b, 105) + } + + // type + ver = 3 + return to.read(b, 3) +} + +func (to *TLSObfs) Write(b []byte) (int, error) { + length := len(b) + for i := 0; i < length; i += chunkSize { + end := i + chunkSize + if end > length { + end = length + } + + n, err := to.write(b[i:end]) + if err != nil { + return n, err + } + } + return length, nil +} + +func (to *TLSObfs) write(b []byte) (int, error) { + if to.firstRequest { + helloMsg := makeClientHelloMsg(b, to.server) + _, err := to.Conn.Write(helloMsg) + to.firstRequest = false + return len(b), err + } + + buf := pool.GetBytesBuffer() + defer pool.PutBytesBuffer(buf) + buf.PutSlice([]byte{0x17, 0x03, 0x03}) + buf.PutUint16be(uint16(len(b))) + buf.PutSlice(b) + _, err := to.Conn.Write(buf.Bytes()) + return len(b), err +} + +// NewTLSObfs return a SimpleObfs +func NewTLSObfs(conn net.Conn, server string) net.Conn { + return &TLSObfs{ + Conn: conn, + server: server, + firstRequest: true, + firstResponse: true, + } +} + +func makeClientHelloMsg(data []byte, server string) []byte { + buf := protobytes.BytesWriter{} + + // handshake, TLS 1.0 version, length + buf.PutUint8(22) + buf.PutSlice([]byte{0x03, 0x01}) + length := uint16(212 + len(data) + len(server)) + buf.PutUint16be(length) + + // clientHello, length, TLS 1.2 version + buf.PutUint8(1) + buf.PutUint8(0) + buf.PutUint16be(uint16(208 + len(data) + len(server))) + buf.PutSlice([]byte{0x03, 0x03}) + + // random with timestamp, sid len, sid + buf.PutUint32be(uint32(time.Now().Unix())) + buf.ReadFull(rand.Reader, 28) + buf.PutUint8(32) + buf.ReadFull(rand.Reader, 32) + + // cipher suites + buf.PutSlice([]byte{0x00, 0x38}) + buf.PutSlice([]byte{ + 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, + 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, + 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, + 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, + }) + + // compression + buf.PutSlice([]byte{0x01, 0x00}) + + // extension length + buf.PutUint16be(uint16(79 + len(data) + len(server))) + + // session ticket + buf.PutSlice([]byte{0x00, 0x23}) + buf.PutUint16be(uint16(len(data))) + buf.PutSlice(data) + + // server name + buf.PutSlice([]byte{0x00, 0x00}) + buf.PutUint16be(uint16(len(server) + 5)) + buf.PutUint16be(uint16(len(server) + 3)) + buf.PutUint8(0) + buf.PutUint16be(uint16(len(server))) + buf.PutSlice([]byte(server)) + + // ec_point + buf.PutSlice([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02}) + + // groups + buf.PutSlice([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18}) + + // signature + buf.PutSlice([]byte{ + 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, + 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, + 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, + }) + + // encrypt then mac + buf.PutSlice([]byte{0x00, 0x16, 0x00, 0x00}) + + // extended master secret + buf.PutSlice([]byte{0x00, 0x17, 0x00, 0x00}) + + return buf.Bytes() +} diff --git a/transport/snell/cipher.go b/transport/snell/cipher.go new file mode 100644 index 0000000..24999e2 --- /dev/null +++ b/transport/snell/cipher.go @@ -0,0 +1,56 @@ +package snell + +import ( + "crypto/aes" + "crypto/cipher" + + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/chacha20poly1305" +) + +type snellCipher struct { + psk []byte + keySize int + makeAEAD func(key []byte) (cipher.AEAD, error) +} + +func (sc *snellCipher) KeySize() int { return sc.keySize } +func (sc *snellCipher) SaltSize() int { return 16 } +func (sc *snellCipher) Encrypter(salt []byte) (cipher.AEAD, error) { + return sc.makeAEAD(snellKDF(sc.psk, salt, sc.KeySize())) +} + +func (sc *snellCipher) Decrypter(salt []byte) (cipher.AEAD, error) { + return sc.makeAEAD(snellKDF(sc.psk, salt, sc.KeySize())) +} + +func snellKDF(psk, salt []byte, keySize int) []byte { + // snell use a special kdf function + return argon2.IDKey(psk, salt, 3, 8, 1, 32)[:keySize] +} + +func aesGCM(key []byte) (cipher.AEAD, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return cipher.NewGCM(blk) +} + +func NewAES128GCM(psk []byte) shadowaead.Cipher { + return &snellCipher{ + psk: psk, + keySize: 16, + makeAEAD: aesGCM, + } +} + +func NewChacha20Poly1305(psk []byte) shadowaead.Cipher { + return &snellCipher{ + psk: psk, + keySize: 32, + makeAEAD: chacha20poly1305.New, + } +} diff --git a/transport/snell/pool.go b/transport/snell/pool.go new file mode 100644 index 0000000..54b9234 --- /dev/null +++ b/transport/snell/pool.go @@ -0,0 +1,84 @@ +package snell + +import ( + "context" + "net" + "time" + + "github.com/Dreamacro/clash/component/pool" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" +) + +type Pool struct { + pool *pool.Pool +} + +func (p *Pool) Get() (net.Conn, error) { + return p.GetContext(context.Background()) +} + +func (p *Pool) GetContext(ctx context.Context) (net.Conn, error) { + elm, err := p.pool.GetContext(ctx) + if err != nil { + return nil, err + } + + return &PoolConn{elm.(*Snell), p}, nil +} + +func (p *Pool) Put(conn net.Conn) { + if err := HalfClose(conn); err != nil { + conn.Close() + return + } + + p.pool.Put(conn) +} + +type PoolConn struct { + *Snell + pool *Pool +} + +func (pc *PoolConn) Read(b []byte) (int, error) { + // save old status of reply (it mutable by Read) + reply := pc.Snell.reply + + n, err := pc.Snell.Read(b) + if err == shadowaead.ErrZeroChunk { + // if reply is false, it should be client halfclose. + // ignore error and read data again. + if !reply { + pc.Snell.reply = false + return pc.Snell.Read(b) + } + } + return n, err +} + +func (pc *PoolConn) Write(b []byte) (int, error) { + return pc.Snell.Write(b) +} + +func (pc *PoolConn) Close() error { + // clash use SetReadDeadline to break bidirectional copy between client and server. + // reset it before reuse connection to avoid io timeout error. + pc.Snell.Conn.SetReadDeadline(time.Time{}) + pc.pool.Put(pc.Snell) + return nil +} + +func NewPool(factory func(context.Context) (*Snell, error)) *Pool { + p := pool.New( + func(ctx context.Context) (any, error) { + return factory(ctx) + }, + pool.WithAge(15000), + pool.WithSize(10), + pool.WithEvict(func(item any) { + item.(*Snell).Close() + }), + ) + + return &Pool{p} +} diff --git a/transport/snell/snell.go b/transport/snell/snell.go new file mode 100644 index 0000000..13de1af --- /dev/null +++ b/transport/snell/snell.go @@ -0,0 +1,279 @@ +package snell + +import ( + "errors" + "fmt" + "io" + "net" + "sync" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "github.com/Dreamacro/clash/transport/socks5" +) + +const ( + Version1 = 1 + Version2 = 2 + Version3 = 3 + DefaultSnellVersion = Version1 + + // max packet length + maxLength = 0x3FFF +) + +const ( + CommandPing byte = 0 + CommandConnect byte = 1 + CommandConnectV2 byte = 5 + CommandUDP byte = 6 + CommondUDPForward byte = 1 + + CommandTunnel byte = 0 + CommandPong byte = 1 + CommandError byte = 2 + + Version byte = 1 +) + +var endSignal = []byte{} + +type Snell struct { + net.Conn + buffer [1]byte + reply bool +} + +func (s *Snell) Read(b []byte) (int, error) { + if s.reply { + return s.Conn.Read(b) + } + + s.reply = true + if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil { + return 0, err + } + + if s.buffer[0] == CommandTunnel { + return s.Conn.Read(b) + } else if s.buffer[0] != CommandError { + return 0, errors.New("command not support") + } + + // CommandError + // 1 byte error code + if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil { + return 0, err + } + errcode := int(s.buffer[0]) + + // 1 byte error message length + if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil { + return 0, err + } + length := int(s.buffer[0]) + msg := make([]byte, length) + + if _, err := io.ReadFull(s.Conn, msg); err != nil { + return 0, err + } + + return 0, fmt.Errorf("server reported code: %d, message: %s", errcode, string(msg)) +} + +func WriteHeader(conn net.Conn, host string, port uint, version int) error { + buf := pool.GetBytesBuffer() + defer pool.PutBytesBuffer(buf) + buf.PutUint8(Version) + if version == Version2 { + buf.PutUint8(CommandConnectV2) + } else { + buf.PutUint8(CommandConnect) + } + + // clientID length & id + buf.PutUint8(0) + + // host & port + buf.PutUint8(uint8(len(host))) + buf.PutString(host) + buf.PutUint16be(uint16(port)) + + if _, err := conn.Write(buf.Bytes()); err != nil { + return err + } + + return nil +} + +func WriteUDPHeader(conn net.Conn, version int) error { + if version < Version3 { + return errors.New("unsupport UDP version") + } + + // version, command, clientID length + _, err := conn.Write([]byte{Version, CommandUDP, 0x00}) + return err +} + +// HalfClose works only on version2 +func HalfClose(conn net.Conn) error { + if _, err := conn.Write(endSignal); err != nil { + return err + } + + if s, ok := conn.(*Snell); ok { + s.reply = false + } + return nil +} + +func StreamConn(conn net.Conn, psk []byte, version int) *Snell { + var cipher shadowaead.Cipher + if version != Version1 { + cipher = NewAES128GCM(psk) + } else { + cipher = NewChacha20Poly1305(psk) + } + return &Snell{Conn: shadowaead.NewConn(conn, cipher)} +} + +func PacketConn(conn net.Conn) net.PacketConn { + return &packetConn{ + Conn: conn, + } +} + +func writePacket(w io.Writer, socks5Addr, payload []byte) (int, error) { + buf := pool.GetBytesBuffer() + defer pool.PutBytesBuffer(buf) + + // compose snell UDP address format (refer: icpz/snell-server-reversed) + // a brand new wheel to replace socks5 address format, well done Yachen + buf.PutUint8(CommondUDPForward) + switch socks5Addr[0] { + case socks5.AtypDomainName: + hostLen := socks5Addr[1] + buf.PutSlice(socks5Addr[1 : 1+1+hostLen+2]) + case socks5.AtypIPv4: + buf.PutSlice([]byte{0x00, 0x04}) + buf.PutSlice(socks5Addr[1 : 1+net.IPv4len+2]) + case socks5.AtypIPv6: + buf.PutSlice([]byte{0x00, 0x06}) + buf.PutSlice(socks5Addr[1 : 1+net.IPv6len+2]) + } + + buf.PutSlice(payload) + _, err := w.Write(buf.Bytes()) + if err != nil { + return 0, err + } + return len(payload), nil +} + +func WritePacket(w io.Writer, socks5Addr, payload []byte) (int, error) { + if len(payload) <= maxLength { + return writePacket(w, socks5Addr, payload) + } + + offset := 0 + total := len(payload) + for { + cursor := offset + maxLength + if cursor > total { + cursor = total + } + + n, err := writePacket(w, socks5Addr, payload[offset:cursor]) + if err != nil { + return offset + n, err + } + + offset = cursor + if offset == total { + break + } + } + + return total, nil +} + +func ReadPacket(r io.Reader, payload []byte) (net.Addr, int, error) { + buf := pool.Get(pool.UDPBufferSize) + defer pool.Put(buf) + + n, err := r.Read(buf) + headLen := 1 + if err != nil { + return nil, 0, err + } + if n < headLen { + return nil, 0, errors.New("insufficient UDP length") + } + + // parse snell UDP response address format + switch buf[0] { + case 0x04: + headLen += net.IPv4len + 2 + if n < headLen { + err = errors.New("insufficient UDP length") + break + } + buf[0] = socks5.AtypIPv4 + case 0x06: + headLen += net.IPv6len + 2 + if n < headLen { + err = errors.New("insufficient UDP length") + break + } + buf[0] = socks5.AtypIPv6 + default: + err = errors.New("ip version invalid") + } + + if err != nil { + return nil, 0, err + } + + addr := socks5.SplitAddr(buf[0:]) + if addr == nil { + return nil, 0, errors.New("remote address invalid") + } + uAddr := addr.UDPAddr() + if uAddr == nil { + return nil, 0, errors.New("parse addr error") + } + + length := len(payload) + if n-headLen < length { + length = n - headLen + } + copy(payload[:], buf[headLen:headLen+length]) + + return uAddr, length, nil +} + +type packetConn struct { + net.Conn + rMux sync.Mutex + wMux sync.Mutex +} + +func (pc *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) { + pc.wMux.Lock() + defer pc.wMux.Unlock() + + return WritePacket(pc, socks5.ParseAddr(addr.String()), b) +} + +func (pc *packetConn) ReadFrom(b []byte) (int, net.Addr, error) { + pc.rMux.Lock() + defer pc.rMux.Unlock() + + addr, n, err := ReadPacket(pc.Conn, b) + if err != nil { + return 0, nil, err + } + + return n, addr, nil +} diff --git a/transport/socks4/socks4.go b/transport/socks4/socks4.go new file mode 100644 index 0000000..5397a40 --- /dev/null +++ b/transport/socks4/socks4.go @@ -0,0 +1,196 @@ +package socks4 + +import ( + "errors" + "io" + "net" + "net/netip" + "strconv" + + "github.com/Dreamacro/clash/component/auth" + + "github.com/Dreamacro/protobytes" +) + +const Version = 0x04 + +type Command = uint8 + +const ( + CmdConnect Command = 0x01 + CmdBind Command = 0x02 +) + +type Code = uint8 + +const ( + RequestGranted Code = 90 + RequestRejected Code = 91 + RequestIdentdFailed Code = 92 + RequestIdentdMismatched Code = 93 +) + +var ( + errVersionMismatched = errors.New("version code mismatched") + errCommandNotSupported = errors.New("command not supported") + errIPv6NotSupported = errors.New("IPv6 not supported") + + ErrRequestRejected = errors.New("request rejected or failed") + ErrRequestIdentdFailed = errors.New("request rejected because SOCKS server cannot connect to identd on the client") + ErrRequestIdentdMismatched = errors.New("request rejected because the client program and identd report different user-ids") + ErrRequestUnknownCode = errors.New("request failed with unknown code") +) + +func ServerHandshake(rw io.ReadWriter, authenticator auth.Authenticator) (addr string, command Command, err error) { + var req [8]byte + if _, err = io.ReadFull(rw, req[:]); err != nil { + return + } + + r := protobytes.BytesReader(req[:]) + if r.ReadUint8() != Version { + err = errVersionMismatched + return + } + + if command = r.ReadUint8(); command != CmdConnect { + err = errCommandNotSupported + return + } + + var ( + host string + port string + code uint8 + userID []byte + ) + if userID, err = readUntilNull(rw); err != nil { + return + } + + dstPort := r.ReadUint16be() + dstAddr := r.ReadIPv4() + if isReservedIP(dstAddr) { + var target []byte + if target, err = readUntilNull(rw); err != nil { + return + } + host = string(target) + } + + port = strconv.Itoa(int(dstPort)) + if host != "" { + addr = net.JoinHostPort(host, port) + } else { + addr = net.JoinHostPort(dstAddr.String(), port) + } + + // SOCKS4 only support USERID auth. + if authenticator == nil || authenticator.Verify(string(userID), "") { + code = RequestGranted + } else { + code = RequestIdentdMismatched + err = ErrRequestIdentdMismatched + } + + reply := protobytes.BytesWriter(make([]byte, 0, 8)) + reply.PutUint8(0) // reply code + reply.PutUint8(code) // result code + reply.PutUint16be(dstPort) + reply.PutSlice(dstAddr.AsSlice()) + + _, wErr := rw.Write(reply.Bytes()) + if err == nil { + err = wErr + } + return +} + +func ClientHandshake(rw io.ReadWriter, addr string, command Command, userID string) (err error) { + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return err + } + + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return err + } + + ip, err := netip.ParseAddr(host) + if err != nil { // Host + ip = netip.AddrFrom4([4]byte{0, 0, 0, 1}) + } else if ip.Is6() { // IPv6 + return errIPv6NotSupported + } + + req := protobytes.BytesWriter{} + req.PutUint8(Version) + req.PutUint8(command) + req.PutUint16be(uint16(port)) + req.PutSlice(ip.AsSlice()) + req.PutString(userID) + req.PutUint8(0) /* NULL */ + + if isReservedIP(ip) /* SOCKS4A */ { + req.PutString(host) + req.PutUint8(0) /* NULL */ + } + + if _, err = rw.Write(req.Bytes()); err != nil { + return err + } + + var resp [8]byte + if _, err = io.ReadFull(rw, resp[:]); err != nil { + return err + } + + if resp[0] != 0x00 { + return errVersionMismatched + } + + switch resp[1] { + case RequestGranted: + return nil + case RequestRejected: + return ErrRequestRejected + case RequestIdentdFailed: + return ErrRequestIdentdFailed + case RequestIdentdMismatched: + return ErrRequestIdentdMismatched + default: + return ErrRequestUnknownCode + } +} + +// For version 4A, if the client cannot resolve the destination host's +// domain name to find its IP address, it should set the first three bytes +// of DSTIP to NULL and the last byte to a non-zero value. (This corresponds +// to IP address 0.0.0.x, with x nonzero. As decreed by IANA -- The +// Internet Assigned Numbers Authority -- such an address is inadmissible +// as a destination IP address and thus should never occur if the client +// can resolve the domain name.) +func isReservedIP(ip netip.Addr) bool { + subnet := netip.PrefixFrom( + netip.AddrFrom4([4]byte{0, 0, 0, 0}), + 24, + ) + + return !ip.IsUnspecified() && subnet.Contains(ip) +} + +func readUntilNull(r io.Reader) ([]byte, error) { + buf := protobytes.BytesWriter{} + var data [1]byte + + for { + if _, err := r.Read(data[:]); err != nil { + return nil, err + } + if data[0] == 0 { + return buf.Bytes(), nil + } + buf.PutUint8(data[0]) + } +} diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go new file mode 100644 index 0000000..ba30ea9 --- /dev/null +++ b/transport/socks5/socks5.go @@ -0,0 +1,455 @@ +package socks5 + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "net" + "net/netip" + "strconv" + + "github.com/Dreamacro/clash/component/auth" + + "github.com/Dreamacro/protobytes" +) + +// Error represents a SOCKS error +type Error byte + +func (err Error) Error() string { + return "SOCKS error: " + strconv.Itoa(int(err)) +} + +// Command is request commands as defined in RFC 1928 section 4. +type Command = uint8 + +const Version = 5 + +// SOCKS request commands as defined in RFC 1928 section 4. +const ( + CmdConnect Command = 1 + CmdBind Command = 2 + CmdUDPAssociate Command = 3 +) + +// SOCKS address types as defined in RFC 1928 section 5. +const ( + AtypIPv4 = 1 + AtypDomainName = 3 + AtypIPv6 = 4 +) + +// MaxAddrLen is the maximum size of SOCKS address in bytes. +const MaxAddrLen = 1 + 1 + 255 + 2 + +// MaxAuthLen is the maximum size of user/password field in SOCKS5 Auth +const MaxAuthLen = 255 + +// Addr represents a SOCKS address as defined in RFC 1928 section 5. +type Addr []byte + +func (a Addr) String() string { + var host, port string + + switch a[0] { + case AtypDomainName: + hostLen := uint16(a[1]) + host = string(a[2 : 2+hostLen]) + port = strconv.Itoa((int(a[2+hostLen]) << 8) | int(a[2+hostLen+1])) + case AtypIPv4: + host = net.IP(a[1 : 1+net.IPv4len]).String() + port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1])) + case AtypIPv6: + host = net.IP(a[1 : 1+net.IPv6len]).String() + port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1])) + } + + return net.JoinHostPort(host, port) +} + +// UDPAddr converts a socks5.Addr to *net.UDPAddr +func (a Addr) UDPAddr() *net.UDPAddr { + if len(a) == 0 { + return nil + } + switch a[0] { + case AtypIPv4: + var ip [net.IPv4len]byte + copy(ip[0:], a[1:1+net.IPv4len]) + return &net.UDPAddr{IP: net.IP(ip[:]), Port: int(binary.BigEndian.Uint16(a[1+net.IPv4len : 1+net.IPv4len+2]))} + case AtypIPv6: + var ip [net.IPv6len]byte + copy(ip[0:], a[1:1+net.IPv6len]) + return &net.UDPAddr{IP: net.IP(ip[:]), Port: int(binary.BigEndian.Uint16(a[1+net.IPv6len : 1+net.IPv6len+2]))} + } + // Other Atyp + return nil +} + +// SOCKS errors as defined in RFC 1928 section 6. +const ( + ErrGeneralFailure = Error(1) + ErrConnectionNotAllowed = Error(2) + ErrNetworkUnreachable = Error(3) + ErrHostUnreachable = Error(4) + ErrConnectionRefused = Error(5) + ErrTTLExpired = Error(6) + ErrCommandNotSupported = Error(7) + ErrAddressNotSupported = Error(8) +) + +// Auth errors used to return a specific "Auth failed" error +var ErrAuth = errors.New("auth failed") + +type User struct { + Username string + Password string +} + +// ServerHandshake fast-tracks SOCKS initialization to get target address to connect on server side. +func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr, command Command, err error) { + // Read RFC 1928 for request and reply structure and sizes. + buf := make([]byte, MaxAddrLen) + // read VER, NMETHODS, METHODS + if _, err = io.ReadFull(rw, buf[:2]); err != nil { + return + } + nmethods := buf[1] + if _, err = io.ReadFull(rw, buf[:nmethods]); err != nil { + return + } + + // write VER METHOD + if authenticator != nil { + if _, err = rw.Write([]byte{5, 2}); err != nil { + return + } + + // Get header + header := make([]byte, 2) + if _, err = io.ReadFull(rw, header); err != nil { + return + } + + authBuf := make([]byte, MaxAuthLen) + // Get username + userLen := int(header[1]) + if userLen <= 0 { + rw.Write([]byte{1, 1}) + err = ErrAuth + return + } + if _, err = io.ReadFull(rw, authBuf[:userLen]); err != nil { + return + } + user := string(authBuf[:userLen]) + + // Get password + if _, err = rw.Read(header[:1]); err != nil { + return + } + passLen := int(header[0]) + if passLen <= 0 { + rw.Write([]byte{1, 1}) + err = ErrAuth + return + } + if _, err = io.ReadFull(rw, authBuf[:passLen]); err != nil { + return + } + pass := string(authBuf[:passLen]) + + // Verify + if ok := authenticator.Verify(user, pass); !ok { + rw.Write([]byte{1, 1}) + err = ErrAuth + return + } + + // Response auth state + if _, err = rw.Write([]byte{1, 0}); err != nil { + return + } + } else { + if _, err = rw.Write([]byte{5, 0}); err != nil { + return + } + } + + // read VER CMD RSV ATYP DST.ADDR DST.PORT + if _, err = io.ReadFull(rw, buf[:3]); err != nil { + return + } + + command = buf[1] + addr, err = ReadAddr(rw, buf) + if err != nil { + return + } + + switch command { + case CmdConnect, CmdUDPAssociate: + // Acquire server listened address info + localAddr := ParseAddr(rw.LocalAddr().String()) + if localAddr == nil { + err = ErrAddressNotSupported + } else { + // write VER REP RSV ATYP BND.ADDR BND.PORT + _, err = rw.Write(bytes.Join([][]byte{{5, 0, 0}, localAddr}, []byte{})) + } + case CmdBind: + fallthrough + default: + err = ErrCommandNotSupported + } + + return +} + +// ClientHandshake fast-tracks SOCKS initialization to get target address to connect on client side. +func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (Addr, error) { + buf := make([]byte, MaxAddrLen) + var err error + + // VER, NMETHODS, METHODS + if user != nil { + _, err = rw.Write([]byte{5, 1, 2}) + } else { + _, err = rw.Write([]byte{5, 1, 0}) + } + if err != nil { + return nil, err + } + + // VER, METHOD + if _, err := io.ReadFull(rw, buf[:2]); err != nil { + return nil, err + } + + if buf[0] != 5 { + return nil, errors.New("SOCKS version error") + } + + if buf[1] == 2 { + if user == nil { + return nil, ErrAuth + } + + // password protocol version + authMsg := protobytes.BytesWriter{} + authMsg.PutUint8(1) + authMsg.PutUint8(uint8(len(user.Username))) + authMsg.PutString(user.Username) + authMsg.PutUint8(uint8(len(user.Password))) + authMsg.PutString(user.Password) + + if _, err := rw.Write(authMsg.Bytes()); err != nil { + return nil, err + } + + if _, err := io.ReadFull(rw, buf[:2]); err != nil { + return nil, err + } + + if buf[1] != 0 { + return nil, errors.New("rejected username/password") + } + } else if buf[1] != 0 { + return nil, errors.New("SOCKS need auth") + } + + // VER, CMD, RSV, ADDR + if _, err := rw.Write(bytes.Join([][]byte{{5, command, 0}, addr}, []byte{})); err != nil { + return nil, err + } + + // VER, REP, RSV + if _, err := io.ReadFull(rw, buf[:3]); err != nil { + return nil, err + } + + return ReadAddr(rw, buf) +} + +func ReadAddr(r io.Reader, b []byte) (Addr, error) { + if len(b) < MaxAddrLen { + return nil, io.ErrShortBuffer + } + _, err := io.ReadFull(r, b[:1]) // read 1st byte for address type + if err != nil { + return nil, err + } + + switch b[0] { + case AtypDomainName: + _, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length + if err != nil { + return nil, err + } + domainLength := uint16(b[1]) + _, err = io.ReadFull(r, b[2:2+domainLength+2]) + return b[:1+1+domainLength+2], err + case AtypIPv4: + _, err = io.ReadFull(r, b[1:1+net.IPv4len+2]) + return b[:1+net.IPv4len+2], err + case AtypIPv6: + _, err = io.ReadFull(r, b[1:1+net.IPv6len+2]) + return b[:1+net.IPv6len+2], err + } + + return nil, ErrAddressNotSupported +} + +// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed. +func SplitAddr(b []byte) Addr { + addrLen := 1 + if len(b) < addrLen { + return nil + } + + switch b[0] { + case AtypDomainName: + if len(b) < 2 { + return nil + } + addrLen = 1 + 1 + int(b[1]) + 2 + case AtypIPv4: + addrLen = 1 + net.IPv4len + 2 + case AtypIPv6: + addrLen = 1 + net.IPv6len + 2 + default: + return nil + + } + + if len(b) < addrLen { + return nil + } + + return b[:addrLen] +} + +// ParseAddr parses the address in string s. Returns nil if failed. +func ParseAddr(s string) Addr { + buf := protobytes.BytesWriter{} + host, port, err := net.SplitHostPort(s) + if err != nil { + return nil + } + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf.PutUint8(AtypIPv4) + buf.PutSlice(ip4) + } else { + buf.PutUint8(AtypIPv6) + buf.PutSlice(ip) + } + } else { + if len(host) > 255 { + return nil + } + buf.PutUint8(AtypDomainName) + buf.PutUint8(byte(len(host))) + buf.PutString(host) + } + + portnum, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return nil + } + + buf.PutUint16be(uint16(portnum)) + return Addr(buf.Bytes()) +} + +// ParseAddrToSocksAddr parse a socks addr from net.addr +// This is a fast path of ParseAddr(addr.String()) +func ParseAddrToSocksAddr(addr net.Addr) Addr { + var hostip net.IP + var port int + if udpaddr, ok := addr.(*net.UDPAddr); ok { + hostip = udpaddr.IP + port = udpaddr.Port + } else if tcpaddr, ok := addr.(*net.TCPAddr); ok { + hostip = tcpaddr.IP + port = tcpaddr.Port + } + + // fallback parse + if hostip == nil { + return ParseAddr(addr.String()) + } + + var parsed protobytes.BytesWriter + if ip4 := hostip.To4(); ip4.DefaultMask() != nil { + parsed = make([]byte, 0, 1+net.IPv4len+2) + parsed.PutUint8(AtypIPv4) + parsed.PutSlice(ip4) + parsed.PutUint16be(uint16(port)) + } else { + parsed = make([]byte, 0, 1+net.IPv6len+2) + parsed.PutUint8(AtypIPv6) + parsed.PutSlice(hostip) + parsed.PutUint16be(uint16(port)) + } + return Addr(parsed) +} + +func AddrFromStdAddrPort(addrPort netip.AddrPort) Addr { + addr := addrPort.Addr() + if addr.Is4() { + ip4 := addr.As4() + return []byte{AtypIPv4, ip4[0], ip4[1], ip4[2], ip4[3], byte(addrPort.Port() >> 8), byte(addrPort.Port())} + } + + buf := make([]byte, 1+net.IPv6len+2) + buf[0] = AtypIPv6 + copy(buf[1:], addr.AsSlice()) + buf[1+net.IPv6len] = byte(addrPort.Port() >> 8) + buf[1+net.IPv6len+1] = byte(addrPort.Port()) + return buf +} + +// DecodeUDPPacket split `packet` to addr payload, and this function is mutable with `packet` +func DecodeUDPPacket(packet []byte) (addr Addr, payload []byte, err error) { + r := protobytes.BytesReader(packet) + + if r.Len() < 5 { + err = errors.New("insufficient length of packet") + return + } + + // packet[0] and packet[1] are reserved + reserved, r := r.SplitAt(2) + if !bytes.Equal(reserved, []byte{0, 0}) { + err = errors.New("reserved fields should be zero") + return + } + + if r.ReadUint8() != 0 /* fragments */ { + err = errors.New("discarding fragmented payload") + return + } + + addr = SplitAddr(r) + if addr == nil { + err = errors.New("failed to read UDP header") + } + + _, payload = r.SplitAt(len(addr)) + return +} + +func EncodeUDPPacket(addr Addr, payload []byte) (packet []byte, err error) { + if addr == nil { + err = errors.New("address is invalid") + return + } + w := protobytes.BytesWriter{} + w.PutSlice([]byte{0, 0, 0}) + w.PutSlice(addr) + w.PutSlice(payload) + packet = w.Bytes() + return +} diff --git a/transport/ssr/obfs/base.go b/transport/ssr/obfs/base.go new file mode 100644 index 0000000..7fd1b84 --- /dev/null +++ b/transport/ssr/obfs/base.go @@ -0,0 +1,9 @@ +package obfs + +type Base struct { + Host string + Port int + Key []byte + IVSize int + Param string +} diff --git a/transport/ssr/obfs/http_post.go b/transport/ssr/obfs/http_post.go new file mode 100644 index 0000000..4be6cbe --- /dev/null +++ b/transport/ssr/obfs/http_post.go @@ -0,0 +1,9 @@ +package obfs + +func init() { + register("http_post", newHTTPPost, 0) +} + +func newHTTPPost(b *Base) Obfs { + return &httpObfs{Base: b, post: true} +} diff --git a/transport/ssr/obfs/http_simple.go b/transport/ssr/obfs/http_simple.go new file mode 100644 index 0000000..c1ea767 --- /dev/null +++ b/transport/ssr/obfs/http_simple.go @@ -0,0 +1,405 @@ +package obfs + +import ( + "bytes" + "encoding/hex" + "io" + "math/rand" + "net" + "strconv" + "strings" + + "github.com/Dreamacro/clash/common/pool" +) + +func init() { + register("http_simple", newHTTPSimple, 0) +} + +type httpObfs struct { + *Base + post bool +} + +func newHTTPSimple(b *Base) Obfs { + return &httpObfs{Base: b} +} + +type httpConn struct { + net.Conn + *httpObfs + hasSentHeader bool + hasRecvHeader bool + buf []byte +} + +func (h *httpObfs) StreamConn(c net.Conn) net.Conn { + return &httpConn{Conn: c, httpObfs: h} +} + +func (c *httpConn) Read(b []byte) (int, error) { + if c.buf != nil { + n := copy(b, c.buf) + if n == len(c.buf) { + c.buf = nil + } else { + c.buf = c.buf[n:] + } + return n, nil + } + + if c.hasRecvHeader { + return c.Conn.Read(b) + } + + buf := pool.Get(pool.RelayBufferSize) + defer pool.Put(buf) + n, err := c.Conn.Read(buf) + if err != nil { + return 0, err + } + pos := bytes.Index(buf[:n], []byte("\r\n\r\n")) + if pos == -1 { + return 0, io.EOF + } + c.hasRecvHeader = true + dataLength := n - pos - 4 + n = copy(b, buf[4+pos:n]) + if dataLength > n { + c.buf = append(c.buf, buf[4+pos+n:4+pos+dataLength]...) + } + return n, nil +} + +func (c *httpConn) Write(b []byte) (int, error) { + if c.hasSentHeader { + return c.Conn.Write(b) + } + // 30: head length + headLength := c.IVSize + 30 + + bLength := len(b) + headDataLength := bLength + if bLength-headLength > 64 { + headDataLength = headLength + rand.Intn(65) + } + headData := b[:headDataLength] + b = b[headDataLength:] + + var body string + host := c.Host + if len(c.Param) > 0 { + pos := strings.Index(c.Param, "#") + if pos != -1 { + body = strings.ReplaceAll(c.Param[pos+1:], "\n", "\r\n") + body = strings.ReplaceAll(body, "\\n", "\r\n") + host = c.Param[:pos] + } else { + host = c.Param + } + } + hosts := strings.Split(host, ",") + host = hosts[rand.Intn(len(hosts))] + + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + if c.post { + buf.WriteString("POST /") + } else { + buf.WriteString("GET /") + } + packURLEncodedHeadData(buf, headData) + buf.WriteString(" HTTP/1.1\r\nHost: " + host) + if c.Port != 80 { + buf.WriteString(":" + strconv.Itoa(c.Port)) + } + buf.WriteString("\r\n") + if len(body) > 0 { + buf.WriteString(body + "\r\n\r\n") + } else { + buf.WriteString("User-Agent: ") + buf.WriteString(userAgent[rand.Intn(len(userAgent))]) + buf.WriteString("\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\n") + if c.post { + packBoundary(buf) + } + buf.WriteString("DNT: 1\r\nConnection: keep-alive\r\n\r\n") + } + buf.Write(b) + _, err := c.Conn.Write(buf.Bytes()) + if err != nil { + return 0, nil + } + c.hasSentHeader = true + return bLength, nil +} + +func packURLEncodedHeadData(buf *bytes.Buffer, data []byte) { + dataLength := len(data) + for i := 0; i < dataLength; i++ { + buf.WriteRune('%') + buf.WriteString(hex.EncodeToString(data[i : i+1])) + } +} + +func packBoundary(buf *bytes.Buffer) { + buf.WriteString("Content-Type: multipart/form-data; boundary=") + set := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for i := 0; i < 32; i++ { + buf.WriteByte(set[rand.Intn(62)]) + } + buf.WriteString("\r\n") +} + +var userAgent = []string{ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", + "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36", + "Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", + "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", + "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0", + "Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", + "Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", + "Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24", + "Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", + "Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", + "Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1", + "Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", + "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36", + "Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", +} diff --git a/transport/ssr/obfs/obfs.go b/transport/ssr/obfs/obfs.go new file mode 100644 index 0000000..c56acc8 --- /dev/null +++ b/transport/ssr/obfs/obfs.go @@ -0,0 +1,42 @@ +package obfs + +import ( + "errors" + "fmt" + "net" +) + +var ( + errTLS12TicketAuthIncorrectMagicNumber = errors.New("tls1.2_ticket_auth incorrect magic number") + errTLS12TicketAuthTooShortData = errors.New("tls1.2_ticket_auth too short data") + errTLS12TicketAuthHMACError = errors.New("tls1.2_ticket_auth hmac verifying failed") +) + +type authData struct { + clientID [32]byte +} + +type Obfs interface { + StreamConn(net.Conn) net.Conn +} + +type obfsCreator func(b *Base) Obfs + +var obfsList = make(map[string]struct { + overhead int + new obfsCreator +}) + +func register(name string, c obfsCreator, o int) { + obfsList[name] = struct { + overhead int + new obfsCreator + }{overhead: o, new: c} +} + +func PickObfs(name string, b *Base) (Obfs, int, error) { + if choice, ok := obfsList[name]; ok { + return choice.new(b), choice.overhead, nil + } + return nil, 0, fmt.Errorf("Obfs %s not supported", name) +} diff --git a/transport/ssr/obfs/plain.go b/transport/ssr/obfs/plain.go new file mode 100644 index 0000000..eb998a4 --- /dev/null +++ b/transport/ssr/obfs/plain.go @@ -0,0 +1,15 @@ +package obfs + +import "net" + +type plain struct{} + +func init() { + register("plain", newPlain, 0) +} + +func newPlain(b *Base) Obfs { + return &plain{} +} + +func (p *plain) StreamConn(c net.Conn) net.Conn { return c } diff --git a/transport/ssr/obfs/random_head.go b/transport/ssr/obfs/random_head.go new file mode 100644 index 0000000..0a4d3a5 --- /dev/null +++ b/transport/ssr/obfs/random_head.go @@ -0,0 +1,72 @@ +package obfs + +import ( + "crypto/rand" + "encoding/binary" + "hash/crc32" + mathRand "math/rand" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +func init() { + register("random_head", newRandomHead, 0) +} + +type randomHead struct { + *Base +} + +func newRandomHead(b *Base) Obfs { + return &randomHead{Base: b} +} + +type randomHeadConn struct { + net.Conn + *randomHead + hasSentHeader bool + rawTransSent bool + rawTransRecv bool + buf []byte +} + +func (r *randomHead) StreamConn(c net.Conn) net.Conn { + return &randomHeadConn{Conn: c, randomHead: r} +} + +func (c *randomHeadConn) Read(b []byte) (int, error) { + if c.rawTransRecv { + return c.Conn.Read(b) + } + buf := pool.Get(pool.RelayBufferSize) + defer pool.Put(buf) + c.Conn.Read(buf) + c.rawTransRecv = true + c.Write(nil) + return 0, nil +} + +func (c *randomHeadConn) Write(b []byte) (int, error) { + if c.rawTransSent { + return c.Conn.Write(b) + } + c.buf = append(c.buf, b...) + if !c.hasSentHeader { + c.hasSentHeader = true + dataLength := mathRand.Intn(96) + 4 + buf := pool.Get(dataLength + 4) + defer pool.Put(buf) + rand.Read(buf[:dataLength]) + binary.LittleEndian.PutUint32(buf[dataLength:], 0xffffffff-crc32.ChecksumIEEE(buf[:dataLength])) + _, err := c.Conn.Write(buf) + return len(b), err + } + if c.rawTransRecv { + _, err := c.Conn.Write(c.buf) + c.buf = nil + c.rawTransSent = true + return len(b), err + } + return len(b), nil +} diff --git a/transport/ssr/obfs/tls1.2_ticket_auth.go b/transport/ssr/obfs/tls1.2_ticket_auth.go new file mode 100644 index 0000000..6e3c7e5 --- /dev/null +++ b/transport/ssr/obfs/tls1.2_ticket_auth.go @@ -0,0 +1,227 @@ +package obfs + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "encoding/binary" + mathRand "math/rand" + "net" + "strings" + "time" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/transport/ssr/tools" +) + +func init() { + register("tls1.2_ticket_auth", newTLS12Ticket, 5) + register("tls1.2_ticket_fastauth", newTLS12Ticket, 5) +} + +type tls12Ticket struct { + *Base + *authData +} + +func newTLS12Ticket(b *Base) Obfs { + r := &tls12Ticket{Base: b, authData: &authData{}} + rand.Read(r.clientID[:]) + return r +} + +type tls12TicketConn struct { + net.Conn + *tls12Ticket + handshakeStatus int + decoded bytes.Buffer + underDecoded bytes.Buffer + sendBuf bytes.Buffer +} + +func (t *tls12Ticket) StreamConn(c net.Conn) net.Conn { + return &tls12TicketConn{Conn: c, tls12Ticket: t} +} + +func (c *tls12TicketConn) Read(b []byte) (int, error) { + if c.decoded.Len() > 0 { + return c.decoded.Read(b) + } + + buf := pool.Get(pool.RelayBufferSize) + defer pool.Put(buf) + n, err := c.Conn.Read(buf) + if err != nil { + return 0, err + } + + if c.handshakeStatus == 8 { + c.underDecoded.Write(buf[:n]) + for c.underDecoded.Len() > 5 { + if !bytes.Equal(c.underDecoded.Bytes()[:3], []byte{0x17, 3, 3}) { + c.underDecoded.Reset() + return 0, errTLS12TicketAuthIncorrectMagicNumber + } + size := int(binary.BigEndian.Uint16(c.underDecoded.Bytes()[3:5])) + if c.underDecoded.Len() < 5+size { + break + } + c.underDecoded.Next(5) + c.decoded.Write(c.underDecoded.Next(size)) + } + n, _ = c.decoded.Read(b) + return n, nil + } + + if n < 11+32+1+32 { + return 0, errTLS12TicketAuthTooShortData + } + + if !hmac.Equal(buf[33:43], c.hmacSHA1(buf[11:33])[:10]) || !hmac.Equal(buf[n-10:n], c.hmacSHA1(buf[:n-10])[:10]) { + return 0, errTLS12TicketAuthHMACError + } + + c.Write(nil) + return 0, nil +} + +func (c *tls12TicketConn) Write(b []byte) (int, error) { + length := len(b) + if c.handshakeStatus == 8 { + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + for len(b) > 2048 { + size := mathRand.Intn(4096) + 100 + if len(b) < size { + size = len(b) + } + packData(buf, b[:size]) + b = b[size:] + } + if len(b) > 0 { + packData(buf, b) + } + _, err := c.Conn.Write(buf.Bytes()) + if err != nil { + return 0, err + } + return length, nil + } + + if len(b) > 0 { + packData(&c.sendBuf, b) + } + + if c.handshakeStatus == 0 { + c.handshakeStatus = 1 + + data := pool.GetBuffer() + defer pool.PutBuffer(data) + + data.Write([]byte{3, 3}) + c.packAuthData(data) + data.WriteByte(0x20) + data.Write(c.clientID[:]) + data.Write([]byte{0x00, 0x1c, 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, 0xc0, 0x0a, 0xc0, 0x14, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x9c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0x0a}) + data.Write([]byte{0x1, 0x0}) + + ext := pool.GetBuffer() + defer pool.PutBuffer(ext) + + host := c.getHost() + ext.Write([]byte{0xff, 0x01, 0x00, 0x01, 0x00}) + packSNIData(ext, host) + ext.Write([]byte{0, 0x17, 0, 0}) + c.packTicketBuf(ext, host) + ext.Write([]byte{0x00, 0x0d, 0x00, 0x16, 0x00, 0x14, 0x06, 0x01, 0x06, 0x03, 0x05, 0x01, 0x05, 0x03, 0x04, 0x01, 0x04, 0x03, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03}) + ext.Write([]byte{0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00}) + ext.Write([]byte{0x00, 0x12, 0x00, 0x00}) + ext.Write([]byte{0x75, 0x50, 0x00, 0x00}) + ext.Write([]byte{0x00, 0x0b, 0x00, 0x02, 0x01, 0x00}) + ext.Write([]byte{0x00, 0x0a, 0x00, 0x06, 0x00, 0x04, 0x00, 0x17, 0x00, 0x18}) + + binary.Write(data, binary.BigEndian, uint16(ext.Len())) + data.ReadFrom(ext) + + ret := pool.GetBuffer() + defer pool.PutBuffer(ret) + + ret.Write([]byte{0x16, 3, 1}) + binary.Write(ret, binary.BigEndian, uint16(data.Len()+4)) + ret.Write([]byte{1, 0}) + binary.Write(ret, binary.BigEndian, uint16(data.Len())) + ret.ReadFrom(data) + + _, err := c.Conn.Write(ret.Bytes()) + if err != nil { + return 0, err + } + return length, nil + } else if c.handshakeStatus == 1 && len(b) == 0 { + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + + buf.Write([]byte{0x14, 3, 3, 0, 1, 1, 0x16, 3, 3, 0, 0x20}) + tools.AppendRandBytes(buf, 22) + buf.Write(c.hmacSHA1(buf.Bytes())[:10]) + buf.ReadFrom(&c.sendBuf) + + c.handshakeStatus = 8 + + _, err := c.Conn.Write(buf.Bytes()) + return 0, err + } + return length, nil +} + +func packData(buf *bytes.Buffer, data []byte) { + buf.Write([]byte{0x17, 3, 3}) + binary.Write(buf, binary.BigEndian, uint16(len(data))) + buf.Write(data) +} + +func (t *tls12Ticket) packAuthData(buf *bytes.Buffer) { + binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) + tools.AppendRandBytes(buf, 18) + buf.Write(t.hmacSHA1(buf.Bytes()[buf.Len()-22:])[:10]) +} + +func packSNIData(buf *bytes.Buffer, u string) { + len := uint16(len(u)) + buf.Write([]byte{0, 0}) + binary.Write(buf, binary.BigEndian, len+5) + binary.Write(buf, binary.BigEndian, len+3) + buf.WriteByte(0) + binary.Write(buf, binary.BigEndian, len) + buf.WriteString(u) +} + +func (c *tls12TicketConn) packTicketBuf(buf *bytes.Buffer, u string) { + length := 16 * (mathRand.Intn(17) + 8) + buf.Write([]byte{0, 0x23}) + binary.Write(buf, binary.BigEndian, uint16(length)) + tools.AppendRandBytes(buf, length) +} + +func (t *tls12Ticket) hmacSHA1(data []byte) []byte { + key := pool.Get(len(t.Key) + 32) + defer pool.Put(key) + copy(key, t.Key) + copy(key[len(t.Key):], t.clientID[:]) + + sha1Data := tools.HmacSHA1(key, data) + return sha1Data[:10] +} + +func (t *tls12Ticket) getHost() string { + host := t.Param + if len(host) == 0 { + host = t.Host + } + if len(host) > 0 && host[len(host)-1] >= '0' && host[len(host)-1] <= '9' { + host = "" + } + hosts := strings.Split(host, ",") + host = hosts[mathRand.Intn(len(hosts))] + return host +} diff --git a/transport/ssr/protocol/auth_aes128_md5.go b/transport/ssr/protocol/auth_aes128_md5.go new file mode 100644 index 0000000..d3bc941 --- /dev/null +++ b/transport/ssr/protocol/auth_aes128_md5.go @@ -0,0 +1,18 @@ +package protocol + +import "github.com/Dreamacro/clash/transport/ssr/tools" + +func init() { + register("auth_aes128_md5", newAuthAES128MD5, 9) +} + +func newAuthAES128MD5(b *Base) Protocol { + a := &authAES128{ + Base: b, + authData: &authData{}, + authAES128Function: &authAES128Function{salt: "auth_aes128_md5", hmac: tools.HmacMD5, hashDigest: tools.MD5Sum}, + userData: &userData{}, + } + a.initUserData() + return a +} diff --git a/transport/ssr/protocol/auth_aes128_sha1.go b/transport/ssr/protocol/auth_aes128_sha1.go new file mode 100644 index 0000000..cd662d8 --- /dev/null +++ b/transport/ssr/protocol/auth_aes128_sha1.go @@ -0,0 +1,281 @@ +package protocol + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "math" + mathRand "math/rand" + "net" + "strconv" + "strings" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/ssr/tools" +) + +type ( + hmacMethod func(key, data []byte) []byte + hashDigestMethod func([]byte) []byte +) + +func init() { + register("auth_aes128_sha1", newAuthAES128SHA1, 9) +} + +type authAES128Function struct { + salt string + hmac hmacMethod + hashDigest hashDigestMethod +} + +type authAES128 struct { + *Base + *authData + *authAES128Function + *userData + iv []byte + hasSentHeader bool + rawTrans bool + packID uint32 + recvID uint32 +} + +func newAuthAES128SHA1(b *Base) Protocol { + a := &authAES128{ + Base: b, + authData: &authData{}, + authAES128Function: &authAES128Function{salt: "auth_aes128_sha1", hmac: tools.HmacSHA1, hashDigest: tools.SHA1Sum}, + userData: &userData{}, + } + a.initUserData() + return a +} + +func (a *authAES128) initUserData() { + params := strings.Split(a.Param, ":") + if len(params) > 1 { + if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil { + binary.LittleEndian.PutUint32(a.userID[:], uint32(userID)) + a.userKey = a.hashDigest([]byte(params[1])) + } else { + log.Warnln("Wrong protocol-param for %s, only digits are expected before ':'", a.salt) + } + } + if len(a.userKey) == 0 { + a.userKey = a.Key + rand.Read(a.userID[:]) + } +} + +func (a *authAES128) StreamConn(c net.Conn, iv []byte) net.Conn { + p := &authAES128{ + Base: a.Base, + authData: a.next(), + authAES128Function: a.authAES128Function, + userData: a.userData, + packID: 1, + recvID: 1, + } + p.iv = iv + return &Conn{Conn: c, Protocol: p} +} + +func (a *authAES128) PacketConn(c net.PacketConn) net.PacketConn { + p := &authAES128{ + Base: a.Base, + authAES128Function: a.authAES128Function, + userData: a.userData, + } + return &PacketConn{PacketConn: c, Protocol: p} +} + +func (a *authAES128) Decode(dst, src *bytes.Buffer) error { + if a.rawTrans { + dst.ReadFrom(src) + return nil + } + for src.Len() > 4 { + macKey := pool.Get(len(a.userKey) + 4) + defer pool.Put(macKey) + copy(macKey, a.userKey) + binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.recvID) + if !bytes.Equal(a.hmac(macKey, src.Bytes()[:2])[:2], src.Bytes()[2:4]) { + src.Reset() + return errAuthAES128MACError + } + + length := int(binary.LittleEndian.Uint16(src.Bytes()[:2])) + if length >= 8192 || length < 7 { + a.rawTrans = true + src.Reset() + return errAuthAES128LengthError + } + if length > src.Len() { + break + } + + if !bytes.Equal(a.hmac(macKey, src.Bytes()[:length-4])[:4], src.Bytes()[length-4:length]) { + a.rawTrans = true + src.Reset() + return errAuthAES128ChksumError + } + + a.recvID++ + + pos := int(src.Bytes()[4]) + if pos < 255 { + pos += 4 + } else { + pos = int(binary.LittleEndian.Uint16(src.Bytes()[5:7])) + 4 + } + dst.Write(src.Bytes()[pos : length-4]) + src.Next(length) + } + return nil +} + +func (a *authAES128) Encode(buf *bytes.Buffer, b []byte) error { + fullDataLength := len(b) + if !a.hasSentHeader { + dataLength := getDataLength(b) + a.packAuthData(buf, b[:dataLength]) + b = b[dataLength:] + a.hasSentHeader = true + } + for len(b) > 8100 { + a.packData(buf, b[:8100], fullDataLength) + b = b[8100:] + } + if len(b) > 0 { + a.packData(buf, b, fullDataLength) + } + return nil +} + +func (a *authAES128) DecodePacket(b []byte) ([]byte, error) { + if len(b) < 4 { + return nil, errAuthAES128LengthError + } + if !bytes.Equal(a.hmac(a.Key, b[:len(b)-4])[:4], b[len(b)-4:]) { + return nil, errAuthAES128ChksumError + } + return b[:len(b)-4], nil +} + +func (a *authAES128) EncodePacket(buf *bytes.Buffer, b []byte) error { + buf.Write(b) + buf.Write(a.userID[:]) + buf.Write(a.hmac(a.userKey, buf.Bytes())[:4]) + return nil +} + +func (a *authAES128) packData(poolBuf *bytes.Buffer, data []byte, fullDataLength int) { + dataLength := len(data) + randDataLength := a.getRandDataLengthForPackData(dataLength, fullDataLength) + /* + 2: uint16 LittleEndian packedDataLength + 2: hmac of packedDataLength + 3: maxRandDataLengthPrefix (min:1) + 4: hmac of packedData except the last 4 bytes + */ + packedDataLength := 2 + 2 + 3 + randDataLength + dataLength + 4 + if randDataLength < 128 { + packedDataLength -= 2 + } + + macKey := pool.Get(len(a.userKey) + 4) + defer pool.Put(macKey) + copy(macKey, a.userKey) + binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.packID) + a.packID++ + + binary.Write(poolBuf, binary.LittleEndian, uint16(packedDataLength)) + poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[poolBuf.Len()-2:])[:2]) + a.packRandData(poolBuf, randDataLength) + poolBuf.Write(data) + poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[poolBuf.Len()-packedDataLength+4:])[:4]) +} + +func trapezoidRandom(max int, d float64) int { + base := mathRand.Float64() + if d-0 > 1e-6 { + a := 1 - d + base = (math.Sqrt(a*a+4*d*base) - a) / (2 * d) + } + return int(base * float64(max)) +} + +func (a *authAES128) getRandDataLengthForPackData(dataLength, fullDataLength int) int { + if fullDataLength >= 32*1024-a.Overhead { + return 0 + } + // 1460: tcp_mss + revLength := 1460 - dataLength - 9 + if revLength == 0 { + return 0 + } + if revLength < 0 { + if revLength > -1460 { + return trapezoidRandom(revLength+1460, -0.3) + } + return mathRand.Intn(32) + } + if dataLength > 900 { + return mathRand.Intn(revLength) + } + return trapezoidRandom(revLength, -0.3) +} + +func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) { + if len(data) == 0 { + return + } + dataLength := len(data) + randDataLength := a.getRandDataLengthForPackAuthData(dataLength) + /* + 7: checkHead(1) and hmac of checkHead(6) + 4: userID + 16: encrypted data of authdata(12), uint16 BigEndian packedDataLength(2) and uint16 BigEndian randDataLength(2) + 4: hmac of userID and encrypted data + 4: hmac of packedAuthData except the last 4 bytes + */ + packedAuthDataLength := 7 + 4 + 16 + 4 + randDataLength + dataLength + 4 + + macKey := pool.Get(len(a.iv) + len(a.Key)) + defer pool.Put(macKey) + copy(macKey, a.iv) + copy(macKey[len(a.iv):], a.Key) + + poolBuf.WriteByte(byte(mathRand.Intn(256))) + poolBuf.Write(a.hmac(macKey, poolBuf.Bytes())[:6]) + poolBuf.Write(a.userID[:]) + err := a.authData.putEncryptedData(poolBuf, a.userKey, [2]int{packedAuthDataLength, randDataLength}, a.salt) + if err != nil { + poolBuf.Reset() + return + } + poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[7:])[:4]) + tools.AppendRandBytes(poolBuf, randDataLength) + poolBuf.Write(data) + poolBuf.Write(a.hmac(a.userKey, poolBuf.Bytes())[:4]) +} + +func (a *authAES128) getRandDataLengthForPackAuthData(size int) int { + if size > 400 { + return mathRand.Intn(512) + } + return mathRand.Intn(1024) +} + +func (a *authAES128) packRandData(poolBuf *bytes.Buffer, size int) { + if size < 128 { + poolBuf.WriteByte(byte(size + 1)) + tools.AppendRandBytes(poolBuf, size) + return + } + poolBuf.WriteByte(255) + binary.Write(poolBuf, binary.LittleEndian, uint16(size+3)) + tools.AppendRandBytes(poolBuf, size) +} diff --git a/transport/ssr/protocol/auth_chain_a.go b/transport/ssr/protocol/auth_chain_a.go new file mode 100644 index 0000000..6b12ab9 --- /dev/null +++ b/transport/ssr/protocol/auth_chain_a.go @@ -0,0 +1,309 @@ +package protocol + +import ( + "bytes" + "crypto/cipher" + "crypto/rand" + "crypto/rc4" + "encoding/base64" + "encoding/binary" + "net" + "strconv" + "strings" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/shadowsocks/core" + "github.com/Dreamacro/clash/transport/ssr/tools" +) + +func init() { + register("auth_chain_a", newAuthChainA, 4) +} + +type randDataLengthMethod func(int, []byte, *tools.XorShift128Plus) int + +type authChainA struct { + *Base + *authData + *userData + iv []byte + salt string + hasSentHeader bool + rawTrans bool + lastClientHash []byte + lastServerHash []byte + encrypter cipher.Stream + decrypter cipher.Stream + randomClient tools.XorShift128Plus + randomServer tools.XorShift128Plus + randDataLength randDataLengthMethod + packID uint32 + recvID uint32 +} + +func newAuthChainA(b *Base) Protocol { + a := &authChainA{ + Base: b, + authData: &authData{}, + userData: &userData{}, + salt: "auth_chain_a", + } + a.initUserData() + return a +} + +func (a *authChainA) initUserData() { + params := strings.Split(a.Param, ":") + if len(params) > 1 { + if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil { + binary.LittleEndian.PutUint32(a.userID[:], uint32(userID)) + a.userKey = []byte(params[1]) + } else { + log.Warnln("Wrong protocol-param for %s, only digits are expected before ':'", a.salt) + } + } + if len(a.userKey) == 0 { + a.userKey = a.Key + rand.Read(a.userID[:]) + } +} + +func (a *authChainA) StreamConn(c net.Conn, iv []byte) net.Conn { + p := &authChainA{ + Base: a.Base, + authData: a.next(), + userData: a.userData, + salt: a.salt, + packID: 1, + recvID: 1, + } + p.iv = iv + p.randDataLength = p.getRandLength + return &Conn{Conn: c, Protocol: p} +} + +func (a *authChainA) PacketConn(c net.PacketConn) net.PacketConn { + p := &authChainA{ + Base: a.Base, + salt: a.salt, + userData: a.userData, + } + return &PacketConn{PacketConn: c, Protocol: p} +} + +func (a *authChainA) Decode(dst, src *bytes.Buffer) error { + if a.rawTrans { + dst.ReadFrom(src) + return nil + } + for src.Len() > 4 { + macKey := pool.Get(len(a.userKey) + 4) + defer pool.Put(macKey) + copy(macKey, a.userKey) + binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.recvID) + + dataLength := int(binary.LittleEndian.Uint16(src.Bytes()[:2]) ^ binary.LittleEndian.Uint16(a.lastServerHash[14:16])) + randDataLength := a.randDataLength(dataLength, a.lastServerHash, &a.randomServer) + length := dataLength + randDataLength + + if length >= 4096 { + a.rawTrans = true + src.Reset() + return errAuthChainLengthError + } + + if 4+length > src.Len() { + break + } + + serverHash := tools.HmacMD5(macKey, src.Bytes()[:length+2]) + if !bytes.Equal(serverHash[:2], src.Bytes()[length+2:length+4]) { + a.rawTrans = true + src.Reset() + return errAuthChainChksumError + } + a.lastServerHash = serverHash + + pos := 2 + if dataLength > 0 && randDataLength > 0 { + pos += getRandStartPos(randDataLength, &a.randomServer) + } + wantedData := src.Bytes()[pos : pos+dataLength] + a.decrypter.XORKeyStream(wantedData, wantedData) + if a.recvID == 1 { + dst.Write(wantedData[2:]) + } else { + dst.Write(wantedData) + } + a.recvID++ + src.Next(length + 4) + } + return nil +} + +func (a *authChainA) Encode(buf *bytes.Buffer, b []byte) error { + if !a.hasSentHeader { + dataLength := getDataLength(b) + a.packAuthData(buf, b[:dataLength]) + b = b[dataLength:] + a.hasSentHeader = true + } + for len(b) > 2800 { + a.packData(buf, b[:2800]) + b = b[2800:] + } + if len(b) > 0 { + a.packData(buf, b) + } + return nil +} + +func (a *authChainA) DecodePacket(b []byte) ([]byte, error) { + if len(b) < 9 { + return nil, errAuthChainLengthError + } + if !bytes.Equal(tools.HmacMD5(a.userKey, b[:len(b)-1])[:1], b[len(b)-1:]) { + return nil, errAuthChainChksumError + } + md5Data := tools.HmacMD5(a.Key, b[len(b)-8:len(b)-1]) + + randDataLength := udpGetRandLength(md5Data, &a.randomServer) + + key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(md5Data), 16) + rc4Cipher, err := rc4.NewCipher(key) + if err != nil { + return nil, err + } + wantedData := b[:len(b)-8-randDataLength] + rc4Cipher.XORKeyStream(wantedData, wantedData) + return wantedData, nil +} + +func (a *authChainA) EncodePacket(buf *bytes.Buffer, b []byte) error { + authData := pool.Get(3) + defer pool.Put(authData) + rand.Read(authData) + + md5Data := tools.HmacMD5(a.Key, authData) + + randDataLength := udpGetRandLength(md5Data, &a.randomClient) + + key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(md5Data), 16) + rc4Cipher, err := rc4.NewCipher(key) + if err != nil { + return err + } + rc4Cipher.XORKeyStream(b, b) + + buf.Write(b) + tools.AppendRandBytes(buf, randDataLength) + buf.Write(authData) + binary.Write(buf, binary.LittleEndian, binary.LittleEndian.Uint32(a.userID[:])^binary.LittleEndian.Uint32(md5Data[:4])) + buf.Write(tools.HmacMD5(a.userKey, buf.Bytes())[:1]) + return nil +} + +func (a *authChainA) packAuthData(poolBuf *bytes.Buffer, data []byte) { + /* + dataLength := len(data) + 12: checkHead(4) and hmac of checkHead(8) + 4: uint32 LittleEndian uid (uid = userID ^ last client hash) + 16: encrypted data of authdata(12), uint16 LittleEndian overhead(2) and uint16 LittleEndian number zero(2) + 4: last server hash(4) + packedAuthDataLength := 12 + 4 + 16 + 4 + dataLength + */ + + macKey := pool.Get(len(a.iv) + len(a.Key)) + defer pool.Put(macKey) + copy(macKey, a.iv) + copy(macKey[len(a.iv):], a.Key) + + // check head + tools.AppendRandBytes(poolBuf, 4) + a.lastClientHash = tools.HmacMD5(macKey, poolBuf.Bytes()) + a.initRC4Cipher() + poolBuf.Write(a.lastClientHash[:8]) + // uid + binary.Write(poolBuf, binary.LittleEndian, binary.LittleEndian.Uint32(a.userID[:])^binary.LittleEndian.Uint32(a.lastClientHash[8:12])) + // encrypted data + err := a.putEncryptedData(poolBuf, a.userKey, [2]int{a.Overhead, 0}, a.salt) + if err != nil { + poolBuf.Reset() + return + } + // last server hash + a.lastServerHash = tools.HmacMD5(a.userKey, poolBuf.Bytes()[12:]) + poolBuf.Write(a.lastServerHash[:4]) + // packed data + a.packData(poolBuf, data) +} + +func (a *authChainA) packData(poolBuf *bytes.Buffer, data []byte) { + a.encrypter.XORKeyStream(data, data) + + macKey := pool.Get(len(a.userKey) + 4) + defer pool.Put(macKey) + copy(macKey, a.userKey) + binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.packID) + a.packID++ + + length := uint16(len(data)) ^ binary.LittleEndian.Uint16(a.lastClientHash[14:16]) + + originalLength := poolBuf.Len() + binary.Write(poolBuf, binary.LittleEndian, length) + a.putMixedRandDataAndData(poolBuf, data) + a.lastClientHash = tools.HmacMD5(macKey, poolBuf.Bytes()[originalLength:]) + poolBuf.Write(a.lastClientHash[:2]) +} + +func (a *authChainA) putMixedRandDataAndData(poolBuf *bytes.Buffer, data []byte) { + randDataLength := a.randDataLength(len(data), a.lastClientHash, &a.randomClient) + if len(data) == 0 { + tools.AppendRandBytes(poolBuf, randDataLength) + return + } + if randDataLength > 0 { + startPos := getRandStartPos(randDataLength, &a.randomClient) + tools.AppendRandBytes(poolBuf, startPos) + poolBuf.Write(data) + tools.AppendRandBytes(poolBuf, randDataLength-startPos) + return + } + poolBuf.Write(data) +} + +func getRandStartPos(length int, random *tools.XorShift128Plus) int { + if length == 0 { + return 0 + } + return int(int64(random.Next()%8589934609) % int64(length)) +} + +func (a *authChainA) getRandLength(length int, lastHash []byte, random *tools.XorShift128Plus) int { + if length > 1440 { + return 0 + } + random.InitFromBinAndLength(lastHash, length) + if length > 1300 { + return int(random.Next() % 31) + } + if length > 900 { + return int(random.Next() % 127) + } + if length > 400 { + return int(random.Next() % 521) + } + return int(random.Next() % 1021) +} + +func (a *authChainA) initRC4Cipher() { + key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(a.lastClientHash), 16) + a.encrypter, _ = rc4.NewCipher(key) + a.decrypter, _ = rc4.NewCipher(key) +} + +func udpGetRandLength(lastHash []byte, random *tools.XorShift128Plus) int { + random.InitFromBin(lastHash) + return int(random.Next() % 127) +} diff --git a/transport/ssr/protocol/auth_chain_b.go b/transport/ssr/protocol/auth_chain_b.go new file mode 100644 index 0000000..857b2a3 --- /dev/null +++ b/transport/ssr/protocol/auth_chain_b.go @@ -0,0 +1,97 @@ +package protocol + +import ( + "net" + "sort" + + "github.com/Dreamacro/clash/transport/ssr/tools" +) + +func init() { + register("auth_chain_b", newAuthChainB, 4) +} + +type authChainB struct { + *authChainA + dataSizeList []int + dataSizeList2 []int +} + +func newAuthChainB(b *Base) Protocol { + a := &authChainB{ + authChainA: &authChainA{ + Base: b, + authData: &authData{}, + userData: &userData{}, + salt: "auth_chain_b", + }, + } + a.initUserData() + return a +} + +func (a *authChainB) StreamConn(c net.Conn, iv []byte) net.Conn { + p := &authChainB{ + authChainA: &authChainA{ + Base: a.Base, + authData: a.next(), + userData: a.userData, + salt: a.salt, + packID: 1, + recvID: 1, + }, + } + p.iv = iv + p.randDataLength = p.getRandLength + p.initDataSize() + return &Conn{Conn: c, Protocol: p} +} + +func (a *authChainB) initDataSize() { + a.dataSizeList = a.dataSizeList[:0] + a.dataSizeList2 = a.dataSizeList2[:0] + + a.randomServer.InitFromBin(a.Key) + length := a.randomServer.Next()%8 + 4 + for ; length > 0; length-- { + a.dataSizeList = append(a.dataSizeList, int(a.randomServer.Next()%2340%2040%1440)) + } + sort.Ints(a.dataSizeList) + + length = a.randomServer.Next()%16 + 8 + for ; length > 0; length-- { + a.dataSizeList2 = append(a.dataSizeList2, int(a.randomServer.Next()%2340%2040%1440)) + } + sort.Ints(a.dataSizeList2) +} + +func (a *authChainB) getRandLength(length int, lashHash []byte, random *tools.XorShift128Plus) int { + if length >= 1440 { + return 0 + } + random.InitFromBinAndLength(lashHash, length) + pos := sort.Search(len(a.dataSizeList), func(i int) bool { return a.dataSizeList[i] >= length+a.Overhead }) + finalPos := pos + int(random.Next()%uint64(len(a.dataSizeList))) + if finalPos < len(a.dataSizeList) { + return a.dataSizeList[finalPos] - length - a.Overhead + } + + pos = sort.Search(len(a.dataSizeList2), func(i int) bool { return a.dataSizeList2[i] >= length+a.Overhead }) + finalPos = pos + int(random.Next()%uint64(len(a.dataSizeList2))) + if finalPos < len(a.dataSizeList2) { + return a.dataSizeList2[finalPos] - length - a.Overhead + } + if finalPos < pos+len(a.dataSizeList2)-1 { + return 0 + } + if length > 1300 { + return int(random.Next() % 31) + } + if length > 900 { + return int(random.Next() % 127) + } + if length > 400 { + return int(random.Next() % 521) + } + return int(random.Next() % 1021) +} diff --git a/transport/ssr/protocol/auth_sha1_v4.go b/transport/ssr/protocol/auth_sha1_v4.go new file mode 100644 index 0000000..30392c9 --- /dev/null +++ b/transport/ssr/protocol/auth_sha1_v4.go @@ -0,0 +1,182 @@ +package protocol + +import ( + "bytes" + "encoding/binary" + "hash/adler32" + "hash/crc32" + "math/rand" + "net" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/transport/ssr/tools" +) + +func init() { + register("auth_sha1_v4", newAuthSHA1V4, 7) +} + +type authSHA1V4 struct { + *Base + *authData + iv []byte + hasSentHeader bool + rawTrans bool +} + +func newAuthSHA1V4(b *Base) Protocol { + return &authSHA1V4{Base: b, authData: &authData{}} +} + +func (a *authSHA1V4) StreamConn(c net.Conn, iv []byte) net.Conn { + p := &authSHA1V4{Base: a.Base, authData: a.next()} + p.iv = iv + return &Conn{Conn: c, Protocol: p} +} + +func (a *authSHA1V4) PacketConn(c net.PacketConn) net.PacketConn { + return c +} + +func (a *authSHA1V4) Decode(dst, src *bytes.Buffer) error { + if a.rawTrans { + dst.ReadFrom(src) + return nil + } + for src.Len() > 4 { + if uint16(crc32.ChecksumIEEE(src.Bytes()[:2])&0xffff) != binary.LittleEndian.Uint16(src.Bytes()[2:4]) { + src.Reset() + return errAuthSHA1V4CRC32Error + } + + length := int(binary.BigEndian.Uint16(src.Bytes()[:2])) + if length >= 8192 || length < 7 { + a.rawTrans = true + src.Reset() + return errAuthSHA1V4LengthError + } + if length > src.Len() { + break + } + + if adler32.Checksum(src.Bytes()[:length-4]) != binary.LittleEndian.Uint32(src.Bytes()[length-4:length]) { + a.rawTrans = true + src.Reset() + return errAuthSHA1V4Adler32Error + } + + pos := int(src.Bytes()[4]) + if pos < 255 { + pos += 4 + } else { + pos = int(binary.BigEndian.Uint16(src.Bytes()[5:7])) + 4 + } + dst.Write(src.Bytes()[pos : length-4]) + src.Next(length) + } + return nil +} + +func (a *authSHA1V4) Encode(buf *bytes.Buffer, b []byte) error { + if !a.hasSentHeader { + dataLength := getDataLength(b) + + a.packAuthData(buf, b[:dataLength]) + b = b[dataLength:] + + a.hasSentHeader = true + } + for len(b) > 8100 { + a.packData(buf, b[:8100]) + b = b[8100:] + } + if len(b) > 0 { + a.packData(buf, b) + } + + return nil +} + +func (a *authSHA1V4) DecodePacket(b []byte) ([]byte, error) { return b, nil } + +func (a *authSHA1V4) EncodePacket(buf *bytes.Buffer, b []byte) error { + buf.Write(b) + return nil +} + +func (a *authSHA1V4) packData(poolBuf *bytes.Buffer, data []byte) { + dataLength := len(data) + randDataLength := a.getRandDataLength(dataLength) + /* + 2: uint16 BigEndian packedDataLength + 2: uint16 LittleEndian crc32Data & 0xffff + 3: maxRandDataLengthPrefix (min:1) + 4: adler32Data + */ + packedDataLength := 2 + 2 + 3 + randDataLength + dataLength + 4 + if randDataLength < 128 { + packedDataLength -= 2 + } + + binary.Write(poolBuf, binary.BigEndian, uint16(packedDataLength)) + binary.Write(poolBuf, binary.LittleEndian, uint16(crc32.ChecksumIEEE(poolBuf.Bytes()[poolBuf.Len()-2:])&0xffff)) + a.packRandData(poolBuf, randDataLength) + poolBuf.Write(data) + binary.Write(poolBuf, binary.LittleEndian, adler32.Checksum(poolBuf.Bytes()[poolBuf.Len()-packedDataLength+4:])) +} + +func (a *authSHA1V4) packAuthData(poolBuf *bytes.Buffer, data []byte) { + dataLength := len(data) + randDataLength := a.getRandDataLength(12 + dataLength) + /* + 2: uint16 BigEndian packedAuthDataLength + 4: uint32 LittleEndian crc32Data + 3: maxRandDataLengthPrefix (min: 1) + 12: authDataLength + 10: hmacSHA1DataLength + */ + packedAuthDataLength := 2 + 4 + 3 + randDataLength + 12 + dataLength + 10 + if randDataLength < 128 { + packedAuthDataLength -= 2 + } + + salt := []byte("auth_sha1_v4") + crcData := pool.Get(len(salt) + len(a.Key) + 2) + defer pool.Put(crcData) + binary.BigEndian.PutUint16(crcData, uint16(packedAuthDataLength)) + copy(crcData[2:], salt) + copy(crcData[2+len(salt):], a.Key) + + key := pool.Get(len(a.iv) + len(a.Key)) + defer pool.Put(key) + copy(key, a.iv) + copy(key[len(a.iv):], a.Key) + + poolBuf.Write(crcData[:2]) + binary.Write(poolBuf, binary.LittleEndian, crc32.ChecksumIEEE(crcData)) + a.packRandData(poolBuf, randDataLength) + a.putAuthData(poolBuf) + poolBuf.Write(data) + poolBuf.Write(tools.HmacSHA1(key, poolBuf.Bytes()[poolBuf.Len()-packedAuthDataLength+10:])[:10]) +} + +func (a *authSHA1V4) packRandData(poolBuf *bytes.Buffer, size int) { + if size < 128 { + poolBuf.WriteByte(byte(size + 1)) + tools.AppendRandBytes(poolBuf, size) + return + } + poolBuf.WriteByte(255) + binary.Write(poolBuf, binary.BigEndian, uint16(size+3)) + tools.AppendRandBytes(poolBuf, size) +} + +func (a *authSHA1V4) getRandDataLength(size int) int { + if size > 1200 { + return 0 + } + if size > 400 { + return rand.Intn(256) + } + return rand.Intn(512) +} diff --git a/transport/ssr/protocol/base.go b/transport/ssr/protocol/base.go new file mode 100644 index 0000000..3f41bfa --- /dev/null +++ b/transport/ssr/protocol/base.go @@ -0,0 +1,78 @@ +package protocol + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/binary" + mathRand "math/rand" + "sync" + "time" + + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/shadowsocks/core" +) + +type Base struct { + Key []byte + Overhead int + Param string +} + +type userData struct { + userKey []byte + userID [4]byte +} + +type authData struct { + clientID [4]byte + connectionID uint32 + mutex sync.Mutex +} + +func (a *authData) next() *authData { + r := &authData{} + a.mutex.Lock() + defer a.mutex.Unlock() + if a.connectionID > 0xff000000 || a.connectionID == 0 { + rand.Read(a.clientID[:]) + a.connectionID = mathRand.Uint32() & 0xffffff + } + a.connectionID++ + copy(r.clientID[:], a.clientID[:]) + r.connectionID = a.connectionID + return r +} + +func (a *authData) putAuthData(buf *bytes.Buffer) { + binary.Write(buf, binary.LittleEndian, uint32(time.Now().Unix())) + buf.Write(a.clientID[:]) + binary.Write(buf, binary.LittleEndian, a.connectionID) +} + +func (a *authData) putEncryptedData(b *bytes.Buffer, userKey []byte, paddings [2]int, salt string) error { + encrypt := pool.Get(16) + defer pool.Put(encrypt) + binary.LittleEndian.PutUint32(encrypt, uint32(time.Now().Unix())) + copy(encrypt[4:], a.clientID[:]) + binary.LittleEndian.PutUint32(encrypt[8:], a.connectionID) + binary.LittleEndian.PutUint16(encrypt[12:], uint16(paddings[0])) + binary.LittleEndian.PutUint16(encrypt[14:], uint16(paddings[1])) + + cipherKey := core.Kdf(base64.StdEncoding.EncodeToString(userKey)+salt, 16) + block, err := aes.NewCipher(cipherKey) + if err != nil { + log.Warnln("New cipher error: %s", err.Error()) + return err + } + iv := bytes.Repeat([]byte{0}, 16) + cbcCipher := cipher.NewCBCEncrypter(block, iv) + + cbcCipher.CryptBlocks(encrypt, encrypt) + + b.Write(encrypt) + return nil +} diff --git a/transport/ssr/protocol/origin.go b/transport/ssr/protocol/origin.go new file mode 100644 index 0000000..80fdfa9 --- /dev/null +++ b/transport/ssr/protocol/origin.go @@ -0,0 +1,33 @@ +package protocol + +import ( + "bytes" + "net" +) + +type origin struct{} + +func init() { register("origin", newOrigin, 0) } + +func newOrigin(b *Base) Protocol { return &origin{} } + +func (o *origin) StreamConn(c net.Conn, iv []byte) net.Conn { return c } + +func (o *origin) PacketConn(c net.PacketConn) net.PacketConn { return c } + +func (o *origin) Decode(dst, src *bytes.Buffer) error { + dst.ReadFrom(src) + return nil +} + +func (o *origin) Encode(buf *bytes.Buffer, b []byte) error { + buf.Write(b) + return nil +} + +func (o *origin) DecodePacket(b []byte) ([]byte, error) { return b, nil } + +func (o *origin) EncodePacket(buf *bytes.Buffer, b []byte) error { + buf.Write(b) + return nil +} diff --git a/transport/ssr/protocol/packet.go b/transport/ssr/protocol/packet.go new file mode 100644 index 0000000..249db70 --- /dev/null +++ b/transport/ssr/protocol/packet.go @@ -0,0 +1,36 @@ +package protocol + +import ( + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +type PacketConn struct { + net.PacketConn + Protocol +} + +func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + err := c.EncodePacket(buf, b) + if err != nil { + return 0, err + } + _, err = c.PacketConn.WriteTo(buf.Bytes(), addr) + return len(b), err +} + +func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, addr, err := c.PacketConn.ReadFrom(b) + if err != nil { + return n, addr, err + } + decoded, err := c.DecodePacket(b[:n]) + if err != nil { + return n, addr, err + } + copy(b, decoded) + return len(decoded), addr, nil +} diff --git a/transport/ssr/protocol/protocol.go b/transport/ssr/protocol/protocol.go new file mode 100644 index 0000000..41bd984 --- /dev/null +++ b/transport/ssr/protocol/protocol.go @@ -0,0 +1,76 @@ +package protocol + +import ( + "bytes" + "errors" + "fmt" + "math/rand" + "net" +) + +var ( + errAuthSHA1V4CRC32Error = errors.New("auth_sha1_v4 decode data wrong crc32") + errAuthSHA1V4LengthError = errors.New("auth_sha1_v4 decode data wrong length") + errAuthSHA1V4Adler32Error = errors.New("auth_sha1_v4 decode data wrong adler32") + errAuthAES128MACError = errors.New("auth_aes128 decode data wrong mac") + errAuthAES128LengthError = errors.New("auth_aes128 decode data wrong length") + errAuthAES128ChksumError = errors.New("auth_aes128 decode data wrong checksum") + errAuthChainLengthError = errors.New("auth_chain decode data wrong length") + errAuthChainChksumError = errors.New("auth_chain decode data wrong checksum") +) + +type Protocol interface { + StreamConn(net.Conn, []byte) net.Conn + PacketConn(net.PacketConn) net.PacketConn + Decode(dst, src *bytes.Buffer) error + Encode(buf *bytes.Buffer, b []byte) error + DecodePacket([]byte) ([]byte, error) + EncodePacket(buf *bytes.Buffer, b []byte) error +} + +type protocolCreator func(b *Base) Protocol + +var protocolList = make(map[string]struct { + overhead int + new protocolCreator +}) + +func register(name string, c protocolCreator, o int) { + protocolList[name] = struct { + overhead int + new protocolCreator + }{overhead: o, new: c} +} + +func PickProtocol(name string, b *Base) (Protocol, error) { + if choice, ok := protocolList[name]; ok { + b.Overhead += choice.overhead + return choice.new(b), nil + } + return nil, fmt.Errorf("protocol %s not supported", name) +} + +func getHeadSize(b []byte, defaultValue int) int { + if len(b) < 2 { + return defaultValue + } + headType := b[0] & 7 + switch headType { + case 1: + return 7 + case 4: + return 19 + case 3: + return 4 + int(b[1]) + } + return defaultValue +} + +func getDataLength(b []byte) int { + bLength := len(b) + dataLength := getHeadSize(b, 30) + rand.Intn(32) + if bLength < dataLength { + return bLength + } + return dataLength +} diff --git a/transport/ssr/protocol/stream.go b/transport/ssr/protocol/stream.go new file mode 100644 index 0000000..3c84615 --- /dev/null +++ b/transport/ssr/protocol/stream.go @@ -0,0 +1,50 @@ +package protocol + +import ( + "bytes" + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +type Conn struct { + net.Conn + Protocol + decoded bytes.Buffer + underDecoded bytes.Buffer +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.decoded.Len() > 0 { + return c.decoded.Read(b) + } + + buf := pool.Get(pool.RelayBufferSize) + defer pool.Put(buf) + n, err := c.Conn.Read(buf) + if err != nil { + return 0, err + } + c.underDecoded.Write(buf[:n]) + err = c.Decode(&c.decoded, &c.underDecoded) + if err != nil { + return 0, err + } + n, _ = c.decoded.Read(b) + return n, nil +} + +func (c *Conn) Write(b []byte) (int, error) { + bLength := len(b) + buf := pool.GetBuffer() + defer pool.PutBuffer(buf) + err := c.Encode(buf, b) + if err != nil { + return 0, err + } + _, err = c.Conn.Write(buf.Bytes()) + if err != nil { + return 0, err + } + return bLength, nil +} diff --git a/transport/ssr/tools/bufPool.go b/transport/ssr/tools/bufPool.go new file mode 100644 index 0000000..ac15c97 --- /dev/null +++ b/transport/ssr/tools/bufPool.go @@ -0,0 +1,11 @@ +package tools + +import ( + "bytes" + "crypto/rand" + "io" +) + +func AppendRandBytes(b *bytes.Buffer, length int) { + b.ReadFrom(io.LimitReader(rand.Reader, int64(length))) +} diff --git a/transport/ssr/tools/crypto.go b/transport/ssr/tools/crypto.go new file mode 100644 index 0000000..b2a4156 --- /dev/null +++ b/transport/ssr/tools/crypto.go @@ -0,0 +1,33 @@ +package tools + +import ( + "crypto/hmac" + "crypto/md5" + "crypto/sha1" +) + +const HmacSHA1Len = 10 + +func HmacMD5(key, data []byte) []byte { + hmacMD5 := hmac.New(md5.New, key) + hmacMD5.Write(data) + return hmacMD5.Sum(nil) +} + +func HmacSHA1(key, data []byte) []byte { + hmacSHA1 := hmac.New(sha1.New, key) + hmacSHA1.Write(data) + return hmacSHA1.Sum(nil) +} + +func MD5Sum(b []byte) []byte { + h := md5.New() + h.Write(b) + return h.Sum(nil) +} + +func SHA1Sum(b []byte) []byte { + h := sha1.New() + h.Write(b) + return h.Sum(nil) +} diff --git a/transport/ssr/tools/random.go b/transport/ssr/tools/random.go new file mode 100644 index 0000000..338543e --- /dev/null +++ b/transport/ssr/tools/random.go @@ -0,0 +1,57 @@ +package tools + +import ( + "encoding/binary" + + "github.com/Dreamacro/clash/common/pool" +) + +// XorShift128Plus - a pseudorandom number generator +type XorShift128Plus struct { + s [2]uint64 +} + +func (r *XorShift128Plus) Next() uint64 { + x := r.s[0] + y := r.s[1] + r.s[0] = y + x ^= x << 23 + x ^= y ^ (x >> 17) ^ (y >> 26) + r.s[1] = x + return x + y +} + +func (r *XorShift128Plus) InitFromBin(bin []byte) { + var full []byte + if len(bin) < 16 { + full := pool.Get(16)[:0] + defer pool.Put(full) + full = append(full, bin...) + for len(full) < 16 { + full = append(full, 0) + } + } else { + full = bin + } + r.s[0] = binary.LittleEndian.Uint64(full[:8]) + r.s[1] = binary.LittleEndian.Uint64(full[8:16]) +} + +func (r *XorShift128Plus) InitFromBinAndLength(bin []byte, length int) { + var full []byte + if len(bin) < 16 { + full := pool.Get(16)[:0] + defer pool.Put(full) + full = append(full, bin...) + for len(full) < 16 { + full = append(full, 0) + } + } + full = bin + binary.LittleEndian.PutUint16(full, uint16(length)) + r.s[0] = binary.LittleEndian.Uint64(full[:8]) + r.s[1] = binary.LittleEndian.Uint64(full[8:16]) + for i := 0; i < 4; i++ { + r.Next() + } +} diff --git a/transport/trojan/trojan.go b/transport/trojan/trojan.go new file mode 100644 index 0000000..fec3f02 --- /dev/null +++ b/transport/trojan/trojan.go @@ -0,0 +1,256 @@ +package trojan + +import ( + "context" + "crypto/sha256" + "crypto/tls" + "encoding/binary" + "encoding/hex" + "errors" + "io" + "net" + "net/http" + "sync" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" + "github.com/Dreamacro/clash/transport/vmess" + + "github.com/Dreamacro/protobytes" +) + +const ( + // max packet length + maxLength = 8192 +) + +var ( + defaultALPN = []string{"h2", "http/1.1"} + defaultWebsocketALPN = []string{"http/1.1"} + + crlf = []byte{'\r', '\n'} +) + +type Command = byte + +var ( + CommandTCP byte = 1 + CommandUDP byte = 3 +) + +type Option struct { + Password string + ALPN []string + ServerName string + SkipCertVerify bool +} + +type WebsocketOption struct { + Host string + Port string + Path string + Headers http.Header +} + +type Trojan struct { + option *Option + hexPassword []byte +} + +func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) { + alpn := defaultALPN + if len(t.option.ALPN) != 0 { + alpn = t.option.ALPN + } + + tlsConfig := &tls.Config{ + NextProtos: alpn, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: t.option.SkipCertVerify, + ServerName: t.option.ServerName, + } + + tlsConn := tls.Client(conn, tlsConfig) + + // fix tls handshake not timeout + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + if err := tlsConn.HandshakeContext(ctx); err != nil { + return nil, err + } + + return tlsConn, nil +} + +func (t *Trojan) StreamWebsocketConn(conn net.Conn, wsOptions *WebsocketOption) (net.Conn, error) { + alpn := defaultWebsocketALPN + if len(t.option.ALPN) != 0 { + alpn = t.option.ALPN + } + + tlsConfig := &tls.Config{ + NextProtos: alpn, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: t.option.SkipCertVerify, + ServerName: t.option.ServerName, + } + + return vmess.StreamWebsocketConn(conn, &vmess.WebsocketConfig{ + Host: wsOptions.Host, + Port: wsOptions.Port, + Path: wsOptions.Path, + Headers: wsOptions.Headers, + TLS: true, + TLSConfig: tlsConfig, + }) +} + +func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) error { + buf := protobytes.BytesWriter{} + buf.PutSlice(t.hexPassword) + buf.PutSlice(crlf) + + buf.PutUint8(command) + buf.PutSlice(socks5Addr) + buf.PutSlice(crlf) + + _, err := w.Write(buf.Bytes()) + return err +} + +func (t *Trojan) PacketConn(conn net.Conn) net.PacketConn { + return &PacketConn{ + Conn: conn, + } +} + +func writePacket(w io.Writer, socks5Addr, payload []byte) (int, error) { + buf := protobytes.BytesWriter{} + buf.PutSlice(socks5Addr) + buf.PutUint16be(uint16(len(payload))) + buf.PutSlice(crlf) + buf.PutSlice(payload) + return w.Write(buf.Bytes()) +} + +func WritePacket(w io.Writer, socks5Addr, payload []byte) (int, error) { + if len(payload) <= maxLength { + return writePacket(w, socks5Addr, payload) + } + + offset := 0 + total := len(payload) + for { + cursor := offset + maxLength + if cursor > total { + cursor = total + } + + n, err := writePacket(w, socks5Addr, payload[offset:cursor]) + if err != nil { + return offset + n, err + } + + offset = cursor + if offset == total { + break + } + } + + return total, nil +} + +func ReadPacket(r io.Reader, payload []byte) (net.Addr, int, int, error) { + addr, err := socks5.ReadAddr(r, payload) + if err != nil { + return nil, 0, 0, errors.New("read addr error") + } + uAddr := addr.UDPAddr() + if uAddr == nil { + return nil, 0, 0, errors.New("parse addr error") + } + + if _, err = io.ReadFull(r, payload[:2]); err != nil { + return nil, 0, 0, errors.New("read length error") + } + + total := int(binary.BigEndian.Uint16(payload[:2])) + if total > maxLength { + return nil, 0, 0, errors.New("packet invalid") + } + + // read crlf + if _, err = io.ReadFull(r, payload[:2]); err != nil { + return nil, 0, 0, errors.New("read crlf error") + } + + length := len(payload) + if total < length { + length = total + } + + if _, err = io.ReadFull(r, payload[:length]); err != nil { + return nil, 0, 0, errors.New("read packet error") + } + + return uAddr, length, total - length, nil +} + +func New(option *Option) *Trojan { + return &Trojan{option, hexSha224([]byte(option.Password))} +} + +type PacketConn struct { + net.Conn + remain int + rAddr net.Addr + mux sync.Mutex +} + +func (pc *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + return WritePacket(pc, socks5.ParseAddr(addr.String()), b) +} + +func (pc *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + pc.mux.Lock() + defer pc.mux.Unlock() + if pc.remain != 0 { + length := len(b) + if pc.remain < length { + length = pc.remain + } + + n, err := pc.Conn.Read(b[:length]) + if err != nil { + return 0, nil, err + } + + pc.remain -= n + addr := pc.rAddr + if pc.remain == 0 { + pc.rAddr = nil + } + + return n, addr, nil + } + + addr, n, remain, err := ReadPacket(pc.Conn, b) + if err != nil { + return 0, nil, err + } + + if remain != 0 { + pc.remain = remain + pc.rAddr = addr + } + + return n, addr, nil +} + +func hexSha224(data []byte) []byte { + buf := make([]byte, 56) + hash := sha256.New224() + hash.Write(data) + hex.Encode(buf, hash.Sum(nil)) + return buf +} diff --git a/transport/v2ray-plugin/mux.go b/transport/v2ray-plugin/mux.go new file mode 100644 index 0000000..aea1619 --- /dev/null +++ b/transport/v2ray-plugin/mux.go @@ -0,0 +1,175 @@ +package obfs + +import ( + "encoding/binary" + "errors" + "io" + "net" + "net/netip" + + "github.com/Dreamacro/protobytes" +) + +type SessionStatus = byte + +const ( + SessionStatusNew SessionStatus = 0x01 + SessionStatusKeep SessionStatus = 0x02 + SessionStatusEnd SessionStatus = 0x03 + SessionStatusKeepAlive SessionStatus = 0x04 +) + +const ( + OptionNone = byte(0x00) + OptionData = byte(0x01) + OptionError = byte(0x02) +) + +type MuxOption struct { + ID [2]byte + Port uint16 + Host string + Type string +} + +// Mux is an mux-compatible client for v2ray-plugin, not a complete implementation +type Mux struct { + net.Conn + buf protobytes.BytesWriter + id [2]byte + length [2]byte + status [2]byte + otb []byte + remain int +} + +func (m *Mux) Read(b []byte) (int, error) { + if m.remain != 0 { + length := m.remain + if len(b) < m.remain { + length = len(b) + } + + n, err := m.Conn.Read(b[:length]) + if err != nil { + return 0, err + } + m.remain -= n + return n, nil + } + + for { + _, err := io.ReadFull(m.Conn, m.length[:]) + if err != nil { + return 0, err + } + length := binary.BigEndian.Uint16(m.length[:]) + if length > 512 { + return 0, errors.New("invalid metalen") + } + + _, err = io.ReadFull(m.Conn, m.id[:]) + if err != nil { + return 0, err + } + + _, err = m.Conn.Read(m.status[:]) + if err != nil { + return 0, err + } + + opcode := m.status[0] + if opcode == SessionStatusKeepAlive { + continue + } + + opts := m.status[1] + + if opts != OptionData { + continue + } + + _, err = io.ReadFull(m.Conn, m.length[:]) + if err != nil { + return 0, err + } + dataLen := int(binary.BigEndian.Uint16(m.length[:])) + m.remain = dataLen + if dataLen > len(b) { + dataLen = len(b) + } + + n, err := m.Conn.Read(b[:dataLen]) + m.remain -= n + return n, err + } +} + +func (m *Mux) Write(b []byte) (int, error) { + if m.otb != nil { + // create a sub connection + if _, err := m.Conn.Write(m.otb); err != nil { + return 0, err + } + m.otb = nil + } + m.buf.Reset() + m.buf.PutUint16be(4) + m.buf.PutSlice(m.id[:]) + m.buf.PutUint8(SessionStatusKeep) + m.buf.PutUint8(OptionData) + m.buf.PutUint16be(uint16(len(b))) + m.buf.Write(b) + + return m.Conn.Write(m.buf.Bytes()) +} + +func (m *Mux) Close() error { + _, err := m.Conn.Write([]byte{0x0, 0x4, m.id[0], m.id[1], SessionStatusEnd, OptionNone}) + if err != nil { + return err + } + return m.Conn.Close() +} + +func NewMux(conn net.Conn, option MuxOption) *Mux { + buf := protobytes.BytesWriter{} + + // fill empty length + buf.PutSlice([]byte{0x0, 0x0}) + buf.PutSlice(option.ID[:]) + buf.PutUint8(SessionStatusNew) + buf.PutUint8(OptionNone) + + // tcp + netType := byte(0x1) + if option.Type == "udp" { + netType = byte(0x2) + } + buf.PutUint8(netType) + + // port + buf.PutUint16be(option.Port) + + // address + ip, err := netip.ParseAddr(option.Host) + if err != nil { + buf.PutUint8(0x2) + buf.PutString(option.Host) + } else if ip.Is4() { + buf.PutUint8(0x1) + buf.PutSlice(ip.AsSlice()) + } else { + buf.PutUint8(0x3) + buf.PutSlice(ip.AsSlice()) + } + + metadata := buf.Bytes() + binary.BigEndian.PutUint16(metadata[:2], uint16(len(metadata)-2)) + + return &Mux{ + Conn: conn, + id: option.ID, + otb: metadata, + } +} diff --git a/transport/v2ray-plugin/websocket.go b/transport/v2ray-plugin/websocket.go new file mode 100644 index 0000000..7591b4a --- /dev/null +++ b/transport/v2ray-plugin/websocket.go @@ -0,0 +1,62 @@ +package obfs + +import ( + "crypto/tls" + "net" + "net/http" + + "github.com/Dreamacro/clash/transport/vmess" +) + +// Option is options of websocket obfs +type Option struct { + Host string + Port string + Path string + Headers map[string]string + TLS bool + SkipCertVerify bool + Mux bool +} + +// NewV2rayObfs return a HTTPObfs +func NewV2rayObfs(conn net.Conn, option *Option) (net.Conn, error) { + header := http.Header{} + for k, v := range option.Headers { + header.Add(k, v) + } + + config := &vmess.WebsocketConfig{ + Host: option.Host, + Port: option.Port, + Path: option.Path, + Headers: header, + } + + if option.TLS { + config.TLS = true + config.TLSConfig = &tls.Config{ + ServerName: option.Host, + InsecureSkipVerify: option.SkipCertVerify, + NextProtos: []string{"http/1.1"}, + } + if host := config.Headers.Get("Host"); host != "" { + config.TLSConfig.ServerName = host + } + } + + var err error + conn, err = vmess.StreamWebsocketConn(conn, config) + if err != nil { + return nil, err + } + + if option.Mux { + conn = NewMux(conn, MuxOption{ + ID: [2]byte{0, 0}, + Host: "127.0.0.1", + Port: 0, + }) + } + return conn, nil +} diff --git a/transport/vmess/aead.go b/transport/vmess/aead.go new file mode 100644 index 0000000..d4fbf2d --- /dev/null +++ b/transport/vmess/aead.go @@ -0,0 +1,124 @@ +package vmess + +import ( + "crypto/cipher" + "encoding/binary" + "errors" + "io" + "sync" + + "github.com/Dreamacro/clash/common/pool" +) + +type aeadWriter struct { + io.Writer + cipher.AEAD + nonce [32]byte + count uint16 + iv []byte + + writeLock sync.Mutex +} + +func newAEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) *aeadWriter { + return &aeadWriter{Writer: w, AEAD: aead, iv: iv} +} + +func (w *aeadWriter) Write(b []byte) (n int, err error) { + w.writeLock.Lock() + buf := pool.Get(pool.RelayBufferSize) + defer func() { + w.writeLock.Unlock() + pool.Put(buf) + }() + length := len(b) + for { + if length == 0 { + break + } + readLen := chunkSize - w.Overhead() + if length < readLen { + readLen = length + } + payloadBuf := buf[lenSize : lenSize+chunkSize-w.Overhead()] + copy(payloadBuf, b[n:n+readLen]) + + binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen+w.Overhead())) + binary.BigEndian.PutUint16(w.nonce[:2], w.count) + copy(w.nonce[2:], w.iv[2:12]) + + w.Seal(payloadBuf[:0], w.nonce[:w.NonceSize()], payloadBuf[:readLen], nil) + w.count++ + + _, err = w.Writer.Write(buf[:lenSize+readLen+w.Overhead()]) + if err != nil { + break + } + n += readLen + length -= readLen + } + return +} + +type aeadReader struct { + io.Reader + cipher.AEAD + nonce [32]byte + buf []byte + offset int + iv []byte + sizeBuf []byte + count uint16 +} + +func newAEADReader(r io.Reader, aead cipher.AEAD, iv []byte) *aeadReader { + return &aeadReader{Reader: r, AEAD: aead, iv: iv, sizeBuf: make([]byte, lenSize)} +} + +func (r *aeadReader) Read(b []byte) (int, error) { + if r.buf != nil { + n := copy(b, r.buf[r.offset:]) + r.offset += n + if r.offset == len(r.buf) { + pool.Put(r.buf) + r.buf = nil + } + return n, nil + } + + _, err := io.ReadFull(r.Reader, r.sizeBuf) + if err != nil { + return 0, err + } + + size := int(binary.BigEndian.Uint16(r.sizeBuf)) + if size > maxSize { + return 0, errors.New("buffer is larger than standard") + } + + buf := pool.Get(size) + _, err = io.ReadFull(r.Reader, buf[:size]) + if err != nil { + pool.Put(buf) + return 0, err + } + + binary.BigEndian.PutUint16(r.nonce[:2], r.count) + copy(r.nonce[2:], r.iv[2:12]) + + _, err = r.Open(buf[:0], r.nonce[:r.NonceSize()], buf[:size], nil) + r.count++ + if err != nil { + return 0, err + } + realLen := size - r.Overhead() + n := copy(b, buf[:realLen]) + if len(b) >= realLen { + pool.Put(buf) + return n, nil + } + + r.offset = n + r.buf = buf[:realLen] + return n, nil +} diff --git a/transport/vmess/chunk.go b/transport/vmess/chunk.go new file mode 100644 index 0000000..ab1adb6 --- /dev/null +++ b/transport/vmess/chunk.go @@ -0,0 +1,102 @@ +package vmess + +import ( + "encoding/binary" + "errors" + "io" + + "github.com/Dreamacro/clash/common/pool" +) + +const ( + lenSize = 2 + chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024 + maxSize = 17 * 1024 // 2 + chunkSize + aead.Overhead() +) + +type chunkReader struct { + io.Reader + buf []byte + sizeBuf []byte + offset int +} + +func newChunkReader(reader io.Reader) *chunkReader { + return &chunkReader{Reader: reader, sizeBuf: make([]byte, lenSize)} +} + +func newChunkWriter(writer io.WriteCloser) *chunkWriter { + return &chunkWriter{Writer: writer} +} + +func (cr *chunkReader) Read(b []byte) (int, error) { + if cr.buf != nil { + n := copy(b, cr.buf[cr.offset:]) + cr.offset += n + if cr.offset == len(cr.buf) { + pool.Put(cr.buf) + cr.buf = nil + } + return n, nil + } + + _, err := io.ReadFull(cr.Reader, cr.sizeBuf) + if err != nil { + return 0, err + } + + size := int(binary.BigEndian.Uint16(cr.sizeBuf)) + if size > maxSize { + return 0, errors.New("buffer is larger than standard") + } + + if len(b) >= size { + _, err := io.ReadFull(cr.Reader, b[:size]) + if err != nil { + return 0, err + } + + return size, nil + } + + buf := pool.Get(size) + _, err = io.ReadFull(cr.Reader, buf) + if err != nil { + pool.Put(buf) + return 0, err + } + n := copy(b, buf) + cr.offset = n + cr.buf = buf + return n, nil +} + +type chunkWriter struct { + io.Writer +} + +func (cw *chunkWriter) Write(b []byte) (n int, err error) { + buf := pool.Get(pool.RelayBufferSize) + defer pool.Put(buf) + length := len(b) + for { + if length == 0 { + break + } + readLen := chunkSize + if length < chunkSize { + readLen = length + } + payloadBuf := buf[lenSize : lenSize+chunkSize] + copy(payloadBuf, b[n:n+readLen]) + + binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen)) + _, err = cw.Writer.Write(buf[:lenSize+readLen]) + if err != nil { + break + } + n += readLen + length -= readLen + } + return +} diff --git a/transport/vmess/conn.go b/transport/vmess/conn.go new file mode 100644 index 0000000..bb77cef --- /dev/null +++ b/transport/vmess/conn.go @@ -0,0 +1,284 @@ +package vmess + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/md5" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "errors" + "hash/fnv" + "io" + mathRand "math/rand" + "net" + "time" + + "github.com/Dreamacro/protobytes" + "golang.org/x/crypto/chacha20poly1305" +) + +// Conn wrapper a net.Conn with vmess protocol +type Conn struct { + net.Conn + reader io.Reader + writer io.Writer + dst *DstAddr + id *ID + reqBodyIV []byte + reqBodyKey []byte + respBodyIV []byte + respBodyKey []byte + respV byte + security byte + option byte + isAead bool + + received bool +} + +func (vc *Conn) Write(b []byte) (int, error) { + return vc.writer.Write(b) +} + +func (vc *Conn) Read(b []byte) (int, error) { + if vc.received { + return vc.reader.Read(b) + } + + if err := vc.recvResponse(); err != nil { + return 0, err + } + vc.received = true + return vc.reader.Read(b) +} + +func (vc *Conn) sendRequest() error { + timestamp := time.Now() + + mbuf := protobytes.BytesWriter{} + + if !vc.isAead { + h := hmac.New(md5.New, vc.id.UUID.Bytes()) + binary.Write(h, binary.BigEndian, uint64(timestamp.Unix())) + mbuf.PutSlice(h.Sum(nil)) + } + + buf := protobytes.BytesWriter{} + + // Ver IV Key V Opt + buf.PutUint8(Version) + buf.PutSlice(vc.reqBodyIV[:]) + buf.PutSlice(vc.reqBodyKey[:]) + buf.PutUint8(vc.respV) + buf.PutUint8(vc.option) + + p := mathRand.Intn(16) + // P Sec Reserve Cmd + buf.PutUint8(byte(p<<4) | vc.security) + buf.PutUint8(0) + if vc.dst.UDP { + buf.PutUint8(CommandUDP) + } else { + buf.PutUint8(CommandTCP) + } + + // Port AddrType Addr + buf.PutUint16be(uint16(vc.dst.Port)) + buf.PutUint8(vc.dst.AddrType) + buf.PutSlice(vc.dst.Addr) + + // padding + if p > 0 { + buf.ReadFull(rand.Reader, p) + } + + fnv1a := fnv.New32a() + fnv1a.Write(buf.Bytes()) + buf.PutSlice(fnv1a.Sum(nil)) + + if !vc.isAead { + block, err := aes.NewCipher(vc.id.CmdKey) + if err != nil { + return err + } + + stream := cipher.NewCFBEncrypter(block, hashTimestamp(timestamp)) + stream.XORKeyStream(buf.Bytes(), buf.Bytes()) + mbuf.PutSlice(buf.Bytes()) + _, err = vc.Conn.Write(mbuf.Bytes()) + return err + } + + var fixedLengthCmdKey [16]byte + copy(fixedLengthCmdKey[:], vc.id.CmdKey) + vmessout := sealVMessAEADHeader(fixedLengthCmdKey, buf.Bytes(), timestamp) + _, err := vc.Conn.Write(vmessout) + return err +} + +func (vc *Conn) recvResponse() error { + var buf []byte + if !vc.isAead { + block, err := aes.NewCipher(vc.respBodyKey[:]) + if err != nil { + return err + } + + stream := cipher.NewCFBDecrypter(block, vc.respBodyIV[:]) + buf = make([]byte, 4) + _, err = io.ReadFull(vc.Conn, buf) + if err != nil { + return err + } + stream.XORKeyStream(buf, buf) + } else { + aeadResponseHeaderLengthEncryptionKey := kdf(vc.respBodyKey[:], kdfSaltConstAEADRespHeaderLenKey)[:16] + aeadResponseHeaderLengthEncryptionIV := kdf(vc.respBodyIV[:], kdfSaltConstAEADRespHeaderLenIV)[:12] + + aeadResponseHeaderLengthEncryptionKeyAESBlock, _ := aes.NewCipher(aeadResponseHeaderLengthEncryptionKey) + aeadResponseHeaderLengthEncryptionAEAD, _ := cipher.NewGCM(aeadResponseHeaderLengthEncryptionKeyAESBlock) + + aeadEncryptedResponseHeaderLength := make([]byte, 18) + if _, err := io.ReadFull(vc.Conn, aeadEncryptedResponseHeaderLength); err != nil { + return err + } + + decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil) + if err != nil { + return err + } + + decryptedResponseHeaderLength := binary.BigEndian.Uint16(decryptedResponseHeaderLengthBinaryBuffer) + aeadResponseHeaderPayloadEncryptionKey := kdf(vc.respBodyKey[:], kdfSaltConstAEADRespHeaderPayloadKey)[:16] + aeadResponseHeaderPayloadEncryptionIV := kdf(vc.respBodyIV[:], kdfSaltConstAEADRespHeaderPayloadIV)[:12] + aeadResponseHeaderPayloadEncryptionKeyAESBlock, _ := aes.NewCipher(aeadResponseHeaderPayloadEncryptionKey) + aeadResponseHeaderPayloadEncryptionAEAD, _ := cipher.NewGCM(aeadResponseHeaderPayloadEncryptionKeyAESBlock) + + encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16) + if _, err := io.ReadFull(vc.Conn, encryptedResponseHeaderBuffer); err != nil { + return err + } + + buf, err = aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil) + if err != nil { + return err + } + + if len(buf) < 4 { + return errors.New("unexpected buffer length") + } + } + + if buf[0] != vc.respV { + return errors.New("unexpected response header") + } + + if buf[2] != 0 { + return errors.New("dynamic port is not supported now") + } + + return nil +} + +func hashTimestamp(t time.Time) []byte { + md5hash := md5.New() + ts := make([]byte, 8) + binary.BigEndian.PutUint64(ts, uint64(t.Unix())) + md5hash.Write(ts) + md5hash.Write(ts) + md5hash.Write(ts) + md5hash.Write(ts) + return md5hash.Sum(nil) +} + +// newConn return a Conn instance +func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security, isAead bool) (*Conn, error) { + randBytes := make([]byte, 33) + rand.Read(randBytes) + reqBodyIV := make([]byte, 16) + reqBodyKey := make([]byte, 16) + copy(reqBodyIV[:], randBytes[:16]) + copy(reqBodyKey[:], randBytes[16:32]) + respV := randBytes[32] + option := OptionChunkStream + + var ( + respBodyKey []byte + respBodyIV []byte + ) + + if isAead { + bodyKey := sha256.Sum256(reqBodyKey) + bodyIV := sha256.Sum256(reqBodyIV) + respBodyKey = bodyKey[:16] + respBodyIV = bodyIV[:16] + } else { + bodyKey := md5.Sum(reqBodyKey) + bodyIV := md5.Sum(reqBodyIV) + respBodyKey = bodyKey[:] + respBodyIV = bodyIV[:] + } + + var writer io.Writer + var reader io.Reader + switch security { + case SecurityZero: + security = SecurityNone + if !dst.UDP { + reader = conn + writer = conn + option = 0 + } else { + reader = newChunkReader(conn) + writer = newChunkWriter(conn) + } + case SecurityNone: + reader = newChunkReader(conn) + writer = newChunkWriter(conn) + case SecurityAES128GCM: + block, _ := aes.NewCipher(reqBodyKey[:]) + aead, _ := cipher.NewGCM(block) + writer = newAEADWriter(conn, aead, reqBodyIV[:]) + + block, _ = aes.NewCipher(respBodyKey[:]) + aead, _ = cipher.NewGCM(block) + reader = newAEADReader(conn, aead, respBodyIV[:]) + case SecurityCHACHA20POLY1305: + key := make([]byte, 32) + t := md5.Sum(reqBodyKey[:]) + copy(key, t[:]) + t = md5.Sum(key[:16]) + copy(key[16:], t[:]) + aead, _ := chacha20poly1305.New(key) + writer = newAEADWriter(conn, aead, reqBodyIV[:]) + + t = md5.Sum(respBodyKey[:]) + copy(key, t[:]) + t = md5.Sum(key[:16]) + copy(key[16:], t[:]) + aead, _ = chacha20poly1305.New(key) + reader = newAEADReader(conn, aead, respBodyIV[:]) + } + + c := &Conn{ + Conn: conn, + id: id, + dst: dst, + reqBodyIV: reqBodyIV, + reqBodyKey: reqBodyKey, + respV: respV, + respBodyIV: respBodyIV[:], + respBodyKey: respBodyKey[:], + reader: reader, + writer: writer, + security: security, + option: option, + isAead: isAead, + } + if err := c.sendRequest(); err != nil { + return nil, err + } + return c, nil +} diff --git a/transport/vmess/h2.go b/transport/vmess/h2.go new file mode 100644 index 0000000..330c8b6 --- /dev/null +++ b/transport/vmess/h2.go @@ -0,0 +1,109 @@ +package vmess + +import ( + "io" + "math/rand" + "net" + "net/http" + "net/url" + + "golang.org/x/net/http2" +) + +type h2Conn struct { + net.Conn + *http2.ClientConn + pwriter *io.PipeWriter + res *http.Response + cfg *H2Config +} + +type H2Config struct { + Hosts []string + Path string +} + +func (hc *h2Conn) establishConn() error { + preader, pwriter := io.Pipe() + + host := hc.cfg.Hosts[rand.Intn(len(hc.cfg.Hosts))] + path := hc.cfg.Path + // TODO: connect use VMess Host instead of H2 Host + req := http.Request{ + Method: http.MethodPut, + Host: host, + URL: &url.URL{ + Scheme: "https", + Host: host, + Path: path, + }, + Proto: "HTTP/2", + ProtoMajor: 2, + ProtoMinor: 0, + Body: preader, + Header: map[string][]string{ + "Accept-Encoding": {"identity"}, + }, + } + + // it will be close at : `func (hc *h2Conn) Close() error` + res, err := hc.ClientConn.RoundTrip(&req) + if err != nil { + return err + } + + hc.pwriter = pwriter + hc.res = res + + return nil +} + +// Read implements net.Conn.Read() +func (hc *h2Conn) Read(b []byte) (int, error) { + if hc.res != nil && !hc.res.Close { + n, err := hc.res.Body.Read(b) + return n, err + } + + if err := hc.establishConn(); err != nil { + return 0, err + } + return hc.res.Body.Read(b) +} + +// Write implements io.Writer. +func (hc *h2Conn) Write(b []byte) (int, error) { + if hc.pwriter != nil { + return hc.pwriter.Write(b) + } + + if err := hc.establishConn(); err != nil { + return 0, err + } + return hc.pwriter.Write(b) +} + +func (hc *h2Conn) Close() error { + if err := hc.pwriter.Close(); err != nil { + return err + } + if err := hc.ClientConn.Shutdown(hc.res.Request.Context()); err != nil { + return err + } + return hc.Conn.Close() +} + +func StreamH2Conn(conn net.Conn, cfg *H2Config) (net.Conn, error) { + transport := &http2.Transport{} + + cconn, err := transport.NewClientConn(conn) + if err != nil { + return nil, err + } + + return &h2Conn{ + Conn: conn, + ClientConn: cconn, + cfg: cfg, + }, nil +} diff --git a/transport/vmess/header.go b/transport/vmess/header.go new file mode 100644 index 0000000..506c243 --- /dev/null +++ b/transport/vmess/header.go @@ -0,0 +1,101 @@ +package vmess + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "hash" + "hash/crc32" + "time" + + "github.com/Dreamacro/protobytes" +) + +const ( + kdfSaltConstAuthIDEncryptionKey = "AES Auth ID Encryption" + kdfSaltConstAEADRespHeaderLenKey = "AEAD Resp Header Len Key" + kdfSaltConstAEADRespHeaderLenIV = "AEAD Resp Header Len IV" + kdfSaltConstAEADRespHeaderPayloadKey = "AEAD Resp Header Key" + kdfSaltConstAEADRespHeaderPayloadIV = "AEAD Resp Header IV" + kdfSaltConstVMessAEADKDF = "VMess AEAD KDF" + kdfSaltConstVMessHeaderPayloadAEADKey = "VMess Header AEAD Key" + kdfSaltConstVMessHeaderPayloadAEADIV = "VMess Header AEAD Nonce" + kdfSaltConstVMessHeaderPayloadLengthAEADKey = "VMess Header AEAD Key_Length" + kdfSaltConstVMessHeaderPayloadLengthAEADIV = "VMess Header AEAD Nonce_Length" +) + +func kdf(key []byte, path ...string) []byte { + hmacCreator := &hMacCreator{value: []byte(kdfSaltConstVMessAEADKDF)} + for _, v := range path { + hmacCreator = &hMacCreator{value: []byte(v), parent: hmacCreator} + } + hmacf := hmacCreator.Create() + hmacf.Write(key) + return hmacf.Sum(nil) +} + +type hMacCreator struct { + parent *hMacCreator + value []byte +} + +func (h *hMacCreator) Create() hash.Hash { + if h.parent == nil { + return hmac.New(sha256.New, h.value) + } + return hmac.New(h.parent.Create, h.value) +} + +func createAuthID(cmdKey []byte, time int64) [16]byte { + buf := protobytes.BytesWriter{} + buf.PutUint64be(uint64(time)) + buf.ReadFull(rand.Reader, 4) + zero := crc32.ChecksumIEEE(buf.Bytes()) + buf.PutUint32be(zero) + + aesBlock, _ := aes.NewCipher(kdf(cmdKey[:], kdfSaltConstAuthIDEncryptionKey)[:16]) + var result [16]byte + aesBlock.Encrypt(result[:], buf.Bytes()) + return result +} + +func sealVMessAEADHeader(key [16]byte, data []byte, t time.Time) []byte { + generatedAuthID := createAuthID(key[:], t.Unix()) + connectionNonce := make([]byte, 8) + rand.Read(connectionNonce) + + aeadPayloadLengthSerializedByte := make([]byte, 2) + binary.BigEndian.PutUint16(aeadPayloadLengthSerializedByte, uint16(len(data))) + + var payloadHeaderLengthAEADEncrypted []byte + + { + payloadHeaderLengthAEADKey := kdf(key[:], kdfSaltConstVMessHeaderPayloadLengthAEADKey, string(generatedAuthID[:]), string(connectionNonce))[:16] + payloadHeaderLengthAEADNonce := kdf(key[:], kdfSaltConstVMessHeaderPayloadLengthAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] + payloadHeaderLengthAEADAESBlock, _ := aes.NewCipher(payloadHeaderLengthAEADKey) + payloadHeaderAEAD, _ := cipher.NewGCM(payloadHeaderLengthAEADAESBlock) + payloadHeaderLengthAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderLengthAEADNonce, aeadPayloadLengthSerializedByte, generatedAuthID[:]) + } + + var payloadHeaderAEADEncrypted []byte + + { + payloadHeaderAEADKey := kdf(key[:], kdfSaltConstVMessHeaderPayloadAEADKey, string(generatedAuthID[:]), string(connectionNonce))[:16] + payloadHeaderAEADNonce := kdf(key[:], kdfSaltConstVMessHeaderPayloadAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] + payloadHeaderAEADAESBlock, _ := aes.NewCipher(payloadHeaderAEADKey) + payloadHeaderAEAD, _ := cipher.NewGCM(payloadHeaderAEADAESBlock) + payloadHeaderAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderAEADNonce, data, generatedAuthID[:]) + } + + outputBuffer := protobytes.BytesWriter{} + + outputBuffer.PutSlice(generatedAuthID[:]) + outputBuffer.PutSlice(payloadHeaderLengthAEADEncrypted) + outputBuffer.PutSlice(connectionNonce) + outputBuffer.PutSlice(payloadHeaderAEADEncrypted) + + return outputBuffer.Bytes() +} diff --git a/transport/vmess/http.go b/transport/vmess/http.go new file mode 100644 index 0000000..2b53d26 --- /dev/null +++ b/transport/vmess/http.go @@ -0,0 +1,84 @@ +package vmess + +import ( + "bufio" + "bytes" + "fmt" + "math/rand" + "net" + "net/http" + "net/textproto" + + "github.com/Dreamacro/clash/common/util" +) + +type httpConn struct { + net.Conn + cfg *HTTPConfig + reader *bufio.Reader + whandshake bool +} + +type HTTPConfig struct { + Method string + Host string + Path []string + Headers map[string][]string +} + +// Read implements net.Conn.Read() +func (hc *httpConn) Read(b []byte) (int, error) { + if hc.reader != nil { + n, err := hc.reader.Read(b) + return n, err + } + + reader := textproto.NewConn(hc.Conn) + // First line: GET /index.html HTTP/1.0 + if _, err := reader.ReadLine(); err != nil { + return 0, err + } + + if _, err := reader.ReadMIMEHeader(); err != nil { + return 0, err + } + + hc.reader = reader.R + return reader.R.Read(b) +} + +// Write implements io.Writer. +func (hc *httpConn) Write(b []byte) (int, error) { + if hc.whandshake { + return hc.Conn.Write(b) + } + + path := hc.cfg.Path[rand.Intn(len(hc.cfg.Path))] + host := hc.cfg.Host + if header := hc.cfg.Headers["Host"]; len(header) != 0 { + host = header[rand.Intn(len(header))] + } + + u := fmt.Sprintf("http://%s%s", host, path) + req, _ := http.NewRequest(util.EmptyOr(hc.cfg.Method, http.MethodGet), u, bytes.NewBuffer(b)) + for key, list := range hc.cfg.Headers { + req.Header.Set(key, list[rand.Intn(len(list))]) + } + req.ContentLength = int64(len(b)) + if err := req.Write(hc.Conn); err != nil { + return 0, err + } + hc.whandshake = true + return len(b), nil +} + +func (hc *httpConn) Close() error { + return hc.Conn.Close() +} + +func StreamHTTPConn(conn net.Conn, cfg *HTTPConfig) net.Conn { + return &httpConn{ + Conn: conn, + cfg: cfg, + } +} diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go new file mode 100644 index 0000000..e4f29a2 --- /dev/null +++ b/transport/vmess/tls.go @@ -0,0 +1,31 @@ +package vmess + +import ( + "context" + "crypto/tls" + "net" + + C "github.com/Dreamacro/clash/constant" +) + +type TLSConfig struct { + Host string + SkipCertVerify bool + NextProtos []string +} + +func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { + tlsConfig := &tls.Config{ + ServerName: cfg.Host, + InsecureSkipVerify: cfg.SkipCertVerify, + NextProtos: cfg.NextProtos, + } + + tlsConn := tls.Client(conn, tlsConfig) + + // fix tls handshake not timeout + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + err := tlsConn.HandshakeContext(ctx) + return tlsConn, err +} diff --git a/transport/vmess/user.go b/transport/vmess/user.go new file mode 100644 index 0000000..091df0a --- /dev/null +++ b/transport/vmess/user.go @@ -0,0 +1,55 @@ +package vmess + +import ( + "bytes" + "crypto/md5" + + "github.com/gofrs/uuid/v5" +) + +// ID cmdKey length +const ( + IDBytesLen = 16 +) + +// The ID of en entity, in the form of a UUID. +type ID struct { + UUID *uuid.UUID + CmdKey []byte +} + +// newID returns an ID with given UUID. +func newID(uuid *uuid.UUID) *ID { + id := &ID{UUID: uuid, CmdKey: make([]byte, IDBytesLen)} + md5hash := md5.New() + md5hash.Write(uuid.Bytes()) + md5hash.Write([]byte("c48619fe-8f02-49e0-b9e9-edf763e17e21")) + md5hash.Sum(id.CmdKey[:0]) + return id +} + +func nextID(u *uuid.UUID) *uuid.UUID { + md5hash := md5.New() + md5hash.Write(u.Bytes()) + md5hash.Write([]byte("16167dc8-16b6-4e6d-b8bb-65dd68113a81")) + var newid uuid.UUID + for { + md5hash.Sum(newid[:0]) + if !bytes.Equal(newid.Bytes(), u.Bytes()) { + return &newid + } + md5hash.Write([]byte("533eff8a-4113-4b10-b5ce-0f5d76b98cd2")) + } +} + +func newAlterIDs(primary *ID, alterIDCount uint16) []*ID { + alterIDs := make([]*ID, alterIDCount) + prevID := primary.UUID + for idx := range alterIDs { + newid := nextID(prevID) + alterIDs[idx] = &ID{UUID: newid, CmdKey: primary.CmdKey[:]} + prevID = newid + } + alterIDs = append(alterIDs, primary) + return alterIDs +} diff --git a/transport/vmess/vmess.go b/transport/vmess/vmess.go new file mode 100644 index 0000000..331a0a8 --- /dev/null +++ b/transport/vmess/vmess.go @@ -0,0 +1,109 @@ +package vmess + +import ( + "fmt" + "math/rand" + "net" + "runtime" + + "github.com/gofrs/uuid/v5" +) + +// Version of vmess +const Version byte = 1 + +// Request Options +const ( + OptionChunkStream byte = 1 + OptionChunkMasking byte = 4 +) + +// Security type vmess +type Security = byte + +// Cipher types +const ( + SecurityAES128GCM Security = 3 + SecurityCHACHA20POLY1305 Security = 4 + SecurityNone Security = 5 + SecurityZero Security = 6 +) + +// Command types +const ( + CommandTCP byte = 1 + CommandUDP byte = 2 +) + +// Addr types +const ( + AtypIPv4 byte = 1 + AtypDomainName byte = 2 + AtypIPv6 byte = 3 +) + +// DstAddr store destination address +type DstAddr struct { + UDP bool + AddrType byte + Addr []byte + Port uint +} + +// Client is vmess connection generator +type Client struct { + user []*ID + uuid *uuid.UUID + security Security + isAead bool +} + +// Config of vmess +type Config struct { + UUID string + AlterID uint16 + Security string + Port string + HostName string + IsAead bool +} + +// StreamConn return a Conn with net.Conn and DstAddr +func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) { + r := rand.Intn(len(c.user)) + return newConn(conn, c.user[r], dst, c.security, c.isAead) +} + +// NewClient return Client instance +func NewClient(config Config) (*Client, error) { + uid, err := uuid.FromString(config.UUID) + if err != nil { + return nil, err + } + + var security Security + switch config.Security { + case "aes-128-gcm": + security = SecurityAES128GCM + case "chacha20-poly1305": + security = SecurityCHACHA20POLY1305 + case "none": + security = SecurityNone + case "zero": + security = SecurityZero + case "auto": + security = SecurityCHACHA20POLY1305 + if runtime.GOARCH == "amd64" || runtime.GOARCH == "s390x" || runtime.GOARCH == "arm64" { + security = SecurityAES128GCM + } + default: + return nil, fmt.Errorf("unknown security type: %s", config.Security) + } + + return &Client{ + user: newAlterIDs(newID(&uid), config.AlterID), + uuid: &uid, + security: security, + isAead: config.IsAead, + }, nil +} diff --git a/transport/vmess/websocket.go b/transport/vmess/websocket.go new file mode 100644 index 0000000..b7b369f --- /dev/null +++ b/transport/vmess/websocket.go @@ -0,0 +1,319 @@ +package vmess + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +type websocketConn struct { + conn *websocket.Conn + reader io.Reader + remoteAddr net.Addr + + // https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency + rMux sync.Mutex + wMux sync.Mutex +} + +type websocketWithEarlyDataConn struct { + net.Conn + underlay net.Conn + closed bool + dialed chan bool + cancel context.CancelFunc + ctx context.Context + config *WebsocketConfig +} + +type WebsocketConfig struct { + Host string + Port string + Path string + Headers http.Header + TLS bool + TLSConfig *tls.Config + MaxEarlyData int + EarlyDataHeaderName string +} + +// Read implements net.Conn.Read() +func (wsc *websocketConn) Read(b []byte) (int, error) { + wsc.rMux.Lock() + defer wsc.rMux.Unlock() + for { + reader, err := wsc.getReader() + if err != nil { + return 0, err + } + + nBytes, err := reader.Read(b) + if err == io.EOF { + wsc.reader = nil + continue + } + return nBytes, err + } +} + +// Write implements io.Writer. +func (wsc *websocketConn) Write(b []byte) (int, error) { + wsc.wMux.Lock() + defer wsc.wMux.Unlock() + if err := wsc.conn.WriteMessage(websocket.BinaryMessage, b); err != nil { + return 0, err + } + return len(b), nil +} + +func (wsc *websocketConn) Close() error { + var errors []string + if err := wsc.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second*5)); err != nil { + errors = append(errors, err.Error()) + } + if err := wsc.conn.Close(); err != nil { + errors = append(errors, err.Error()) + } + if len(errors) > 0 { + return fmt.Errorf("failed to close connection: %s", strings.Join(errors, ",")) + } + return nil +} + +func (wsc *websocketConn) getReader() (io.Reader, error) { + if wsc.reader != nil { + return wsc.reader, nil + } + + _, reader, err := wsc.conn.NextReader() + if err != nil { + return nil, err + } + wsc.reader = reader + return reader, nil +} + +func (wsc *websocketConn) LocalAddr() net.Addr { + return wsc.conn.LocalAddr() +} + +func (wsc *websocketConn) RemoteAddr() net.Addr { + return wsc.remoteAddr +} + +func (wsc *websocketConn) SetDeadline(t time.Time) error { + if err := wsc.SetReadDeadline(t); err != nil { + return err + } + return wsc.SetWriteDeadline(t) +} + +func (wsc *websocketConn) SetReadDeadline(t time.Time) error { + return wsc.conn.SetReadDeadline(t) +} + +func (wsc *websocketConn) SetWriteDeadline(t time.Time) error { + return wsc.conn.SetWriteDeadline(t) +} + +func (wsedc *websocketWithEarlyDataConn) Dial(earlyData []byte) error { + base64DataBuf := &bytes.Buffer{} + base64EarlyDataEncoder := base64.NewEncoder(base64.RawURLEncoding, base64DataBuf) + + earlyDataBuf := bytes.NewBuffer(earlyData) + if _, err := base64EarlyDataEncoder.Write(earlyDataBuf.Next(wsedc.config.MaxEarlyData)); err != nil { + return errors.New("failed to encode early data: " + err.Error()) + } + + if errc := base64EarlyDataEncoder.Close(); errc != nil { + return errors.New("failed to encode early data tail: " + errc.Error()) + } + + var err error + if wsedc.Conn, err = streamWebsocketConn(wsedc.underlay, wsedc.config, base64DataBuf); err != nil { + wsedc.Close() + return errors.New("failed to dial WebSocket: " + err.Error()) + } + + wsedc.dialed <- true + if earlyDataBuf.Len() != 0 { + _, err = wsedc.Conn.Write(earlyDataBuf.Bytes()) + } + + return err +} + +func (wsedc *websocketWithEarlyDataConn) Write(b []byte) (int, error) { + if wsedc.closed { + return 0, io.ErrClosedPipe + } + if wsedc.Conn == nil { + if err := wsedc.Dial(b); err != nil { + return 0, err + } + return len(b), nil + } + + return wsedc.Conn.Write(b) +} + +func (wsedc *websocketWithEarlyDataConn) Read(b []byte) (int, error) { + if wsedc.closed { + return 0, io.ErrClosedPipe + } + if wsedc.Conn == nil { + select { + case <-wsedc.ctx.Done(): + return 0, io.ErrUnexpectedEOF + case <-wsedc.dialed: + } + } + return wsedc.Conn.Read(b) +} + +func (wsedc *websocketWithEarlyDataConn) Close() error { + wsedc.closed = true + wsedc.cancel() + if wsedc.Conn == nil { + return nil + } + return wsedc.Conn.Close() +} + +func (wsedc *websocketWithEarlyDataConn) LocalAddr() net.Addr { + if wsedc.Conn == nil { + return wsedc.underlay.LocalAddr() + } + return wsedc.Conn.LocalAddr() +} + +func (wsedc *websocketWithEarlyDataConn) RemoteAddr() net.Addr { + if wsedc.Conn == nil { + return wsedc.underlay.RemoteAddr() + } + return wsedc.Conn.RemoteAddr() +} + +func (wsedc *websocketWithEarlyDataConn) SetDeadline(t time.Time) error { + if err := wsedc.SetReadDeadline(t); err != nil { + return err + } + return wsedc.SetWriteDeadline(t) +} + +func (wsedc *websocketWithEarlyDataConn) SetReadDeadline(t time.Time) error { + if wsedc.Conn == nil { + return nil + } + return wsedc.Conn.SetReadDeadline(t) +} + +func (wsedc *websocketWithEarlyDataConn) SetWriteDeadline(t time.Time) error { + if wsedc.Conn == nil { + return nil + } + return wsedc.Conn.SetWriteDeadline(t) +} + +func streamWebsocketWithEarlyDataConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) { + ctx, cancel := context.WithCancel(context.Background()) + conn = &websocketWithEarlyDataConn{ + dialed: make(chan bool, 1), + cancel: cancel, + ctx: ctx, + underlay: conn, + config: c, + } + return conn, nil +} + +func streamWebsocketConn(conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buffer) (net.Conn, error) { + dialer := &websocket.Dialer{ + NetDial: func(network, addr string) (net.Conn, error) { + return conn, nil + }, + ReadBufferSize: 4 * 1024, + WriteBufferSize: 4 * 1024, + HandshakeTimeout: time.Second * 8, + } + + scheme := "ws" + if c.TLS { + scheme = "wss" + dialer.TLSClientConfig = c.TLSConfig + } + + u, err := url.Parse(c.Path) + if err != nil { + return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) + } + + uri := url.URL{ + Scheme: scheme, + Host: net.JoinHostPort(c.Host, c.Port), + Path: u.Path, + RawQuery: u.RawQuery, + } + + headers := http.Header{} + if c.Headers != nil { + for k := range c.Headers { + headers.Add(k, c.Headers.Get(k)) + } + } + + if earlyData != nil { + if c.EarlyDataHeaderName == "" { + uri.Path += earlyData.String() + } else { + headers.Set(c.EarlyDataHeaderName, earlyData.String()) + } + } + + wsConn, resp, err := dialer.Dial(uri.String(), headers) + if err != nil { + reason := err.Error() + if resp != nil { + reason = resp.Status + } + return nil, fmt.Errorf("dial %s error: %s", uri.Host, reason) + } + + return &websocketConn{ + conn: wsConn, + remoteAddr: conn.RemoteAddr(), + }, nil +} + +func StreamWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) { + if u, err := url.Parse(c.Path); err == nil { + if q := u.Query(); q.Get("ed") != "" { + if ed, err := strconv.Atoi(q.Get("ed")); err == nil { + c.MaxEarlyData = ed + c.EarlyDataHeaderName = "Sec-WebSocket-Protocol" + q.Del("ed") + u.RawQuery = q.Encode() + c.Path = u.String() + } + } + } + + if c.MaxEarlyData > 0 { + return streamWebsocketWithEarlyDataConn(conn, c) + } + + return streamWebsocketConn(conn, c, nil) +} diff --git a/tunnel/connection.go b/tunnel/connection.go new file mode 100644 index 0000000..c220908 --- /dev/null +++ b/tunnel/connection.go @@ -0,0 +1,60 @@ +package tunnel + +import ( + "errors" + "net" + "net/netip" + "time" + + N "github.com/Dreamacro/clash/common/net" + "github.com/Dreamacro/clash/common/pool" + C "github.com/Dreamacro/clash/constant" +) + +func handleUDPToRemote(packet C.UDPPacket, pc C.PacketConn, metadata *C.Metadata) error { + addr := metadata.UDPAddr() + if addr == nil { + return errors.New("udp addr invalid") + } + + if _, err := pc.WriteTo(packet.Data(), addr); err != nil { + return err + } + // reset timeout + pc.SetReadDeadline(time.Now().Add(udpTimeout)) + + return nil +} + +func handleUDPToLocal(packet C.UDPPacket, pc net.PacketConn, key string, oAddr, fAddr netip.Addr) { + buf := pool.Get(pool.UDPBufferSize) + defer pool.Put(buf) + defer natTable.Delete(key) + defer pc.Close() + + for { + pc.SetReadDeadline(time.Now().Add(udpTimeout)) + n, from, err := pc.ReadFrom(buf) + if err != nil { + return + } + + fromUDPAddr := *from.(*net.UDPAddr) + if fAddr.IsValid() { + fromAddr, _ := netip.AddrFromSlice(fromUDPAddr.IP) + fromAddr = fromAddr.Unmap() + if oAddr == fromAddr { + fromUDPAddr.IP = fAddr.AsSlice() + } + } + + _, err = packet.WriteBack(buf[:n], &fromUDPAddr) + if err != nil { + return + } + } +} + +func handleSocket(ctx C.ConnContext, outbound net.Conn) { + N.Relay(ctx.Conn(), outbound) +} diff --git a/tunnel/mode.go b/tunnel/mode.go new file mode 100644 index 0000000..a1697a3 --- /dev/null +++ b/tunnel/mode.go @@ -0,0 +1,69 @@ +package tunnel + +import ( + "encoding/json" + "errors" + "strings" +) + +type TunnelMode int + +// ModeMapping is a mapping for Mode enum +var ModeMapping = map[string]TunnelMode{ + Global.String(): Global, + Rule.String(): Rule, + Direct.String(): Direct, +} + +const ( + Global TunnelMode = iota + Rule + Direct +) + +// UnmarshalJSON unserialize Mode +func (m *TunnelMode) UnmarshalJSON(data []byte) error { + var tp string + json.Unmarshal(data, &tp) + mode, exist := ModeMapping[strings.ToLower(tp)] + if !exist { + return errors.New("invalid mode") + } + *m = mode + return nil +} + +// UnmarshalYAML unserialize Mode with yaml +func (m *TunnelMode) UnmarshalYAML(unmarshal func(any) error) error { + var tp string + unmarshal(&tp) + mode, exist := ModeMapping[strings.ToLower(tp)] + if !exist { + return errors.New("invalid mode") + } + *m = mode + return nil +} + +// MarshalJSON serialize Mode +func (m TunnelMode) MarshalJSON() ([]byte, error) { + return json.Marshal(m.String()) +} + +// MarshalYAML serialize TunnelMode with yaml +func (m TunnelMode) MarshalYAML() (any, error) { + return m.String(), nil +} + +func (m TunnelMode) String() string { + switch m { + case Global: + return "global" + case Rule: + return "rule" + case Direct: + return "direct" + default: + return "Unknown" + } +} diff --git a/tunnel/statistic/manager.go b/tunnel/statistic/manager.go new file mode 100644 index 0000000..e67d387 --- /dev/null +++ b/tunnel/statistic/manager.go @@ -0,0 +1,95 @@ +package statistic + +import ( + "sync" + "time" + + "go.uber.org/atomic" +) + +var DefaultManager *Manager + +func init() { + DefaultManager = &Manager{ + uploadTemp: atomic.NewInt64(0), + downloadTemp: atomic.NewInt64(0), + uploadBlip: atomic.NewInt64(0), + downloadBlip: atomic.NewInt64(0), + uploadTotal: atomic.NewInt64(0), + downloadTotal: atomic.NewInt64(0), + } + + go DefaultManager.handle() +} + +type Manager struct { + connections sync.Map + uploadTemp *atomic.Int64 + downloadTemp *atomic.Int64 + uploadBlip *atomic.Int64 + downloadBlip *atomic.Int64 + uploadTotal *atomic.Int64 + downloadTotal *atomic.Int64 +} + +func (m *Manager) Join(c tracker) { + m.connections.Store(c.ID(), c) +} + +func (m *Manager) Leave(c tracker) { + m.connections.Delete(c.ID()) +} + +func (m *Manager) PushUploaded(size int64) { + m.uploadTemp.Add(size) + m.uploadTotal.Add(size) +} + +func (m *Manager) PushDownloaded(size int64) { + m.downloadTemp.Add(size) + m.downloadTotal.Add(size) +} + +func (m *Manager) Now() (up int64, down int64) { + return m.uploadBlip.Load(), m.downloadBlip.Load() +} + +func (m *Manager) Snapshot() *Snapshot { + connections := []tracker{} + m.connections.Range(func(key, value any) bool { + connections = append(connections, value.(tracker)) + return true + }) + + return &Snapshot{ + UploadTotal: m.uploadTotal.Load(), + DownloadTotal: m.downloadTotal.Load(), + Connections: connections, + } +} + +func (m *Manager) ResetStatistic() { + m.uploadTemp.Store(0) + m.uploadBlip.Store(0) + m.uploadTotal.Store(0) + m.downloadTemp.Store(0) + m.downloadBlip.Store(0) + m.downloadTotal.Store(0) +} + +func (m *Manager) handle() { + ticker := time.NewTicker(time.Second) + + for range ticker.C { + m.uploadBlip.Store(m.uploadTemp.Load()) + m.uploadTemp.Store(0) + m.downloadBlip.Store(m.downloadTemp.Load()) + m.downloadTemp.Store(0) + } +} + +type Snapshot struct { + DownloadTotal int64 `json:"downloadTotal"` + UploadTotal int64 `json:"uploadTotal"` + Connections []tracker `json:"connections"` +} diff --git a/tunnel/statistic/tracker.go b/tunnel/statistic/tracker.go new file mode 100644 index 0000000..3a7ccf6 --- /dev/null +++ b/tunnel/statistic/tracker.go @@ -0,0 +1,141 @@ +package statistic + +import ( + "net" + "time" + + C "github.com/Dreamacro/clash/constant" + + "github.com/gofrs/uuid/v5" + "go.uber.org/atomic" +) + +type tracker interface { + ID() string + Close() error +} + +type trackerInfo struct { + UUID uuid.UUID `json:"id"` + Metadata *C.Metadata `json:"metadata"` + UploadTotal *atomic.Int64 `json:"upload"` + DownloadTotal *atomic.Int64 `json:"download"` + Start time.Time `json:"start"` + Chain C.Chain `json:"chains"` + Rule string `json:"rule"` + RulePayload string `json:"rulePayload"` +} + +type tcpTracker struct { + C.Conn `json:"-"` + *trackerInfo + manager *Manager +} + +func (tt *tcpTracker) ID() string { + return tt.UUID.String() +} + +func (tt *tcpTracker) Read(b []byte) (int, error) { + n, err := tt.Conn.Read(b) + download := int64(n) + tt.manager.PushDownloaded(download) + tt.DownloadTotal.Add(download) + return n, err +} + +func (tt *tcpTracker) Write(b []byte) (int, error) { + n, err := tt.Conn.Write(b) + upload := int64(n) + tt.manager.PushUploaded(upload) + tt.UploadTotal.Add(upload) + return n, err +} + +func (tt *tcpTracker) Close() error { + tt.manager.Leave(tt) + return tt.Conn.Close() +} + +func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule) *tcpTracker { + uuid, _ := uuid.NewV4() + + t := &tcpTracker{ + Conn: conn, + manager: manager, + trackerInfo: &trackerInfo{ + UUID: uuid, + Start: time.Now(), + Metadata: metadata, + Chain: conn.Chains(), + Rule: "", + UploadTotal: atomic.NewInt64(0), + DownloadTotal: atomic.NewInt64(0), + }, + } + + if rule != nil { + t.trackerInfo.Rule = rule.RuleType().String() + t.trackerInfo.RulePayload = rule.Payload() + } + + manager.Join(t) + return t +} + +type udpTracker struct { + C.PacketConn `json:"-"` + *trackerInfo + manager *Manager +} + +func (ut *udpTracker) ID() string { + return ut.UUID.String() +} + +func (ut *udpTracker) ReadFrom(b []byte) (int, net.Addr, error) { + n, addr, err := ut.PacketConn.ReadFrom(b) + download := int64(n) + ut.manager.PushDownloaded(download) + ut.DownloadTotal.Add(download) + return n, addr, err +} + +func (ut *udpTracker) WriteTo(b []byte, addr net.Addr) (int, error) { + n, err := ut.PacketConn.WriteTo(b, addr) + upload := int64(n) + ut.manager.PushUploaded(upload) + ut.UploadTotal.Add(upload) + return n, err +} + +func (ut *udpTracker) Close() error { + ut.manager.Leave(ut) + return ut.PacketConn.Close() +} + +func NewUDPTracker(conn C.PacketConn, manager *Manager, metadata *C.Metadata, rule C.Rule) *udpTracker { + uuid, _ := uuid.NewV4() + + ut := &udpTracker{ + PacketConn: conn, + manager: manager, + trackerInfo: &trackerInfo{ + UUID: uuid, + Start: time.Now(), + Metadata: metadata, + Chain: conn.Chains(), + Rule: "", + UploadTotal: atomic.NewInt64(0), + DownloadTotal: atomic.NewInt64(0), + }, + } + + if rule != nil { + ut.trackerInfo.Rule = rule.RuleType().String() + ut.trackerInfo.RulePayload = rule.Payload() + } + + manager.Join(ut) + return ut +} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go new file mode 100644 index 0000000..c6ed863 --- /dev/null +++ b/tunnel/tunnel.go @@ -0,0 +1,433 @@ +package tunnel + +import ( + "context" + "fmt" + "net" + "net/netip" + "runtime" + "sync" + "time" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/component/nat" + P "github.com/Dreamacro/clash/component/process" + "github.com/Dreamacro/clash/component/resolver" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/constant/provider" + icontext "github.com/Dreamacro/clash/context" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/tunnel/statistic" + + "go.uber.org/atomic" +) + +var ( + tcpQueue = make(chan C.ConnContext, 200) + udpQueue = make(chan *inbound.PacketAdapter, 200) + natTable = nat.New() + rules []C.Rule + proxies = make(map[string]C.Proxy) + providers map[string]provider.ProxyProvider + configMux sync.RWMutex + + // Outbound Rule + mode = Rule + + // default timeout for UDP session + udpTimeout = 60 * time.Second + + // experimental feature + UDPFallbackMatch = atomic.NewBool(false) +) + +func init() { + go process() +} + +// TCPIn return fan-in queue +func TCPIn() chan<- C.ConnContext { + return tcpQueue +} + +// UDPIn return fan-in udp queue +func UDPIn() chan<- *inbound.PacketAdapter { + return udpQueue +} + +// Rules return all rules +func Rules() []C.Rule { + return rules +} + +// UpdateRules handle update rules +func UpdateRules(newRules []C.Rule) { + configMux.Lock() + rules = newRules + configMux.Unlock() +} + +// Proxies return all proxies +func Proxies() map[string]C.Proxy { + return proxies +} + +// Providers return all compatible providers +func Providers() map[string]provider.ProxyProvider { + return providers +} + +// UpdateProxies handle update proxies +func UpdateProxies(newProxies map[string]C.Proxy, newProviders map[string]provider.ProxyProvider) { + configMux.Lock() + proxies = newProxies + providers = newProviders + configMux.Unlock() +} + +// Mode return current mode +func Mode() TunnelMode { + return mode +} + +// SetMode change the mode of tunnel +func SetMode(m TunnelMode) { + mode = m +} + +// processUDP starts a loop to handle udp packet +func processUDP() { + queue := udpQueue + for conn := range queue { + handleUDPConn(conn) + } +} + +func process() { + numUDPWorkers := 4 + if num := runtime.GOMAXPROCS(0); num > numUDPWorkers { + numUDPWorkers = num + } + for i := 0; i < numUDPWorkers; i++ { + go processUDP() + } + + queue := tcpQueue + for conn := range queue { + go handleTCPConn(conn) + } +} + +func needLookupIP(metadata *C.Metadata) bool { + return resolver.MappingEnabled() && metadata.Host == "" && metadata.DstIP != nil +} + +func preHandleMetadata(metadata *C.Metadata) error { + // handle IP string on host + if ip := net.ParseIP(metadata.Host); ip != nil { + metadata.DstIP = ip + metadata.Host = "" + } + + // preprocess enhanced-mode metadata + if needLookupIP(metadata) { + host, exist := resolver.FindHostByIP(metadata.DstIP) + if exist { + metadata.Host = host + metadata.DNSMode = C.DNSMapping + if resolver.FakeIPEnabled() { + metadata.DstIP = nil + metadata.DNSMode = C.DNSFakeIP + } else if node := resolver.DefaultHosts.Search(host); node != nil { + // redir-host should lookup the hosts + metadata.DstIP = node.Data.(net.IP) + } + } else if resolver.IsFakeIP(metadata.DstIP) { + return fmt.Errorf("fake DNS record %s missing", metadata.DstIP) + } + } + + return nil +} + +func resolveMetadata(ctx C.PlainContext, metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { + if metadata.SpecialProxy != "" { + var exist bool + proxy, exist = proxies[metadata.SpecialProxy] + if !exist { + err = fmt.Errorf("proxy %s not found", metadata.SpecialProxy) + } + return + } + + switch mode { + case Direct: + proxy = proxies["DIRECT"] + case Global: + proxy = proxies["GLOBAL"] + case Rule: + proxy, rule, err = match(metadata) + default: + panic(fmt.Sprintf("unknown mode: %s", mode)) + } + + return +} + +func handleUDPConn(packet *inbound.PacketAdapter) { + metadata := packet.Metadata() + if !metadata.Valid() { + packet.Drop() + log.Warnln("[Metadata] not valid: %#v", metadata) + return + } + + // make a fAddr if request ip is fakeip + var fAddr netip.Addr + if resolver.IsExistFakeIP(metadata.DstIP) { + fAddr, _ = netip.AddrFromSlice(metadata.DstIP) + fAddr = fAddr.Unmap() + } + + if err := preHandleMetadata(metadata); err != nil { + packet.Drop() + log.Debugln("[Metadata PreHandle] error: %s", err) + return + } + + // local resolve UDP dns + if !metadata.Resolved() { + ips, err := resolver.LookupIP(context.Background(), metadata.Host) + if err != nil { + packet.Drop() + return + } else if len(ips) == 0 { + packet.Drop() + return + } + metadata.DstIP = ips[0] + } + + key := packet.LocalAddr().String() + + handle := func() bool { + pc := natTable.Get(key) + if pc != nil { + handleUDPToRemote(packet, pc, metadata) + return true + } + return false + } + + if handle() { + packet.Drop() + return + } + + lockKey := key + "-lock" + cond, loaded := natTable.GetOrCreateLock(lockKey) + + go func() { + defer packet.Drop() + + if loaded { + cond.L.Lock() + cond.Wait() + handle() + cond.L.Unlock() + return + } + + defer func() { + natTable.Delete(lockKey) + cond.Broadcast() + }() + + pCtx := icontext.NewPacketConnContext(metadata) + proxy, rule, err := resolveMetadata(pCtx, metadata) + if err != nil { + log.Warnln("[UDP] Parse metadata failed: %s", err.Error()) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout) + defer cancel() + rawPc, err := proxy.ListenPacketContext(ctx, metadata.Pure()) + if err != nil { + if rule == nil { + log.Warnln( + "[UDP] dial %s %s --> %s error: %s", + proxy.Name(), + metadata.SourceAddress(), + metadata.RemoteAddress(), + err.Error(), + ) + } else { + log.Warnln("[UDP] dial %s (match %s/%s) %s --> %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.SourceAddress(), metadata.RemoteAddress(), err.Error()) + } + return + } + pCtx.InjectPacketConn(rawPc) + pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule) + + switch true { + case metadata.SpecialProxy != "": + log.Infoln("[UDP] %s --> %s using %s", metadata.SourceAddress(), metadata.RemoteAddress(), metadata.SpecialProxy) + case rule != nil: + log.Infoln( + "[UDP] %s --> %s match %s(%s) using %s", + metadata.SourceAddress(), + metadata.RemoteAddress(), + rule.RuleType().String(), + rule.Payload(), + rawPc.Chains().String(), + ) + case mode == Global: + log.Infoln("[UDP] %s --> %s using GLOBAL", metadata.SourceAddress(), metadata.RemoteAddress()) + case mode == Direct: + log.Infoln("[UDP] %s --> %s using DIRECT", metadata.SourceAddress(), metadata.RemoteAddress()) + default: + log.Infoln( + "[UDP] %s --> %s doesn't match any rule using DIRECT", + metadata.SourceAddress(), + metadata.RemoteAddress(), + ) + } + + oAddr, _ := netip.AddrFromSlice(metadata.DstIP) + oAddr = oAddr.Unmap() + go handleUDPToLocal(packet.UDPPacket, pc, key, oAddr, fAddr) + + natTable.Set(key, pc) + handle() + }() +} + +func handleTCPConn(connCtx C.ConnContext) { + defer connCtx.Conn().Close() + + metadata := connCtx.Metadata() + if !metadata.Valid() { + log.Warnln("[Metadata] not valid: %#v", metadata) + return + } + + if err := preHandleMetadata(metadata); err != nil { + log.Debugln("[Metadata PreHandle] error: %s", err) + return + } + + proxy, rule, err := resolveMetadata(connCtx, metadata) + if err != nil { + log.Warnln("[Metadata] parse failed: %s", err.Error()) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) + defer cancel() + remoteConn, err := proxy.DialContext(ctx, metadata.Pure()) + if err != nil { + if rule == nil { + log.Warnln( + "[TCP] dial %s %s --> %s error: %s", + proxy.Name(), + metadata.SourceAddress(), + metadata.RemoteAddress(), + err.Error(), + ) + } else { + log.Warnln("[TCP] dial %s (match %s/%s) %s --> %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.SourceAddress(), metadata.RemoteAddress(), err.Error()) + } + return + } + remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule) + defer remoteConn.Close() + + switch true { + case metadata.SpecialProxy != "": + log.Infoln("[TCP] %s --> %s using %s", metadata.SourceAddress(), metadata.RemoteAddress(), metadata.SpecialProxy) + case rule != nil: + log.Infoln( + "[TCP] %s --> %s match %s(%s) using %s", + metadata.SourceAddress(), + metadata.RemoteAddress(), + rule.RuleType().String(), + rule.Payload(), + remoteConn.Chains().String(), + ) + case mode == Global: + log.Infoln("[TCP] %s --> %s using GLOBAL", metadata.SourceAddress(), metadata.RemoteAddress()) + case mode == Direct: + log.Infoln("[TCP] %s --> %s using DIRECT", metadata.SourceAddress(), metadata.RemoteAddress()) + default: + log.Infoln( + "[TCP] %s --> %s doesn't match any rule using DIRECT", + metadata.SourceAddress(), + metadata.RemoteAddress(), + ) + } + + handleSocket(connCtx, remoteConn) +} + +func shouldResolveIP(rule C.Rule, metadata *C.Metadata) bool { + return rule.ShouldResolveIP() && metadata.Host != "" && metadata.DstIP == nil +} + +func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { + configMux.RLock() + defer configMux.RUnlock() + + var resolved bool + var processFound bool + + if node := resolver.DefaultHosts.Search(metadata.Host); node != nil { + ip := node.Data.(net.IP) + metadata.DstIP = ip + resolved = true + } + + for _, rule := range rules { + if !resolved && shouldResolveIP(rule, metadata) { + ip, err := resolver.ResolveIP(metadata.Host) + if err != nil { + log.Debugln("[DNS] resolve %s error: %s", metadata.Host, err.Error()) + } else { + log.Debugln("[DNS] %s --> %s", metadata.Host, ip.String()) + metadata.DstIP = ip + } + resolved = true + } + + if !processFound && rule.ShouldFindProcess() { + processFound = true + + srcIP, ok := netip.AddrFromSlice(metadata.SrcIP) + if ok && metadata.OriginDst.IsValid() { + srcIP = srcIP.Unmap() + path, err := P.FindProcessPath(metadata.NetWork.String(), netip.AddrPortFrom(srcIP, uint16(metadata.SrcPort)), metadata.OriginDst) + if err != nil { + log.Debugln("[Process] find process %s: %v", metadata.String(), err) + } else { + log.Debugln("[Process] %s from process %s", metadata.String(), path) + metadata.ProcessPath = path + } + } + } + + if rule.Match(metadata) { + adapter, ok := proxies[rule.Adapter()] + if !ok { + continue + } + + if metadata.NetWork == C.UDP && !adapter.SupportUDP() && UDPFallbackMatch.Load() { + log.Debugln("[Matcher] %s UDP is not supported, skip match", adapter.Name()) + continue + } + return adapter, rule, nil + } + } + + return proxies["DIRECT"], nil, nil +}