diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d68bd2529c..63429cd94b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ -name: Bug report -description: Create a report to help us improve +name: Bug Report +description: "Report Mihomo bug" title: "[Bug] " labels: ["bug"] body: @@ -7,77 +7,74 @@ body: id: ensure attributes: label: Verify steps - description: " -在提交之前,请确认 -Please verify that you've followed these steps -" + description: Before submitting, please check all the options below to confirm that you have read and understood the following requirements; otherwise, this issue will be closed. options: - - label: " -确保你使用的是**本仓库**最新的的 mihomo 或 mihomo Alpha 版本 -Ensure you are using the latest version of Mihomo or Mihomo Alpha from **this repository**. -" - required: true - - label: " -如果你可以自己 debug 并解决的话,提交 PR 吧 -Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome. -" + - label: I have read the [documentation](https://wiki.metacubex.one/) and understand the meaning of all the configuration items I have written, rather than just piling up seemingly useful options or default values. + required: false + - label: I have carefully reviewed the [documentation](https://wiki.metacubex.one/) and have not resolved the issue. + required: false + - label: I have searched the [Issue Tracker](……/) for the issue I want to raise and did not find it. required: false - - label: " -我已经在 [Issue Tracker](……/) 中找过我要提出的问题 -I have searched on the [issue tracker](……/) for a related issue. -" + - label: I am a non-Chinese user. + required: false + - label: I have tested with the latest Alpha branch version, and the issue still persists. required: true - - label: " -我已经使用 Alpha 分支版本测试过,问题依旧存在 -I have tested using the dev branch, and the issue still exists. -" + - label: I have provided the server and client configuration files and processes that can reproduce the issue locally, rather than a sanitized complex client configuration file. required: true - - label: " -我已经仔细看过 [Documentation](https://wiki.metacubex.one/) 并无法自行解决问题 -I have read the [documentation](https://wiki.metacubex.one/) and was unable to solve the issue. -" + - label: I provided the simplest configuration that can be used to reproduce the errors in my report, rather than relying on remote servers or piling on a lot of unnecessary configurations for reproduction. required: true - - label: " -这是 Mihomo 核心的问题,并非我所使用的 Mihomo 衍生版本(如 OpenMihomo、KoolMihomo 等)的特定问题 -This is an issue of the Mihomo core *per se*, not to the derivatives of Mihomo, like OpenMihomo or KoolMihomo. -" + - label: I have provided complete logs, rather than just the parts I think are useful out of confidence in my own intelligence. required: true - - type: input - attributes: - label: Mihomo version - description: "use `mihomo -v`" - validations: - required: true + - label: I have directly reproduced the error using the Mihomo command-line program, rather than using other tools or scripts. + required: true + - type: dropdown - id: os attributes: - label: What OS are you seeing the problem on? + label: Operating System + description: "Please provide the type of operating system." multiple: true options: - - macOS + - MacOS - Windows - Linux - OpenBSD/FreeBSD + - Android + - type: input + attributes: + label: System Version + description: "Please provide the version of the operating system where the issue occurred." + validations: + required: true - type: textarea attributes: - render: yaml - label: "Mihomo config" - description: " -在下方附上 Mihomo core 配置文件,请确保配置文件中没有敏感信息(比如:服务器地址,密码,端口等) -Paste the Mihomo core configuration file below, please make sure that there is no sensitive information in the configuration file (e.g., server address/url, password, port) -" + label: Mihomo Version + description: "Provide the output of the `mihomo -v` command." validations: required: true - type: textarea attributes: - render: shell - label: Mihomo log - description: " -在下方附上 Mihomo Core 的日志,log level 使用 DEBUG -Paste the Mihomo core log below with the log level set to `DEBUG`. -" + render: yaml + label: Configuration File + description: |- + Please attach the Mihomo configuration file below. + Make sure there is no sensitive information in the configuration file (such as server addresses, passwords, ports, etc.) + Also, ensure that the configuration file can reproduce the error using the Mihomo command-line program locally (if it's a proxy protocol issue, make sure the local server can be used for reproduction). + validations: + required: true - type: textarea attributes: label: Description + description: "Please provide a detailed description of the error." + validations: + required: true + - type: textarea + attributes: + label: Reproduction Steps + description: "Please provide the steps to reproduce the error." validations: - required: true \ No newline at end of file + required: true + - type: textarea + attributes: + label: Logs + description: "Attach the running logs of Mihomo Core below, with `log-level` set to `DEBUG`." + render: shell \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report_zh.yml b/.github/ISSUE_TEMPLATE/bug_report_zh.yml new file mode 100644 index 0000000000..3538189e77 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_zh.yml @@ -0,0 +1,80 @@ +name: 错误反馈 +description: "提交 Mihomo 漏洞" +title: "[Bug] " +labels: ["bug"] +body: + - type: checkboxes + id: ensure + attributes: + label: 验证步骤 + description: 在提交之前,请勾选以下选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。 + options: + - label: 我已经阅读了 [文档](https://wiki.metacubex.one/),了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。 + required: false + - label: 我仔细看过 [文档](https://wiki.metacubex.one/) 并未解决问题 + required: false + - label: 我已在 [Issue Tracker](……/) 中寻找过我要提出的问题,并且没有找到 + required: false + - label: 我是中文用户,而非其他语言用户 + required: false + - label: 我已经使用最新的 Alpha 分支版本测试过,问题依旧存在 + required: true + - label: 我提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。 + required: true + - label: 我提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器或者堆砌大量对于复现无用的配置等。 + required: true + - label: 我提供了完整的日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。 + required: true + - label: 我直接使用 Mihomo 命令行程序重现了错误,而不是使用其他工具或脚本。 + required: true + + - type: dropdown + attributes: + label: 操作系统 + description: 请提供操作系统类型 + multiple: true + options: + - MacOS + - Windows + - Linux + - OpenBSD/FreeBSD + - Android + - type: input + attributes: + label: 系统版本 + description: 请提供出现问题的操作系统版本 + validations: + required: true + - type: textarea + attributes: + label: Mihomo 版本 + description: 提供 `mihomo -v` 命令的输出 + validations: + required: true + - type: textarea + attributes: + render: yaml + label: 配置文件 + description: |- + 在下方附上 Mihomo 配置文件 + 请确保配置文件中没有敏感信息(比如:服务器地址,密码,端口等) + 以及确保可以在本地(如果是代理协议问题,请确保本地服务器可用于复现)使用 Mihomo 原始命令行程序重现错误的配置文件 + validations: + required: true + - type: textarea + attributes: + label: 描述 + description: 请提供错误的详细描述。 + validations: + required: true + - type: textarea + attributes: + label: 重现方式 + description: 请提供重现错误的步骤 + validations: + required: true + - type: textarea + attributes: + label: 日志 + description: 在下方附上 Mihomo Core 的运行日志,`log-level` 使用 `DEBUG` + render: shell \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7987526c46..77dc6cddd3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,37 +1,23 @@ -name: Feature request -description: Suggest an idea for this project +name: Feature Request +description: Suggest improvements for this project title: "[Feature] " labels: ["enhancement"] body: - type: checkboxes id: ensure attributes: - label: Verify steps - description: " -在提交之前,请确认 -Please verify that you've followed these steps -" + label: Verification Steps + description: Before submitting, please check the following options to confirm that you have read and understood the requirements below; otherwise, this issue will be closed. options: - - label: " -我已经在 [Issue Tracker](……/) 中找过我要提出的请求 -I have searched on the [issue tracker](……/) for a related feature request. -" - required: true - - label: " -我已经仔细看过 [Documentation](https://wiki.metacubex.one/) 并无法找到这个功能 -I have read the [documentation](https://wiki.metacubex.one/) and was unable to solve the issue. -" - required: true + - label: I have read the [documentation](https://wiki.metacubex.one/) and confirmed that this feature is not implemented + required: false + - label: I have searched for the feature request I want to propose in the [Issue Tracker](……/) and did not find it + required: false + - label: I am a non-Chinese user. + required: false - type: textarea attributes: label: Description - description: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Mihomo Core 的行为是什麽? + description: Please provide a detailed description of the feature, rather than vague statements. validations: - required: true - - type: textarea - attributes: - label: Possible Solution - description: " -此项非必须,但是如果你有想法的话欢迎提出。 -Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change -" \ No newline at end of file + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request_zh.yml b/.github/ISSUE_TEMPLATE/feature_request_zh.yml new file mode 100644 index 0000000000..ab7eccf4f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_zh.yml @@ -0,0 +1,23 @@ +name: 功能请求 +description: 为该项目提出建议 +title: "[Feature] " +labels: ["enhancement"] +body: + - type: checkboxes + id: ensure + attributes: + label: 验证步骤 + description: 在提交之前,请勾选以下选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。 + options: + - label: 我已经阅读了 [文档](https://wiki.metacubex.one/),确认了该功能没有实现 + required: false + - label: 我已在 [Issue Tracker](……/) 中寻找过我要提出的功能请求,并且没有找到 + required: false + - label: 我是中文用户,而非其他语言用户 + required: false + - type: textarea + attributes: + label: 描述 + description: 请提供对于该功能的详细描述,而不是莫名其妙的话术。 + validations: + required: true \ No newline at end of file diff --git a/.github/genReleaseNote.sh b/.github/genReleaseNote.sh deleted file mode 100755 index ab617fd0c0..0000000000 --- a/.github/genReleaseNote.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -while getopts "v:" opt; do - case $opt in - v) - version_range=$OPTARG - ;; - \?) - echo "Invalid option: -$OPTARG" >&2 - exit 1 - ;; - esac -done - -if [ -z "$version_range" ]; then - echo "Please provide the version range using -v option. Example: ./genReleashNote.sh -v v1.14.1...v1.14.2" - exit 1 -fi - -echo "## What's Changed" > release.md -git log --pretty=format:"* %h %s by @%an" --grep="^feat" -i $version_range | sort -f | uniq >> release.md -echo "" >> release.md - -echo "## BUG & Fix" >> release.md -git log --pretty=format:"* %h %s by @%an" --grep="^fix" -i $version_range | sort -f | uniq >> release.md -echo "" >> release.md - -echo "## Maintenance" >> release.md -git log --pretty=format:"* %h %s by @%an" --grep="^chore\|^docs\|^refactor" -i $version_range | sort -f | uniq >> release.md -echo "" >> release.md - -echo "**Full Changelog**: https://github.com/MetaCubeX/mihomo/compare/$version_range" >> release.md diff --git a/.github/release.sh b/.github/release.sh deleted file mode 100644 index 4757cdbec0..0000000000 --- a/.github/release.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -FILENAMES=$(ls) -for FILENAME in $FILENAMES -do - if [[ ! ($FILENAME =~ ".exe" || $FILENAME =~ ".sh")]];then - gzip -S ".gz" $FILENAME - elif [[ $FILENAME =~ ".exe" ]];then - zip -m ${FILENAME%.*}.zip $FILENAME - else echo "skip $FILENAME" - fi -done - -FILENAMES=$(ls) -for FILENAME in $FILENAMES -do - if [[ $FILENAME =~ ".zip" ]];then - echo "rename $FILENAME" - mv $FILENAME ${FILENAME%.*}-${VERSION}.zip - elif [[ $FILENAME =~ ".gz" ]];then - echo "rename $FILENAME" - mv $FILENAME ${FILENAME%.*}-${VERSION}.gz - else - echo "skip $FILENAME" - fi -done \ No newline at end of file diff --git a/.github/release/.fpm_systemd b/.github/release/.fpm_systemd deleted file mode 100644 index 85ec6c7e14..0000000000 --- a/.github/release/.fpm_systemd +++ /dev/null @@ -1,18 +0,0 @@ --s dir ---name mihomo ---category net ---license GPL-3.0-or-later ---description "The universal proxy platform." ---url "https://wiki.metacubex.one/" ---maintainer "MetaCubeX " ---deb-field "Bug: https://github.com/MetaCubeX/mihomo/issues" ---no-deb-generate-changes ---config-files /etc/mihomo/config.yaml - -.github/release/config.yaml=/etc/mihomo/config.yaml - -.github/release/mihomo.service=/usr/lib/systemd/system/mihomo.service -.github/release/mihomo@.service=/usr/lib/systemd/system/mihomo@.service - - -LICENSE=/usr/share/licenses/mihomo/LICENSE \ No newline at end of file diff --git a/.github/release/config.yaml b/.github/release/config.yaml deleted file mode 100644 index 9864130fc3..0000000000 --- a/.github/release/config.yaml +++ /dev/null @@ -1,15 +0,0 @@ -mixed-port: 7890 - -dns: - enable: true - ipv6: true - enhanced-mode: fake-ip - fake-ip-filter: - - "*" - - "+.lan" - - "+.local" - nameserver: - - system - -rules: - - MATCH,DIRECT \ No newline at end of file diff --git a/.github/release/mihomo.service b/.github/release/mihomo.service deleted file mode 100644 index 7ecc848661..0000000000 --- a/.github/release/mihomo.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=mihomo Daemon, Another Clash Kernel. -Documentation=https://wiki.metacubex.one -After=network.target nss-lookup.target network-online.target - -[Service] -Type=simple -CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE -AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE -ExecStart=/usr/bin/mihomo -d /etc/mihomo -ExecReload=/bin/kill -HUP $MAINPID -Restart=on-failure -RestartSec=10 -LimitNOFILE=infinity - -[Install] -WantedBy=multi-user.target diff --git a/.github/release/mihomo@.service b/.github/release/mihomo@.service deleted file mode 100644 index 7ecc848661..0000000000 --- a/.github/release/mihomo@.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=mihomo Daemon, Another Clash Kernel. -Documentation=https://wiki.metacubex.one -After=network.target nss-lookup.target network-online.target - -[Service] -Type=simple -CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE -AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE -ExecStart=/usr/bin/mihomo -d /etc/mihomo -ExecReload=/bin/kill -HUP $MAINPID -Restart=on-failure -RestartSec=10 -LimitNOFILE=infinity - -[Install] -WantedBy=multi-user.target diff --git a/.github/rename-cgo.sh b/.github/rename-cgo.sh deleted file mode 100644 index 2bfdb3c6ea..0000000000 --- a/.github/rename-cgo.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -FILENAMES=$(ls) -for FILENAME in $FILENAMES -do - if [[ $FILENAME =~ "darwin-10.16-arm64" ]];then - echo "rename darwin-10.16-arm64 $FILENAME" - mv $FILENAME mihomo-darwin-arm64-cgo - elif [[ $FILENAME =~ "darwin-10.16-amd64" ]];then - echo "rename darwin-10.16-amd64 $FILENAME" - mv $FILENAME mihomo-darwin-amd64-cgo - elif [[ $FILENAME =~ "windows-4.0-386" ]];then - echo "rename windows 386 $FILENAME" - mv $FILENAME mihomo-windows-386-cgo.exe - elif [[ $FILENAME =~ "windows-4.0-amd64" ]];then - echo "rename windows amd64 $FILENAME" - mv $FILENAME mihomo-windows-amd64-cgo.exe - elif [[ $FILENAME =~ "mihomo-linux-arm-5" ]];then - echo "rename mihomo-linux-arm-5 $FILENAME" - mv $FILENAME mihomo-linux-armv5-cgo - elif [[ $FILENAME =~ "mihomo-linux-arm-6" ]];then - echo "rename mihomo-linux-arm-6 $FILENAME" - mv $FILENAME mihomo-linux-armv6-cgo - elif [[ $FILENAME =~ "mihomo-linux-arm-7" ]];then - echo "rename mihomo-linux-arm-7 $FILENAME" - mv $FILENAME mihomo-linux-armv7-cgo - elif [[ $FILENAME =~ "linux" ]];then - echo "rename linux $FILENAME" - mv $FILENAME $FILENAME-cgo - elif [[ $FILENAME =~ "android" ]];then - echo "rename android $FILENAME" - mv $FILENAME $FILENAME-cgo - else echo "skip $FILENAME" - fi -done \ No newline at end of file diff --git a/.github/rename-go120.sh b/.github/rename-go120.sh deleted file mode 100644 index eddb1769c7..0000000000 --- a/.github/rename-go120.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -FILENAMES=$(ls) -for FILENAME in $FILENAMES -do - if [[ ! ($FILENAME =~ ".exe" || $FILENAME =~ ".sh")]];then - mv $FILENAME ${FILENAME}-go120 - elif [[ $FILENAME =~ ".exe" ]];then - mv $FILENAME ${FILENAME%.*}-go120.exe - else echo "skip $FILENAME" - fi -done \ No newline at end of file diff --git a/.github/workflows/Delete.yml b/.github/workflows/Delete.yml new file mode 100644 index 0000000000..7e346b821a --- /dev/null +++ b/.github/workflows/Delete.yml @@ -0,0 +1,17 @@ +name: Delete old workflow +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' +# Run monthly, at 00:00 on the 1st day of month. + +jobs: + del_runs: + runs-on: ubuntu-latest + steps: + - name: Delete workflow runs + uses: GitRML/delete-workflow-runs@main + with: + token: ${{ secrets.AUTH_PAT }} + repository: ${{ github.repository }} + retain_days: 30 \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1caf2cebc..f77ef813c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,525 +5,31 @@ on: version: description: "Tag version to release" required: true - push: - paths-ignore: - - "docs/**" - - "README.md" - - ".github/ISSUE_TEMPLATE/**" - branches: - - Alpha - tags: - - "v*" - pull_request: - branches: - - Alpha -concurrency: - group: "${{ github.workflow }}-${{ github.ref }}" - cancel-in-progress: true - -env: - REGISTRY: docker.io -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - jobs: - - { goos: darwin, goarch: arm64, output: arm64 } - - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible } - - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 } - - - { goos: linux, goarch: '386', go386: sse2, output: '386', debian: i386, rpm: i386} - - { goos: linux, goarch: '386', go386: softfloat, output: '386-softfloat' } - - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, test: test } - - { goos: linux, goarch: amd64, goamd64: v3, output: amd64, debian: amd64, rpm: x86_64, pacman: x86_64} - - { goos: linux, goarch: arm64, output: arm64, debian: arm64, rpm: aarch64, pacman: aarch64} - - { goos: linux, goarch: arm, goarm: '5', output: armv5 } - - { goos: linux, goarch: arm, goarm: '6', output: armv6, debian: armel, rpm: armv6hl} - - { goos: linux, goarch: arm, goarm: '7', output: armv7, debian: armhf, rpm: armv7hl, pacman: armv7hl} - - { goos: linux, goarch: mips, gomips: hardfloat, output: mips-hardfloat } - - { goos: linux, goarch: mips, gomips: softfloat, output: mips-softfloat } - - { goos: linux, goarch: mipsle, gomips: hardfloat, output: mipsle-hardfloat } - - { goos: linux, goarch: mipsle, gomips: softfloat, output: mipsle-softfloat } - - { goos: linux, goarch: mips64, output: mips64 } - - { goos: linux, goarch: mips64le, output: mips64le, debian: mips64el, rpm: mips64el } - - { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1', debian: loongarch64, rpm: loongarch64 } - - { goos: linux, goarch: loong64, output: loong64-abi2, abi: '2', debian: loong64, rpm: loong64 } - - { goos: linux, goarch: riscv64, output: riscv64, debian: riscv64, rpm: riscv64 } - - { goos: linux, goarch: s390x, output: s390x, debian: s390x, rpm: s390x } - - { goos: linux, goarch: ppc64le, output: ppc64le, debian: ppc64el, rpm: ppc64le } - - - { goos: windows, goarch: '386', output: '386' } - - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible } - - { goos: windows, goarch: amd64, goamd64: v3, output: amd64 } - - { goos: windows, goarch: arm64, output: arm64 } - - - { goos: freebsd, goarch: '386', output: '386' } - - { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-compatible } - - { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64 } - - { goos: freebsd, goarch: arm64, output: arm64 } - - - { goos: android, goarch: '386', ndk: i686-linux-android34, output: '386' } - - { goos: android, goarch: amd64, ndk: x86_64-linux-android34, output: amd64 } - - { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 } - - { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 } - - # Go 1.23 with special patch can work on Windows 7 - # https://github.com/MetaCubeX/go/commits/release-branch.go1.23/ - - { goos: windows, goarch: '386', output: '386-go123', goversion: '1.23' } - - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go123, goversion: '1.23' } - - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go123, goversion: '1.23' } - - # Go 1.22 with special patch can work on Windows 7 - # https://github.com/MetaCubeX/go/commits/release-branch.go1.22/ - - { goos: windows, goarch: '386', output: '386-go122', goversion: '1.22' } - - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go122, goversion: '1.22' } - - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go122, goversion: '1.22' } - - # Go 1.21 can revert commit `9e4385` to work on Windows 7 - # https://github.com/golang/go/issues/64622#issuecomment-1847475161 - # (OR we can just use golang1.21.4 which unneeded any patch) - - { goos: windows, goarch: '386', output: '386-go121', goversion: '1.21' } - - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go121, goversion: '1.21' } - - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go121, goversion: '1.21' } - - # Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016. - - { goos: windows, goarch: '386', output: '386-go120', goversion: '1.20' } - - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' } - - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } - - # Go 1.22 is the last release that will run on macOS 10.15 Catalina. Go 1.23 will require macOS 11 Big Sur or later. - - { goos: darwin, goarch: arm64, output: arm64-go122, goversion: '1.22' } - - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go122, goversion: '1.22' } - - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go122, goversion: '1.22' } - - # Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later. - - { goos: darwin, goarch: arm64, output: arm64-go120, goversion: '1.20' } - - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' } - - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } - - # Go 1.23 is the last release that requires Linux kernel version 2.6.32 or later. Go 1.24 will require Linux kernel version 3.2 or later. - - { goos: linux, goarch: '386', output: '386-go123', goversion: '1.23' } - - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go123, goversion: '1.23', test: test } - - { goos: linux, goarch: amd64, goamd64: v3, output: amd64-go123, goversion: '1.23' } - - # only for test - - { goos: linux, goarch: '386', output: '386-go120', goversion: '1.20' } - - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20', test: test } - - { goos: linux, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } - - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - if: ${{ matrix.jobs.goversion == '' && matrix.jobs.abi != '1' }} - uses: actions/setup-go@v5 - with: - go-version: '1.24' - - - name: Set up Go - if: ${{ matrix.jobs.goversion != '' && matrix.jobs.abi != '1' }} - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.jobs.goversion }} - - - name: Set up Go1.24 loongarch abi1 - if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }} - run: | - wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.24.0/go1.24.0.linux-amd64-abi1.tar.gz - sudo tar zxf go1.24.0.linux-amd64-abi1.tar.gz -C /usr/local - echo "/usr/local/go/bin" >> $GITHUB_PATH - - # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 - # this patch file only works on golang1.24.x - # that means after golang1.25 release it must be changed - # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/ - # revert: - # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" - # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" - # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" - # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" - - name: Revert Golang1.24 commit for Windows7/8 - if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }} - run: | - cd $(go env GOROOT) - curl https://github.com/MetaCubeX/go/commit/2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/7b1fd7d39c6be0185fbe1d929578ab372ac5c632.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/979d6d8bab3823ff572ace26767fd2ce3cf351ae.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/ac3e93c061779dfefc0dd13a5b6e6f764a25621e.diff | patch --verbose -p 1 - - # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 - # this patch file only works on golang1.23.x - # that means after golang1.24 release it must be changed - # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/ - # revert: - # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" - # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" - # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" - # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" - - name: Revert Golang1.23 commit for Windows7/8 - if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.23' }} - run: | - cd $(go env GOROOT) - curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1 - - # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 - # this patch file only works on golang1.22.x - # that means after golang1.23 release it must be changed - # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/ - # revert: - # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" - # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" - # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" - # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" - - name: Revert Golang1.22 commit for Windows7/8 - if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.22' }} - run: | - cd $(go env GOROOT) - curl https://github.com/MetaCubeX/go/commit/9779155f18b6556a034f7bb79fb7fb2aad1e26a9.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/ef0606261340e608017860b423ffae5c1ce78239.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/7f83badcb925a7e743188041cb6e561fc9b5b642.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/83ff9782e024cb328b690cbf0da4e7848a327f4f.diff | patch --verbose -p 1 - - # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 - - name: Revert Golang1.21 commit for Windows7/8 - if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.21' }} - run: | - cd $(go env GOROOT) - curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1 - - - name: Set variables - run: | - VERSION="${GITHUB_REF_NAME,,}-$(git rev-parse --short HEAD)" - PackageVersion="$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | jq -r '.tag_name' | sed 's/v//g' | awk -F '.' '{$NF = $NF + 1; print}' OFS='.').${VERSION/-/.}" - if [ -n "${{ github.event.inputs.version }}" ]; then - VERSION=${{ github.event.inputs.version }} - PackageVersion="${VERSION#v}" >> $GITHUB_ENV - fi - echo "VERSION=${VERSION}" >> $GITHUB_ENV - echo "PackageVersion=${PackageVersion}" >> $GITHUB_ENV - - echo "BUILDTIME=$(date)" >> $GITHUB_ENV - echo "CGO_ENABLED=0" >> $GITHUB_ENV - echo "BUILDTAG=-extldflags --static" >> $GITHUB_ENV - echo "GOTOOLCHAIN=local" >> $GITHUB_ENV - - - name: Setup NDK - if: ${{ matrix.jobs.goos == 'android' }} - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: r29-beta1 - - - name: Set NDK path - if: ${{ matrix.jobs.goos == 'android' }} - run: | - echo "CC=${{steps.setup-ndk.outputs.ndk-path}}/toolchains/llvm/prebuilt/linux-x86_64/bin/${{matrix.jobs.ndk}}-clang" >> $GITHUB_ENV - echo "CGO_ENABLED=1" >> $GITHUB_ENV - echo "BUILDTAG=" >> $GITHUB_ENV - - - name: Test - if: ${{ matrix.jobs.test == 'test' }} - run: | - go test ./... - echo "---test with_gvisor---" - go test ./... -tags "with_gvisor" -count=1 - - - name: Update CA - run: | - sudo apt-get update && sudo apt-get install ca-certificates - sudo update-ca-certificates - cp -f /etc/ssl/certs/ca-certificates.crt component/ca/ca-certificates.crt - - - name: Build core - env: - GOOS: ${{matrix.jobs.goos}} - GOARCH: ${{matrix.jobs.goarch}} - GOAMD64: ${{matrix.jobs.goamd64}} - GO386: ${{matrix.jobs.go386}} - GOARM: ${{matrix.jobs.goarm}} - GOMIPS: ${{matrix.jobs.gomips}} - run: | - go env - go build -v -tags "with_gvisor" -trimpath -ldflags "${BUILDTAG} -X 'github.com/metacubex/mihomo/constant.Version=${VERSION}' -X 'github.com/metacubex/mihomo/constant.BuildTime=${BUILDTIME}' -w -s -buildid=" - if [ "${{matrix.jobs.goos}}" = "windows" ]; then - cp mihomo.exe mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}.exe - zip -r mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.zip mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}.exe - else - cp mihomo mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} - gzip -c mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.gz - rm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} - fi - - - name: Package DEB - if: matrix.jobs.debian != '' - run: | - set -xeuo pipefail - sudo gem install fpm - cp .github/release/.fpm_systemd .fpm - - fpm -t deb \ - -v "${PackageVersion}" \ - -p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb" \ - --architecture ${{ matrix.jobs.debian }} \ - mihomo=/usr/bin/mihomo - - name: Package RPM - if: matrix.jobs.rpm != '' - run: | - set -xeuo pipefail - sudo gem install fpm - cp .github/release/.fpm_systemd .fpm +permissions: + contents: write + packages: write - fpm -t rpm \ - -v "${PackageVersion}" \ - -p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.rpm" \ - --architecture ${{ matrix.jobs.rpm }} \ - mihomo=/usr/bin/mihomo - - - name: Package Pacman - if: matrix.jobs.pacman != '' - run: | - set -xeuo pipefail - sudo gem install fpm - sudo apt-get update && sudo apt-get install -y libarchive-tools - cp .github/release/.fpm_systemd .fpm - - fpm -t pacman \ - -v "${PackageVersion}" \ - -p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.pkg.tar.zst" \ - --architecture ${{ matrix.jobs.pacman }} \ - mihomo=/usr/bin/mihomo - - - name: Save version - run: | - echo ${VERSION} > version.txt - shell: bash - - - name: Archive production artifacts - uses: actions/upload-artifact@v4 - with: - name: "${{ matrix.jobs.goos }}-${{ matrix.jobs.output }}" - path: | - mihomo*.gz - mihomo*.deb - mihomo*.rpm - mihomo*.pkg.tar.zst - mihomo*.zip - version.txt - checksums.txt - - Upload-Prerelease: - permissions: write-all - if: ${{ github.event_name != 'workflow_dispatch' && github.ref_type == 'branch' && !startsWith(github.event_name, 'pull_request') }} - needs: [build] +jobs: + release_archive: runs-on: ubuntu-latest - steps: - - name: Download all workflow run artifacts - uses: actions/download-artifact@v4 - with: - path: bin/ - merge-multiple: true - - - name: Calculate checksums - run: | - cd bin/ - find . -type f -not -name "checksums.*" -not -name "version.txt" | sort | xargs sha256sum > checksums.txt - cat checksums.txt - shell: bash + if: github.repository == 'KT-Yeh/mihomo' - - name: Delete current release assets - uses: 8Mi-Tech/delete-release-assets-action@main - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - tag: Prerelease-${{ github.ref_name }} - deleteOnlyFromDrafts: false - - name: Set Env - run: | - echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV - shell: bash - - - name: Tag Repo - uses: richardsimko/update-tag@v1 - with: - tag_name: Prerelease-${{ github.ref_name }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - run: | - cat > release.txt << 'EOF' - Release created at ${{ env.BUILDTIME }} - Synchronize ${{ github.ref_name }} branch code updates, keeping only the latest version -
- [我应该下载哪个文件? / Which file should I download?](https://github.com/MetaCubeX/mihomo/wiki/FAQ) - [二进制文件筛选 / Binary file selector](https://metacubex.github.io/Meta-Docs/startup/#_1) - [查看文档 / Docs](https://metacubex.github.io/Meta-Docs/) - EOF - - - name: Upload Prerelease - uses: softprops/action-gh-release@v1 - if: ${{ success() }} - with: - tag_name: Prerelease-${{ github.ref_name }} - files: | - bin/* - prerelease: true - generate_release_notes: true - body_path: release.txt - - Upload-Release: - permissions: write-all - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} - needs: [build] - runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 - with: - ref: Meta - fetch-depth: '0' - fetch-tags: 'true' - - - name: Get tags - run: | - echo "CURRENTVERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - git fetch --tags - echo "PREVERSION=$(git describe --tags --abbrev=0 HEAD)" >> $GITHUB_ENV - - - name: Force push Alpha branch to Meta - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git fetch origin Alpha:Alpha - git push origin Alpha:Meta --force - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Tag the commit on Alpha - run: | - git checkout Alpha - git tag ${{ github.event.inputs.version }} - git push origin ${{ github.event.inputs.version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate release notes - run: | - cp ./.github/genReleaseNote.sh ./ - bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION} - rm ./genReleaseNote.sh - - - uses: actions/download-artifact@v4 - with: - path: bin/ - merge-multiple: true - - - name: Display structure of downloaded files - run: ls -R - working-directory: bin - - - name: Upload Release - uses: softprops/action-gh-release@v2 - if: ${{ success() }} - with: - tag_name: ${{ github.event.inputs.version }} - files: bin/* - body_path: release.md - - Docker: - if: ${{ !startsWith(github.event_name, 'pull_request') }} - permissions: write-all - needs: [build] - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: - fetch-depth: 0 + submodules: true - - uses: actions/download-artifact@v4 + - name: Archive Release + uses: thedoctor0/zip-release@0.7.1 with: - path: bin/ - merge-multiple: true - - - name: Display structure of downloaded files - run: ls -R - working-directory: bin + type: zip + filename: 'mihomo_${{ github.ref_name }}.zip' + exclusions: '*.git*' - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3 - with: - version: latest - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - if: ${{ github.event_name != 'workflow_dispatch' }} - id: meta_alpha - uses: docker/metadata-action@v5 - with: - images: '${{ env.REGISTRY }}/${{ github.repository }}' - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} - id: meta_release - uses: docker/metadata-action@v5 - with: - images: '${{ env.REGISTRY }}/${{ github.repository }}' - tags: | - ${{ github.event.inputs.version }} - flavor: | - latest=true - labels: org.opencontainers.image.version=${{ github.event.inputs.version }} - - - name: Show files - run: | - ls . - ls bin/ - - - name: login to docker REGISTRY - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - if: ${{ github.event_name != 'workflow_dispatch' }} - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - push: ${{ github.event_name != 'pull_request' }} - platforms: | - linux/386 - linux/amd64 - linux/arm64 - linux/arm/v7 - tags: ${{ steps.meta_alpha.outputs.tags }} - labels: ${{ steps.meta_alpha.outputs.labels }} - - - name: Build and push Docker image - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} - uses: docker/build-push-action@v5 + - name: Release + uses: softprops/action-gh-release@v1 with: - context: . - file: ./Dockerfile - push: ${{ github.event_name != 'pull_request' }} - platforms: | - linux/386 - linux/amd64 - linux/arm64 - linux/arm/v7 - tags: ${{ steps.meta_release.outputs.tags }} - labels: ${{ steps.meta_release.outputs.labels }} \ No newline at end of file + generate_release_notes: true + files: 'mihomo_${{ github.ref_name }}.zip' \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index db7ac0373e..0000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Test -on: - push: - paths-ignore: - - "docs/**" - - "README.md" - - ".github/ISSUE_TEMPLATE/**" - branches: - - Alpha - tags: - - "v*" - pull_request: - branches: - - Alpha - -jobs: - test: - strategy: - matrix: - os: - - 'ubuntu-latest' # amd64 linux - - 'windows-latest' # amd64 windows - - 'macos-latest' # arm64 macos - - 'ubuntu-24.04-arm' # arm64 linux - - 'macos-13' # amd64 macos - go-version: - - '1.24' - - '1.23' - - '1.22' - - '1.21' - - '1.20' - fail-fast: false - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - env: - CGO_ENABLED: 0 - GOTOOLCHAIN: local - # Fix mingw trying to be smart and converting paths https://github.com/moby/moby/issues/24029#issuecomment-250412919 - MSYS_NO_PATHCONV: true - steps: - - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - - # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 - # this patch file only works on golang1.24.x - # that means after golang1.25 release it must be changed - # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/ - # revert: - # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" - # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" - # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" - # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" - - name: Revert Golang1.24 commit for Windows7/8 - if: ${{ runner.os == 'Windows' && matrix.go-version == '1.24' }} - run: | - cd $(go env GOROOT) - curl https://github.com/MetaCubeX/go/commit/2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/7b1fd7d39c6be0185fbe1d929578ab372ac5c632.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/979d6d8bab3823ff572ace26767fd2ce3cf351ae.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/ac3e93c061779dfefc0dd13a5b6e6f764a25621e.diff | patch --verbose -p 1 - - # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 - # this patch file only works on golang1.23.x - # that means after golang1.24 release it must be changed - # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/ - # revert: - # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" - # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" - # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" - # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" - - name: Revert Golang1.23 commit for Windows7/8 - if: ${{ runner.os == 'Windows' && matrix.go-version == '1.23' }} - run: | - cd $(go env GOROOT) - curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1 - - # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 - # this patch file only works on golang1.22.x - # that means after golang1.23 release it must be changed - # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/ - # revert: - # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" - # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" - # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" - # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" - - name: Revert Golang1.22 commit for Windows7/8 - if: ${{ runner.os == 'Windows' && matrix.go-version == '1.22' }} - run: | - cd $(go env GOROOT) - curl https://github.com/MetaCubeX/go/commit/9779155f18b6556a034f7bb79fb7fb2aad1e26a9.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/ef0606261340e608017860b423ffae5c1ce78239.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/7f83badcb925a7e743188041cb6e561fc9b5b642.diff | patch --verbose -p 1 - curl https://github.com/MetaCubeX/go/commit/83ff9782e024cb328b690cbf0da4e7848a327f4f.diff | patch --verbose -p 1 - - # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 - - name: Revert Golang1.21 commit for Windows7/8 - if: ${{ runner.os == 'Windows' && matrix.go-version == '1.21' }} - run: | - cd $(go env GOROOT) - curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1 - - - name: Test - run: go test ./... -v -count=1 - - - name: Test with tag with_gvisor - run: go test ./... -v -count=1 -tags "with_gvisor" \ No newline at end of file diff --git a/.github/workflows/trigger-cmfa-update.yml b/.github/workflows/trigger-cmfa-update.yml deleted file mode 100644 index d9523a07c1..0000000000 --- a/.github/workflows/trigger-cmfa-update.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Trigger CMFA Update -on: - workflow_dispatch: - push: - paths-ignore: - - "docs/**" - - "README.md" - - ".github/ISSUE_TEMPLATE/**" - branches: - - Alpha - tags: - - "v*" - pull_request_target: - branches: - - Alpha - -jobs: - # Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies - trigger-CMFA-update: - runs-on: ubuntu-latest - steps: - - uses: tibdex/github-app-token@v1 - id: generate-token - with: - app_id: ${{ secrets.MAINTAINER_APPID }} - private_key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }} - - - name: Trigger update-dependencies - run: | - curl -X POST https://api.github.com/repos/MetaCubeX/ClashMetaForAndroid/dispatches \ - -H "Accept: application/vnd.github.everest-preview+json" \ - -H "Authorization: token ${{ steps.generate-token.outputs.token }}" \ - -d '{"event_type": "core-updated"}' diff --git a/.gitignore b/.gitignore index d880126252..79421885c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,5 @@ -# 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* -/output -.vscode/ -.fleet/ \ No newline at end of file +__pycache__ +venv +build +mihomo.egg-info +*.ipynb \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml deleted file mode 100644 index 1de71ad84b..0000000000 --- a/.golangci.yaml +++ /dev/null @@ -1,17 +0,0 @@ -linters: - disable-all: true - enable: - - gofumpt - - staticcheck - - govet - - gci - -linters-settings: - gci: - custom-order: true - sections: - - standard - - prefix(github.com/metacubex/mihomo) - - default - staticcheck: - go: '1.19' diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 67d4a6e56e..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM alpine:latest as builder -ARG TARGETPLATFORM -RUN echo "I'm building for $TARGETPLATFORM" - -RUN apk add --no-cache gzip && \ - mkdir /mihomo-config && \ - wget -O /mihomo-config/geoip.metadb https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb && \ - wget -O /mihomo-config/geosite.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat && \ - wget -O /mihomo-config/geoip.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat - -COPY docker/file-name.sh /mihomo/file-name.sh -WORKDIR /mihomo -COPY bin/ bin/ -RUN FILE_NAME=`sh file-name.sh` && echo $FILE_NAME && \ - FILE_NAME=`ls bin/ | egrep "$FILE_NAME.gz"|awk NR==1` && echo $FILE_NAME && \ - mv bin/$FILE_NAME mihomo.gz && gzip -d mihomo.gz && chmod +x mihomo && echo "$FILE_NAME" > /mihomo-config/test -FROM alpine:latest -LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/mihomo" - -RUN apk add --no-cache ca-certificates tzdata iptables - -VOLUME ["/root/.config/mihomo/"] - -COPY --from=builder /mihomo-config/ /root/.config/mihomo/ -COPY --from=builder /mihomo/mihomo /mihomo -ENTRYPOINT [ "/mihomo" ] diff --git a/LICENSE b/LICENSE index f288702d2f..ff3ae5789c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,7 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 +Copyright 2023 KT - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - Preamble +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - 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. - - - Copyright (C) - - 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 . - -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: - - Copyright (C) - 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 -. - - 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 -. +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 36c640d553..0000000000 --- a/Makefile +++ /dev/null @@ -1,165 +0,0 @@ -NAME=mihomo -BINDIR=bin -BRANCH=$(shell git branch --show-current) -ifeq ($(BRANCH),Alpha) -VERSION=alpha-$(shell git rev-parse --short HEAD) -else ifeq ($(BRANCH),Beta) -VERSION=beta-$(shell git rev-parse --short HEAD) -else ifeq ($(BRANCH),) -VERSION=$(shell git describe --tags) -else -VERSION=$(shell git rev-parse --short HEAD) -endif - -BUILDTIME=$(shell date -u) -GOBUILD=CGO_ENABLED=0 go build -tags with_gvisor -trimpath -ldflags '-X "github.com/metacubex/mihomo/constant.Version=$(VERSION)" \ - -X "github.com/metacubex/mihomo/constant.BuildTime=$(BUILDTIME)" \ - -w -s -buildid=' - -PLATFORM_LIST = \ - darwin-amd64-compatible \ - darwin-amd64 \ - darwin-arm64 \ - linux-amd64-compatible \ - linux-amd64 \ - linux-armv5 \ - linux-armv6 \ - linux-armv7 \ - linux-arm64 \ - linux-mips64 \ - linux-mips64le \ - linux-mips-softfloat \ - linux-mips-hardfloat \ - linux-mipsle-softfloat \ - linux-mipsle-hardfloat \ - linux-riscv64 \ - linux-loong64 \ - android-arm64 \ - freebsd-386 \ - freebsd-amd64 \ - freebsd-arm64 - -WINDOWS_ARCH_LIST = \ - windows-386 \ - windows-amd64-compatible \ - windows-amd64 \ - windows-arm64 \ - windows-arm32v7 - -all:linux-amd64 linux-arm64\ - darwin-amd64 darwin-arm64\ - windows-amd64 windows-arm64\ - - -darwin-all: darwin-amd64 darwin-arm64 - -docker: - GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -darwin-amd64: - GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -darwin-amd64-compatible: - GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(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 GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-amd64-compatible: - GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-arm64: - GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-armv5: - GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-armv6: - GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-armv7: - GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mips-softfloat: - GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mips-hardfloat: - GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mipsle-softfloat: - GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mipsle-hardfloat: - GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mips64: - GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mips64le: - GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-riscv64: - GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-loong64: - GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -android-arm64: - GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -freebsd-386: - GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -freebsd-amd64: - GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -freebsd-arm64: - GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -windows-386: - GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -windows-amd64: - GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -windows-amd64-compatible: - GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -windows-arm64: - GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -windows-arm32v7: - GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -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) - -vet: - go test ./... - -lint: - golangci-lint run ./... - -clean: - rm $(BINDIR)/* - -CLANG ?= clang-14 -CFLAGS := -O2 -g -Wall -Werror $(CFLAGS) - diff --git a/Meta.png b/Meta.png deleted file mode 100644 index 1f6323dad3..0000000000 Binary files a/Meta.png and /dev/null differ diff --git a/README.md b/README.md index 05f03799eb..f45f4f6dc6 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,115 @@ -

- Meta Kennel -
Meta Kernel
-

+# mihomo +A simple python pydantic model (type hint and autocompletion support) for Honkai: Star Rail parsed data from the Mihomo API. -

Another Mihomo Kernel.

+API url: https://api.mihomo.me/sr_info_parsed/{UID}?lang={LANG} -

- - - - - - - - - - -

- -## Features - -- Local HTTP/HTTPS/SOCKS server with authentication support -- VMess, VLESS, Shadowsocks, Trojan, Snell, TUIC, Hysteria protocol support -- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP. -- Rules based off domains, GEOIP, IPCIDR or Process to forward packets to different nodes -- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node - based off latency -- Remote providers, allowing users to get node lists remotely instead of hard-coding in config -- Netfilter TCP redirecting. Deploy Mihomo on your Internet gateway with `iptables`. -- Comprehensive HTTP RESTful API controller - -## Dashboard - -A web dashboard with first-class support for this project has been created; it can be checked out at [metacubexd](https://github.com/MetaCubeX/metacubexd). - -## Configration example - -Configuration example is located at [/docs/config.yaml](https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml). - -## Docs - -Documentation can be found in [mihomo Docs](https://wiki.metacubex.one/). - -## For development - -Requirements: -[Go 1.20 or newer](https://go.dev/dl/) - -Build mihomo: - -```shell -git clone https://github.com/MetaCubeX/mihomo.git -cd mihomo && go mod download -go build +## Installation ``` - -Set go proxy if a connection to GitHub is not possible: - -```shell -go env -w GOPROXY=https://goproxy.io,direct +pip install -U git+https://github.com/KT-Yeh/mihomo.git ``` -Build with gvisor tun stack: - -```shell -go build -tags with_gvisor +## Usage + +### Basic +There are two parsed data formats: +- V1: + - URL: https://api.mihomo.me/sr_info_parsed/800333171?lang=en&version=v1 + - Fetching: use `client.fetch_user_v1(800333171)` + - Data model: `mihomo.models.v1.StarrailInfoParsedV1` + - All models defined in `mihomo/models/v1` directory. +- V2: + - URL: https://api.mihomo.me/sr_info_parsed/800333171?lang=en + - Fetching: use `client.fetch_user(800333171)` + - Data model: `mihomo.models.StarrailInfoParsed` + - All models defined in `mihomo/models` directory. + +If you don't want to use `client.get_icon_url` to get the image url everytime, you can use `client.fetch_user(800333171, replace_icon_name_with_url=True)` to get the parsed data with asset urls. + +### Example +```py +import asyncio + +from mihomo import Language, MihomoAPI +from mihomo.models import StarrailInfoParsed +from mihomo.models.v1 import StarrailInfoParsedV1 + +client = MihomoAPI(language=Language.EN) + + +async def v1(): + data: StarrailInfoParsedV1 = await client.fetch_user_v1(800333171) + + print(f"Name: {data.player.name}") + print(f"Level: {data.player.level}") + print(f"Signature: {data.player.signature}") + print(f"Achievements: {data.player_details.achievements}") + print(f"Characters count: {data.player_details.characters}") + print(f"Profile picture url: {client.get_icon_url(data.player.icon)}") + for character in data.characters: + print("-----------") + print(f"Name: {character.name}") + print(f"Rarity: {character.rarity}") + print(f"Level: {character.level}") + print(f"Avatar url: {client.get_icon_url(character.icon)}") + print(f"Preview url: {client.get_icon_url(character.preview)}") + print(f"Portrait url: {client.get_icon_url(character.portrait)}") + + +async def v2(): + data: StarrailInfoParsed = await client.fetch_user(800333171, replace_icon_name_with_url=True) + + print(f"Name: {data.player.name}") + print(f"Level: {data.player.level}") + print(f"Signature: {data.player.signature}") + print(f"Profile picture url: {data.player.avatar.icon}") + for character in data.characters: + print("-----------") + print(f"Name: {character.name}") + print(f"Rarity: {character.rarity}") + print(f"Portrait url: {character.portrait}") + +asyncio.run(v1()) +asyncio.run(v2()) ``` -### IPTABLES configuration - -Work on Linux OS which supported `iptables` - -```yaml -# Enable the TPROXY listener -tproxy-port: 9898 - -iptables: - enable: true # default is false - inbound-interface: eth0 # detect the inbound interface, default is 'lo' +### Tools +`from mihomo import tools` +#### Remove Duplicate Character +```py + data = await client.fetch_user(800333171) + data = tools.remove_duplicate_character(data) ``` -## Debugging +#### Merge Character Data +```py + old_data = await client.fetch_user(800333171) -Check [wiki](https://wiki.metacubex.one/api/#debug) to get an instruction on using debug -API. + # Change characters in game and wait for the API to refresh + # ... -## Credits - -- [Dreamacro/clash](https://github.com/Dreamacro/clash) -- [SagerNet/sing-box](https://github.com/SagerNet/sing-box) -- [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2) -- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core) -- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go) -- [yaling888/clash-plus-pro](https://github.com/yaling888/clash) - -## License - -This software is released under the GPL-3.0 license. + new_data = await client.fetch_user(800333171) + data = tools.merge_character_data(new_data, old_data) +``` -**In addition, any downstream projects not affiliated with `MetaCubeX` shall not contain the word `mihomo` in their names.** \ No newline at end of file +### Data Persistence +Take pickle and json as an example +```py +import pickle +import zlib +from mihomo import MihomoAPI, Language, StarrailInfoParsed + +client = MihomoAPI(language=Language.EN) +data = await client.fetch_user(800333171) + +# Save +pickle_data = zlib.compress(pickle.dumps(data)) +print(len(pickle_data)) +json_data = data.json(by_alias=True, ensure_ascii=False) +print(len(json_data)) + +# Load +data_from_pickle = pickle.loads(zlib.decompress(pickle_data)) +data_from_json = StarrailInfoParsed.parse_raw(json_data) +print(type(data_from_pickle)) +print(type(data_from_json)) +``` diff --git a/adapter/adapter.go b/adapter/adapter.go deleted file mode 100644 index 5e89e8bf5d..0000000000 --- a/adapter/adapter.go +++ /dev/null @@ -1,330 +0,0 @@ -package adapter - -import ( - "context" - "crypto/tls" - "encoding/json" - "fmt" - "net" - "net/http" - "net/netip" - "net/url" - "strconv" - "strings" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/queue" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/ca" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - "github.com/puzpuzpuz/xsync/v3" -) - -var UnifiedDelay = atomic.NewBool(false) - -const ( - defaultHistoriesNum = 10 -) - -type internalProxyState struct { - alive atomic.Bool - history *queue.Queue[C.DelayHistory] -} - -type Proxy struct { - C.ProxyAdapter - alive atomic.Bool - history *queue.Queue[C.DelayHistory] - extra *xsync.MapOf[string, *internalProxyState] -} - -// Adapter implements C.Proxy -func (p *Proxy) Adapter() C.ProxyAdapter { - return p.ProxyAdapter -} - -// AliveForTestUrl implements C.Proxy -func (p *Proxy) AliveForTestUrl(url string) bool { - if state, ok := p.extra.Load(url); ok { - return state.alive.Load() - } - - 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) (C.Conn, error) { - conn, err := p.ProxyAdapter.DialContext(ctx, metadata) - 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) (C.PacketConn, error) { - pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata) - return pc, err -} - -// DelayHistory implements C.Proxy -func (p *Proxy) DelayHistory() []C.DelayHistory { - queueM := p.history.Copy() - histories := []C.DelayHistory{} - for _, item := range queueM { - histories = append(histories, item) - } - return histories -} - -// DelayHistoryForTestUrl implements C.Proxy -func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory { - var queueM []C.DelayHistory - - if state, ok := p.extra.Load(url); ok { - queueM = state.history.Copy() - } - histories := []C.DelayHistory{} - for _, item := range queueM { - histories = append(histories, item) - } - return histories -} - -// ExtraDelayHistories return all delay histories for each test URL -// implements C.Proxy -func (p *Proxy) ExtraDelayHistories() map[string]C.ProxyState { - histories := map[string]C.ProxyState{} - - p.extra.Range(func(k string, v *internalProxyState) bool { - testUrl := k - state := v - - queueM := state.history.Copy() - var history []C.DelayHistory - - for _, item := range queueM { - history = append(history, item) - } - - histories[testUrl] = C.ProxyState{ - Alive: state.alive.Load(), - History: history, - } - return true - }) - return histories -} - -// LastDelayForTestUrl return last history record of the specified URL. if proxy is not alive, return the max value of uint16. -// implements C.Proxy -func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) { - var maxDelay uint16 = 0xffff - - alive := false - var history C.DelayHistory - - if state, ok := p.extra.Load(url); ok { - alive = state.alive.Load() - history = state.history.Last() - } - - if !alive || history.Delay == 0 { - return maxDelay - } - 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["extra"] = p.ExtraDelayHistories() - mapping["alive"] = p.alive.Load() - mapping["name"] = p.Name() - mapping["udp"] = p.SupportUDP() - mapping["uot"] = p.SupportUOT() - - proxyInfo := p.ProxyInfo() - mapping["xudp"] = proxyInfo.XUDP - mapping["tfo"] = proxyInfo.TFO - mapping["mptcp"] = proxyInfo.MPTCP - mapping["smux"] = proxyInfo.SMUX - mapping["interface"] = proxyInfo.Interface - mapping["dialer-proxy"] = proxyInfo.DialerProxy - mapping["routing-mark"] = proxyInfo.RoutingMark - - return json.Marshal(mapping) -} - -// URLTest get the delay for the specified URL -// implements C.Proxy -func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (t uint16, err error) { - var satisfied bool - - defer func() { - alive := err == nil - record := C.DelayHistory{Time: time.Now()} - if alive { - record.Delay = t - } - - p.alive.Store(alive) - p.history.Put(record) - if p.history.Len() > defaultHistoriesNum { - p.history.Pop() - } - - state, ok := p.extra.Load(url) - if !ok { - state = &internalProxyState{ - history: queue.New[C.DelayHistory](defaultHistoriesNum), - alive: atomic.NewBool(true), - } - p.extra.Store(url, state) - } - - if !satisfied { - record.Delay = 0 - alive = false - } - - state.alive.Store(alive) - state.history.Put(record) - if state.history.Len() > defaultHistoriesNum { - state.history.Pop() - } - - }() - - unifiedDelay := UnifiedDelay.Load() - - addr, err := urlToMetadata(url) - if err != nil { - return - } - - start := time.Now() - instance, err := p.DialContext(ctx, &addr) - if err != nil { - return - } - defer func() { - _ = instance.Close() - }() - - req, err := http.NewRequest(http.MethodHead, url, nil) - if err != nil { - return - } - req = req.WithContext(ctx) - - transport := &http.Transport{ - DialContext: func(context.Context, 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, - TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}), - } - - client := http.Client{ - Timeout: 30 * time.Second, - 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() - - if unifiedDelay { - second := time.Now() - var ignoredErr error - var secondResp *http.Response - secondResp, ignoredErr = client.Do(req) - if ignoredErr == nil { - resp = secondResp - _ = resp.Body.Close() - start = second - } else { - if strings.HasPrefix(url, "http://") { - log.Errorln("%s failed to get the second response from %s: %v", p.Name(), url, ignoredErr) - log.Warnln("It is recommended to use HTTPS for provider.health-check.url and group.url to ensure better reliability. Due to some proxy providers hijacking test addresses and not being compatible with repeated HEAD requests, using HTTP may result in failed tests.") - } - } - } - - satisfied = resp != nil && (expectedStatus == nil || expectedStatus.Check(uint16(resp.StatusCode))) - t = uint16(time.Since(start) / time.Millisecond) - return -} - -func NewProxy(adapter C.ProxyAdapter) *Proxy { - return &Proxy{ - ProxyAdapter: adapter, - history: queue.New[C.DelayHistory](defaultHistoriesNum), - alive: atomic.NewBool(true), - extra: xsync.NewMapOf[string, *internalProxyState]()} -} - -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 - } - } - uintPort, err := strconv.ParseUint(port, 10, 16) - if err != nil { - return - } - - addr = C.Metadata{ - Host: u.Hostname(), - DstIP: netip.Addr{}, - DstPort: uint16(uintPort), - } - return -} diff --git a/adapter/inbound/addition.go b/adapter/inbound/addition.go deleted file mode 100644 index 894910aac1..0000000000 --- a/adapter/inbound/addition.go +++ /dev/null @@ -1,73 +0,0 @@ -package inbound - -import ( - "net" - - C "github.com/metacubex/mihomo/constant" -) - -type Addition func(metadata *C.Metadata) - -func ApplyAdditions(metadata *C.Metadata, additions ...Addition) { - for _, addition := range additions { - addition(metadata) - } -} - -func WithInName(name string) Addition { - return func(metadata *C.Metadata) { - metadata.InName = name - } -} - -func WithInUser(user string) Addition { - return func(metadata *C.Metadata) { - metadata.InUser = user - } -} - -func WithSpecialRules(specialRules string) Addition { - return func(metadata *C.Metadata) { - metadata.SpecialRules = specialRules - } -} - -func WithSpecialProxy(specialProxy string) Addition { - return func(metadata *C.Metadata) { - metadata.SpecialProxy = specialProxy - } -} - -func WithDstAddr(addr net.Addr) Addition { - return func(metadata *C.Metadata) { - _ = metadata.SetRemoteAddr(addr) - } -} - -func WithSrcAddr(addr net.Addr) Addition { - return func(metadata *C.Metadata) { - m := C.Metadata{} - if err := m.SetRemoteAddr(addr); err == nil { - metadata.SrcIP = m.DstIP - metadata.SrcPort = m.DstPort - } - } -} - -func WithInAddr(addr net.Addr) Addition { - return func(metadata *C.Metadata) { - m := C.Metadata{} - if err := m.SetRemoteAddr(addr); err == nil { - metadata.InIP = m.DstIP - metadata.InPort = m.DstPort - } - } -} - -func WithDSCP(dscp uint8) Addition { - return func(metadata *C.Metadata) { - metadata.DSCP = dscp - } -} - -func Placeholder(metadata *C.Metadata) {} diff --git a/adapter/inbound/auth.go b/adapter/inbound/auth.go deleted file mode 100644 index 8317274687..0000000000 --- a/adapter/inbound/auth.go +++ /dev/null @@ -1,38 +0,0 @@ -package inbound - -import ( - "net" - "net/netip" - - C "github.com/metacubex/mihomo/constant" -) - -var skipAuthPrefixes []netip.Prefix - -func SetSkipAuthPrefixes(prefixes []netip.Prefix) { - skipAuthPrefixes = prefixes -} - -func SkipAuthPrefixes() []netip.Prefix { - return skipAuthPrefixes -} - -func SkipAuthRemoteAddr(addr net.Addr) bool { - m := C.Metadata{} - if err := m.SetRemoteAddr(addr); err != nil { - return false - } - return skipAuth(m.AddrPort().Addr()) -} - -func SkipAuthRemoteAddress(addr string) bool { - m := C.Metadata{} - if err := m.SetRemoteAddress(addr); err != nil { - return false - } - return skipAuth(m.AddrPort().Addr()) -} - -func skipAuth(addr netip.Addr) bool { - return prefixesContains(skipAuthPrefixes, addr) -} diff --git a/adapter/inbound/http.go b/adapter/inbound/http.go deleted file mode 100644 index f7d45399fb..0000000000 --- a/adapter/inbound/http.go +++ /dev/null @@ -1,20 +0,0 @@ -package inbound - -import ( - "net" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" -) - -// NewHTTP receive normal http request and return HTTPContext -func NewHTTP(target socks5.Addr, srcConn net.Conn, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) { - metadata := parseSocksAddr(target) - metadata.NetWork = C.TCP - metadata.Type = C.HTTP - metadata.RawSrcAddr = srcConn.RemoteAddr() - metadata.RawDstAddr = srcConn.LocalAddr() - ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(srcConn.LocalAddr())) - ApplyAdditions(metadata, additions...) - return conn, metadata -} diff --git a/adapter/inbound/https.go b/adapter/inbound/https.go deleted file mode 100644 index 24b30804b7..0000000000 --- a/adapter/inbound/https.go +++ /dev/null @@ -1,19 +0,0 @@ -package inbound - -import ( - "net" - "net/http" - - C "github.com/metacubex/mihomo/constant" -) - -// NewHTTPS receive CONNECT request and return ConnContext -func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) { - metadata := parseHTTPAddr(request) - metadata.Type = C.HTTPS - metadata.RawSrcAddr = conn.RemoteAddr() - metadata.RawDstAddr = conn.LocalAddr() - ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) - ApplyAdditions(metadata, additions...) - return conn, metadata -} diff --git a/adapter/inbound/ipfilter.go b/adapter/inbound/ipfilter.go deleted file mode 100644 index 872d0c85a1..0000000000 --- a/adapter/inbound/ipfilter.go +++ /dev/null @@ -1,47 +0,0 @@ -package inbound - -import ( - "net" - "net/netip" - - C "github.com/metacubex/mihomo/constant" -) - -var lanAllowedIPs []netip.Prefix -var lanDisAllowedIPs []netip.Prefix - -func SetAllowedIPs(prefixes []netip.Prefix) { - lanAllowedIPs = prefixes -} - -func SetDisAllowedIPs(prefixes []netip.Prefix) { - lanDisAllowedIPs = prefixes -} - -func AllowedIPs() []netip.Prefix { - return lanAllowedIPs -} - -func DisAllowedIPs() []netip.Prefix { - return lanDisAllowedIPs -} - -func IsRemoteAddrDisAllowed(addr net.Addr) bool { - m := C.Metadata{} - if err := m.SetRemoteAddr(addr); err != nil { - return false - } - ipAddr := m.AddrPort().Addr() - if ipAddr.IsValid() { - return isAllowed(ipAddr) && !isDisAllowed(ipAddr) - } - return false -} - -func isAllowed(addr netip.Addr) bool { - return prefixesContains(lanAllowedIPs, addr) -} - -func isDisAllowed(addr netip.Addr) bool { - return prefixesContains(lanDisAllowedIPs, addr) -} diff --git a/adapter/inbound/listen.go b/adapter/inbound/listen.go deleted file mode 100644 index b4c6bf6b6a..0000000000 --- a/adapter/inbound/listen.go +++ /dev/null @@ -1,106 +0,0 @@ -package inbound - -import ( - "context" - "fmt" - "net" - "net/netip" - "sync" - - "github.com/metacubex/mihomo/component/keepalive" - - "github.com/metacubex/tfo-go" -) - -var ( - lc = tfo.ListenConfig{ - DisableTFO: true, - } - mutex sync.RWMutex -) - -func SetTfo(open bool) { - mutex.Lock() - defer mutex.Unlock() - lc.DisableTFO = !open -} - -func Tfo() bool { - mutex.RLock() - defer mutex.RUnlock() - return !lc.DisableTFO -} - -func SetMPTCP(open bool) { - mutex.Lock() - defer mutex.Unlock() - setMultiPathTCP(&lc.ListenConfig, open) -} - -func MPTCP() bool { - mutex.RLock() - defer mutex.RUnlock() - return getMultiPathTCP(&lc.ListenConfig) -} - -func preResolve(network, address string) (string, error) { - switch network { // like net.Resolver.internetAddrList but filter domain to avoid call net.Resolver.lookupIPAddr - case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6": - if host, port, err := net.SplitHostPort(address); err == nil { - switch host { - case "localhost": - switch network { - case "tcp6", "udp6", "ip6": - address = net.JoinHostPort("::1", port) - default: - address = net.JoinHostPort("127.0.0.1", port) - } - case "": // internetAddrList can handle this special case - break - default: - if _, err := netip.ParseAddr(host); err != nil { // not ip - return "", fmt.Errorf("invalid network address: %s", address) - } - } - } - } - return address, nil -} - -func ListenContext(ctx context.Context, network, address string) (net.Listener, error) { - address, err := preResolve(network, address) - if err != nil { - return nil, err - } - - mutex.RLock() - defer mutex.RUnlock() - return lc.Listen(ctx, network, address) -} - -func Listen(network, address string) (net.Listener, error) { - return ListenContext(context.Background(), network, address) -} - -func ListenPacketContext(ctx context.Context, network, address string) (net.PacketConn, error) { - address, err := preResolve(network, address) - if err != nil { - return nil, err - } - - mutex.RLock() - defer mutex.RUnlock() - return lc.ListenPacket(ctx, network, address) -} - -func ListenPacket(network, address string) (net.PacketConn, error) { - return ListenPacketContext(context.Background(), network, address) -} - -func init() { - keepalive.SetDisableKeepAliveCallback.Register(func(b bool) { - mutex.Lock() - defer mutex.Unlock() - keepalive.SetNetListenConfig(&lc.ListenConfig) - }) -} diff --git a/adapter/inbound/listen_notwindows.go b/adapter/inbound/listen_notwindows.go deleted file mode 100644 index 8fdfb7b8e6..0000000000 --- a/adapter/inbound/listen_notwindows.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !windows - -package inbound - -import ( - "net" - "os" -) - -const SupportNamedPipe = false - -func ListenNamedPipe(path string) (net.Listener, error) { - return nil, os.ErrInvalid -} diff --git a/adapter/inbound/listen_windows.go b/adapter/inbound/listen_windows.go deleted file mode 100644 index d19239da18..0000000000 --- a/adapter/inbound/listen_windows.go +++ /dev/null @@ -1,32 +0,0 @@ -package inbound - -import ( - "net" - "os" - - "github.com/metacubex/wireguard-go/ipc/namedpipe" - "golang.org/x/sys/windows" -) - -const SupportNamedPipe = true - -// windowsSDDL is the Security Descriptor set on the namedpipe. -// It provides read/write access to all users and the local system. -const windowsSDDL = "D:PAI(A;OICI;GWGR;;;BU)(A;OICI;GWGR;;;SY)" - -func ListenNamedPipe(path string) (net.Listener, error) { - sddl := os.Getenv("LISTEN_NAMEDPIPE_SDDL") - if sddl == "" { - sddl = windowsSDDL - } - securityDescriptor, err := windows.SecurityDescriptorFromString(sddl) - if err != nil { - return nil, err - } - namedpipeLC := namedpipe.ListenConfig{ - SecurityDescriptor: securityDescriptor, - InputBufferSize: 256 * 1024, - OutputBufferSize: 256 * 1024, - } - return namedpipeLC.Listen(path) -} diff --git a/adapter/inbound/mptcp_go120.go b/adapter/inbound/mptcp_go120.go deleted file mode 100644 index faae6ec5d8..0000000000 --- a/adapter/inbound/mptcp_go120.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !go1.21 - -package inbound - -import "net" - -const multipathTCPAvailable = false - -func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) { -} - -func getMultiPathTCP(listenConfig *net.ListenConfig) bool { - return false -} diff --git a/adapter/inbound/mptcp_go121.go b/adapter/inbound/mptcp_go121.go deleted file mode 100644 index 9163878a70..0000000000 --- a/adapter/inbound/mptcp_go121.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build go1.21 - -package inbound - -import "net" - -const multipathTCPAvailable = true - -func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) { - listenConfig.SetMultipathTCP(open) -} - -func getMultiPathTCP(listenConfig *net.ListenConfig) bool { - return listenConfig.MultipathTCP() -} diff --git a/adapter/inbound/packet.go b/adapter/inbound/packet.go deleted file mode 100644 index a10d402ea8..0000000000 --- a/adapter/inbound/packet.go +++ /dev/null @@ -1,22 +0,0 @@ -package inbound - -import ( - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" -) - -// NewPacket is PacketAdapter generator -func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) (C.UDPPacket, *C.Metadata) { - metadata := parseSocksAddr(target) - metadata.NetWork = C.UDP - metadata.Type = source - metadata.RawSrcAddr = packet.LocalAddr() - metadata.RawDstAddr = metadata.UDPAddr() - ApplyAdditions(metadata, WithSrcAddr(packet.LocalAddr())) - if p, ok := packet.(C.UDPPacketInAddr); ok { - ApplyAdditions(metadata, WithInAddr(p.InAddr())) - } - ApplyAdditions(metadata, additions...) - - return packet, metadata -} diff --git a/adapter/inbound/socket.go b/adapter/inbound/socket.go deleted file mode 100644 index 8cd301f7e0..0000000000 --- a/adapter/inbound/socket.go +++ /dev/null @@ -1,18 +0,0 @@ -package inbound - -import ( - "net" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" -) - -// NewSocket receive TCP inbound and return ConnContext -func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) (net.Conn, *C.Metadata) { - metadata := parseSocksAddr(target) - metadata.NetWork = C.TCP - metadata.Type = source - ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) - ApplyAdditions(metadata, additions...) - return conn, metadata -} diff --git a/adapter/inbound/util.go b/adapter/inbound/util.go deleted file mode 100644 index d67a96dbf2..0000000000 --- a/adapter/inbound/util.go +++ /dev/null @@ -1,78 +0,0 @@ -package inbound - -import ( - "net" - "net/http" - "net/netip" - "strconv" - "strings" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/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 = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) - case socks5.AtypIPv4: - metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv4len]) - metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) - case socks5.AtypIPv6: - metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv6len]) - metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) - } - metadata.DstIP = metadata.DstIP.Unmap() - - return metadata -} - -func parseHTTPAddr(request *http.Request) *C.Metadata { - host := request.URL.Hostname() - port := request.URL.Port() - if port == "" { - port = "80" - } - - // trim FQDN (#737) - host = strings.TrimRight(host, ".") - - var uint16Port uint16 - if port, err := strconv.ParseUint(port, 10, 16); err == nil { - uint16Port = uint16(port) - } - - metadata := &C.Metadata{ - NetWork: C.TCP, - Host: host, - DstIP: netip.Addr{}, - DstPort: uint16Port, - } - - ip, err := netip.ParseAddr(host) - if err == nil { - metadata.DstIP = ip - } - - return metadata -} - -func prefixesContains(prefixes []netip.Prefix, addr netip.Addr) bool { - if len(prefixes) == 0 { - return false - } - if !addr.IsValid() { - return false - } - addr = addr.Unmap().WithZone("") // netip.Prefix.Contains returns false if ip has an IPv6 zone - for _, prefix := range prefixes { - if prefix.Contains(addr) { - return true - } - } - return false -} diff --git a/adapter/outbound/anytls.go b/adapter/outbound/anytls.go deleted file mode 100644 index 0e3b07de39..0000000000 --- a/adapter/outbound/anytls.go +++ /dev/null @@ -1,134 +0,0 @@ -package outbound - -import ( - "context" - "errors" - "net" - "strconv" - "time" - - CN "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/anytls" - "github.com/metacubex/mihomo/transport/vmess" - - M "github.com/metacubex/sing/common/metadata" - "github.com/metacubex/sing/common/uot" -) - -type AnyTLS struct { - *Base - client *anytls.Client - dialer proxydialer.SingDialer - option *AnyTLSOption -} - -type AnyTLSOption 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"` - ClientFingerprint string `proxy:"client-fingerprint,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - UDP bool `proxy:"udp,omitempty"` - IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"` - IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"` - MinIdleSession int `proxy:"min-idle-session,omitempty"` -} - -func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - c, err := t.client.CreateProxy(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) - if err != nil { - return nil, err - } - return NewConn(c, t), nil -} - -func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - // create tcp - c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2)) - if err != nil { - return nil, err - } - - // create uot on tcp - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - destination := M.SocksaddrFromNet(metadata.UDPAddr()) - return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil -} - -// SupportUOT implements C.ProxyAdapter -func (t *AnyTLS) SupportUOT() bool { - return true -} - -// ProxyInfo implements C.ProxyAdapter -func (t *AnyTLS) ProxyInfo() C.ProxyInfo { - info := t.Base.ProxyInfo() - info.DialerProxy = t.option.DialerProxy - return info -} - -// Close implements C.ProxyAdapter -func (t *AnyTLS) Close() error { - return t.client.Close() -} - -func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) { - addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - outbound := &AnyTLS{ - Base: &Base{ - name: option.Name, - addr: addr, - tp: C.AnyTLS, - udp: option.UDP, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - } - - singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer(outbound.DialOptions()...)) - outbound.dialer = singDialer - - tOption := anytls.ClientConfig{ - Password: option.Password, - Server: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)), - Dialer: singDialer, - IdleSessionCheckInterval: time.Duration(option.IdleSessionCheckInterval) * time.Second, - IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second, - MinIdleSession: option.MinIdleSession, - } - tlsConfig := &vmess.TLSConfig{ - Host: option.SNI, - SkipCertVerify: option.SkipCertVerify, - NextProtos: option.ALPN, - FingerPrint: option.Fingerprint, - ClientFingerprint: option.ClientFingerprint, - } - if tlsConfig.Host == "" { - tlsConfig.Host = option.Server - } - tOption.TLSConfig = tlsConfig - - client := anytls.NewClient(context.TODO(), tOption) - outbound.client = client - - return outbound, nil -} diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go deleted file mode 100644 index 7b4b93ab34..0000000000 --- a/adapter/outbound/base.go +++ /dev/null @@ -1,380 +0,0 @@ -package outbound - -import ( - "context" - "encoding/json" - "net" - "runtime" - "strings" - "sync" - "syscall" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/dialer" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type ProxyAdapter interface { - C.ProxyAdapter - DialOptions() []dialer.Option -} - -type Base struct { - name string - addr string - iface string - tp C.AdapterType - udp bool - xudp bool - tfo bool - mpTcp bool - rmark int - id string - prefer C.DNSPrefer -} - -// Name implements C.ProxyAdapter -func (b *Base) Name() string { - return b.name -} - -// Id implements C.ProxyAdapter -func (b *Base) Id() string { - if b.id == "" { - b.id = utils.NewUUIDV6().String() - } - - return b.id -} - -// Type implements C.ProxyAdapter -func (b *Base) Type() C.AdapterType { - return b.tp -} - -// StreamConnContext implements C.ProxyAdapter -func (b *Base) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { - return c, C.ErrNotSupport -} - -func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - return nil, C.ErrNotSupport -} - -// DialContextWithDialer implements C.ProxyAdapter -func (b *Base) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - return nil, C.ErrNotSupport -} - -// ListenPacketContext implements C.ProxyAdapter -func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - return nil, C.ErrNotSupport -} - -// ListenPacketWithDialer implements C.ProxyAdapter -func (b *Base) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { - return nil, C.ErrNotSupport -} - -// SupportWithDialer implements C.ProxyAdapter -func (b *Base) SupportWithDialer() C.NetWork { - return C.InvalidNet -} - -// SupportUOT implements C.ProxyAdapter -func (b *Base) SupportUOT() bool { - return false -} - -// SupportUDP implements C.ProxyAdapter -func (b *Base) SupportUDP() bool { - return b.udp -} - -// ProxyInfo implements C.ProxyAdapter -func (b *Base) ProxyInfo() (info C.ProxyInfo) { - info.XUDP = b.xudp - info.TFO = b.tfo - info.MPTCP = b.mpTcp - info.SMUX = false - info.Interface = b.iface - info.RoutingMark = b.rmark - return -} - -// IsL3Protocol implements C.ProxyAdapter -func (b *Base) IsL3Protocol(metadata *C.Metadata) bool { - return false -} - -// MarshalJSON implements C.ProxyAdapter -func (b *Base) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]string{ - "type": b.Type().String(), - "id": b.Id(), - }) -} - -// Addr implements C.ProxyAdapter -func (b *Base) Addr() string { - return b.addr -} - -// Unwrap implements C.ProxyAdapter -func (b *Base) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { - return nil -} - -// DialOptions return []dialer.Option from struct -func (b *Base) DialOptions() (opts []dialer.Option) { - if b.iface != "" { - opts = append(opts, dialer.WithInterface(b.iface)) - } - - if b.rmark != 0 { - opts = append(opts, dialer.WithRoutingMark(b.rmark)) - } - - switch b.prefer { - case C.IPv4Only: - opts = append(opts, dialer.WithOnlySingleStack(true)) - case C.IPv6Only: - opts = append(opts, dialer.WithOnlySingleStack(false)) - case C.IPv4Prefer: - opts = append(opts, dialer.WithPreferIPv4()) - case C.IPv6Prefer: - opts = append(opts, dialer.WithPreferIPv6()) - default: - } - - if b.tfo { - opts = append(opts, dialer.WithTFO(true)) - } - - if b.mpTcp { - opts = append(opts, dialer.WithMPTCP(true)) - } - - return opts -} - -func (b *Base) Close() error { - return nil -} - -type BasicOption struct { - TFO bool `proxy:"tfo,omitempty"` - MPTCP bool `proxy:"mptcp,omitempty"` - Interface string `proxy:"interface-name,omitempty"` - RoutingMark int `proxy:"routing-mark,omitempty"` - IPVersion string `proxy:"ip-version,omitempty"` - DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy -} - -type BaseOption struct { - Name string - Addr string - Type C.AdapterType - UDP bool - XUDP bool - TFO bool - MPTCP bool - Interface string - RoutingMark int - Prefer C.DNSPrefer -} - -func NewBase(opt BaseOption) *Base { - return &Base{ - name: opt.Name, - addr: opt.Addr, - tp: opt.Type, - udp: opt.UDP, - xudp: opt.XUDP, - tfo: opt.TFO, - mpTcp: opt.MPTCP, - iface: opt.Interface, - rmark: opt.RoutingMark, - prefer: opt.Prefer, - } -} - -type conn struct { - N.ExtendedConn - chain C.Chain - actualRemoteDestination string -} - -func (c *conn) RemoteDestination() string { - return c.actualRemoteDestination -} - -// 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 (c *conn) Upstream() any { - return c.ExtendedConn -} - -func (c *conn) WriterReplaceable() bool { - return true -} - -func (c *conn) ReaderReplaceable() bool { - return true -} - -func (c *conn) AddRef(ref any) { - c.ExtendedConn = N.NewRefConn(c.ExtendedConn, ref) // add ref for autoCloseProxyAdapter -} - -func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { - if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn - c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly - } - return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())} -} - -type packetConn struct { - N.EnhancePacketConn - chain C.Chain - adapterName string - connID string - actualRemoteDestination string -} - -func (c *packetConn) RemoteDestination() string { - return c.actualRemoteDestination -} - -// 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 (c *packetConn) LocalAddr() net.Addr { - lAddr := c.EnhancePacketConn.LocalAddr() - return N.NewCustomAddr(c.adapterName, c.connID, lAddr) // make quic-go's connMultiplexer happy -} - -func (c *packetConn) Upstream() any { - return c.EnhancePacketConn -} - -func (c *packetConn) WriterReplaceable() bool { - return true -} - -func (c *packetConn) ReaderReplaceable() bool { - return true -} - -func (c *packetConn) AddRef(ref any) { - c.EnhancePacketConn = N.NewRefPacketConn(c.EnhancePacketConn, ref) // add ref for autoCloseProxyAdapter -} - -func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { - epc := N.NewEnhancePacketConn(pc) - if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn - epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly - } - return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())} -} - -func parseRemoteDestination(addr string) string { - if dst, _, err := net.SplitHostPort(addr); err == nil { - return dst - } else { - if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") { - return dst - } else { - return "" - } - } -} - -type AddRef interface { - AddRef(ref any) -} - -type autoCloseProxyAdapter struct { - ProxyAdapter - closeOnce sync.Once - closeErr error -} - -func (p *autoCloseProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - c, err := p.ProxyAdapter.DialContext(ctx, metadata) - if err != nil { - return nil, err - } - if c, ok := c.(AddRef); ok { - c.AddRef(p) - } - return c, nil -} - -func (p *autoCloseProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - c, err := p.ProxyAdapter.DialContextWithDialer(ctx, dialer, metadata) - if err != nil { - return nil, err - } - if c, ok := c.(AddRef); ok { - c.AddRef(p) - } - return c, nil -} - -func (p *autoCloseProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata) - if err != nil { - return nil, err - } - if pc, ok := pc.(AddRef); ok { - pc.AddRef(p) - } - return pc, nil -} - -func (p *autoCloseProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { - pc, err := p.ProxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata) - if err != nil { - return nil, err - } - if pc, ok := pc.(AddRef); ok { - pc.AddRef(p) - } - return pc, nil -} - -func (p *autoCloseProxyAdapter) Close() error { - p.closeOnce.Do(func() { - log.Debugln("Closing outdated proxy [%s]", p.Name()) - runtime.SetFinalizer(p, nil) - p.closeErr = p.ProxyAdapter.Close() - }) - return p.closeErr -} - -func NewAutoCloseProxyAdapter(adapter ProxyAdapter) ProxyAdapter { - proxy := &autoCloseProxyAdapter{ - ProxyAdapter: adapter, - } - // auto close ProxyAdapter - runtime.SetFinalizer(proxy, (*autoCloseProxyAdapter).Close) - return proxy -} diff --git a/adapter/outbound/direct.go b/adapter/outbound/direct.go deleted file mode 100644 index 788a4b933e..0000000000 --- a/adapter/outbound/direct.go +++ /dev/null @@ -1,98 +0,0 @@ -package outbound - -import ( - "context" - "errors" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/loopback" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" -) - -type Direct struct { - *Base - loopBack *loopback.Detector -} - -type DirectOption struct { - BasicOption - Name string `proxy:"name"` -} - -// DialContext implements C.ProxyAdapter -func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - if err := d.loopBack.CheckConn(metadata); err != nil { - return nil, err - } - opts := d.DialOptions() - opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver)) - c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), opts...) - if err != nil { - return nil, err - } - return d.loopBack.NewConn(NewConn(c, d)), nil -} - -// ListenPacketContext implements C.ProxyAdapter -func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - if err := d.loopBack.CheckPacketConn(metadata); err != nil { - return nil, err - } - // net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - pc, err := dialer.NewDialer(d.DialOptions()...).ListenPacket(ctx, "udp", "", metadata.AddrPort()) - if err != nil { - return nil, err - } - return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil -} - -func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool { - return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver -} - -func NewDirectWithOption(option DirectOption) *Direct { - return &Direct{ - Base: &Base{ - name: option.Name, - tp: C.Direct, - udp: true, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - loopBack: loopback.NewDetector(), - } -} - -func NewDirect() *Direct { - return &Direct{ - Base: &Base{ - name: "DIRECT", - tp: C.Direct, - udp: true, - prefer: C.DualStack, - }, - loopBack: loopback.NewDetector(), - } -} - -func NewCompatible() *Direct { - return &Direct{ - Base: &Base{ - name: "COMPATIBLE", - tp: C.Compatible, - udp: true, - prefer: C.DualStack, - }, - loopBack: loopback.NewDetector(), - } -} diff --git a/adapter/outbound/dns.go b/adapter/outbound/dns.go deleted file mode 100644 index 40e70f2590..0000000000 --- a/adapter/outbound/dns.go +++ /dev/null @@ -1,158 +0,0 @@ -package outbound - -import ( - "context" - "net" - "time" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type Dns struct { - *Base -} - -type DnsOption struct { - BasicOption - Name string `proxy:"name"` -} - -// DialContext implements C.ProxyAdapter -func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - left, right := N.Pipe() - go resolver.RelayDnsConn(context.Background(), right, 0) - return NewConn(left, d), nil -} - -// ListenPacketContext implements C.ProxyAdapter -func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort()) - - ctx, cancel := context.WithCancel(context.Background()) - - return newPacketConn(&dnsPacketConn{ - response: make(chan dnsPacket, 1), - ctx: ctx, - cancel: cancel, - }, d), nil -} - -type dnsPacket struct { - data []byte - put func() - addr net.Addr -} - -// dnsPacketConn implements net.PacketConn -type dnsPacketConn struct { - response chan dnsPacket - ctx context.Context - cancel context.CancelFunc -} - -func (d *dnsPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - select { - case packet := <-d.response: - return packet.data, packet.put, packet.addr, nil - case <-d.ctx.Done(): - return nil, nil, nil, net.ErrClosed - } -} - -func (d *dnsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - select { - case packet := <-d.response: - n = copy(p, packet.data) - if packet.put != nil { - packet.put() - } - return n, packet.addr, nil - case <-d.ctx.Done(): - return 0, nil, net.ErrClosed - } -} - -func (d *dnsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - select { - case <-d.ctx.Done(): - return 0, net.ErrClosed - default: - } - - if len(p) > resolver.SafeDnsPacketSize { - // wtf??? - return len(p), nil - } - - buf := pool.Get(resolver.SafeDnsPacketSize) - put := func() { _ = pool.Put(buf) } - copy(buf, p) // avoid p be changed after WriteTo returned - - go func() { // don't block the WriteTo function - ctx, cancel := context.WithTimeout(d.ctx, resolver.DefaultDnsRelayTimeout) - defer cancel() - - buf, err = resolver.RelayDnsPacket(ctx, buf[:len(p)], buf) - if err != nil { - put() - return - } - - packet := dnsPacket{ - data: buf, - put: put, - addr: addr, - } - select { - case d.response <- packet: - break - case <-d.ctx.Done(): - put() - } - }() - return len(p), nil -} - -func (d *dnsPacketConn) Close() error { - d.cancel() - return nil -} - -func (*dnsPacketConn) LocalAddr() net.Addr { - return &net.UDPAddr{ - IP: net.IPv4(127, 0, 0, 1), - Port: 53, - Zone: "", - } -} - -func (*dnsPacketConn) SetDeadline(t time.Time) error { - return nil -} - -func (*dnsPacketConn) SetReadDeadline(t time.Time) error { - return nil -} - -func (*dnsPacketConn) SetWriteDeadline(t time.Time) error { - return nil -} - -func NewDnsWithOption(option DnsOption) *Dns { - return &Dns{ - Base: &Base{ - name: option.Name, - tp: C.Dns, - udp: true, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - } -} diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go deleted file mode 100644 index f02308b920..0000000000 --- a/adapter/outbound/http.go +++ /dev/null @@ -1,195 +0,0 @@ -package outbound - -import ( - "bufio" - "context" - "crypto/tls" - "encoding/base64" - "errors" - "fmt" - "net" - "net/http" - "strconv" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - C "github.com/metacubex/mihomo/constant" -) - -type Http struct { - *Base - user string - pass string - tlsConfig *tls.Config - option *HttpOption -} - -type HttpOption struct { - BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - UserName string `proxy:"username,omitempty"` - Password string `proxy:"password,omitempty"` - TLS bool `proxy:"tls,omitempty"` - SNI string `proxy:"sni,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - Headers map[string]string `proxy:"headers,omitempty"` -} - -// StreamConnContext implements C.ProxyAdapter -func (h *Http) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { - if h.tlsConfig != nil { - cc := tls.Client(c, h.tlsConfig) - err := cc.HandshakeContext(ctx) - c = cc - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", h.addr, err) - } - } - - if err := h.shakeHandContext(ctx, c, metadata); err != nil { - return nil, err - } - return c, nil -} - -// DialContext implements C.ProxyAdapter -func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - return h.DialContextWithDialer(ctx, dialer.NewDialer(h.DialOptions()...), metadata) -} - -// DialContextWithDialer implements C.ProxyAdapter -func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - if len(h.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(h.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", h.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", h.addr, err) - } - - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = h.StreamConnContext(ctx, c, metadata) - if err != nil { - return nil, err - } - - return NewConn(c, h), nil -} - -// SupportWithDialer implements C.ProxyAdapter -func (h *Http) SupportWithDialer() C.NetWork { - return C.TCP -} - -// ProxyInfo implements C.ProxyAdapter -func (h *Http) ProxyInfo() C.ProxyInfo { - info := h.Base.ProxyInfo() - info.DialerProxy = h.option.DialerProxy - return info -} - -func (h *Http) shakeHandContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - - addr := metadata.RemoteAddress() - HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n" - tempHeaders := map[string]string{ - "Host": addr, - "User-Agent": "Go-http-client/1.1", - "Proxy-Connection": "Keep-Alive", - } - - for key, value := range h.option.Headers { - tempHeaders[key] = value - } - - if h.user != "" && h.pass != "" { - auth := h.user + ":" + h.pass - tempHeaders["Proxy-Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) - } - - for key, value := range tempHeaders { - HeaderString += key + ": " + value + "\r\n" - } - - HeaderString += "\r\n" - - _, err = c.Write([]byte(HeaderString)) - - if err != nil { - return err - } - - resp, err := http.ReadResponse(bufio.NewReader(c), nil) - - 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, error) { - var tlsConfig *tls.Config - if option.TLS { - sni := option.Server - if option.SNI != "" { - sni = option.SNI - } - var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ - InsecureSkipVerify: option.SkipCertVerify, - ServerName: sni, - }, option.Fingerprint) - if err != nil { - return nil, err - } - } - - return &Http{ - Base: &Base{ - name: option.Name, - addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), - tp: C.Http, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - user: option.UserName, - pass: option.Password, - tlsConfig: tlsConfig, - option: &option, - }, nil -} diff --git a/adapter/outbound/hysteria.go b/adapter/outbound/hysteria.go deleted file mode 100644 index 55caf58b83..0000000000 --- a/adapter/outbound/hysteria.go +++ /dev/null @@ -1,304 +0,0 @@ -package outbound - -import ( - "context" - "crypto/tls" - "encoding/base64" - "fmt" - "net" - "net/netip" - "strconv" - "time" - - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - hyCongestion "github.com/metacubex/mihomo/transport/hysteria/congestion" - "github.com/metacubex/mihomo/transport/hysteria/core" - "github.com/metacubex/mihomo/transport/hysteria/obfs" - "github.com/metacubex/mihomo/transport/hysteria/pmtud_fix" - "github.com/metacubex/mihomo/transport/hysteria/transport" - "github.com/metacubex/mihomo/transport/hysteria/utils" - - "github.com/metacubex/quic-go" - "github.com/metacubex/quic-go/congestion" - M "github.com/metacubex/sing/common/metadata" -) - -const ( - mbpsToBps = 125000 - - DefaultStreamReceiveWindow = 15728640 // 15 MB/s - DefaultConnectionReceiveWindow = 67108864 // 64 MB/s - - DefaultALPN = "hysteria" - DefaultProtocol = "udp" - DefaultHopInterval = 10 -) - -type Hysteria struct { - *Base - - option *HysteriaOption - client *core.Client -} - -func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx)) - if err != nil { - return nil, err - } - - return NewConn(tcpConn, h), nil -} - -func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - udpConn, err := h.client.DialUDP(h.genHdc(ctx)) - if err != nil { - return nil, err - } - return newPacketConn(&hyPacketConn{udpConn}, h), nil -} - -func (h *Hysteria) genHdc(ctx context.Context) utils.PacketDialer { - return &hyDialerWithContext{ - ctx: context.Background(), - hyDialer: func(network string, rAddr net.Addr) (net.PacketConn, error) { - var err error - var cDialer C.Dialer = dialer.NewDialer(h.DialOptions()...) - if len(h.option.DialerProxy) > 0 { - cDialer, err = proxydialer.NewByName(h.option.DialerProxy, cDialer) - if err != nil { - return nil, err - } - } - rAddrPort, _ := netip.ParseAddrPort(rAddr.String()) - return cDialer.ListenPacket(ctx, network, "", rAddrPort) - }, - remoteAddr: func(addr string) (net.Addr, error) { - return resolveUDPAddr(ctx, "udp", addr, h.prefer) - }, - } -} - -// ProxyInfo implements C.ProxyAdapter -func (h *Hysteria) ProxyInfo() C.ProxyInfo { - info := h.Base.ProxyInfo() - info.DialerProxy = h.option.DialerProxy - return info -} - -type HysteriaOption struct { - BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port,omitempty"` - Ports string `proxy:"ports,omitempty"` - Protocol string `proxy:"protocol,omitempty"` - ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash - Up string `proxy:"up"` - UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash - Down string `proxy:"down"` - DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash - Auth string `proxy:"auth,omitempty"` - AuthString string `proxy:"auth-str,omitempty"` - Obfs string `proxy:"obfs,omitempty"` - SNI string `proxy:"sni,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - ALPN []string `proxy:"alpn,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` - ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` - ReceiveWindow int `proxy:"recv-window,omitempty"` - DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` - FastOpen bool `proxy:"fast-open,omitempty"` - HopInterval int `proxy:"hop-interval,omitempty"` -} - -func (c *HysteriaOption) Speed() (uint64, uint64, error) { - var up, down uint64 - up = StringToBps(c.Up) - if up == 0 { - return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up) - } - - down = StringToBps(c.Down) - if down == 0 { - return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down) - } - - return up, down, nil -} - -func NewHysteria(option HysteriaOption) (*Hysteria, error) { - clientTransport := &transport.ClientTransport{} - addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - ports := option.Ports - - serverName := option.Server - if option.SNI != "" { - serverName = option.SNI - } - - tlsConfig := &tls.Config{ - ServerName: serverName, - InsecureSkipVerify: option.SkipCertVerify, - MinVersion: tls.VersionTLS13, - } - - var err error - tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) - if err != nil { - return nil, err - } - - if len(option.ALPN) > 0 { - tlsConfig.NextProtos = option.ALPN - } else { - tlsConfig.NextProtos = []string{DefaultALPN} - } - quicConfig := &quic.Config{ - InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), - MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), - InitialConnectionReceiveWindow: uint64(option.ReceiveWindow), - MaxConnectionReceiveWindow: uint64(option.ReceiveWindow), - KeepAlivePeriod: 10 * time.Second, - DisablePathMTUDiscovery: option.DisableMTUDiscovery, - EnableDatagrams: true, - } - if option.ObfsProtocol != "" { - option.Protocol = option.ObfsProtocol - } - if option.Protocol == "" { - option.Protocol = DefaultProtocol - } - if option.HopInterval == 0 { - option.HopInterval = DefaultHopInterval - } - hopInterval := time.Duration(int64(option.HopInterval)) * time.Second - if option.ReceiveWindow == 0 { - quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow / 10 - quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow - } - if option.ReceiveWindow == 0 { - quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow / 10 - quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow - } - if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery { - log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform") - } - - var auth = []byte(option.AuthString) - if option.Auth != "" { - auth, err = base64.StdEncoding.DecodeString(option.Auth) - if err != nil { - return nil, err - } - } - var obfuscator obfs.Obfuscator - if len(option.Obfs) > 0 { - obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs)) - } - - up, down, err := option.Speed() - if err != nil { - return nil, err - } - if option.UpSpeed != 0 { - up = uint64(option.UpSpeed * mbpsToBps) - } - if option.DownSpeed != 0 { - down = uint64(option.DownSpeed * mbpsToBps) - } - client, err := core.NewClient( - addr, ports, option.Protocol, auth, tlsC.UConfig(tlsConfig), quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl { - return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) - }, obfuscator, hopInterval, option.FastOpen, - ) - if err != nil { - return nil, fmt.Errorf("hysteria %s create error: %w", addr, err) - } - outbound := &Hysteria{ - Base: &Base{ - name: option.Name, - addr: addr, - tp: C.Hysteria, - udp: true, - tfo: option.FastOpen, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - client: client, - } - - return outbound, nil -} - -// Close implements C.ProxyAdapter -func (h *Hysteria) Close() error { - if h.client != nil { - return h.client.Close() - } - return nil -} - -type hyPacketConn struct { - core.UDPConn -} - -func (c *hyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - b, addrStr, err := c.UDPConn.ReadFrom() - if err != nil { - return - } - n = copy(p, b) - addr = M.ParseSocksaddr(addrStr).UDPAddr() - return -} - -func (c *hyPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - b, addrStr, err := c.UDPConn.ReadFrom() - if err != nil { - return - } - data = b - addr = M.ParseSocksaddr(addrStr).UDPAddr() - return -} - -func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String()) - if err != nil { - return - } - n = len(p) - return -} - -type hyDialerWithContext struct { - hyDialer func(network string, rAddr net.Addr) (net.PacketConn, error) - ctx context.Context - remoteAddr func(host string) (net.Addr, error) -} - -func (h *hyDialerWithContext) ListenPacket(rAddr net.Addr) (net.PacketConn, error) { - network := "udp" - if addrPort, err := netip.ParseAddrPort(rAddr.String()); err == nil { - network = dialer.ParseNetwork(network, addrPort.Addr()) - } - return h.hyDialer(network, rAddr) -} - -func (h *hyDialerWithContext) Context() context.Context { - return h.ctx -} - -func (h *hyDialerWithContext) RemoteAddr(host string) (net.Addr, error) { - return h.remoteAddr(host) -} diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go deleted file mode 100644 index 131a8b3c03..0000000000 --- a/adapter/outbound/hysteria2.go +++ /dev/null @@ -1,223 +0,0 @@ -package outbound - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net" - "strconv" - "time" - - CN "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - tuicCommon "github.com/metacubex/mihomo/transport/tuic/common" - - "github.com/metacubex/quic-go" - "github.com/metacubex/randv2" - "github.com/metacubex/sing-quic/hysteria2" - M "github.com/metacubex/sing/common/metadata" -) - -func init() { - hysteria2.SetCongestionController = tuicCommon.SetCongestionController -} - -const minHopInterval = 5 -const defaultHopInterval = 30 - -type Hysteria2 struct { - *Base - - option *Hysteria2Option - client *hysteria2.Client - dialer proxydialer.SingDialer -} - -type Hysteria2Option struct { - BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port,omitempty"` - Ports string `proxy:"ports,omitempty"` - HopInterval int `proxy:"hop-interval,omitempty"` - Up string `proxy:"up,omitempty"` - Down string `proxy:"down,omitempty"` - Password string `proxy:"password,omitempty"` - Obfs string `proxy:"obfs,omitempty"` - ObfsPassword string `proxy:"obfs-password,omitempty"` - SNI string `proxy:"sni,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - ALPN []string `proxy:"alpn,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` - CWND int `proxy:"cwnd,omitempty"` - UdpMTU int `proxy:"udp-mtu,omitempty"` - - // quic-go special config - InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"` - MaxStreamReceiveWindow uint64 `proxy:"max-stream-receive-window,omitempty"` - InitialConnectionReceiveWindow uint64 `proxy:"initial-connection-receive-window,omitempty"` - MaxConnectionReceiveWindow uint64 `proxy:"max-connection-receive-window,omitempty"` -} - -func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) - if err != nil { - return nil, err - } - return NewConn(c, h), nil -} - -func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - pc, err := h.client.ListenPacket(ctx) - if err != nil { - return nil, err - } - if pc == nil { - return nil, errors.New("packetConn is nil") - } - return newPacketConn(CN.NewThreadSafePacketConn(pc), h), nil -} - -// Close implements C.ProxyAdapter -func (h *Hysteria2) Close() error { - if h.client != nil { - return h.client.CloseWithError(errors.New("proxy removed")) - } - return nil -} - -// ProxyInfo implements C.ProxyAdapter -func (h *Hysteria2) ProxyInfo() C.ProxyInfo { - info := h.Base.ProxyInfo() - info.DialerProxy = h.option.DialerProxy - return info -} - -func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { - addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - outbound := &Hysteria2{ - Base: &Base{ - name: option.Name, - addr: addr, - tp: C.Hysteria2, - udp: true, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - } - - singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer(outbound.DialOptions()...)) - outbound.dialer = singDialer - - var salamanderPassword string - if len(option.Obfs) > 0 { - if option.ObfsPassword == "" { - return nil, errors.New("missing obfs password") - } - switch option.Obfs { - case hysteria2.ObfsTypeSalamander: - salamanderPassword = option.ObfsPassword - default: - return nil, fmt.Errorf("unknown obfs type: %s", option.Obfs) - } - } - - serverName := option.Server - if option.SNI != "" { - serverName = option.SNI - } - - tlsConfig := &tls.Config{ - ServerName: serverName, - InsecureSkipVerify: option.SkipCertVerify, - MinVersion: tls.VersionTLS13, - } - - var err error - tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) - if err != nil { - return nil, err - } - - if len(option.ALPN) > 0 { - tlsConfig.NextProtos = option.ALPN - } - - if option.UdpMTU == 0 { - // "1200" from quic-go's MaxDatagramSize - // "-3" from quic-go's DatagramFrame.MaxDataLen - option.UdpMTU = 1200 - 3 - } - - quicConfig := &quic.Config{ - InitialStreamReceiveWindow: option.InitialStreamReceiveWindow, - MaxStreamReceiveWindow: option.MaxStreamReceiveWindow, - InitialConnectionReceiveWindow: option.InitialConnectionReceiveWindow, - MaxConnectionReceiveWindow: option.MaxConnectionReceiveWindow, - } - - clientOptions := hysteria2.ClientOptions{ - Context: context.TODO(), - Dialer: singDialer, - Logger: log.SingLogger, - SendBPS: StringToBps(option.Up), - ReceiveBPS: StringToBps(option.Down), - SalamanderPassword: salamanderPassword, - Password: option.Password, - TLSConfig: tlsC.UConfig(tlsConfig), - QUICConfig: quicConfig, - UDPDisabled: false, - CWND: option.CWND, - UdpMTU: option.UdpMTU, - ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) { - return resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion)) - }, - } - - var ranges utils.IntRanges[uint16] - var serverAddress []string - if option.Ports != "" { - ranges, err = utils.NewUnsignedRanges[uint16](option.Ports) - if err != nil { - return nil, err - } - ranges.Range(func(port uint16) bool { - serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port)))) - return true - }) - if len(serverAddress) > 0 { - clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) { - return resolveUDPAddr(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion)) - } - - if option.HopInterval == 0 { - option.HopInterval = defaultHopInterval - } else if option.HopInterval < minHopInterval { - option.HopInterval = minHopInterval - } - clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second - } - } - if option.Port == 0 && len(serverAddress) == 0 { - return nil, errors.New("invalid port") - } - - client, err := hysteria2.NewClient(clientOptions) - if err != nil { - return nil, err - } - outbound.client = client - - return outbound, nil -} diff --git a/adapter/outbound/mieru.go b/adapter/outbound/mieru.go deleted file mode 100644 index 1d8c78f751..0000000000 --- a/adapter/outbound/mieru.go +++ /dev/null @@ -1,301 +0,0 @@ -package outbound - -import ( - "context" - "fmt" - "net" - "strconv" - "sync" - - CN "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - C "github.com/metacubex/mihomo/constant" - - mieruclient "github.com/enfein/mieru/v3/apis/client" - mierucommon "github.com/enfein/mieru/v3/apis/common" - mierumodel "github.com/enfein/mieru/v3/apis/model" - mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb" - "google.golang.org/protobuf/proto" -) - -type Mieru struct { - *Base - option *MieruOption - client mieruclient.Client - mu sync.Mutex -} - -type MieruOption struct { - BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port,omitempty"` - PortRange string `proxy:"port-range,omitempty"` - Transport string `proxy:"transport"` - UDP bool `proxy:"udp,omitempty"` - UserName string `proxy:"username"` - Password string `proxy:"password"` - Multiplexing string `proxy:"multiplexing,omitempty"` -} - -// DialContext implements C.ProxyAdapter -func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - if err := m.ensureClientIsRunning(); err != nil { - return nil, err - } - addr := metadataToMieruNetAddrSpec(metadata) - c, err := m.client.DialContext(ctx, addr) - if err != nil { - return nil, fmt.Errorf("dial to %s failed: %w", addr, err) - } - return NewConn(c, m), nil -} - -// ListenPacketContext implements C.ProxyAdapter -func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - if err := m.ensureClientIsRunning(); err != nil { - return nil, err - } - c, err := m.client.DialContext(ctx, metadata.UDPAddr()) - if err != nil { - return nil, fmt.Errorf("dial to %s failed: %w", metadata.UDPAddr(), err) - } - return newPacketConn(CN.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil -} - -// SupportUOT implements C.ProxyAdapter -func (m *Mieru) SupportUOT() bool { - return true -} - -// ProxyInfo implements C.ProxyAdapter -func (m *Mieru) ProxyInfo() C.ProxyInfo { - info := m.Base.ProxyInfo() - info.DialerProxy = m.option.DialerProxy - return info -} - -func (m *Mieru) ensureClientIsRunning() error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.client.IsRunning() { - return nil - } - - // Create a dialer and add it to the client config, before starting the client. - var dialer C.Dialer = dialer.NewDialer(m.DialOptions()...) - var err error - if len(m.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(m.option.DialerProxy, dialer) - if err != nil { - return err - } - } - config, err := m.client.Load() - if err != nil { - return err - } - config.Dialer = dialer - if err := m.client.Store(config); err != nil { - return err - } - - if err := m.client.Start(); err != nil { - return fmt.Errorf("failed to start mieru client: %w", err) - } - return nil -} - -func NewMieru(option MieruOption) (*Mieru, error) { - config, err := buildMieruClientConfig(option) - if err != nil { - return nil, fmt.Errorf("failed to build mieru client config: %w", err) - } - c := mieruclient.NewClient() - if err := c.Store(config); err != nil { - return nil, fmt.Errorf("failed to store mieru client config: %w", err) - } - // Client is started lazily on the first use. - - var addr string - if option.Port != 0 { - addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - } else { - beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange) - addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort)) - } - outbound := &Mieru{ - Base: &Base{ - name: option.Name, - addr: addr, - iface: option.Interface, - tp: C.Mieru, - udp: option.UDP, - xudp: false, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - client: c, - } - return outbound, nil -} - -// Close implements C.ProxyAdapter -func (m *Mieru) Close() error { - m.mu.Lock() - defer m.mu.Unlock() - if m.client != nil && m.client.IsRunning() { - return m.client.Stop() - } - return nil -} - -func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec { - if metadata.Host != "" { - return mierumodel.NetAddrSpec{ - AddrSpec: mierumodel.AddrSpec{ - FQDN: metadata.Host, - Port: int(metadata.DstPort), - }, - Net: "tcp", - } - } else { - return mierumodel.NetAddrSpec{ - AddrSpec: mierumodel.AddrSpec{ - IP: metadata.DstIP.AsSlice(), - Port: int(metadata.DstPort), - }, - Net: "tcp", - } - } -} - -func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, error) { - if err := validateMieruOption(option); err != nil { - return nil, fmt.Errorf("failed to validate mieru option: %w", err) - } - - transportProtocol := mierupb.TransportProtocol_TCP.Enum() - var server *mierupb.ServerEndpoint - if net.ParseIP(option.Server) != nil { - // server is an IP address - if option.PortRange != "" { - server = &mierupb.ServerEndpoint{ - IpAddress: proto.String(option.Server), - PortBindings: []*mierupb.PortBinding{ - { - PortRange: proto.String(option.PortRange), - Protocol: transportProtocol, - }, - }, - } - } else { - server = &mierupb.ServerEndpoint{ - IpAddress: proto.String(option.Server), - PortBindings: []*mierupb.PortBinding{ - { - Port: proto.Int32(int32(option.Port)), - Protocol: transportProtocol, - }, - }, - } - } - } else { - // server is a domain name - if option.PortRange != "" { - server = &mierupb.ServerEndpoint{ - DomainName: proto.String(option.Server), - PortBindings: []*mierupb.PortBinding{ - { - PortRange: proto.String(option.PortRange), - Protocol: transportProtocol, - }, - }, - } - } else { - server = &mierupb.ServerEndpoint{ - DomainName: proto.String(option.Server), - PortBindings: []*mierupb.PortBinding{ - { - Port: proto.Int32(int32(option.Port)), - Protocol: transportProtocol, - }, - }, - } - } - } - config := &mieruclient.ClientConfig{ - Profile: &mierupb.ClientProfile{ - ProfileName: proto.String(option.Name), - User: &mierupb.User{ - Name: proto.String(option.UserName), - Password: proto.String(option.Password), - }, - Servers: []*mierupb.ServerEndpoint{server}, - }, - } - if multiplexing, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; ok { - config.Profile.Multiplexing = &mierupb.MultiplexingConfig{ - Level: mierupb.MultiplexingLevel(multiplexing).Enum(), - } - } - return config, nil -} - -func validateMieruOption(option MieruOption) error { - if option.Name == "" { - return fmt.Errorf("name is empty") - } - if option.Server == "" { - return fmt.Errorf("server is empty") - } - if option.Port == 0 && option.PortRange == "" { - return fmt.Errorf("either port or port-range must be set") - } - if option.Port != 0 && option.PortRange != "" { - return fmt.Errorf("port and port-range cannot be set at the same time") - } - if option.Port != 0 && (option.Port < 1 || option.Port > 65535) { - return fmt.Errorf("port must be between 1 and 65535") - } - if option.PortRange != "" { - begin, end, err := beginAndEndPortFromPortRange(option.PortRange) - if err != nil { - return fmt.Errorf("invalid port-range format") - } - if begin < 1 || begin > 65535 { - return fmt.Errorf("begin port must be between 1 and 65535") - } - if end < 1 || end > 65535 { - return fmt.Errorf("end port must be between 1 and 65535") - } - if begin > end { - return fmt.Errorf("begin port must be less than or equal to end port") - } - } - - if option.Transport != "TCP" { - return fmt.Errorf("transport must be TCP") - } - if option.UserName == "" { - return fmt.Errorf("username is empty") - } - if option.Password == "" { - return fmt.Errorf("password is empty") - } - if option.Multiplexing != "" { - if _, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; !ok { - return fmt.Errorf("invalid multiplexing level: %s", option.Multiplexing) - } - } - return nil -} - -func beginAndEndPortFromPortRange(portRange string) (int, int, error) { - var begin, end int - _, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end) - return begin, end, err -} diff --git a/adapter/outbound/mieru_test.go b/adapter/outbound/mieru_test.go deleted file mode 100644 index 086b791044..0000000000 --- a/adapter/outbound/mieru_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package outbound - -import "testing" - -func TestNewMieru(t *testing.T) { - testCases := []struct { - option MieruOption - wantBaseAddr string - }{ - { - option: MieruOption{ - Name: "test", - Server: "1.2.3.4", - Port: 10000, - Transport: "TCP", - UserName: "test", - Password: "test", - }, - wantBaseAddr: "1.2.3.4:10000", - }, - { - option: MieruOption{ - Name: "test", - Server: "2001:db8::1", - PortRange: "10001-10002", - Transport: "TCP", - UserName: "test", - Password: "test", - }, - wantBaseAddr: "[2001:db8::1]:10001", - }, - { - option: MieruOption{ - Name: "test", - Server: "example.com", - Port: 10003, - Transport: "TCP", - UserName: "test", - Password: "test", - }, - wantBaseAddr: "example.com:10003", - }, - } - - for _, testCase := range testCases { - mieru, err := NewMieru(testCase.option) - if err != nil { - t.Error(err) - } - if mieru.addr != testCase.wantBaseAddr { - t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr) - } - } -} - -func TestBeginAndEndPortFromPortRange(t *testing.T) { - testCases := []struct { - input string - begin int - end int - hasErr bool - }{ - {"1-10", 1, 10, false}, - {"1000-2000", 1000, 2000, false}, - {"65535-65535", 65535, 65535, false}, - {"1", 0, 0, true}, - {"1-", 0, 0, true}, - {"-10", 0, 0, true}, - {"a-b", 0, 0, true}, - {"1-b", 0, 0, true}, - {"a-10", 0, 0, true}, - } - - for _, testCase := range testCases { - begin, end, err := beginAndEndPortFromPortRange(testCase.input) - if testCase.hasErr { - if err == nil { - t.Errorf("beginAndEndPortFromPortRange(%s) should return an error", testCase.input) - } - } else { - if err != nil { - t.Errorf("beginAndEndPortFromPortRange(%s) should not return an error, but got %v", testCase.input, err) - } - if begin != testCase.begin { - t.Errorf("beginAndEndPortFromPortRange(%s) begin port mismatch, got %d, want %d", testCase.input, begin, testCase.begin) - } - if end != testCase.end { - t.Errorf("beginAndEndPortFromPortRange(%s) end port mismatch, got %d, want %d", testCase.input, end, testCase.end) - } - } - } -} diff --git a/adapter/outbound/reality.go b/adapter/outbound/reality.go deleted file mode 100644 index e5b090a8b8..0000000000 --- a/adapter/outbound/reality.go +++ /dev/null @@ -1,44 +0,0 @@ -package outbound - -import ( - "crypto/ecdh" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - - tlsC "github.com/metacubex/mihomo/component/tls" -) - -type RealityOptions struct { - PublicKey string `proxy:"public-key"` - ShortID string `proxy:"short-id"` -} - -func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) { - if o.PublicKey != "" { - config := new(tlsC.RealityConfig) - - const x25519ScalarSize = 32 - publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey) - if err != nil || len(publicKey) != x25519ScalarSize { - return nil, errors.New("invalid REALITY public key") - } - config.PublicKey, err = ecdh.X25519().NewPublicKey(publicKey) - if err != nil { - return nil, fmt.Errorf("fail to create REALITY public key: %w", err) - } - - n := hex.DecodedLen(len(o.ShortID)) - if n > tlsC.RealityMaxShortIDLen { - return nil, errors.New("invalid REALITY short id") - } - n, err = hex.Decode(config.ShortID[:], []byte(o.ShortID)) - if err != nil || n > tlsC.RealityMaxShortIDLen { - return nil, errors.New("invalid REALITY short ID") - } - - return config, nil - } - return nil, nil -} diff --git a/adapter/outbound/reject.go b/adapter/outbound/reject.go deleted file mode 100644 index da3a4e3c60..0000000000 --- a/adapter/outbound/reject.go +++ /dev/null @@ -1,127 +0,0 @@ -package outbound - -import ( - "context" - "io" - "net" - "time" - - "github.com/metacubex/mihomo/common/buf" - C "github.com/metacubex/mihomo/constant" -) - -type Reject struct { - *Base - drop bool -} - -type RejectOption struct { - Name string `proxy:"name"` -} - -// DialContext implements C.ProxyAdapter -func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - if r.drop { - return NewConn(dropConn{}, r), nil - } - return NewConn(nopConn{}, r), nil -} - -// ListenPacketContext implements C.ProxyAdapter -func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - return newPacketConn(&nopPacketConn{}, r), nil -} - -func NewRejectWithOption(option RejectOption) *Reject { - return &Reject{ - Base: &Base{ - name: option.Name, - tp: C.Reject, - udp: true, - }, - } -} - -func NewReject() *Reject { - return &Reject{ - Base: &Base{ - name: "REJECT", - tp: C.Reject, - udp: true, - prefer: C.DualStack, - }, - } -} - -func NewRejectDrop() *Reject { - return &Reject{ - Base: &Base{ - name: "REJECT-DROP", - tp: C.RejectDrop, - udp: true, - prefer: C.DualStack, - }, - drop: true, - } -} - -func NewPass() *Reject { - return &Reject{ - Base: &Base{ - name: "PASS", - tp: C.Pass, - udp: true, - prefer: C.DualStack, - }, - } -} - -type nopConn struct{} - -func (rw nopConn) Read(b []byte) (int, error) { return 0, io.EOF } - -func (rw nopConn) ReadBuffer(buffer *buf.Buffer) error { return io.EOF } - -func (rw nopConn) Write(b []byte) (int, error) { return 0, io.EOF } -func (rw nopConn) WriteBuffer(buffer *buf.Buffer) error { return 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 } - -var udpAddrIPv4Unspecified = &net.UDPAddr{IP: net.IPv4zero, Port: 0} - -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) WaitReadFrom() ([]byte, func(), net.Addr, error) { - return nil, nil, nil, io.EOF -} -func (npc nopPacketConn) Close() error { return nil } -func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified } -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 } - -type dropConn struct{} - -func (rw dropConn) Read(b []byte) (int, error) { return 0, io.EOF } -func (rw dropConn) ReadBuffer(buffer *buf.Buffer) error { - time.Sleep(C.DefaultDropTime) - return io.EOF -} -func (rw dropConn) Write(b []byte) (int, error) { return 0, io.EOF } -func (rw dropConn) WriteBuffer(buffer *buf.Buffer) error { return io.EOF } -func (rw dropConn) Close() error { return nil } -func (rw dropConn) LocalAddr() net.Addr { return nil } -func (rw dropConn) RemoteAddr() net.Addr { return nil } -func (rw dropConn) SetDeadline(time.Time) error { return nil } -func (rw dropConn) SetReadDeadline(time.Time) error { return nil } -func (rw dropConn) SetWriteDeadline(time.Time) error { return nil } diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go deleted file mode 100644 index 0b215ca45f..0000000000 --- a/adapter/outbound/shadowsocks.go +++ /dev/null @@ -1,395 +0,0 @@ -package outbound - -import ( - "context" - "errors" - "fmt" - "net" - "strconv" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/structure" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - gost "github.com/metacubex/mihomo/transport/gost-plugin" - "github.com/metacubex/mihomo/transport/restls" - obfs "github.com/metacubex/mihomo/transport/simple-obfs" - shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" - v2rayObfs "github.com/metacubex/mihomo/transport/v2ray-plugin" - - shadowsocks "github.com/metacubex/sing-shadowsocks2" - "github.com/metacubex/sing/common/bufio" - M "github.com/metacubex/sing/common/metadata" - "github.com/metacubex/sing/common/uot" -) - -type ShadowSocks struct { - *Base - method shadowsocks.Method - - option *ShadowSocksOption - // obfs - obfsMode string - obfsOption *simpleObfsOption - v2rayOption *v2rayObfs.Option - gostOption *gost.Option - shadowTLSOption *shadowtls.ShadowTLSOption - restlsConfig *restls.Config -} - -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"` - UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"` - UDPOverTCPVersion int `proxy:"udp-over-tcp-version,omitempty"` - ClientFingerprint string `proxy:"client-fingerprint,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"` - Fingerprint string `obfs:"fingerprint,omitempty"` - Headers map[string]string `obfs:"headers,omitempty"` - SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` - Mux bool `obfs:"mux,omitempty"` - V2rayHttpUpgrade bool `obfs:"v2ray-http-upgrade,omitempty"` - V2rayHttpUpgradeFastOpen bool `obfs:"v2ray-http-upgrade-fast-open,omitempty"` -} - -type gostObfsOption struct { - Mode string `obfs:"mode"` - Host string `obfs:"host,omitempty"` - Path string `obfs:"path,omitempty"` - TLS bool `obfs:"tls,omitempty"` - Fingerprint string `obfs:"fingerprint,omitempty"` - Headers map[string]string `obfs:"headers,omitempty"` - SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` - Mux bool `obfs:"mux,omitempty"` -} - -type shadowTLSOption struct { - Password string `obfs:"password,omitempty"` - Host string `obfs:"host"` - Fingerprint string `obfs:"fingerprint,omitempty"` - SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` - Version int `obfs:"version,omitempty"` - ALPN []string `obfs:"alpn,omitempty"` -} - -type restlsOption struct { - Password string `obfs:"password"` - Host string `obfs:"host"` - VersionHint string `obfs:"version-hint"` - RestlsScript string `obfs:"restls-script,omitempty"` -} - -// StreamConnContext implements C.ProxyAdapter -func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { - useEarly := false - 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": - if ss.v2rayOption != nil { - c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption) - } else if ss.gostOption != nil { - c, err = gost.NewGostWebsocket(ctx, c, ss.gostOption) - } else { - return nil, fmt.Errorf("plugin options is required") - } - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) - } - case shadowtls.Mode: - c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption) - if err != nil { - return nil, err - } - useEarly = true - case restls.Mode: - c, err = restls.NewRestls(ctx, c, ss.restlsConfig) - if err != nil { - return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err) - } - useEarly = true - } - useEarly = useEarly || N.NeedHandshake(c) - if !useEarly { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - } - if metadata.NetWork == C.UDP && ss.option.UDPOverTCP { - uotDestination := uot.RequestDestination(uint8(ss.option.UDPOverTCPVersion)) - if useEarly { - return ss.method.DialEarlyConn(c, uotDestination), nil - } else { - return ss.method.DialConn(c, uotDestination) - } - } - if useEarly { - return ss.method.DialEarlyConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)), nil - } else { - return ss.method.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) - } -} - -// DialContext implements C.ProxyAdapter -func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), metadata) -} - -// DialContextWithDialer implements C.ProxyAdapter -func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - if len(ss.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", ss.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) - } - - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = ss.StreamConnContext(ctx, c, metadata) - return NewConn(c, ss), err -} - -// ListenPacketContext implements C.ProxyAdapter -func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - return ss.ListenPacketWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), metadata) -} - -// ListenPacketWithDialer implements C.ProxyAdapter -func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { - if ss.option.UDPOverTCP { - tcpConn, err := ss.DialContextWithDialer(ctx, dialer, metadata) - if err != nil { - return nil, err - } - return ss.ListenPacketOnStreamConn(ctx, tcpConn, metadata) - } - if len(ss.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer) - if err != nil { - return nil, err - } - - pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) - if err != nil { - return nil, err - } - pc = ss.method.DialPacketConn(bufio.NewBindPacketConn(pc, addr)) - return newPacketConn(pc, ss), nil -} - -// SupportWithDialer implements C.ProxyAdapter -func (ss *ShadowSocks) SupportWithDialer() C.NetWork { - return C.ALLNet -} - -// ProxyInfo implements C.ProxyAdapter -func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo { - info := ss.Base.ProxyInfo() - info.DialerProxy = ss.option.DialerProxy - return info -} - -// ListenPacketOnStreamConn implements C.ProxyAdapter -func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { - if ss.option.UDPOverTCP { - // ss uot use stream-oriented udp with a special address, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - - destination := M.SocksaddrFromNet(metadata.UDPAddr()) - if ss.option.UDPOverTCPVersion == uot.LegacyVersion { - return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil - } else { - return newPacketConn(N.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), ss), nil - } - } - return nil, C.ErrNotSupport -} - -// SupportUOT implements C.ProxyAdapter -func (ss *ShadowSocks) SupportUOT() bool { - return ss.option.UDPOverTCP -} - -func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { - addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - method, err := shadowsocks.CreateMethod(context.Background(), option.Cipher, shadowsocks.MethodOptions{ - Password: option.Password, - }) - if err != nil { - return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err) - } - - var v2rayOption *v2rayObfs.Option - var gostOption *gost.Option - var obfsOption *simpleObfsOption - var shadowTLSOpt *shadowtls.ShadowTLSOption - var restlsConfig *restls.Config - 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, - V2rayHttpUpgrade: opts.V2rayHttpUpgrade, - V2rayHttpUpgradeFastOpen: opts.V2rayHttpUpgradeFastOpen, - } - - if opts.TLS { - v2rayOption.TLS = true - v2rayOption.SkipCertVerify = opts.SkipCertVerify - v2rayOption.Fingerprint = opts.Fingerprint - } - } else if option.Plugin == "gost-plugin" { - opts := gostObfsOption{Host: "bing.com", Mux: true} - if err := decoder.Decode(option.PluginOpts, &opts); err != nil { - return nil, fmt.Errorf("ss %s initialize gost-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 - gostOption = &gost.Option{ - Host: opts.Host, - Path: opts.Path, - Headers: opts.Headers, - Mux: opts.Mux, - } - - if opts.TLS { - gostOption.TLS = true - gostOption.SkipCertVerify = opts.SkipCertVerify - gostOption.Fingerprint = opts.Fingerprint - } - } else if option.Plugin == shadowtls.Mode { - obfsMode = shadowtls.Mode - opt := &shadowTLSOption{ - Version: 2, - } - if err := decoder.Decode(option.PluginOpts, opt); err != nil { - return nil, fmt.Errorf("ss %s initialize shadow-tls-plugin error: %w", addr, err) - } - - shadowTLSOpt = &shadowtls.ShadowTLSOption{ - Password: opt.Password, - Host: opt.Host, - Fingerprint: opt.Fingerprint, - ClientFingerprint: option.ClientFingerprint, - SkipCertVerify: opt.SkipCertVerify, - Version: opt.Version, - } - - if opt.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array - shadowTLSOpt.ALPN = opt.ALPN - } else { - shadowTLSOpt.ALPN = shadowtls.DefaultALPN - } - } else if option.Plugin == restls.Mode { - obfsMode = restls.Mode - restlsOpt := &restlsOption{} - if err := decoder.Decode(option.PluginOpts, restlsOpt); err != nil { - return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) - } - - restlsConfig, err = restls.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint) - if err != nil { - return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) - } - - } - switch option.UDPOverTCPVersion { - case uot.Version, uot.LegacyVersion: - case 0: - option.UDPOverTCPVersion = uot.LegacyVersion - default: - return nil, fmt.Errorf("ss %s unknown udp over tcp protocol version: %d", addr, option.UDPOverTCPVersion) - } - - return &ShadowSocks{ - Base: &Base{ - name: option.Name, - addr: addr, - tp: C.Shadowsocks, - udp: option.UDP, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - method: method, - - option: &option, - obfsMode: obfsMode, - v2rayOption: v2rayOption, - gostOption: gostOption, - obfsOption: obfsOption, - shadowTLSOption: shadowTLSOpt, - restlsConfig: restlsConfig, - }, nil -} diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go deleted file mode 100644 index d7b932e6a6..0000000000 --- a/adapter/outbound/shadowsocksr.go +++ /dev/null @@ -1,262 +0,0 @@ -package outbound - -import ( - "context" - "errors" - "fmt" - "net" - "strconv" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/shadowsocks/core" - "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" - "github.com/metacubex/mihomo/transport/shadowsocks/shadowstream" - "github.com/metacubex/mihomo/transport/socks5" - "github.com/metacubex/mihomo/transport/ssr/obfs" - "github.com/metacubex/mihomo/transport/ssr/protocol" -) - -type ShadowSocksR struct { - *Base - option *ShadowSocksROption - 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"` -} - -// StreamConnContext implements C.ProxyAdapter -func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - c = ssr.obfs.StreamConn(c) - c = ssr.cipher.StreamConn(c) - var ( - iv []byte - ) - 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) (_ C.Conn, err error) { - return ssr.DialContextWithDialer(ctx, dialer.NewDialer(ssr.DialOptions()...), metadata) -} - -// DialContextWithDialer implements C.ProxyAdapter -func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - if len(ssr.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(ssr.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", ssr.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) - } - - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = ssr.StreamConnContext(ctx, c, metadata) - return NewConn(c, ssr), err -} - -// ListenPacketContext implements C.ProxyAdapter -func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - return ssr.ListenPacketWithDialer(ctx, dialer.NewDialer(ssr.DialOptions()...), metadata) -} - -// ListenPacketWithDialer implements C.ProxyAdapter -func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { - if len(ssr.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(ssr.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer) - if err != nil { - return nil, err - } - - pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) - if err != nil { - return nil, err - } - - epc := ssr.cipher.PacketConn(N.NewEnhancePacketConn(pc)) - epc = ssr.protocol.PacketConn(epc) - return newPacketConn(&ssrPacketConn{EnhancePacketConn: epc, rAddr: addr}, ssr), nil -} - -// SupportWithDialer implements C.ProxyAdapter -func (ssr *ShadowSocksR) SupportWithDialer() C.NetWork { - return C.ALLNet -} - -// ProxyInfo implements C.ProxyAdapter -func (ssr *ShadowSocksR) ProxyInfo() C.ProxyInfo { - info := ssr.Base.ProxyInfo() - info.DialerProxy = ssr.option.DialerProxy - return info -} - -func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { - // SSR protocol compatibility - // https://github.com/metacubex/mihomo/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 cipher: %s initialize error: %w", addr, cipher, 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, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - cipher: coreCiph, - obfs: obfs, - protocol: protocol, - }, nil -} - -type ssrPacketConn struct { - N.EnhancePacketConn - rAddr net.Addr -} - -func (spc *ssrPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { - packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) - if err != nil { - return - } - return spc.EnhancePacketConn.WriteTo(packet[3:], spc.rAddr) -} - -func (spc *ssrPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { - n, _, e := spc.EnhancePacketConn.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 -} - -func (spc *ssrPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - data, put, _, err = spc.EnhancePacketConn.WaitReadFrom() - if err != nil { - return nil, nil, nil, err - } - - _addr := socks5.SplitAddr(data) - if _addr == nil { - if put != nil { - put() - } - return nil, nil, nil, errors.New("parse addr error") - } - - addr = _addr.UDPAddr() - if addr == nil { - if put != nil { - put() - } - return nil, nil, nil, errors.New("parse addr error") - } - - data = data[len(_addr):] - return -} diff --git a/adapter/outbound/singmux.go b/adapter/outbound/singmux.go deleted file mode 100644 index 819f23877c..0000000000 --- a/adapter/outbound/singmux.go +++ /dev/null @@ -1,133 +0,0 @@ -package outbound - -import ( - "context" - "errors" - - CN "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - mux "github.com/metacubex/sing-mux" - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/sing/common/metadata" -) - -type SingMux struct { - ProxyAdapter - client *mux.Client - dialer proxydialer.SingDialer - onlyTcp bool -} - -type SingMuxOption struct { - Enabled bool `proxy:"enabled,omitempty"` - Protocol string `proxy:"protocol,omitempty"` - MaxConnections int `proxy:"max-connections,omitempty"` - MinStreams int `proxy:"min-streams,omitempty"` - MaxStreams int `proxy:"max-streams,omitempty"` - Padding bool `proxy:"padding,omitempty"` - Statistic bool `proxy:"statistic,omitempty"` - OnlyTcp bool `proxy:"only-tcp,omitempty"` - BrutalOpts BrutalOption `proxy:"brutal-opts,omitempty"` -} - -type BrutalOption struct { - Enabled bool `proxy:"enabled,omitempty"` - Up string `proxy:"up,omitempty"` - Down string `proxy:"down,omitempty"` -} - -func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) - if err != nil { - return nil, err - } - return NewConn(c, s), err -} - -func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - if s.onlyTcp { - return s.ProxyAdapter.ListenPacketContext(ctx, metadata) - } - - // sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - - pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr())) - if err != nil { - return nil, err - } - if pc == nil { - return nil, E.New("packetConn is nil") - } - return newPacketConn(CN.NewThreadSafePacketConn(pc), s), nil -} - -func (s *SingMux) SupportUDP() bool { - if s.onlyTcp { - return s.ProxyAdapter.SupportUDP() - } - return true -} - -func (s *SingMux) SupportUOT() bool { - if s.onlyTcp { - return s.ProxyAdapter.SupportUOT() - } - return true -} - -func (s *SingMux) ProxyInfo() C.ProxyInfo { - info := s.ProxyAdapter.ProxyInfo() - info.SMUX = true - return info -} - -// Close implements C.ProxyAdapter -func (s *SingMux) Close() error { - if s.client != nil { - _ = s.client.Close() - } - return s.ProxyAdapter.Close() -} - -func NewSingMux(option SingMuxOption, proxy ProxyAdapter) (ProxyAdapter, error) { - // TODO - // "TCP Brutal is only supported on Linux-based systems" - - singDialer := proxydialer.NewSingDialer(proxy, dialer.NewDialer(proxy.DialOptions()...), option.Statistic) - client, err := mux.NewClient(mux.Options{ - Dialer: singDialer, - Logger: log.SingLogger, - Protocol: option.Protocol, - MaxConnections: option.MaxConnections, - MinStreams: option.MinStreams, - MaxStreams: option.MaxStreams, - Padding: option.Padding, - Brutal: mux.BrutalOptions{ - Enabled: option.BrutalOpts.Enabled, - SendBPS: StringToBps(option.BrutalOpts.Up), - ReceiveBPS: StringToBps(option.BrutalOpts.Down), - }, - }) - if err != nil { - return nil, err - } - outbound := &SingMux{ - ProxyAdapter: proxy, - client: client, - dialer: singDialer, - onlyTcp: option.OnlyTcp, - } - return outbound, nil -} diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go deleted file mode 100644 index 275f726369..0000000000 --- a/adapter/outbound/snell.go +++ /dev/null @@ -1,226 +0,0 @@ -package outbound - -import ( - "context" - "fmt" - "net" - "strconv" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/structure" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - C "github.com/metacubex/mihomo/constant" - obfs "github.com/metacubex/mihomo/transport/simple-obfs" - "github.com/metacubex/mihomo/transport/snell" -) - -type Snell struct { - *Base - option *SnellOption - 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 snellStreamConn(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) -} - -// StreamConnContext implements C.ProxyAdapter -func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { - c = snellStreamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) - err := s.writeHeaderContext(ctx, c, metadata) - return c, err -} - -func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - - if metadata.NetWork == C.UDP { - err = snell.WriteUDPHeader(c, s.version) - return - } - err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version) - return -} - -// DialContext implements C.ProxyAdapter -func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - if s.version == snell.Version2 { - c, err := s.pool.Get() - if err != nil { - return nil, err - } - - if err = s.writeHeaderContext(ctx, c, metadata); err != nil { - _ = c.Close() - return nil, err - } - return NewConn(c, s), err - } - - return s.DialContextWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), metadata) -} - -// DialContextWithDialer implements C.ProxyAdapter -func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - if len(s.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(s.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", s.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", s.addr, err) - } - - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = s.StreamConnContext(ctx, c, metadata) - return NewConn(c, s), err -} - -// ListenPacketContext implements C.ProxyAdapter -func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - return s.ListenPacketWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), metadata) -} - -// ListenPacketWithDialer implements C.ProxyAdapter -func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) { - var err error - if len(s.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(s.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", s.addr) - if err != nil { - return nil, err - } - - c, err = s.StreamConnContext(ctx, c, metadata) - - pc := snell.PacketConn(c) - return newPacketConn(pc, s), nil -} - -// SupportWithDialer implements C.ProxyAdapter -func (s *Snell) SupportWithDialer() C.NetWork { - return C.ALLNet -} - -// SupportUOT implements C.ProxyAdapter -func (s *Snell) SupportUOT() bool { - return true -} - -// ProxyInfo implements C.ProxyAdapter -func (s *Snell) ProxyInfo() C.ProxyInfo { - info := s.Base.ProxyInfo() - info.DialerProxy = s.option.DialerProxy - return info -} - -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, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - psk: psk, - obfsOption: obfsOption, - version: option.Version, - } - - if option.Version == snell.Version2 { - s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) { - var err error - var cDialer C.Dialer = dialer.NewDialer(s.DialOptions()...) - if len(s.option.DialerProxy) > 0 { - cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer) - if err != nil { - return nil, err - } - } - c, err := cDialer.DialContext(ctx, "tcp", addr) - if err != nil { - return nil, err - } - - return snellStreamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil - }) - } - return s, nil -} diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go deleted file mode 100644 index 4bb774b93d..0000000000 --- a/adapter/outbound/socks5.go +++ /dev/null @@ -1,263 +0,0 @@ -package outbound - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "io" - "net" - "net/netip" - "strconv" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" -) - -type Socks5 struct { - *Base - option *Socks5Option - 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"` - Fingerprint string `proxy:"fingerprint,omitempty"` -} - -// StreamConnContext implements C.ProxyAdapter -func (ss *Socks5) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { - if ss.tls { - cc := tls.Client(c, ss.tlsConfig) - 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 := ss.clientHandshakeContext(ctx, 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) (_ C.Conn, err error) { - return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), metadata) -} - -// DialContextWithDialer implements C.ProxyAdapter -func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - if len(ss.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", ss.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) - } - - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = ss.StreamConnContext(ctx, c, metadata) - if err != nil { - return nil, err - } - - return NewConn(c, ss), nil -} - -// SupportWithDialer implements C.ProxyAdapter -func (ss *Socks5) SupportWithDialer() C.NetWork { - return C.TCP -} - -// ListenPacketContext implements C.ProxyAdapter -func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - var cDialer C.Dialer = dialer.NewDialer(ss.DialOptions()...) - if len(ss.option.DialerProxy) > 0 { - cDialer, err = proxydialer.NewByName(ss.option.DialerProxy, cDialer) - if err != nil { - return nil, err - } - } - c, err := cDialer.DialContext(ctx, "tcp", ss.addr) - 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) - - 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 := ss.clientHandshakeContext(ctx, c, udpAssocateAddr, socks5.CmdUDPAssociate, user) - if err != nil { - err = fmt.Errorf("client hanshake error: %w", err) - return - } - - // 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(ctx, "udp", ss.Addr(), C.IPv4Prefer) - if err != nil { - return nil, err - } - - bindUDPAddr.IP = serverAddr.IP - } - - pc, err := cDialer.ListenPacket(ctx, "udp", "", bindUDPAddr.AddrPort()) - 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() - }() - - return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil -} - -// ProxyInfo implements C.ProxyAdapter -func (ss *Socks5) ProxyInfo() C.ProxyInfo { - info := ss.Base.ProxyInfo() - info.DialerProxy = ss.option.DialerProxy - return info -} - -func (ss *Socks5) clientHandshakeContext(ctx context.Context, c net.Conn, addr socks5.Addr, command socks5.Command, user *socks5.User) (_ socks5.Addr, err error) { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - return socks5.ClientHandshake(c, addr, command, user) -} - -func NewSocks5(option Socks5Option) (*Socks5, error) { - var tlsConfig *tls.Config - if option.TLS { - tlsConfig = &tls.Config{ - InsecureSkipVerify: option.SkipCertVerify, - ServerName: option.Server, - } - - var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) - if err != nil { - return nil, err - } - } - - return &Socks5{ - Base: &Base{ - name: option.Name, - addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), - tp: C.Socks5, - udp: option.UDP, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - user: option.UserName, - pass: option.Password, - tls: option.TLS, - skipCertVerify: option.SkipCertVerify, - tlsConfig: tlsConfig, - }, nil -} - -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/ssh.go b/adapter/outbound/ssh.go deleted file mode 100644 index 3b9151474f..0000000000 --- a/adapter/outbound/ssh.go +++ /dev/null @@ -1,208 +0,0 @@ -package outbound - -import ( - "bytes" - "context" - "encoding/base64" - "fmt" - "net" - "os" - "strconv" - "strings" - "sync" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - C "github.com/metacubex/mihomo/constant" - - "github.com/metacubex/randv2" - "golang.org/x/crypto/ssh" -) - -type Ssh struct { - *Base - - option *SshOption - - config *ssh.ClientConfig - client *ssh.Client - cMutex sync.Mutex -} - -type SshOption struct { - BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - UserName string `proxy:"username"` - Password string `proxy:"password,omitempty"` - PrivateKey string `proxy:"private-key,omitempty"` - PrivateKeyPassphrase string `proxy:"private-key-passphrase,omitempty"` - HostKey []string `proxy:"host-key,omitempty"` - HostKeyAlgorithms []string `proxy:"host-key-algorithms,omitempty"` -} - -func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - var cDialer C.Dialer = dialer.NewDialer(s.DialOptions()...) - if len(s.option.DialerProxy) > 0 { - cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer) - if err != nil { - return nil, err - } - } - client, err := s.connect(ctx, cDialer, s.addr) - if err != nil { - return nil, err - } - c, err := client.DialContext(ctx, "tcp", metadata.RemoteAddress()) - if err != nil { - return nil, err - } - - return NewConn(c, s), nil -} - -func (s *Ssh) connect(ctx context.Context, cDialer C.Dialer, addr string) (client *ssh.Client, err error) { - s.cMutex.Lock() - defer s.cMutex.Unlock() - if s.client != nil { - return s.client, nil - } - c, err := cDialer.DialContext(ctx, "tcp", addr) - if err != nil { - return nil, err - } - - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - - clientConn, chans, reqs, err := ssh.NewClientConn(c, addr, s.config) - if err != nil { - return nil, err - } - client = ssh.NewClient(clientConn, chans, reqs) - - s.client = client - - go func() { - _ = client.Wait() // wait shutdown - _ = client.Close() - s.cMutex.Lock() - defer s.cMutex.Unlock() - if s.client == client { - s.client = nil - } - }() - - return client, nil -} - -// ProxyInfo implements C.ProxyAdapter -func (s *Ssh) ProxyInfo() C.ProxyInfo { - info := s.Base.ProxyInfo() - info.DialerProxy = s.option.DialerProxy - return info -} - -// Close implements C.ProxyAdapter -func (s *Ssh) Close() error { - s.cMutex.Lock() - defer s.cMutex.Unlock() - if s.client != nil { - return s.client.Close() - } - return nil -} - -func NewSsh(option SshOption) (*Ssh, error) { - addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - - config := ssh.ClientConfig{ - User: option.UserName, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - HostKeyAlgorithms: option.HostKeyAlgorithms, - } - - if option.PrivateKey != "" { - var b []byte - var err error - if strings.Contains(option.PrivateKey, "PRIVATE KEY") { - b = []byte(option.PrivateKey) - } else { - path := C.Path.Resolve(option.PrivateKey) - if !C.Path.IsSafePath(path) { - return nil, C.Path.ErrNotSafePath(path) - } - b, err = os.ReadFile(path) - if err != nil { - return nil, err - } - } - var pKey ssh.Signer - if option.PrivateKeyPassphrase != "" { - pKey, err = ssh.ParsePrivateKeyWithPassphrase(b, []byte(option.PrivateKeyPassphrase)) - } else { - pKey, err = ssh.ParsePrivateKey(b) - } - if err != nil { - return nil, err - } - - config.Auth = append(config.Auth, ssh.PublicKeys(pKey)) - } - - if option.Password != "" { - config.Auth = append(config.Auth, ssh.Password(option.Password)) - } - - if len(option.HostKey) != 0 { - keys := make([]ssh.PublicKey, len(option.HostKey)) - for i, hostKey := range option.HostKey { - key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey)) - if err != nil { - return nil, fmt.Errorf("parse host key :%s", key) - } - keys[i] = key - } - config.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error { - serverKey := key.Marshal() - for _, hostKey := range keys { - if bytes.Equal(serverKey, hostKey.Marshal()) { - return nil - } - } - return fmt.Errorf("host key mismatch, server send :%s %s", key.Type(), base64.StdEncoding.EncodeToString(serverKey)) - } - } - - version := "SSH-2.0-OpenSSH_" - if randv2.IntN(2) == 0 { - version += "7." + strconv.Itoa(randv2.IntN(10)) - } else { - version += "8." + strconv.Itoa(randv2.IntN(9)) - } - config.ClientVersion = version - - outbound := &Ssh{ - Base: &Base{ - name: option.Name, - addr: addr, - tp: C.Ssh, - udp: false, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - config: &config, - } - - return outbound, nil -} diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go deleted file mode 100644 index a321caf0e6..0000000000 --- a/adapter/outbound/trojan.go +++ /dev/null @@ -1,379 +0,0 @@ -package outbound - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net" - "net/http" - "strconv" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/gun" - "github.com/metacubex/mihomo/transport/shadowsocks/core" - "github.com/metacubex/mihomo/transport/trojan" - "github.com/metacubex/mihomo/transport/vmess" -) - -type Trojan struct { - *Base - option *TrojanOption - hexPassword [trojan.KeyLength]byte - - // for gun mux - gunTLSConfig *tls.Config - gunConfig *gun.Config - transport *gun.TransportWrap - - realityConfig *tlsC.RealityConfig - - ssCipher core.Cipher -} - -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"` - Fingerprint string `proxy:"fingerprint,omitempty"` - UDP bool `proxy:"udp,omitempty"` - Network string `proxy:"network,omitempty"` - RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` - GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` - WSOpts WSOptions `proxy:"ws-opts,omitempty"` - SSOpts TrojanSSOption `proxy:"ss-opts,omitempty"` - ClientFingerprint string `proxy:"client-fingerprint,omitempty"` -} - -// TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 -type TrojanSSOption struct { - Enabled bool `proxy:"enabled,omitempty"` - Method string `proxy:"method,omitempty"` - Password string `proxy:"password,omitempty"` -} - -// StreamConnContext implements C.ProxyAdapter -func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { - switch t.option.Network { - case "ws": - host, port, _ := net.SplitHostPort(t.addr) - - wsOpts := &vmess.WebsocketConfig{ - Host: host, - Port: port, - Path: t.option.WSOpts.Path, - MaxEarlyData: t.option.WSOpts.MaxEarlyData, - EarlyDataHeaderName: t.option.WSOpts.EarlyDataHeaderName, - V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade, - V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen, - ClientFingerprint: t.option.ClientFingerprint, - Headers: http.Header{}, - } - - if t.option.SNI != "" { - wsOpts.Host = t.option.SNI - } - - if len(t.option.WSOpts.Headers) != 0 { - for key, value := range t.option.WSOpts.Headers { - wsOpts.Headers.Add(key, value) - } - } - - alpn := trojan.DefaultWebsocketALPN - if len(t.option.ALPN) != 0 { - alpn = t.option.ALPN - } - - wsOpts.TLS = true - tlsConfig := &tls.Config{ - NextProtos: alpn, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: t.option.SkipCertVerify, - ServerName: t.option.SNI, - } - - wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, t.option.Fingerprint) - if err != nil { - return nil, err - } - - c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) - case "grpc": - c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig) - default: - // default tcp network - // handle TLS - alpn := trojan.DefaultALPN - if len(t.option.ALPN) != 0 { - alpn = t.option.ALPN - } - c, err = vmess.StreamTLSConn(ctx, c, &vmess.TLSConfig{ - Host: t.option.SNI, - SkipCertVerify: t.option.SkipCertVerify, - FingerPrint: t.option.Fingerprint, - ClientFingerprint: t.option.ClientFingerprint, - NextProtos: alpn, - Reality: t.realityConfig, - }) - } - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.addr, err) - } - - return t.streamConnContext(ctx, c, metadata) -} - -func (t *Trojan) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { - if t.ssCipher != nil { - c = t.ssCipher.StreamConn(c) - } - - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - command := trojan.CommandTCP - if metadata.NetWork == C.UDP { - command = trojan.CommandUDP - } - err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata)) - return c, err -} - -func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - command := trojan.CommandTCP - if metadata.NetWork == C.UDP { - command = trojan.CommandUDP - } - err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata)) - return err -} - -// DialContext implements C.ProxyAdapter -func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - var c net.Conn - // gun transport - if t.transport != nil { - c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig) - if err != nil { - return nil, err - } - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = t.streamConnContext(ctx, c, metadata) - if err != nil { - return nil, err - } - - return NewConn(c, t), nil - } - return t.DialContextWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata) -} - -// DialContextWithDialer implements C.ProxyAdapter -func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - if len(t.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", t.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.addr, err) - } - - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = t.StreamConnContext(ctx, 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) (_ C.PacketConn, err error) { - var c net.Conn - - // grpc transport - if t.transport != nil { - c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.addr, err) - } - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = t.streamConnContext(ctx, c, metadata) - if err != nil { - return nil, err - } - - pc := trojan.NewPacketConn(c) - return newPacketConn(pc, t), err - } - return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata) -} - -// ListenPacketWithDialer implements C.ProxyAdapter -func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { - if len(t.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", t.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.addr, err) - } - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - c, err = t.StreamConnContext(ctx, c, metadata) - if err != nil { - return nil, err - } - - pc := trojan.NewPacketConn(c) - return newPacketConn(pc, t), err -} - -// SupportWithDialer implements C.ProxyAdapter -func (t *Trojan) SupportWithDialer() C.NetWork { - return C.ALLNet -} - -// ListenPacketOnStreamConn implements C.ProxyAdapter -func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { - pc := trojan.NewPacketConn(c) - return newPacketConn(pc, t), err -} - -// SupportUOT implements C.ProxyAdapter -func (t *Trojan) SupportUOT() bool { - return true -} - -// ProxyInfo implements C.ProxyAdapter -func (t *Trojan) ProxyInfo() C.ProxyInfo { - info := t.Base.ProxyInfo() - info.DialerProxy = t.option.DialerProxy - return info -} - -// Close implements C.ProxyAdapter -func (t *Trojan) Close() error { - if t.transport != nil { - return t.transport.Close() - } - return nil -} - -func NewTrojan(option TrojanOption) (*Trojan, error) { - addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - - if option.SNI == "" { - option.SNI = option.Server - } - - t := &Trojan{ - Base: &Base{ - name: option.Name, - addr: addr, - tp: C.Trojan, - udp: option.UDP, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - hexPassword: trojan.Key(option.Password), - } - - var err error - t.realityConfig, err = option.RealityOpts.Parse() - if err != nil { - return nil, err - } - - if option.SSOpts.Enabled { - if option.SSOpts.Password == "" { - return nil, errors.New("empty password") - } - if option.SSOpts.Method == "" { - option.SSOpts.Method = "AES-128-GCM" - } - ciph, err := core.PickCipher(option.SSOpts.Method, nil, option.SSOpts.Password) - if err != nil { - return nil, err - } - t.ssCipher = ciph - } - - if option.Network == "grpc" { - dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { - var err error - var cDialer C.Dialer = dialer.NewDialer(t.DialOptions()...) - if len(t.option.DialerProxy) > 0 { - cDialer, err = proxydialer.NewByName(t.option.DialerProxy, cDialer) - if err != nil { - return nil, err - } - } - c, err := cDialer.DialContext(ctx, "tcp", t.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) - } - return c, nil - } - - tlsConfig := &tls.Config{ - NextProtos: option.ALPN, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: option.SkipCertVerify, - ServerName: option.SNI, - } - - var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) - if err != nil { - return nil, err - } - - t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.realityConfig) - - t.gunTLSConfig = tlsConfig - t.gunConfig = &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - Host: option.SNI, - ClientFingerprint: option.ClientFingerprint, - } - } - - return t, nil -} diff --git a/adapter/outbound/tuic.go b/adapter/outbound/tuic.go deleted file mode 100644 index f062f83027..0000000000 --- a/adapter/outbound/tuic.go +++ /dev/null @@ -1,324 +0,0 @@ -package outbound - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "math" - "net" - "strconv" - "time" - - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - "github.com/metacubex/mihomo/component/resolver" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/tuic" - - "github.com/gofrs/uuid/v5" - "github.com/metacubex/quic-go" - M "github.com/metacubex/sing/common/metadata" - "github.com/metacubex/sing/common/uot" -) - -type Tuic struct { - *Base - option *TuicOption - client *tuic.PoolClient -} - -type TuicOption struct { - BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - Token string `proxy:"token,omitempty"` - UUID string `proxy:"uuid,omitempty"` - Password string `proxy:"password,omitempty"` - Ip string `proxy:"ip,omitempty"` - HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"` - ALPN []string `proxy:"alpn,omitempty"` - ReduceRtt bool `proxy:"reduce-rtt,omitempty"` - RequestTimeout int `proxy:"request-timeout,omitempty"` - UdpRelayMode string `proxy:"udp-relay-mode,omitempty"` - CongestionController string `proxy:"congestion-controller,omitempty"` - DisableSni bool `proxy:"disable-sni,omitempty"` - MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"` - - FastOpen bool `proxy:"fast-open,omitempty"` - MaxOpenStreams int `proxy:"max-open-streams,omitempty"` - CWND int `proxy:"cwnd,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` - ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` - ReceiveWindow int `proxy:"recv-window,omitempty"` - DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` - MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"` - SNI string `proxy:"sni,omitempty"` - - UDPOverStream bool `proxy:"udp-over-stream,omitempty"` - UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"` -} - -// DialContext implements C.ProxyAdapter -func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - return t.DialContextWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata) -} - -// DialContextWithDialer implements C.ProxyAdapter -func (t *Tuic) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) { - conn, err := t.client.DialContextWithDialer(ctx, metadata, dialer, t.dialWithDialer) - if err != nil { - return nil, err - } - return NewConn(conn, t), err -} - -// ListenPacketContext implements C.ProxyAdapter -func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata) -} - -// ListenPacketWithDialer implements C.ProxyAdapter -func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { - if t.option.UDPOverStream { - uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion)) - uotMetadata := *metadata - uotMetadata.Host = uotDestination.Fqdn - uotMetadata.DstPort = uotDestination.Port - c, err := t.DialContextWithDialer(ctx, dialer, &uotMetadata) - if err != nil { - return nil, err - } - - // tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - - destination := M.SocksaddrFromNet(metadata.UDPAddr()) - if t.option.UDPOverStreamVersion == uot.LegacyVersion { - return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), t), nil - } else { - return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), t), nil - } - } - pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer) - if err != nil { - return nil, err - } - return newPacketConn(pc, t), nil -} - -// SupportWithDialer implements C.ProxyAdapter -func (t *Tuic) SupportWithDialer() C.NetWork { - return C.ALLNet -} - -func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *quic.Transport, addr net.Addr, err error) { - if len(t.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer) - if err != nil { - return nil, nil, err - } - } - udpAddr, err := resolveUDPAddr(ctx, "udp", t.addr, t.prefer) - if err != nil { - return nil, nil, err - } - addr = udpAddr - var pc net.PacketConn - pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort()) - if err != nil { - return nil, nil, err - } - transport = &quic.Transport{Conn: pc} - transport.SetCreatedConn(true) // auto close conn - transport.SetSingleUse(true) // auto close transport - return -} - -// ProxyInfo implements C.ProxyAdapter -func (t *Tuic) ProxyInfo() C.ProxyInfo { - info := t.Base.ProxyInfo() - info.DialerProxy = t.option.DialerProxy - return info -} - -func NewTuic(option TuicOption) (*Tuic, error) { - addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - serverName := option.Server - tlsConfig := &tls.Config{ - ServerName: serverName, - InsecureSkipVerify: option.SkipCertVerify, - MinVersion: tls.VersionTLS13, - } - if option.SNI != "" { - tlsConfig.ServerName = option.SNI - } - - var err error - tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) - if err != nil { - return nil, err - } - - if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array - tlsConfig.NextProtos = option.ALPN - } else { - tlsConfig.NextProtos = []string{"h3"} - } - - if option.RequestTimeout == 0 { - option.RequestTimeout = 8000 - } - - if option.HeartbeatInterval <= 0 { - option.HeartbeatInterval = 10000 - } - - udpRelayMode := tuic.QUIC - if option.UdpRelayMode != "quic" { - udpRelayMode = tuic.NATIVE - } - - if option.MaxUdpRelayPacketSize == 0 { - option.MaxUdpRelayPacketSize = 1252 - } - - if option.MaxOpenStreams == 0 { - option.MaxOpenStreams = 100 - } - - if option.CWND == 0 { - option.CWND = 32 - } - - packetOverHead := tuic.PacketOverHeadV4 - if len(option.Token) == 0 { - packetOverHead = tuic.PacketOverHeadV5 - } - - if option.MaxDatagramFrameSize == 0 { - option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + packetOverHead - } - - if option.MaxDatagramFrameSize > 1400 { - option.MaxDatagramFrameSize = 1400 - } - option.MaxUdpRelayPacketSize = option.MaxDatagramFrameSize - packetOverHead - - // ensure server's incoming stream can handle correctly, increase to 1.1x - quicMaxOpenStreams := int64(option.MaxOpenStreams) - quicMaxOpenStreams = quicMaxOpenStreams + int64(math.Ceil(float64(quicMaxOpenStreams)/10.0)) - quicConfig := &quic.Config{ - InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), - MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), - InitialConnectionReceiveWindow: uint64(option.ReceiveWindow), - MaxConnectionReceiveWindow: uint64(option.ReceiveWindow), - MaxIncomingStreams: quicMaxOpenStreams, - MaxIncomingUniStreams: quicMaxOpenStreams, - KeepAlivePeriod: time.Duration(option.HeartbeatInterval) * time.Millisecond, - DisablePathMTUDiscovery: option.DisableMTUDiscovery, - MaxDatagramFrameSize: int64(option.MaxDatagramFrameSize), - EnableDatagrams: true, - } - if option.ReceiveWindowConn == 0 { - quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10 - quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow - } - if option.ReceiveWindow == 0 { - quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10 - quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow - } - - if len(option.Ip) > 0 { - addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port)) - } - if option.DisableSni { - tlsConfig.ServerName = "" - tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config - } - - switch option.UDPOverStreamVersion { - case uot.Version, uot.LegacyVersion: - case 0: - option.UDPOverStreamVersion = uot.LegacyVersion - default: - return nil, fmt.Errorf("tuic %s unknown udp over stream protocol version: %d", addr, option.UDPOverStreamVersion) - } - - t := &Tuic{ - Base: &Base{ - name: option.Name, - addr: addr, - tp: C.Tuic, - udp: true, - tfo: option.FastOpen, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - option: &option, - } - - clientMaxOpenStreams := int64(option.MaxOpenStreams) - - // to avoid tuic's "too many open streams", decrease to 0.9x - if clientMaxOpenStreams == 100 { - clientMaxOpenStreams = clientMaxOpenStreams - int64(math.Ceil(float64(clientMaxOpenStreams)/10.0)) - } - - if clientMaxOpenStreams < 1 { - clientMaxOpenStreams = 1 - } - - if len(option.Token) > 0 { - tkn := tuic.GenTKN(option.Token) - clientOption := &tuic.ClientOptionV4{ - TlsConfig: tlsC.UConfig(tlsConfig), - QuicConfig: quicConfig, - Token: tkn, - UdpRelayMode: udpRelayMode, - CongestionController: option.CongestionController, - ReduceRtt: option.ReduceRtt, - RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond, - MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize, - FastOpen: option.FastOpen, - MaxOpenStreams: clientMaxOpenStreams, - CWND: option.CWND, - } - - t.client = tuic.NewPoolClientV4(clientOption) - } else { - maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize - if maxUdpRelayPacketSize > tuic.MaxFragSizeV5 { - maxUdpRelayPacketSize = tuic.MaxFragSizeV5 - } - clientOption := &tuic.ClientOptionV5{ - TlsConfig: tlsC.UConfig(tlsConfig), - QuicConfig: quicConfig, - Uuid: uuid.FromStringOrNil(option.UUID), - Password: option.Password, - UdpRelayMode: udpRelayMode, - CongestionController: option.CongestionController, - ReduceRtt: option.ReduceRtt, - MaxUdpRelayPacketSize: maxUdpRelayPacketSize, - MaxOpenStreams: clientMaxOpenStreams, - CWND: option.CWND, - } - - t.client = tuic.NewPoolClientV5(clientOption) - } - - return t, nil -} diff --git a/adapter/outbound/util.go b/adapter/outbound/util.go deleted file mode 100644 index f84193a5bb..0000000000 --- a/adapter/outbound/util.go +++ /dev/null @@ -1,105 +0,0 @@ -package outbound - -import ( - "bytes" - "context" - "fmt" - "net" - "net/netip" - "regexp" - "strconv" - - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" -) - -func serializesSocksAddr(metadata *C.Metadata) []byte { - var buf [][]byte - addrType := metadata.AddrType() - p := uint(metadata.DstPort) - port := []byte{uint8(p >> 8), uint8(p & 0xff)} - switch addrType { - case C.AtypDomainName: - lenM := uint8(len(metadata.Host)) - host := []byte(metadata.Host) - buf = [][]byte{{socks5.AtypDomainName, lenM}, host, port} - case C.AtypIPv4: - host := metadata.DstIP.AsSlice() - buf = [][]byte{{socks5.AtypIPv4}, host, port} - case C.AtypIPv6: - host := metadata.DstIP.AsSlice() - buf = [][]byte{{socks5.AtypIPv6}, host, port} - } - return bytes.Join(buf, nil) -} - -func resolveUDPAddr(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, err - } - var ip netip.Addr - switch prefer { - case C.IPv4Only: - ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver) - case C.IPv6Only: - ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver) - case C.IPv6Prefer: - ip, err = resolver.ResolveIPPrefer6WithResolver(ctx, host, resolver.ProxyServerHostResolver) - default: - ip, err = resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver) - } - - if err != nil { - return nil, err - } - - ip, port = resolver.LookupIP4P(ip, port) - return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port)) -} - -func safeConnClose(c net.Conn, err error) { - if err != nil && c != nil { - _ = c.Close() - } -} - -var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`) - -func StringToBps(s string) uint64 { - if s == "" { - return 0 - } - - // when have not unit, use Mbps - if v, err := strconv.Atoi(s); err == nil { - return StringToBps(fmt.Sprintf("%d Mbps", v)) - } - - m := rateStringRegexp.FindStringSubmatch(s) - if m == nil { - return 0 - } - var n uint64 = 1 - switch m[2] { - case "T": - n *= 1000 - fallthrough - case "G": - n *= 1000 - fallthrough - case "M": - n *= 1000 - fallthrough - case "K": - n *= 1000 - } - v, _ := strconv.ParseUint(m[1], 10, 64) - n *= v - if m[3] == "b" { - // Bits, need to convert to bytes - n /= 8 - } - return n -} diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go deleted file mode 100644 index b075e7199b..0000000000 --- a/adapter/outbound/vless.go +++ /dev/null @@ -1,618 +0,0 @@ -package outbound - -import ( - "context" - "crypto/tls" - "encoding/binary" - "errors" - "fmt" - "io" - "net" - "net/http" - "strconv" - "sync" - - "github.com/metacubex/mihomo/common/convert" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - "github.com/metacubex/mihomo/component/resolver" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/gun" - "github.com/metacubex/mihomo/transport/vless" - "github.com/metacubex/mihomo/transport/vmess" - - vmessSing "github.com/metacubex/sing-vmess" - "github.com/metacubex/sing-vmess/packetaddr" - M "github.com/metacubex/sing/common/metadata" -) - -const ( - // max packet length - maxLength = 1024 << 3 -) - -type Vless struct { - *Base - client *vless.Client - option *VlessOption - - // for gun mux - gunTLSConfig *tls.Config - gunConfig *gun.Config - transport *gun.TransportWrap - - realityConfig *tlsC.RealityConfig -} - -type VlessOption struct { - BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - UUID string `proxy:"uuid"` - Flow string `proxy:"flow,omitempty"` - TLS bool `proxy:"tls,omitempty"` - ALPN []string `proxy:"alpn,omitempty"` - UDP bool `proxy:"udp,omitempty"` - PacketAddr bool `proxy:"packet-addr,omitempty"` - XUDP bool `proxy:"xudp,omitempty"` - PacketEncoding string `proxy:"packet-encoding,omitempty"` - Network string `proxy:"network,omitempty"` - RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` - HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` - HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` - GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` - WSOpts WSOptions `proxy:"ws-opts,omitempty"` - WSPath string `proxy:"ws-path,omitempty"` - WSHeaders map[string]string `proxy:"ws-headers,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - ServerName string `proxy:"servername,omitempty"` - ClientFingerprint string `proxy:"client-fingerprint,omitempty"` -} - -func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, 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, - V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, - V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, - ClientFingerprint: v.option.ClientFingerprint, - Headers: http.Header{}, - } - - if len(v.option.WSOpts.Headers) != 0 { - for key, value := range v.option.WSOpts.Headers { - wsOpts.Headers.Add(key, value) - } - } - if v.option.TLS { - wsOpts.TLS = true - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - ServerName: host, - InsecureSkipVerify: v.option.SkipCertVerify, - NextProtos: []string{"http/1.1"}, - } - - wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) - if err != nil { - return nil, err - } - - if v.option.ServerName != "" { - wsOpts.TLSConfig.ServerName = v.option.ServerName - } else if host := wsOpts.Headers.Get("Host"); host != "" { - wsOpts.TLSConfig.ServerName = host - } - } else { - if host := wsOpts.Headers.Get("Host"); host == "" { - wsOpts.Headers.Set("Host", convert.RandHost()) - convert.SetUserAgent(wsOpts.Headers) - } - } - c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) - case "http": - // readability first, so just copy default TLS logic - c, err = v.streamTLSConn(ctx, c, false) - 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": - c, err = v.streamTLSConn(ctx, c, true) - if err != nil { - return nil, err - } - - h2Opts := &vmess.H2Config{ - Hosts: v.option.HTTP2Opts.Host, - Path: v.option.HTTP2Opts.Path, - } - - c, err = vmess.StreamH2Conn(ctx, c, h2Opts) - case "grpc": - c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) - default: - // default tcp network - // handle TLS - c, err = v.streamTLSConn(ctx, c, false) - } - - if err != nil { - return nil, err - } - - return v.streamConnContext(ctx, c, metadata) -} - -func (v *Vless) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - if metadata.NetWork == C.UDP { - if v.option.PacketAddr { - metadata = &C.Metadata{ - NetWork: C.UDP, - Host: packetaddr.SeqPacketMagicAddress, - DstPort: 443, - } - } else { - metadata = &C.Metadata{ // a clear metadata only contains ip - NetWork: C.UDP, - DstIP: metadata.DstIP, - DstPort: metadata.DstPort, - } - } - conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP)) - if v.option.PacketAddr { - conn = packetaddr.NewBindConn(conn) - } - } else { - conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, false)) - } - if err != nil { - conn = nil - } - return -} - -func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) { - if v.option.TLS { - host, _, _ := net.SplitHostPort(v.addr) - - tlsOpts := vmess.TLSConfig{ - Host: host, - SkipCertVerify: v.option.SkipCertVerify, - FingerPrint: v.option.Fingerprint, - ClientFingerprint: v.option.ClientFingerprint, - Reality: v.realityConfig, - NextProtos: v.option.ALPN, - } - - if isH2 { - tlsOpts.NextProtos = []string{"h2"} - } - - if v.option.ServerName != "" { - tlsOpts.Host = v.option.ServerName - } - - return vmess.StreamTLSConn(ctx, conn, &tlsOpts) - } - - return conn, nil -} - -// DialContext implements C.ProxyAdapter -func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - var c net.Conn - // gun transport - if v.transport != nil { - c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) - if err != nil { - return nil, err - } - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = v.streamConnContext(ctx, c, metadata) - if err != nil { - return nil, err - } - - return NewConn(c, v), nil - } - return v.DialContextWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata) -} - -// DialContextWithDialer implements C.ProxyAdapter -func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - if len(v.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", v.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) - } - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = v.StreamConnContext(ctx, c, metadata) - if err != nil { - return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) - } - return NewConn(c, v), err -} - -// ListenPacketContext implements C.ProxyAdapter -func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - // vless use stream-oriented udp with a special address, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, 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 { - 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.streamConnContext(ctx, c, metadata) - if err != nil { - return nil, fmt.Errorf("new vless client error: %v", err) - } - - return v.ListenPacketOnStreamConn(ctx, c, metadata) - } - return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata) -} - -// ListenPacketWithDialer implements C.ProxyAdapter -func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { - if len(v.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - - // vless use stream-oriented udp with a special address, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - - c, err := dialer.DialContext(ctx, "tcp", v.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) - } - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = v.StreamConnContext(ctx, c, metadata) - if err != nil { - return nil, fmt.Errorf("new vless client error: %v", err) - } - - return v.ListenPacketOnStreamConn(ctx, c, metadata) -} - -// SupportWithDialer implements C.ProxyAdapter -func (v *Vless) SupportWithDialer() C.NetWork { - return C.ALLNet -} - -// ListenPacketOnStreamConn implements C.ProxyAdapter -func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { - // vless use stream-oriented udp with a special address, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - - if v.option.XUDP { - var globalID [8]byte - if metadata.SourceValid() { - globalID = utils.GlobalID(metadata.SourceAddress()) - } - return newPacketConn(N.NewThreadSafePacketConn( - vmessSing.NewXUDPConn(c, - globalID, - M.SocksaddrFromNet(metadata.UDPAddr())), - ), v), nil - } else if v.option.PacketAddr { - return newPacketConn(N.NewThreadSafePacketConn( - packetaddr.NewConn(&vlessPacketConn{ - Conn: c, rAddr: metadata.UDPAddr(), - }, M.SocksaddrFromNet(metadata.UDPAddr())), - ), v), nil - } - return newPacketConn(N.NewThreadSafePacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), v), nil -} - -// SupportUOT implements C.ProxyAdapter -func (v *Vless) SupportUOT() bool { - return true -} - -// ProxyInfo implements C.ProxyAdapter -func (v *Vless) ProxyInfo() C.ProxyInfo { - info := v.Base.ProxyInfo() - info.DialerProxy = v.option.DialerProxy - return info -} - -// Close implements C.ProxyAdapter -func (v *Vless) Close() error { - if v.transport != nil { - return v.transport.Close() - } - return nil -} - -func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr { - var addrType byte - var addr []byte - switch metadata.AddrType() { - case C.AtypIPv4: - addrType = vless.AtypIPv4 - addr = make([]byte, net.IPv4len) - copy(addr[:], metadata.DstIP.AsSlice()) - case C.AtypIPv6: - addrType = vless.AtypIPv6 - addr = make([]byte, net.IPv6len) - copy(addr[:], metadata.DstIP.AsSlice()) - case C.AtypDomainName: - addrType = vless.AtypDomainName - addr = make([]byte, len(metadata.Host)+1) - addr[0] = byte(len(metadata.Host)) - copy(addr[1:], metadata.Host) - } - - return &vless.DstAddr{ - UDP: metadata.NetWork == C.UDP, - AddrType: addrType, - Addr: addr, - Port: metadata.DstPort, - Mux: metadata.NetWork == C.UDP && xudp, - } -} - -type vlessPacketConn struct { - net.Conn - rAddr net.Addr - remain int - mux sync.Mutex - cache [2]byte -} - -func (c *vlessPacketConn) writePacket(payload []byte) (int, error) { - binary.BigEndian.PutUint16(c.cache[:], uint16(len(payload))) - - if _, err := c.Conn.Write(c.cache[:]); err != nil { - return 0, err - } - - return c.Conn.Write(payload) -} - -func (c *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { - total := len(b) - if total == 0 { - return 0, nil - } - - if total <= maxLength { - return c.writePacket(b) - } - - offset := 0 - - for offset < total { - cursor := offset + maxLength - if cursor > total { - cursor = total - } - - n, err := c.writePacket(b[offset:cursor]) - if err != nil { - return offset + n, err - } - - offset = cursor - if offset == total { - break - } - } - - return total, nil -} - -func (c *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { - c.mux.Lock() - defer c.mux.Unlock() - - if c.remain > 0 { - length := len(b) - if c.remain < length { - length = c.remain - } - - n, err := c.Conn.Read(b[:length]) - if err != nil { - return 0, c.rAddr, err - } - - c.remain -= n - return n, c.rAddr, nil - } - - if _, err := c.Conn.Read(b[:2]); err != nil { - return 0, c.rAddr, err - } - - total := int(binary.BigEndian.Uint16(b[:2])) - if total == 0 { - return 0, c.rAddr, nil - } - - length := len(b) - if length > total { - length = total - } - - if _, err := io.ReadFull(c.Conn, b[:length]); err != nil { - return 0, c.rAddr, errors.New("read packet error") - } - - c.remain = total - length - - return length, c.rAddr, nil -} - -func NewVless(option VlessOption) (*Vless, error) { - var addons *vless.Addons - if option.Network != "ws" && len(option.Flow) >= 16 { - option.Flow = option.Flow[:16] - if option.Flow != vless.XRV { - return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) - } - addons = &vless.Addons{ - Flow: option.Flow, - } - } - - switch option.PacketEncoding { - case "packetaddr", "packet": - option.PacketAddr = true - option.XUDP = false - default: // https://github.com/XTLS/Xray-core/pull/1567#issuecomment-1407305458 - if !option.PacketAddr { - option.XUDP = true - } - } - if option.XUDP { - option.PacketAddr = false - } - - client, err := vless.NewClient(option.UUID, addons) - if err != nil { - return nil, err - } - - v := &Vless{ - Base: &Base{ - name: option.Name, - addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), - tp: C.Vless, - udp: option.UDP, - xudp: option.XUDP, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - client: client, - option: &option, - } - - v.realityConfig, err = v.option.RealityOpts.Parse() - if err != nil { - return nil, err - } - - 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(ctx context.Context, network, addr string) (net.Conn, error) { - var err error - var cDialer C.Dialer = dialer.NewDialer(v.DialOptions()...) - if len(v.option.DialerProxy) > 0 { - cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer) - if err != nil { - return nil, err - } - } - c, err := cDialer.DialContext(ctx, "tcp", v.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) - } - return c, nil - } - - gunConfig := &gun.Config{ - ServiceName: v.option.GrpcOpts.GrpcServiceName, - Host: v.option.ServerName, - ClientFingerprint: v.option.ClientFingerprint, - } - if option.ServerName == "" { - gunConfig.Host = v.addr - } - var tlsConfig *tls.Config - if option.TLS { - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ - InsecureSkipVerify: v.option.SkipCertVerify, - ServerName: v.option.ServerName, - }, v.option.Fingerprint) - if err != nil { - return nil, err - } - if option.ServerName == "" { - host, _, _ := net.SplitHostPort(v.addr) - tlsConfig.ServerName = host - } - } - - v.gunTLSConfig = tlsConfig - v.gunConfig = gunConfig - - v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) - } - - return v, nil -} diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go deleted file mode 100644 index 23da3bebf7..0000000000 --- a/adapter/outbound/vmess.go +++ /dev/null @@ -1,553 +0,0 @@ -package outbound - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net" - "net/http" - "strconv" - "strings" - "sync" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - "github.com/metacubex/mihomo/component/resolver" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/ntp" - "github.com/metacubex/mihomo/transport/gun" - mihomoVMess "github.com/metacubex/mihomo/transport/vmess" - - vmess "github.com/metacubex/sing-vmess" - "github.com/metacubex/sing-vmess/packetaddr" - M "github.com/metacubex/sing/common/metadata" -) - -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 *gun.TransportWrap - - realityConfig *tlsC.RealityConfig -} - -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"` - ALPN []string `proxy:"alpn,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - ServerName string `proxy:"servername,omitempty"` - RealityOpts RealityOptions `proxy:"reality-opts,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"` - PacketAddr bool `proxy:"packet-addr,omitempty"` - XUDP bool `proxy:"xudp,omitempty"` - PacketEncoding string `proxy:"packet-encoding,omitempty"` - GlobalPadding bool `proxy:"global-padding,omitempty"` - AuthenticatedLength bool `proxy:"authenticated-length,omitempty"` - ClientFingerprint string `proxy:"client-fingerprint,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"` - V2rayHttpUpgrade bool `proxy:"v2ray-http-upgrade,omitempty"` - V2rayHttpUpgradeFastOpen bool `proxy:"v2ray-http-upgrade-fast-open,omitempty"` -} - -// StreamConnContext implements C.ProxyAdapter -func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { - switch v.option.Network { - case "ws": - host, port, _ := net.SplitHostPort(v.addr) - wsOpts := &mihomoVMess.WebsocketConfig{ - Host: host, - Port: port, - Path: v.option.WSOpts.Path, - MaxEarlyData: v.option.WSOpts.MaxEarlyData, - EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, - V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, - V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, - ClientFingerprint: v.option.ClientFingerprint, - Headers: http.Header{}, - } - - if len(v.option.WSOpts.Headers) != 0 { - for key, value := range v.option.WSOpts.Headers { - wsOpts.Headers.Add(key, value) - } - } - - if v.option.TLS { - wsOpts.TLS = true - tlsConfig := &tls.Config{ - ServerName: host, - InsecureSkipVerify: v.option.SkipCertVerify, - NextProtos: []string{"http/1.1"}, - } - - wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) - if err != nil { - return nil, err - } - - if v.option.ServerName != "" { - wsOpts.TLSConfig.ServerName = v.option.ServerName - } else if host := wsOpts.Headers.Get("Host"); host != "" { - wsOpts.TLSConfig.ServerName = host - } - } - c, err = mihomoVMess.StreamWebsocketConn(ctx, c, wsOpts) - case "http": - // readability first, so just copy default TLS logic - if v.option.TLS { - host, _, _ := net.SplitHostPort(v.addr) - tlsOpts := &mihomoVMess.TLSConfig{ - Host: host, - SkipCertVerify: v.option.SkipCertVerify, - ClientFingerprint: v.option.ClientFingerprint, - Reality: v.realityConfig, - NextProtos: v.option.ALPN, - } - - if v.option.ServerName != "" { - tlsOpts.Host = v.option.ServerName - } - c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts) - if err != nil { - return nil, err - } - } - - host, _, _ := net.SplitHostPort(v.addr) - httpOpts := &mihomoVMess.HTTPConfig{ - Host: host, - Method: v.option.HTTPOpts.Method, - Path: v.option.HTTPOpts.Path, - Headers: v.option.HTTPOpts.Headers, - } - - c = mihomoVMess.StreamHTTPConn(c, httpOpts) - case "h2": - host, _, _ := net.SplitHostPort(v.addr) - tlsOpts := mihomoVMess.TLSConfig{ - Host: host, - SkipCertVerify: v.option.SkipCertVerify, - FingerPrint: v.option.Fingerprint, - NextProtos: []string{"h2"}, - ClientFingerprint: v.option.ClientFingerprint, - Reality: v.realityConfig, - } - - if v.option.ServerName != "" { - tlsOpts.Host = v.option.ServerName - } - - c, err = mihomoVMess.StreamTLSConn(ctx, c, &tlsOpts) - if err != nil { - return nil, err - } - - h2Opts := &mihomoVMess.H2Config{ - Hosts: v.option.HTTP2Opts.Host, - Path: v.option.HTTP2Opts.Path, - } - - c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts) - case "grpc": - c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) - default: - // handle TLS - if v.option.TLS { - host, _, _ := net.SplitHostPort(v.addr) - tlsOpts := &mihomoVMess.TLSConfig{ - Host: host, - SkipCertVerify: v.option.SkipCertVerify, - FingerPrint: v.option.Fingerprint, - ClientFingerprint: v.option.ClientFingerprint, - Reality: v.realityConfig, - NextProtos: v.option.ALPN, - } - - if v.option.ServerName != "" { - tlsOpts.Host = v.option.ServerName - } - - c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts) - } - } - - if err != nil { - return nil, err - } - return v.streamConnContext(ctx, c, metadata) -} - -func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { - useEarly := N.NeedHandshake(c) - if !useEarly { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - } - if metadata.NetWork == C.UDP { - if v.option.XUDP { - var globalID [8]byte - if metadata.SourceValid() { - globalID = utils.GlobalID(metadata.SourceAddress()) - } - if useEarly { - conn = v.client.DialEarlyXUDPPacketConn(c, - globalID, - M.SocksaddrFromNet(metadata.UDPAddr())) - } else { - conn, err = v.client.DialXUDPPacketConn(c, - globalID, - M.SocksaddrFromNet(metadata.UDPAddr())) - } - } else if v.option.PacketAddr { - if useEarly { - conn = v.client.DialEarlyPacketConn(c, - M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443)) - } else { - conn, err = v.client.DialPacketConn(c, - M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443)) - } - conn = packetaddr.NewBindConn(conn) - } else { - if useEarly { - conn = v.client.DialEarlyPacketConn(c, - M.SocksaddrFromNet(metadata.UDPAddr())) - } else { - conn, err = v.client.DialPacketConn(c, - M.SocksaddrFromNet(metadata.UDPAddr())) - } - } - } else { - if useEarly { - conn = v.client.DialEarlyConn(c, - M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) - } else { - conn, err = v.client.DialConn(c, - M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) - } - } - if err != nil { - conn = nil - } - return -} - -// DialContext implements C.ProxyAdapter -func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - var c net.Conn - // gun transport - if v.transport != nil { - c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) - if err != nil { - return nil, err - } - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = v.streamConnContext(ctx, c, metadata) - if err != nil { - return nil, err - } - - return NewConn(c, v), nil - } - return v.DialContextWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata) -} - -// DialContextWithDialer implements C.ProxyAdapter -func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) { - if len(v.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - c, err := dialer.DialContext(ctx, "tcp", v.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) - } - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = v.StreamConnContext(ctx, c, metadata) - return NewConn(c, v), err -} - -// ListenPacketContext implements C.ProxyAdapter -func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, 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 { - 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.streamConnContext(ctx, c, metadata) - if err != nil { - return nil, fmt.Errorf("new vmess client error: %v", err) - } - return v.ListenPacketOnStreamConn(ctx, c, metadata) - } - return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata) -} - -// ListenPacketWithDialer implements C.ProxyAdapter -func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { - if len(v.option.DialerProxy) > 0 { - dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer) - if err != nil { - return nil, err - } - } - - // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - - c, err := dialer.DialContext(ctx, "tcp", v.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) - } - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - c, err = v.StreamConnContext(ctx, c, metadata) - if err != nil { - return nil, fmt.Errorf("new vmess client error: %v", err) - } - return v.ListenPacketOnStreamConn(ctx, c, metadata) -} - -// SupportWithDialer implements C.ProxyAdapter -func (v *Vmess) SupportWithDialer() C.NetWork { - return C.ALLNet -} - -// ProxyInfo implements C.ProxyAdapter -func (v *Vmess) ProxyInfo() C.ProxyInfo { - info := v.Base.ProxyInfo() - info.DialerProxy = v.option.DialerProxy - return info -} - -// Close implements C.ProxyAdapter -func (v *Vmess) Close() error { - if v.transport != nil { - return v.transport.Close() - } - return nil -} - -// ListenPacketOnStreamConn implements C.ProxyAdapter -func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { - // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - - if pc, ok := c.(net.PacketConn); ok { - return newPacketConn(N.NewThreadSafePacketConn(pc), v), nil - } - return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil -} - -// SupportUOT implements C.ProxyAdapter -func (v *Vmess) SupportUOT() bool { - return true -} - -func NewVmess(option VmessOption) (*Vmess, error) { - security := strings.ToLower(option.Cipher) - var options []vmess.ClientOption - if option.GlobalPadding { - options = append(options, vmess.ClientWithGlobalPadding()) - } - if option.AuthenticatedLength { - options = append(options, vmess.ClientWithAuthenticatedLength()) - } - options = append(options, vmess.ClientWithTimeFunc(ntp.Now)) - client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...) - if err != nil { - return nil, err - } - - switch option.PacketEncoding { - case "packetaddr", "packet": - option.PacketAddr = true - case "xudp": - option.XUDP = true - } - if option.XUDP { - option.PacketAddr = false - } - - v := &Vmess{ - Base: &Base{ - name: option.Name, - addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), - tp: C.Vmess, - udp: option.UDP, - xudp: option.XUDP, - tfo: option.TFO, - mpTcp: option.MPTCP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - client: client, - option: &option, - } - - v.realityConfig, err = v.option.RealityOpts.Parse() - if err != nil { - return nil, err - } - - 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(ctx context.Context, network, addr string) (net.Conn, error) { - var err error - var cDialer C.Dialer = dialer.NewDialer(v.DialOptions()...) - if len(v.option.DialerProxy) > 0 { - cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer) - if err != nil { - return nil, err - } - } - c, err := cDialer.DialContext(ctx, "tcp", v.addr) - if err != nil { - return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) - } - return c, nil - } - - gunConfig := &gun.Config{ - ServiceName: v.option.GrpcOpts.GrpcServiceName, - Host: v.option.ServerName, - ClientFingerprint: v.option.ClientFingerprint, - } - if option.ServerName == "" { - gunConfig.Host = v.addr - } - var tlsConfig *tls.Config - if option.TLS { - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ - InsecureSkipVerify: v.option.SkipCertVerify, - ServerName: v.option.ServerName, - }, v.option.Fingerprint) - if err != nil { - return nil, err - } - if option.ServerName == "" { - host, _, _ := net.SplitHostPort(v.addr) - tlsConfig.ServerName = host - } - } - - v.gunTLSConfig = tlsConfig - v.gunConfig = gunConfig - - v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) - } - - return v, nil -} - -type vmessPacketConn struct { - net.Conn - rAddr net.Addr - access sync.Mutex -} - -// 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 - destAddr := addr - if allowedAddr.String() != destAddr.String() { - return 0, ErrUDPRemoteAddrMismatch - } - uc.access.Lock() - defer uc.access.Unlock() - 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/outbound/wireguard.go b/adapter/outbound/wireguard.go deleted file mode 100644 index db0ef95af4..0000000000 --- a/adapter/outbound/wireguard.go +++ /dev/null @@ -1,547 +0,0 @@ -package outbound - -import ( - "context" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - "net" - "net/netip" - "strconv" - "strings" - "sync" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/component/slowdown" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/dns" - "github.com/metacubex/mihomo/log" - - amnezia "github.com/metacubex/amneziawg-go/device" - wireguard "github.com/metacubex/sing-wireguard" - "github.com/metacubex/wireguard-go/device" - - "github.com/metacubex/sing/common/debug" - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/sing/common/metadata" -) - -type wireguardGoDevice interface { - Close() - IpcSet(uapiConf string) error -} - -type WireGuard struct { - *Base - bind *wireguard.ClientBind - device wireguardGoDevice - tunDevice wireguard.Device - dialer proxydialer.SingDialer - resolver resolver.Resolver - - initOk atomic.Bool - initMutex sync.Mutex - initErr error - option WireGuardOption - connectAddr M.Socksaddr - localPrefixes []netip.Prefix - - serverAddrMap map[M.Socksaddr]netip.AddrPort - serverAddrTime atomic.TypedValue[time.Time] - serverAddrMutex sync.Mutex -} - -type WireGuardOption struct { - BasicOption - WireGuardPeerOption - Name string `proxy:"name"` - Ip string `proxy:"ip,omitempty"` - Ipv6 string `proxy:"ipv6,omitempty"` - PrivateKey string `proxy:"private-key"` - Workers int `proxy:"workers,omitempty"` - MTU int `proxy:"mtu,omitempty"` - UDP bool `proxy:"udp,omitempty"` - PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"` - - AmneziaWGOption *AmneziaWGOption `proxy:"amnezia-wg-option,omitempty"` - - Peers []WireGuardPeerOption `proxy:"peers,omitempty"` - - RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"` - Dns []string `proxy:"dns,omitempty"` - - RefreshServerIPInterval int `proxy:"refresh-server-ip-interval,omitempty"` -} - -type WireGuardPeerOption struct { - Server string `proxy:"server"` - Port int `proxy:"port"` - PublicKey string `proxy:"public-key,omitempty"` - PreSharedKey string `proxy:"pre-shared-key,omitempty"` - Reserved []uint8 `proxy:"reserved,omitempty"` - AllowedIPs []string `proxy:"allowed-ips,omitempty"` -} - -type AmneziaWGOption struct { - JC int `proxy:"jc"` - JMin int `proxy:"jmin"` - JMax int `proxy:"jmax"` - S1 int `proxy:"s1"` - S2 int `proxy:"s2"` - H1 uint32 `proxy:"h1"` - H2 uint32 `proxy:"h2"` - H3 uint32 `proxy:"h3"` - H4 uint32 `proxy:"h4"` -} - -type wgSingErrorHandler struct { - name string -} - -var _ E.Handler = (*wgSingErrorHandler)(nil) - -func (w wgSingErrorHandler) NewError(ctx context.Context, err error) { - if E.IsClosedOrCanceled(err) { - log.SingLogger.Debug(fmt.Sprintf("[WG](%s) connection closed: %s", w.name, err)) - return - } - log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", w.name, err)) -} - -type wgNetDialer struct { - tunDevice wireguard.Device -} - -var _ dialer.NetDialer = (*wgNetDialer)(nil) - -func (d wgNetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - return d.tunDevice.DialContext(ctx, network, M.ParseSocksaddr(address).Unwrap()) -} - -func (option WireGuardPeerOption) Addr() M.Socksaddr { - return M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)) -} - -func (option WireGuardOption) Prefixes() ([]netip.Prefix, error) { - localPrefixes := make([]netip.Prefix, 0, 2) - if len(option.Ip) > 0 { - if !strings.Contains(option.Ip, "/") { - option.Ip = option.Ip + "/32" - } - if prefix, err := netip.ParsePrefix(option.Ip); err == nil { - localPrefixes = append(localPrefixes, prefix) - } else { - return nil, E.Cause(err, "ip address parse error") - } - } - if len(option.Ipv6) > 0 { - if !strings.Contains(option.Ipv6, "/") { - option.Ipv6 = option.Ipv6 + "/128" - } - if prefix, err := netip.ParsePrefix(option.Ipv6); err == nil { - localPrefixes = append(localPrefixes, prefix) - } else { - return nil, E.Cause(err, "ipv6 address parse error") - } - } - if len(localPrefixes) == 0 { - return nil, E.New("missing local address") - } - return localPrefixes, nil -} - -func NewWireGuard(option WireGuardOption) (*WireGuard, error) { - outbound := &WireGuard{ - Base: &Base{ - name: option.Name, - addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), - tp: C.WireGuard, - udp: option.UDP, - iface: option.Interface, - rmark: option.RoutingMark, - prefer: C.NewDNSPrefer(option.IPVersion), - }, - } - singDialer := proxydialer.NewSlowDownSingDialer(proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer(outbound.DialOptions()...)), slowdown.New()) - outbound.dialer = singDialer - - var reserved [3]uint8 - if len(option.Reserved) > 0 { - if len(option.Reserved) != 3 { - return nil, E.New("invalid reserved value, required 3 bytes, got ", len(option.Reserved)) - } - copy(reserved[:], option.Reserved) - } - var isConnect bool - if len(option.Peers) < 2 { - isConnect = true - if len(option.Peers) == 1 { - outbound.connectAddr = option.Peers[0].Addr() - } else { - outbound.connectAddr = option.Addr() - } - } - outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, outbound.dialer, isConnect, outbound.connectAddr.AddrPort(), reserved) - - var err error - outbound.localPrefixes, err = option.Prefixes() - if err != nil { - return nil, err - } - - { - bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey) - if err != nil { - return nil, E.Cause(err, "decode private key") - } - option.PrivateKey = hex.EncodeToString(bytes) - } - - if len(option.Peers) > 0 { - for i := range option.Peers { - peer := &option.Peers[i] // we need modify option here - bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey) - if err != nil { - return nil, E.Cause(err, "decode public key for peer ", i) - } - peer.PublicKey = hex.EncodeToString(bytes) - - if peer.PreSharedKey != "" { - bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey) - if err != nil { - return nil, E.Cause(err, "decode pre shared key for peer ", i) - } - peer.PreSharedKey = hex.EncodeToString(bytes) - } - - if len(peer.AllowedIPs) == 0 { - return nil, E.New("missing allowed_ips for peer ", i) - } - - if len(peer.Reserved) > 0 { - if len(peer.Reserved) != 3 { - return nil, E.New("invalid reserved value for peer ", i, ", required 3 bytes, got ", len(peer.Reserved)) - } - } - } - } else { - { - bytes, err := base64.StdEncoding.DecodeString(option.PublicKey) - if err != nil { - return nil, E.Cause(err, "decode peer public key") - } - option.PublicKey = hex.EncodeToString(bytes) - } - if option.PreSharedKey != "" { - bytes, err := base64.StdEncoding.DecodeString(option.PreSharedKey) - if err != nil { - return nil, E.Cause(err, "decode pre shared key") - } - option.PreSharedKey = hex.EncodeToString(bytes) - } - } - outbound.option = option - - mtu := option.MTU - if mtu == 0 { - mtu = 1408 - } - if len(outbound.localPrefixes) == 0 { - return nil, E.New("missing local address") - } - outbound.tunDevice, err = wireguard.NewStackDevice(outbound.localPrefixes, uint32(mtu)) - if err != nil { - return nil, E.Cause(err, "create WireGuard device") - } - logger := &device.Logger{ - Verbosef: func(format string, args ...interface{}) { - log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) - }, - Errorf: func(format string, args ...interface{}) { - log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) - }, - } - if option.AmneziaWGOption != nil { - outbound.bind.SetParseReserved(false) // AmneziaWG don't need parse reserved - outbound.device = amnezia.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers) - } else { - outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers) - } - - var has6 bool - for _, address := range outbound.localPrefixes { - if !address.Addr().Unmap().Is4() { - has6 = true - break - } - } - - if option.RemoteDnsResolve && len(option.Dns) > 0 { - nss, err := dns.ParseNameServer(option.Dns) - if err != nil { - return nil, err - } - for i := range nss { - nss[i].ProxyAdapter = outbound - } - outbound.resolver = dns.NewResolver(dns.Config{ - Main: nss, - IPv6: has6, - }) - } - - return outbound, nil -} - -func (w *WireGuard) resolve(ctx context.Context, address M.Socksaddr) (netip.AddrPort, error) { - if address.Addr.IsValid() { - return address.AddrPort(), nil - } - udpAddr, err := resolveUDPAddr(ctx, "udp", address.String(), w.prefer) - if err != nil { - return netip.AddrPort{}, err - } - // net.ResolveUDPAddr maybe return 4in6 address, so unmap at here - addrPort := udpAddr.AddrPort() - return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()), nil -} - -func (w *WireGuard) init(ctx context.Context) error { - err := w.init0(ctx) - if err != nil { - return err - } - w.updateServerAddr(ctx) - return nil -} - -func (w *WireGuard) init0(ctx context.Context) error { - if w.initOk.Load() { - return nil - } - w.initMutex.Lock() - defer w.initMutex.Unlock() - // double check like sync.Once - if w.initOk.Load() { - return nil - } - if w.initErr != nil { - return w.initErr - } - - w.bind.ResetReservedForEndpoint() - w.serverAddrMap = make(map[M.Socksaddr]netip.AddrPort) - ipcConf, err := w.genIpcConf(ctx, false) - if err != nil { - // !!! do not set initErr here !!! - // let us can retry domain resolve in next time - return err - } - - if debug.Enabled { - log.SingLogger.Trace(fmt.Sprintf("[WG](%s) created wireguard ipc conf: \n %s", w.option.Name, ipcConf)) - } - err = w.device.IpcSet(ipcConf) - if err != nil { - w.initErr = E.Cause(err, "setup wireguard") - return w.initErr - } - w.serverAddrTime.Store(time.Now()) - - err = w.tunDevice.Start() - if err != nil { - w.initErr = err - return w.initErr - } - - w.initOk.Store(true) - return nil -} - -func (w *WireGuard) updateServerAddr(ctx context.Context) { - if w.option.RefreshServerIPInterval != 0 && time.Since(w.serverAddrTime.Load()) > time.Second*time.Duration(w.option.RefreshServerIPInterval) { - if w.serverAddrMutex.TryLock() { - defer w.serverAddrMutex.Unlock() - ipcConf, err := w.genIpcConf(ctx, true) - if err != nil { - log.Warnln("[WG](%s)UpdateServerAddr failed to generate wireguard ipc conf: %s", w.option.Name, err) - return - } - err = w.device.IpcSet(ipcConf) - if err != nil { - log.Warnln("[WG](%s)UpdateServerAddr failed to update wireguard ipc conf: %s", w.option.Name, err) - return - } - w.serverAddrTime.Store(time.Now()) - } - } -} - -func (w *WireGuard) genIpcConf(ctx context.Context, updateOnly bool) (string, error) { - ipcConf := "" - if !updateOnly { - ipcConf += "private_key=" + w.option.PrivateKey + "\n" - if w.option.AmneziaWGOption != nil { - ipcConf += "jc=" + strconv.Itoa(w.option.AmneziaWGOption.JC) + "\n" - ipcConf += "jmin=" + strconv.Itoa(w.option.AmneziaWGOption.JMin) + "\n" - ipcConf += "jmax=" + strconv.Itoa(w.option.AmneziaWGOption.JMax) + "\n" - ipcConf += "s1=" + strconv.Itoa(w.option.AmneziaWGOption.S1) + "\n" - ipcConf += "s2=" + strconv.Itoa(w.option.AmneziaWGOption.S2) + "\n" - ipcConf += "h1=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H1), 10) + "\n" - ipcConf += "h2=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H2), 10) + "\n" - ipcConf += "h3=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H3), 10) + "\n" - ipcConf += "h4=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H4), 10) + "\n" - } - } - if len(w.option.Peers) > 0 { - for i, peer := range w.option.Peers { - peerAddr := peer.Addr() - destination, err := w.resolve(ctx, peerAddr) - if err != nil { - return "", E.Cause(err, "resolve endpoint domain for peer ", i) - } - if w.serverAddrMap[peerAddr] != destination { - w.serverAddrMap[peerAddr] = destination - } else if updateOnly { - continue - } - - if len(w.option.Peers) == 1 { // must call SetConnectAddr if isConnect == true - w.bind.SetConnectAddr(destination) - } - ipcConf += "public_key=" + peer.PublicKey + "\n" - if updateOnly { - ipcConf += "update_only=true\n" - } - ipcConf += "endpoint=" + destination.String() + "\n" - if len(peer.Reserved) > 0 { - var reserved [3]uint8 - copy(reserved[:], w.option.Reserved) - w.bind.SetReservedForEndpoint(destination, reserved) - } - if updateOnly { - continue - } - if peer.PreSharedKey != "" { - ipcConf += "preshared_key=" + peer.PreSharedKey + "\n" - } - for _, allowedIP := range peer.AllowedIPs { - ipcConf += "allowed_ip=" + allowedIP + "\n" - } - if w.option.PersistentKeepalive != 0 { - ipcConf += fmt.Sprintf("persistent_keepalive_interval=%d\n", w.option.PersistentKeepalive) - } - } - } else { - destination, err := w.resolve(ctx, w.connectAddr) - if err != nil { - return "", E.Cause(err, "resolve endpoint domain") - } - if w.serverAddrMap[w.connectAddr] != destination { - w.serverAddrMap[w.connectAddr] = destination - } else if updateOnly { - return "", nil - } - w.bind.SetConnectAddr(destination) // must call SetConnectAddr if isConnect == true - ipcConf += "public_key=" + w.option.PublicKey + "\n" - if updateOnly { - ipcConf += "update_only=true\n" - } - ipcConf += "endpoint=" + destination.String() + "\n" - if updateOnly { - return ipcConf, nil - } - if w.option.PreSharedKey != "" { - ipcConf += "preshared_key=" + w.option.PreSharedKey + "\n" - } - var has4, has6 bool - for _, address := range w.localPrefixes { - if address.Addr().Is4() { - has4 = true - } else { - has6 = true - } - } - if has4 { - ipcConf += "allowed_ip=0.0.0.0/0\n" - } - if has6 { - ipcConf += "allowed_ip=::/0\n" - } - - if w.option.PersistentKeepalive != 0 { - ipcConf += fmt.Sprintf("persistent_keepalive_interval=%d\n", w.option.PersistentKeepalive) - } - } - return ipcConf, nil -} - -// Close implements C.ProxyAdapter -func (w *WireGuard) Close() error { - if w.device != nil { - w.device.Close() - } - return nil -} - -func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { - var conn net.Conn - if err = w.init(ctx); err != nil { - return nil, err - } - if !metadata.Resolved() || w.resolver != nil { - r := resolver.DefaultResolver - if w.resolver != nil { - r = w.resolver - } - options := w.DialOptions() - options = append(options, dialer.WithResolver(r)) - options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice})) - conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress()) - } else { - conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) - } - if err != nil { - return nil, err - } - if conn == nil { - return nil, E.New("conn is nil") - } - return NewConn(conn, w), nil -} - -func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - var pc net.PacketConn - if err = w.init(ctx); err != nil { - return nil, err - } - if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" { - r := resolver.DefaultResolver - if w.resolver != nil { - r = w.resolver - } - ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) - if err != nil { - return nil, errors.New("can't resolve ip") - } - metadata.DstIP = ip - } - pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) - if err != nil { - return nil, err - } - if pc == nil { - return nil, E.New("packetConn is nil") - } - return newPacketConn(pc, w), nil -} - -// IsL3Protocol implements C.ProxyAdapter -func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool { - return true -} diff --git a/adapter/outboundgroup/fallback.go b/adapter/outboundgroup/fallback.go deleted file mode 100644 index 8f8842a1c4..0000000000 --- a/adapter/outboundgroup/fallback.go +++ /dev/null @@ -1,171 +0,0 @@ -package outboundgroup - -import ( - "context" - "encoding/json" - "errors" - "time" - - "github.com/metacubex/mihomo/common/callback" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" -) - -type Fallback struct { - *GroupBase - disableUDP bool - testUrl string - selected string - expectedStatus string - Hidden bool - Icon string -} - -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) (C.Conn, error) { - proxy := f.findAliveProxy(true) - c, err := proxy.DialContext(ctx, metadata) - if err == nil { - c.AppendToChains(f) - } else { - f.onDialFailed(proxy.Type(), err, f.healthCheck) - } - - if N.NeedHandshake(c) { - c = callback.NewFirstWriteCallBackConn(c, func(err error) { - if err == nil { - f.onDialSuccess() - } else { - f.onDialFailed(proxy.Type(), err, f.healthCheck) - } - }) - } - - return c, err -} - -// ListenPacketContext implements C.ProxyAdapter -func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - proxy := f.findAliveProxy(true) - pc, err := proxy.ListenPacketContext(ctx, metadata) - 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() -} - -// IsL3Protocol implements C.ProxyAdapter -func (f *Fallback) IsL3Protocol(metadata *C.Metadata) bool { - return f.findAliveProxy(false).IsL3Protocol(metadata) -} - -// MarshalJSON implements C.ProxyAdapter -func (f *Fallback) MarshalJSON() ([]byte, error) { - all := []string{} - for _, proxy := range f.GetProxies(false) { - all = append(all, proxy.Name()) - } - return json.Marshal(map[string]any{ - "type": f.Type().String(), - "now": f.Now(), - "all": all, - "testUrl": f.testUrl, - "expectedStatus": f.expectedStatus, - "fixed": f.selected, - "hidden": f.Hidden, - "icon": f.Icon, - }) -} - -// Unwrap implements C.ProxyAdapter -func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { - proxy := f.findAliveProxy(touch) - return proxy -} - -func (f *Fallback) findAliveProxy(touch bool) C.Proxy { - proxies := f.GetProxies(touch) - for _, proxy := range proxies { - if len(f.selected) == 0 { - if proxy.AliveForTestUrl(f.testUrl) { - return proxy - } - } else { - if proxy.Name() == f.selected { - if proxy.AliveForTestUrl(f.testUrl) { - return proxy - } else { - f.selected = "" - } - } - } - } - - return proxies[0] -} - -func (f *Fallback) Set(name string) error { - var p C.Proxy - for _, proxy := range f.GetProxies(false) { - if proxy.Name() == name { - p = proxy - break - } - } - - if p == nil { - return errors.New("proxy not exist") - } - - f.selected = name - if !p.AliveForTestUrl(f.testUrl) { - ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000)) - defer cancel() - expectedStatus, _ := utils.NewUnsignedRanges[uint16](f.expectedStatus) - _, _ = p.URLTest(ctx, f.testUrl, expectedStatus) - } - - return nil -} - -func (f *Fallback) ForceSet(name string) { - f.selected = name -} - -func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { - return &Fallback{ - GroupBase: NewGroupBase(GroupBaseOption{ - Name: option.Name, - Type: C.Fallback, - Filter: option.Filter, - ExcludeFilter: option.ExcludeFilter, - ExcludeType: option.ExcludeType, - TestTimeout: option.TestTimeout, - MaxFailedTimes: option.MaxFailedTimes, - Providers: providers, - }), - disableUDP: option.DisableUDP, - testUrl: option.URL, - expectedStatus: option.ExpectedStatus, - Hidden: option.Hidden, - Icon: option.Icon, - } -} diff --git a/adapter/outboundgroup/groupbase.go b/adapter/outboundgroup/groupbase.go deleted file mode 100644 index dc66976bba..0000000000 --- a/adapter/outboundgroup/groupbase.go +++ /dev/null @@ -1,308 +0,0 @@ -package outboundgroup - -import ( - "context" - "errors" - "fmt" - "strings" - "sync" - "time" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" - types "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/tunnel" - - "github.com/dlclark/regexp2" - "golang.org/x/exp/slices" -) - -type GroupBase struct { - *outbound.Base - filterRegs []*regexp2.Regexp - excludeFilterRegs []*regexp2.Regexp - excludeTypeArray []string - providers []provider.ProxyProvider - failedTestMux sync.Mutex - failedTimes int - failedTime time.Time - failedTesting atomic.Bool - TestTimeout int - maxFailedTimes int - - // for GetProxies - getProxiesMutex sync.Mutex - providerVersions []uint32 - providerProxies []C.Proxy -} - -type GroupBaseOption struct { - Name string - Type C.AdapterType - Filter string - ExcludeFilter string - ExcludeType string - TestTimeout int - MaxFailedTimes int - Providers []provider.ProxyProvider -} - -func NewGroupBase(opt GroupBaseOption) *GroupBase { - var excludeTypeArray []string - if opt.ExcludeType != "" { - excludeTypeArray = strings.Split(opt.ExcludeType, "|") - } - - var excludeFilterRegs []*regexp2.Regexp - if opt.ExcludeFilter != "" { - for _, excludeFilter := range strings.Split(opt.ExcludeFilter, "`") { - excludeFilterReg := regexp2.MustCompile(excludeFilter, regexp2.None) - excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg) - } - } - - var filterRegs []*regexp2.Regexp - if opt.Filter != "" { - for _, filter := range strings.Split(opt.Filter, "`") { - filterReg := regexp2.MustCompile(filter, regexp2.None) - filterRegs = append(filterRegs, filterReg) - } - } - - gb := &GroupBase{ - Base: outbound.NewBase(outbound.BaseOption{Name: opt.Name, Type: opt.Type}), - filterRegs: filterRegs, - excludeFilterRegs: excludeFilterRegs, - excludeTypeArray: excludeTypeArray, - providers: opt.Providers, - failedTesting: atomic.NewBool(false), - TestTimeout: opt.TestTimeout, - maxFailedTimes: opt.MaxFailedTimes, - } - - if gb.TestTimeout == 0 { - gb.TestTimeout = 5000 - } - if gb.maxFailedTimes == 0 { - gb.maxFailedTimes = 5 - } - - return gb -} - -func (gb *GroupBase) Touch() { - for _, pd := range gb.providers { - pd.Touch() - } -} - -func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { - providerVersions := make([]uint32, len(gb.providers)) - for i, pd := range gb.providers { - if touch { // touch first - pd.Touch() - } - providerVersions[i] = pd.Version() - } - - // thread safe - gb.getProxiesMutex.Lock() - defer gb.getProxiesMutex.Unlock() - - // return the cached proxies if version not changed - if slices.Equal(providerVersions, gb.providerVersions) { - return gb.providerProxies - } - - var proxies []C.Proxy - if len(gb.filterRegs) == 0 { - for _, pd := range gb.providers { - proxies = append(proxies, pd.Proxies()...) - } - } else { - for _, pd := range gb.providers { - if pd.VehicleType() == types.Compatible { // compatible provider unneeded filter - proxies = append(proxies, pd.Proxies()...) - continue - } - - var newProxies []C.Proxy - proxiesSet := map[string]struct{}{} - for _, filterReg := range gb.filterRegs { - for _, p := range pd.Proxies() { - name := p.Name() - if mat, _ := filterReg.MatchString(name); mat { - if _, ok := proxiesSet[name]; !ok { - proxiesSet[name] = struct{}{} - newProxies = append(newProxies, p) - } - } - } - } - proxies = append(proxies, newProxies...) - } - } - - // Multiple filers means that proxies are sorted in the order in which the filers appear. - // Although the filter has been performed once in the previous process, - // when there are multiple providers, the array needs to be reordered as a whole. - if len(gb.providers) > 1 && len(gb.filterRegs) > 1 { - var newProxies []C.Proxy - proxiesSet := map[string]struct{}{} - for _, filterReg := range gb.filterRegs { - for _, p := range proxies { - name := p.Name() - if mat, _ := filterReg.MatchString(name); mat { - if _, ok := proxiesSet[name]; !ok { - proxiesSet[name] = struct{}{} - newProxies = append(newProxies, p) - } - } - } - } - for _, p := range proxies { // add not matched proxies at the end - name := p.Name() - if _, ok := proxiesSet[name]; !ok { - proxiesSet[name] = struct{}{} - newProxies = append(newProxies, p) - } - } - proxies = newProxies - } - - if len(gb.excludeFilterRegs) > 0 { - var newProxies []C.Proxy - LOOP1: - for _, p := range proxies { - name := p.Name() - for _, excludeFilterReg := range gb.excludeFilterRegs { - if mat, _ := excludeFilterReg.MatchString(name); mat { - continue LOOP1 - } - } - newProxies = append(newProxies, p) - } - proxies = newProxies - } - - if gb.excludeTypeArray != nil { - var newProxies []C.Proxy - LOOP2: - for _, p := range proxies { - mType := p.Type().String() - for _, excludeType := range gb.excludeTypeArray { - if strings.EqualFold(mType, excludeType) { - continue LOOP2 - } - } - newProxies = append(newProxies, p) - } - proxies = newProxies - } - - if len(proxies) == 0 { - return []C.Proxy{tunnel.Proxies()["COMPATIBLE"]} - } - - // only cache when proxies not empty - gb.providerVersions = providerVersions - gb.providerProxies = proxies - - return proxies -} - -func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) { - var wg sync.WaitGroup - var lock sync.Mutex - mp := map[string]uint16{} - proxies := gb.GetProxies(false) - for _, proxy := range proxies { - proxy := proxy - wg.Add(1) - go func() { - delay, err := proxy.URLTest(ctx, url, expectedStatus) - if err == nil { - lock.Lock() - mp[proxy.Name()] = delay - lock.Unlock() - } - - wg.Done() - }() - } - wg.Wait() - - if len(mp) == 0 { - return mp, fmt.Errorf("get delay: all proxies timeout") - } else { - return mp, nil - } -} - -func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error, fn func()) { - if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass || adapterType == C.RejectDrop { - return - } - - if errors.Is(err, C.ErrNotSupport) { - return - } - - go func() { - if strings.Contains(err.Error(), "connection refused") { - fn() - return - } - - gb.failedTestMux.Lock() - defer gb.failedTestMux.Unlock() - - gb.failedTimes++ - if gb.failedTimes == 1 { - log.Debugln("ProxyGroup: %s first failed", gb.Name()) - gb.failedTime = time.Now() - } else { - if time.Since(gb.failedTime) > time.Duration(gb.TestTimeout)*time.Millisecond { - gb.failedTimes = 0 - return - } - - log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes) - if gb.failedTimes >= gb.maxFailedTimes { - log.Warnln("because %s failed multiple times, active health check", gb.Name()) - fn() - } - } - }() -} - -func (gb *GroupBase) healthCheck() { - if gb.failedTesting.Load() { - return - } - - gb.failedTesting.Store(true) - wg := sync.WaitGroup{} - for _, proxyProvider := range gb.providers { - wg.Add(1) - proxyProvider := proxyProvider - go func() { - defer wg.Done() - proxyProvider.HealthCheck() - }() - } - - wg.Wait() - gb.failedTesting.Store(false) - gb.failedTimes = 0 -} - -func (gb *GroupBase) onDialSuccess() { - if !gb.failedTesting.Load() { - gb.failedTimes = 0 - } -} diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go deleted file mode 100644 index 99e11c46fc..0000000000 --- a/adapter/outboundgroup/loadbalance.go +++ /dev/null @@ -1,272 +0,0 @@ -package outboundgroup - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net" - "sync" - "time" - - "github.com/metacubex/mihomo/common/callback" - "github.com/metacubex/mihomo/common/lru" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" - - "golang.org/x/net/publicsuffix" -) - -type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy - -type LoadBalance struct { - *GroupBase - disableUDP bool - strategyFn strategyFn - testUrl string - expectedStatus string - Hidden bool - Icon string -} - -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 == nil { - return "" - } - - 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.IsValid() { - return "" - } - - return metadata.DstIP.String() -} - -func getKeyWithSrcAndDst(metadata *C.Metadata) string { - dst := getKey(metadata) - src := "" - if metadata != nil { - src = metadata.SrcIP.String() - } - - return fmt.Sprintf("%s%s", src, dst) -} - -func jumpHash(key uint64, buckets int32) int32 { - var b, j int64 - - 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) (c C.Conn, err error) { - proxy := lb.Unwrap(metadata, true) - c, err = proxy.DialContext(ctx, metadata) - - if err == nil { - c.AppendToChains(lb) - } else { - lb.onDialFailed(proxy.Type(), err, lb.healthCheck) - } - - if N.NeedHandshake(c) { - c = callback.NewFirstWriteCallBackConn(c, func(err error) { - if err == nil { - lb.onDialSuccess() - } else { - lb.onDialFailed(proxy.Type(), err, lb.healthCheck) - } - }) - } - - return -} - -// ListenPacketContext implements C.ProxyAdapter -func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (pc C.PacketConn, err error) { - defer func() { - if err == nil { - pc.AppendToChains(lb) - } - }() - - proxy := lb.Unwrap(metadata, true) - return proxy.ListenPacketContext(ctx, metadata) -} - -// SupportUDP implements C.ProxyAdapter -func (lb *LoadBalance) SupportUDP() bool { - return !lb.disableUDP -} - -// IsL3Protocol implements C.ProxyAdapter -func (lb *LoadBalance) IsL3Protocol(metadata *C.Metadata) bool { - return lb.Unwrap(metadata, false).IsL3Protocol(metadata) -} - -func strategyRoundRobin(url string) strategyFn { - idx := 0 - idxMutex := sync.Mutex{} - return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { - idxMutex.Lock() - defer idxMutex.Unlock() - - i := 0 - length := len(proxies) - - if touch { - defer func() { - idx = (idx + i) % length - }() - } - - for ; i < length; i++ { - id := (idx + i) % length - proxy := proxies[id] - if proxy.AliveForTestUrl(url) { - i++ - return proxy - } - } - - return proxies[0] - } -} - -func strategyConsistentHashing(url string) strategyFn { - maxRetry := 5 - return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { - key := utils.MapHash(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.AliveForTestUrl(url) { - return proxy - } - } - - // when availability is poor, traverse the entire list to get the available nodes - for _, proxy := range proxies { - if proxy.AliveForTestUrl(url) { - return proxy - } - } - - return proxies[0] - } -} - -func strategyStickySessions(url string) strategyFn { - ttl := time.Minute * 10 - maxRetry := 5 - lruCache := lru.New[uint64, int]( - lru.WithAge[uint64, int](int64(ttl.Seconds())), - lru.WithSize[uint64, int](1000)) - return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { - key := utils.MapHash(getKeyWithSrcAndDst(metadata)) - length := len(proxies) - idx, has := lruCache.Get(key) - if !has { - idx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length))) - } - - nowIdx := idx - for i := 1; i < maxRetry; i++ { - proxy := proxies[nowIdx] - if proxy.AliveForTestUrl(url) { - if !has || nowIdx != idx { - lruCache.Set(key, nowIdx) - } - - return proxy - } else { - nowIdx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length))) - } - } - - lruCache.Set(key, 0) - return proxies[0] - } -} - -// Unwrap implements C.ProxyAdapter -func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { - proxies := lb.GetProxies(touch) - return lb.strategyFn(proxies, metadata, touch) -} - -// MarshalJSON implements C.ProxyAdapter -func (lb *LoadBalance) MarshalJSON() ([]byte, error) { - var all []string - for _, proxy := range lb.GetProxies(false) { - all = append(all, proxy.Name()) - } - return json.Marshal(map[string]any{ - "type": lb.Type().String(), - "all": all, - "testUrl": lb.testUrl, - "expectedStatus": lb.expectedStatus, - "hidden": lb.Hidden, - "icon": lb.Icon, - }) -} - -func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) { - var strategyFn strategyFn - switch strategy { - case "consistent-hashing": - strategyFn = strategyConsistentHashing(option.URL) - case "round-robin": - strategyFn = strategyRoundRobin(option.URL) - case "sticky-sessions": - strategyFn = strategyStickySessions(option.URL) - default: - return nil, fmt.Errorf("%w: %s", errStrategy, strategy) - } - return &LoadBalance{ - GroupBase: NewGroupBase(GroupBaseOption{ - Name: option.Name, - Type: C.LoadBalance, - Filter: option.Filter, - ExcludeFilter: option.ExcludeFilter, - ExcludeType: option.ExcludeType, - TestTimeout: option.TestTimeout, - MaxFailedTimes: option.MaxFailedTimes, - Providers: providers, - }), - strategyFn: strategyFn, - disableUDP: option.DisableUDP, - testUrl: option.URL, - expectedStatus: option.ExpectedStatus, - Hidden: option.Hidden, - Icon: option.Icon, - }, nil -} diff --git a/adapter/outboundgroup/parser.go b/adapter/outboundgroup/parser.go deleted file mode 100644 index 9c09a0f4a2..0000000000 --- a/adapter/outboundgroup/parser.go +++ /dev/null @@ -1,233 +0,0 @@ -package outboundgroup - -import ( - "errors" - "fmt" - "strings" - - "github.com/dlclark/regexp2" - - "github.com/metacubex/mihomo/adapter/provider" - "github.com/metacubex/mihomo/common/structure" - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - types "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/log" -) - -var ( - errFormat = errors.New("format error") - errType = errors.New("unsupported type") - errMissProxy = errors.New("`use` or `proxies` missing") - errDuplicateProvider = errors.New("duplicate provider name") -) - -type GroupCommonOption struct { - 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"` - TestTimeout int `group:"timeout,omitempty"` - MaxFailedTimes int `group:"max-failed-times,omitempty"` - Lazy bool `group:"lazy,omitempty"` - DisableUDP bool `group:"disable-udp,omitempty"` - Filter string `group:"filter,omitempty"` - ExcludeFilter string `group:"exclude-filter,omitempty"` - ExcludeType string `group:"exclude-type,omitempty"` - ExpectedStatus string `group:"expected-status,omitempty"` - IncludeAll bool `group:"include-all,omitempty"` - IncludeAllProxies bool `group:"include-all-proxies,omitempty"` - IncludeAllProviders bool `group:"include-all-providers,omitempty"` - Hidden bool `group:"hidden,omitempty"` - Icon string `group:"icon,omitempty"` - - // removed configs, only for error logging - Interface string `group:"interface-name,omitempty"` - RoutingMark int `group:"routing-mark,omitempty"` -} - -func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider, AllProxies []string, AllProviders []string) (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 - } - - if groupOption.RoutingMark != 0 { - log.Errorln("The group [%s] with routing-mark configuration was removed, please set it directly on the proxy instead", groupOption.Name) - } - if groupOption.Interface != "" { - log.Errorln("The group [%s] with interface-name configuration was removed, please set it directly on the proxy instead", groupOption.Name) - } - - groupName := groupOption.Name - - providers := []types.ProxyProvider{} - - if groupOption.IncludeAll { - groupOption.IncludeAllProviders = true - groupOption.IncludeAllProxies = true - } - - if groupOption.IncludeAllProviders { - groupOption.Use = AllProviders - } - if groupOption.IncludeAllProxies { - if groupOption.Filter != "" { - var filterRegs []*regexp2.Regexp - for _, filter := range strings.Split(groupOption.Filter, "`") { - filterReg := regexp2.MustCompile(filter, regexp2.None) - filterRegs = append(filterRegs, filterReg) - } - for _, p := range AllProxies { - for _, filterReg := range filterRegs { - if mat, _ := filterReg.MatchString(p); mat { - groupOption.Proxies = append(groupOption.Proxies, p) - } - } - } - } else { - groupOption.Proxies = append(groupOption.Proxies, AllProxies...) - } - if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { - groupOption.Proxies = []string{"COMPATIBLE"} - } - } - - if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { - return nil, fmt.Errorf("%s: %w", groupName, errMissProxy) - } - - expectedStatus, err := utils.NewUnsignedRanges[uint16](groupOption.ExpectedStatus) - if err != nil { - return nil, fmt.Errorf("%s: %w", groupName, err) - } - - status := strings.TrimSpace(groupOption.ExpectedStatus) - if status == "" { - status = "*" - } - groupOption.ExpectedStatus = status - - if len(groupOption.Use) != 0 { - PDs, err := getProviders(providersMap, groupOption.Use) - if err != nil { - return nil, fmt.Errorf("%s: %w", groupName, err) - } - - // if test URL is empty, use the first health check URL of providers - if groupOption.URL == "" { - for _, pd := range PDs { - if pd.HealthCheckURL() != "" { - groupOption.URL = pd.HealthCheckURL() - break - } - } - if groupOption.URL == "" { - groupOption.URL = C.DefaultTestURL - } - } else { - addTestUrlToProviders(PDs, groupOption.URL, expectedStatus, groupOption.Filter, uint(groupOption.Interval)) - } - providers = append(providers, PDs...) - } - - 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) - } - - if groupOption.URL == "" { - groupOption.URL = C.DefaultTestURL - } - - // select don't need auto health check - if groupOption.Type != "select" && groupOption.Type != "relay" { - if groupOption.Interval == 0 { - groupOption.Interval = 300 - } - } - - hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.TestTimeout), uint(groupOption.Interval), groupOption.Lazy, expectedStatus) - - pd, err := provider.NewCompatibleProvider(groupName, ps, hc) - if err != nil { - return nil, fmt.Errorf("%s: %w", groupName, err) - } - - providers = append([]types.ProxyProvider{pd}, providers...) - providersMap[groupName] = pd - } - - 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("%w: %s", 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 -} - -func addTestUrlToProviders(providers []types.ProxyProvider, url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { - if len(providers) == 0 || len(url) == 0 { - return - } - - for _, pd := range providers { - pd.RegisterHealthCheckTask(url, expectedStatus, filter, interval) - } -} diff --git a/adapter/outboundgroup/patch_android.go b/adapter/outboundgroup/patch_android.go deleted file mode 100644 index c6566814f8..0000000000 --- a/adapter/outboundgroup/patch_android.go +++ /dev/null @@ -1,64 +0,0 @@ -//go:build android && cmfa - -package outboundgroup - -import ( - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" -) - -type ProxyGroup interface { - C.ProxyAdapter - - Providers() []provider.ProxyProvider - Proxies() []C.Proxy - Now() string -} - -func (f *Fallback) Providers() []provider.ProxyProvider { - return f.providers -} - -func (lb *LoadBalance) Providers() []provider.ProxyProvider { - return lb.providers -} - -func (f *Fallback) Proxies() []C.Proxy { - return f.GetProxies(false) -} - -func (lb *LoadBalance) Proxies() []C.Proxy { - return lb.GetProxies(false) -} - -func (lb *LoadBalance) Now() string { - return "" -} - -func (r *Relay) Providers() []provider.ProxyProvider { - return r.providers -} - -func (r *Relay) Proxies() []C.Proxy { - return r.GetProxies(false) -} - -func (r *Relay) Now() string { - return "" -} - -func (s *Selector) Providers() []provider.ProxyProvider { - return s.providers -} - -func (s *Selector) Proxies() []C.Proxy { - return s.GetProxies(false) -} - -func (u *URLTest) Providers() []provider.ProxyProvider { - return u.providers -} - -func (u *URLTest) Proxies() []C.Proxy { - return u.GetProxies(false) -} diff --git a/adapter/outboundgroup/relay.go b/adapter/outboundgroup/relay.go deleted file mode 100644 index 77a2f03b8e..0000000000 --- a/adapter/outboundgroup/relay.go +++ /dev/null @@ -1,163 +0,0 @@ -package outboundgroup - -import ( - "context" - "encoding/json" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/log" -) - -type Relay struct { - *GroupBase - Hidden bool - Icon string -} - -// DialContext implements C.ProxyAdapter -func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - proxies, chainProxies := r.proxies(metadata, true) - - switch len(proxies) { - case 0: - return outbound.NewDirect().DialContext(ctx, metadata) - case 1: - return proxies[0].DialContext(ctx, metadata) - } - var d C.Dialer - d = dialer.NewDialer() - for _, proxy := range proxies[:len(proxies)-1] { - d = proxydialer.New(proxy, d, false) - } - last := proxies[len(proxies)-1] - conn, err := last.DialContextWithDialer(ctx, d, metadata) - if err != nil { - return nil, err - } - - for i := len(chainProxies) - 2; i >= 0; i-- { - conn.AppendToChains(chainProxies[i]) - } - - conn.AppendToChains(r) - - return conn, nil -} - -// ListenPacketContext implements C.ProxyAdapter -func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { - proxies, chainProxies := r.proxies(metadata, true) - - switch len(proxies) { - case 0: - return outbound.NewDirect().ListenPacketContext(ctx, metadata) - case 1: - return proxies[0].ListenPacketContext(ctx, metadata) - } - - var d C.Dialer - d = dialer.NewDialer() - for _, proxy := range proxies[:len(proxies)-1] { - d = proxydialer.New(proxy, d, false) - } - last := proxies[len(proxies)-1] - pc, err := last.ListenPacketWithDialer(ctx, d, metadata) - if err != nil { - return nil, err - } - - for i := len(chainProxies) - 2; i >= 0; i-- { - pc.AppendToChains(chainProxies[i]) - } - - pc.AppendToChains(r) - - return pc, nil -} - -// SupportUDP implements C.ProxyAdapter -func (r *Relay) SupportUDP() bool { - proxies, _ := r.proxies(nil, false) - if len(proxies) == 0 { // C.Direct - return true - } - for i := len(proxies) - 1; i >= 0; i-- { - proxy := proxies[i] - if !proxy.SupportUDP() { - return false - } - if proxy.SupportUOT() { - return true - } - switch proxy.SupportWithDialer() { - case C.ALLNet: - case C.UDP: - default: // C.TCP and C.InvalidNet - return false - } - } - return true -} - -// MarshalJSON implements C.ProxyAdapter -func (r *Relay) MarshalJSON() ([]byte, error) { - all := []string{} - for _, proxy := range r.GetProxies(false) { - all = append(all, proxy.Name()) - } - return json.Marshal(map[string]any{ - "type": r.Type().String(), - "all": all, - "hidden": r.Hidden, - "icon": r.Icon, - }) -} - -func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy) { - rawProxies := r.GetProxies(touch) - - var proxies []C.Proxy - var chainProxies []C.Proxy - var targetProxies []C.Proxy - - for n, proxy := range rawProxies { - proxies = append(proxies, proxy) - chainProxies = append(chainProxies, proxy) - subproxy := proxy.Unwrap(metadata, touch) - for subproxy != nil { - chainProxies = append(chainProxies, subproxy) - proxies[n] = subproxy - subproxy = subproxy.Unwrap(metadata, touch) - } - } - - for _, proxy := range proxies { - if proxy.Type() != C.Direct && proxy.Type() != C.Compatible { - targetProxies = append(targetProxies, proxy) - } - } - - return targetProxies, chainProxies -} - -func (r *Relay) Addr() string { - proxies, _ := r.proxies(nil, false) - return proxies[len(proxies)-1].Addr() -} - -func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay { - log.Warnln("The group [%s] with relay type is deprecated, please using dialer-proxy instead", option.Name) - return &Relay{ - GroupBase: NewGroupBase(GroupBaseOption{ - Name: option.Name, - Type: C.Relay, - Providers: providers, - }), - Hidden: option.Hidden, - Icon: option.Icon, - } -} diff --git a/adapter/outboundgroup/selector.go b/adapter/outboundgroup/selector.go deleted file mode 100644 index 03ee192c9e..0000000000 --- a/adapter/outboundgroup/selector.go +++ /dev/null @@ -1,129 +0,0 @@ -package outboundgroup - -import ( - "context" - "encoding/json" - "errors" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" -) - -type Selector struct { - *GroupBase - disableUDP bool - selected string - testUrl string - Hidden bool - Icon string -} - -// DialContext implements C.ProxyAdapter -func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - c, err := s.selectedProxy(true).DialContext(ctx, metadata) - if err == nil { - c.AppendToChains(s) - } - return c, err -} - -// ListenPacketContext implements C.ProxyAdapter -func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata) - 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() -} - -// IsL3Protocol implements C.ProxyAdapter -func (s *Selector) IsL3Protocol(metadata *C.Metadata) bool { - return s.selectedProxy(false).IsL3Protocol(metadata) -} - -// MarshalJSON implements C.ProxyAdapter -func (s *Selector) MarshalJSON() ([]byte, error) { - all := []string{} - for _, proxy := range s.GetProxies(false) { - all = append(all, proxy.Name()) - } - // When testurl is the default value - // do not append a value to ensure that the web dashboard follows the settings of the dashboard - var url string - if s.testUrl != C.DefaultTestURL { - url = s.testUrl - } - - return json.Marshal(map[string]any{ - "type": s.Type().String(), - "now": s.Now(), - "all": all, - "testUrl": url, - "hidden": s.Hidden, - "icon": s.Icon, - }) -} - -func (s *Selector) Now() string { - return s.selectedProxy(false).Name() -} - -func (s *Selector) Set(name string) error { - for _, proxy := range s.GetProxies(false) { - if proxy.Name() == name { - s.selected = name - return nil - } - } - - return errors.New("proxy not exist") -} - -func (s *Selector) ForceSet(name string) { - s.selected = name -} - -// Unwrap implements C.ProxyAdapter -func (s *Selector) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { - return s.selectedProxy(touch) -} - -func (s *Selector) selectedProxy(touch bool) C.Proxy { - proxies := s.GetProxies(touch) - for _, proxy := range proxies { - if proxy.Name() == s.selected { - return proxy - } - } - - return proxies[0] -} - -func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector { - return &Selector{ - GroupBase: NewGroupBase(GroupBaseOption{ - Name: option.Name, - Type: C.Selector, - Filter: option.Filter, - ExcludeFilter: option.ExcludeFilter, - ExcludeType: option.ExcludeType, - TestTimeout: option.TestTimeout, - MaxFailedTimes: option.MaxFailedTimes, - Providers: providers, - }), - selected: "COMPATIBLE", - disableUDP: option.DisableUDP, - testUrl: option.URL, - Hidden: option.Hidden, - Icon: option.Icon, - } -} diff --git a/adapter/outboundgroup/urltest.go b/adapter/outboundgroup/urltest.go deleted file mode 100644 index 5dc620547e..0000000000 --- a/adapter/outboundgroup/urltest.go +++ /dev/null @@ -1,230 +0,0 @@ -package outboundgroup - -import ( - "context" - "encoding/json" - "errors" - "time" - - "github.com/metacubex/mihomo/common/callback" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/singledo" - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" -) - -type urlTestOption func(*URLTest) - -func urlTestWithTolerance(tolerance uint16) urlTestOption { - return func(u *URLTest) { - u.tolerance = tolerance - } -} - -type URLTest struct { - *GroupBase - selected string - testUrl string - expectedStatus string - tolerance uint16 - disableUDP bool - Hidden bool - Icon string - fastNode C.Proxy - fastSingle *singledo.Single[C.Proxy] -} - -func (u *URLTest) Now() string { - return u.fast(false).Name() -} - -func (u *URLTest) Set(name string) error { - var p C.Proxy - for _, proxy := range u.GetProxies(false) { - if proxy.Name() == name { - p = proxy - break - } - } - if p == nil { - return errors.New("proxy not exist") - } - u.ForceSet(name) - return nil -} - -func (u *URLTest) ForceSet(name string) { - u.selected = name - u.fastSingle.Reset() -} - -// DialContext implements C.ProxyAdapter -func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { - proxy := u.fast(true) - c, err = proxy.DialContext(ctx, metadata) - if err == nil { - c.AppendToChains(u) - } else { - u.onDialFailed(proxy.Type(), err, u.healthCheck) - } - - if N.NeedHandshake(c) { - c = callback.NewFirstWriteCallBackConn(c, func(err error) { - if err == nil { - u.onDialSuccess() - } else { - u.onDialFailed(proxy.Type(), err, u.healthCheck) - } - }) - } - - return c, err -} - -// ListenPacketContext implements C.ProxyAdapter -func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { - proxy := u.fast(true) - pc, err := proxy.ListenPacketContext(ctx, metadata) - if err == nil { - pc.AppendToChains(u) - } else { - u.onDialFailed(proxy.Type(), err, u.healthCheck) - } - - return pc, err -} - -// Unwrap implements C.ProxyAdapter -func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { - return u.fast(touch) -} - -func (u *URLTest) healthCheck() { - u.fastSingle.Reset() - u.GroupBase.healthCheck() - u.fastSingle.Reset() -} - -func (u *URLTest) fast(touch bool) C.Proxy { - elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) { - proxies := u.GetProxies(touch) - if u.selected != "" { - for _, proxy := range proxies { - if !proxy.AliveForTestUrl(u.testUrl) { - continue - } - if proxy.Name() == u.selected { - u.fastNode = proxy - return proxy, nil - } - } - } - - fast := proxies[0] - minDelay := fast.LastDelayForTestUrl(u.testUrl) - fastNotExist := true - - for _, proxy := range proxies[1:] { - if u.fastNode != nil && proxy.Name() == u.fastNode.Name() { - fastNotExist = false - } - - if !proxy.AliveForTestUrl(u.testUrl) { - continue - } - - delay := proxy.LastDelayForTestUrl(u.testUrl) - if delay < minDelay { - fast = proxy - minDelay = delay - } - - } - // tolerance - if u.fastNode == nil || fastNotExist || !u.fastNode.AliveForTestUrl(u.testUrl) || u.fastNode.LastDelayForTestUrl(u.testUrl) > fast.LastDelayForTestUrl(u.testUrl)+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 - u.Touch() - } - - return elm -} - -// SupportUDP implements C.ProxyAdapter -func (u *URLTest) SupportUDP() bool { - if u.disableUDP { - return false - } - return u.fast(false).SupportUDP() -} - -// IsL3Protocol implements C.ProxyAdapter -func (u *URLTest) IsL3Protocol(metadata *C.Metadata) bool { - return u.fast(false).IsL3Protocol(metadata) -} - -// MarshalJSON implements C.ProxyAdapter -func (u *URLTest) MarshalJSON() ([]byte, error) { - all := []string{} - for _, proxy := range u.GetProxies(false) { - all = append(all, proxy.Name()) - } - return json.Marshal(map[string]any{ - "type": u.Type().String(), - "now": u.Now(), - "all": all, - "testUrl": u.testUrl, - "expectedStatus": u.expectedStatus, - "fixed": u.selected, - "hidden": u.Hidden, - "icon": u.Icon, - }) -} - -func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) { - return u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus) -} - -func parseURLTestOption(config map[string]any) []urlTestOption { - opts := []urlTestOption{} - - // tolerance - if elm, ok := config["tolerance"]; ok { - if tolerance, ok := elm.(int); ok { - opts = append(opts, urlTestWithTolerance(uint16(tolerance))) - } - } - - return opts -} - -func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest { - urlTest := &URLTest{ - GroupBase: NewGroupBase(GroupBaseOption{ - Name: option.Name, - Type: C.URLTest, - Filter: option.Filter, - ExcludeFilter: option.ExcludeFilter, - ExcludeType: option.ExcludeType, - TestTimeout: option.TestTimeout, - MaxFailedTimes: option.MaxFailedTimes, - Providers: providers, - }), - fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), - disableUDP: option.DisableUDP, - testUrl: option.URL, - expectedStatus: option.ExpectedStatus, - Hidden: option.Hidden, - Icon: option.Icon, - } - - for _, option := range options { - option(urlTest) - } - - return urlTest -} diff --git a/adapter/outboundgroup/util.go b/adapter/outboundgroup/util.go deleted file mode 100644 index 66b2510c19..0000000000 --- a/adapter/outboundgroup/util.go +++ /dev/null @@ -1,10 +0,0 @@ -package outboundgroup - -type SelectAble interface { - Set(string) error - ForceSet(name string) -} - -var _ SelectAble = (*Fallback)(nil) -var _ SelectAble = (*URLTest)(nil) -var _ SelectAble = (*Selector)(nil) diff --git a/adapter/parser.go b/adapter/parser.go deleted file mode 100644 index 56febe2817..0000000000 --- a/adapter/parser.go +++ /dev/null @@ -1,179 +0,0 @@ -package adapter - -import ( - "fmt" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/common/structure" - C "github.com/metacubex/mihomo/constant" -) - -func ParseProxy(mapping map[string]any) (C.Proxy, error) { - decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer}) - proxyType, existType := mapping["type"].(string) - if !existType { - return nil, fmt.Errorf("missing type") - } - - var ( - proxy outbound.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, err = outbound.NewSocks5(*socksOption) - case "http": - httpOption := &outbound.HttpOption{} - err = decoder.Decode(mapping, httpOption) - if err != nil { - break - } - proxy, err = 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 "vless": - vlessOption := &outbound.VlessOption{} - err = decoder.Decode(mapping, vlessOption) - if err != nil { - break - } - proxy, err = outbound.NewVless(*vlessOption) - 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) - case "hysteria": - hyOption := &outbound.HysteriaOption{} - err = decoder.Decode(mapping, hyOption) - if err != nil { - break - } - proxy, err = outbound.NewHysteria(*hyOption) - case "hysteria2": - hyOption := &outbound.Hysteria2Option{} - err = decoder.Decode(mapping, hyOption) - if err != nil { - break - } - proxy, err = outbound.NewHysteria2(*hyOption) - case "wireguard": - wgOption := &outbound.WireGuardOption{} - err = decoder.Decode(mapping, wgOption) - if err != nil { - break - } - proxy, err = outbound.NewWireGuard(*wgOption) - case "tuic": - tuicOption := &outbound.TuicOption{} - err = decoder.Decode(mapping, tuicOption) - if err != nil { - break - } - proxy, err = outbound.NewTuic(*tuicOption) - case "direct": - directOption := &outbound.DirectOption{} - err = decoder.Decode(mapping, directOption) - if err != nil { - break - } - proxy = outbound.NewDirectWithOption(*directOption) - case "dns": - dnsOptions := &outbound.DnsOption{} - err = decoder.Decode(mapping, dnsOptions) - if err != nil { - break - } - proxy = outbound.NewDnsWithOption(*dnsOptions) - case "reject": - rejectOption := &outbound.RejectOption{} - err = decoder.Decode(mapping, rejectOption) - if err != nil { - break - } - proxy = outbound.NewRejectWithOption(*rejectOption) - case "ssh": - sshOption := &outbound.SshOption{} - err = decoder.Decode(mapping, sshOption) - if err != nil { - break - } - proxy, err = outbound.NewSsh(*sshOption) - case "mieru": - mieruOption := &outbound.MieruOption{} - err = decoder.Decode(mapping, mieruOption) - if err != nil { - break - } - proxy, err = outbound.NewMieru(*mieruOption) - case "anytls": - anytlsOption := &outbound.AnyTLSOption{} - err = decoder.Decode(mapping, anytlsOption) - if err != nil { - break - } - proxy, err = outbound.NewAnyTLS(*anytlsOption) - default: - return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) - } - - if err != nil { - return nil, err - } - - if muxMapping, muxExist := mapping["smux"].(map[string]any); muxExist { - muxOption := &outbound.SingMuxOption{} - err = decoder.Decode(muxMapping, muxOption) - if err != nil { - return nil, err - } - if muxOption.Enabled { - proxy, err = outbound.NewSingMux(*muxOption, proxy) - if err != nil { - return nil, err - } - } - } - - proxy = outbound.NewAutoCloseProxyAdapter(proxy) - return NewProxy(proxy), nil -} diff --git a/adapter/provider/healthcheck.go b/adapter/provider/healthcheck.go deleted file mode 100644 index f16718e927..0000000000 --- a/adapter/provider/healthcheck.go +++ /dev/null @@ -1,217 +0,0 @@ -package provider - -import ( - "context" - "strings" - "sync" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/singledo" - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - "github.com/dlclark/regexp2" - "golang.org/x/sync/errgroup" -) - -type HealthCheckOption struct { - URL string - Interval uint -} - -type extraOption struct { - expectedStatus utils.IntRanges[uint16] - filters map[string]struct{} -} - -type HealthCheck struct { - ctx context.Context - ctxCancel context.CancelFunc - url string - extra map[string]*extraOption - mu sync.Mutex - proxies []C.Proxy - interval time.Duration - lazy bool - expectedStatus utils.IntRanges[uint16] - lastTouch atomic.TypedValue[time.Time] - singleDo *singledo.Single[struct{}] - timeout time.Duration -} - -func (hc *HealthCheck) process() { - ticker := time.NewTicker(hc.interval) - go hc.check() - for { - select { - case <-ticker.C: - lastTouch := hc.lastTouch.Load() - since := time.Since(lastTouch) - if !hc.lazy || since < hc.interval { - hc.check() - } else { - log.Debugln("Skip once health check because we are lazy") - } - case <-hc.ctx.Done(): - ticker.Stop() - return - } - } -} - -func (hc *HealthCheck) setProxy(proxies []C.Proxy) { - hc.proxies = proxies -} - -func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { - url = strings.TrimSpace(url) - if len(url) == 0 || url == hc.url { - log.Debugln("ignore invalid health check url: %s", url) - return - } - - hc.mu.Lock() - defer hc.mu.Unlock() - - // if the provider has not set up health checks, then modify it to be the same as the group's interval - if hc.interval == 0 { - hc.interval = time.Duration(interval) * time.Second - } - - if hc.extra == nil { - hc.extra = make(map[string]*extraOption) - } - - // prioritize the use of previously registered configurations, especially those from provider - if _, ok := hc.extra[url]; ok { - // provider default health check does not set filter - if url != hc.url && len(filter) != 0 { - splitAndAddFiltersToExtra(filter, hc.extra[url]) - } - - log.Debugln("health check url: %s exists", url) - return - } - - option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus} - splitAndAddFiltersToExtra(filter, option) - hc.extra[url] = option -} - -func splitAndAddFiltersToExtra(filter string, option *extraOption) { - filter = strings.TrimSpace(filter) - if len(filter) != 0 { - for _, regex := range strings.Split(filter, "`") { - regex = strings.TrimSpace(regex) - if len(regex) != 0 { - option.filters[regex] = struct{}{} - } - } - } -} - -func (hc *HealthCheck) auto() bool { - return hc.interval != 0 -} - -func (hc *HealthCheck) touch() { - hc.lastTouch.Store(time.Now()) -} - -func (hc *HealthCheck) check() { - if len(hc.proxies) == 0 { - return - } - - _, _, _ = hc.singleDo.Do(func() (struct{}, error) { - id := utils.NewUUIDV4().String() - log.Debugln("Start New Health Checking {%s}", id) - b := new(errgroup.Group) - b.SetLimit(10) - - // execute default health check - option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus} - hc.execute(b, hc.url, id, option) - - // execute extra health check - if len(hc.extra) != 0 { - for url, option := range hc.extra { - hc.execute(b, url, id, option) - } - } - _ = b.Wait() - log.Debugln("Finish A Health Checking {%s}", id) - return struct{}{}, nil - }) -} - -func (hc *HealthCheck) execute(b *errgroup.Group, url, uid string, option *extraOption) { - url = strings.TrimSpace(url) - if len(url) == 0 { - log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid) - return - } - - var filterReg *regexp2.Regexp - var expectedStatus utils.IntRanges[uint16] - if option != nil { - expectedStatus = option.expectedStatus - if len(option.filters) != 0 { - filters := make([]string, 0, len(option.filters)) - for filter := range option.filters { - filters = append(filters, filter) - } - - filterReg = regexp2.MustCompile(strings.Join(filters, "|"), regexp2.None) - } - } - - for _, proxy := range hc.proxies { - // skip proxies that do not require health check - if filterReg != nil { - if match, _ := filterReg.MatchString(proxy.Name()); !match { - continue - } - } - - p := proxy - b.Go(func() error { - ctx, cancel := context.WithTimeout(hc.ctx, hc.timeout) - defer cancel() - log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid) - _, _ = p.URLTest(ctx, url, expectedStatus) - log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", p.Name(), url, p.AliveForTestUrl(url), p.LastDelayForTestUrl(url), uid) - return nil - }) - } -} - -func (hc *HealthCheck) close() { - hc.ctxCancel() -} - -func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck { - if url == "" { - expectedStatus = nil - interval = 0 - } - if timeout == 0 { - timeout = 5000 - } - ctx, cancel := context.WithCancel(context.Background()) - - return &HealthCheck{ - ctx: ctx, - ctxCancel: cancel, - proxies: proxies, - url: url, - timeout: time.Duration(timeout) * time.Millisecond, - extra: map[string]*extraOption{}, - interval: time.Duration(interval) * time.Second, - lazy: lazy, - expectedStatus: expectedStatus, - singleDo: singledo.NewSingle[struct{}](time.Second), - } -} diff --git a/adapter/provider/parser.go b/adapter/provider/parser.go deleted file mode 100644 index 6907fc69b6..0000000000 --- a/adapter/provider/parser.go +++ /dev/null @@ -1,130 +0,0 @@ -package provider - -import ( - "encoding" - "errors" - "fmt" - "time" - - "github.com/metacubex/mihomo/common/structure" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/resource" - C "github.com/metacubex/mihomo/constant" - types "github.com/metacubex/mihomo/constant/provider" - - "github.com/dlclark/regexp2" -) - -var ( - errVehicleType = errors.New("unsupport vehicle type") -) - -type healthCheckSchema struct { - Enable bool `provider:"enable"` - URL string `provider:"url"` - Interval int `provider:"interval"` - TestTimeout int `provider:"timeout,omitempty"` - Lazy bool `provider:"lazy,omitempty"` - ExpectedStatus string `provider:"expected-status,omitempty"` -} - -type OverrideProxyNameSchema struct { - // matching expression for regex replacement - Pattern *regexp2.Regexp `provider:"pattern"` - // the new content after regex matching - Target string `provider:"target"` -} - -var _ encoding.TextUnmarshaler = (*regexp2.Regexp)(nil) // ensure *regexp2.Regexp can decode direct by structure package - -type OverrideSchema struct { - TFO *bool `provider:"tfo,omitempty"` - MPTcp *bool `provider:"mptcp,omitempty"` - UDP *bool `provider:"udp,omitempty"` - UDPOverTCP *bool `provider:"udp-over-tcp,omitempty"` - Up *string `provider:"up,omitempty"` - Down *string `provider:"down,omitempty"` - DialerProxy *string `provider:"dialer-proxy,omitempty"` - SkipCertVerify *bool `provider:"skip-cert-verify,omitempty"` - Interface *string `provider:"interface-name,omitempty"` - RoutingMark *int `provider:"routing-mark,omitempty"` - IPVersion *string `provider:"ip-version,omitempty"` - AdditionalPrefix *string `provider:"additional-prefix,omitempty"` - AdditionalSuffix *string `provider:"additional-suffix,omitempty"` - - ProxyName []OverrideProxyNameSchema `provider:"proxy-name,omitempty"` -} - -type proxyProviderSchema struct { - Type string `provider:"type"` - Path string `provider:"path,omitempty"` - URL string `provider:"url,omitempty"` - Proxy string `provider:"proxy,omitempty"` - Interval int `provider:"interval,omitempty"` - Filter string `provider:"filter,omitempty"` - ExcludeFilter string `provider:"exclude-filter,omitempty"` - ExcludeType string `provider:"exclude-type,omitempty"` - DialerProxy string `provider:"dialer-proxy,omitempty"` - SizeLimit int64 `provider:"size-limit,omitempty"` - Payload []map[string]any `provider:"payload,omitempty"` - - HealthCheck healthCheckSchema `provider:"health-check,omitempty"` - Override OverrideSchema `provider:"override,omitempty"` - Header map[string][]string `provider:"header,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 - } - - expectedStatus, err := utils.NewUnsignedRanges[uint16](schema.HealthCheck.ExpectedStatus) - if err != nil { - return nil, err - } - - var hcInterval uint - if schema.HealthCheck.Enable { - if schema.HealthCheck.Interval == 0 { - schema.HealthCheck.Interval = 300 - } - hcInterval = uint(schema.HealthCheck.Interval) - } - hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, uint(schema.HealthCheck.TestTimeout), hcInterval, schema.HealthCheck.Lazy, expectedStatus) - - parser, err := NewProxiesParser(schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override) - if err != nil { - return nil, err - } - - var vehicle types.Vehicle - switch schema.Type { - case "file": - path := C.Path.Resolve(schema.Path) - vehicle = resource.NewFileVehicle(path) - case "http": - path := C.Path.GetPathByHash("proxies", schema.URL) - if schema.Path != "" { - path = C.Path.Resolve(schema.Path) - if !C.Path.IsSafePath(path) { - return nil, C.Path.ErrNotSafePath(path) - } - } - vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit) - case "inline": - return NewInlineProvider(name, schema.Payload, parser, hc) - default: - return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) - } - - interval := time.Duration(uint(schema.Interval)) * time.Second - - return NewProxySetProvider(name, interval, schema.Payload, parser, vehicle, hc) -} diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go deleted file mode 100644 index 8c88d1e8bc..0000000000 --- a/adapter/provider/provider.go +++ /dev/null @@ -1,470 +0,0 @@ -package provider - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "reflect" - "runtime" - "strings" - "time" - - "github.com/metacubex/mihomo/adapter" - "github.com/metacubex/mihomo/common/convert" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/profile/cachefile" - "github.com/metacubex/mihomo/component/resource" - C "github.com/metacubex/mihomo/constant" - types "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/tunnel/statistic" - - "github.com/dlclark/regexp2" - "gopkg.in/yaml.v3" -) - -const ( - ReservedName = "default" -) - -type ProxySchema struct { - Proxies []map[string]any `yaml:"proxies"` -} - -type providerForApi struct { - Name string `json:"name"` - Type string `json:"type"` - VehicleType string `json:"vehicleType"` - Proxies []C.Proxy `json:"proxies"` - TestUrl string `json:"testUrl"` - ExpectedStatus string `json:"expectedStatus"` - UpdatedAt time.Time `json:"updatedAt,omitempty"` - SubscriptionInfo *SubscriptionInfo `json:"subscriptionInfo,omitempty"` -} - -type baseProvider struct { - name string - proxies []C.Proxy - healthCheck *HealthCheck - version uint32 -} - -func (bp *baseProvider) Name() string { - return bp.name -} - -func (bp *baseProvider) Version() uint32 { - return bp.version -} - -func (bp *baseProvider) HealthCheck() { - bp.healthCheck.check() -} - -func (bp *baseProvider) Type() types.ProviderType { - return types.Proxy -} - -func (bp *baseProvider) Proxies() []C.Proxy { - return bp.proxies -} - -func (bp *baseProvider) Count() int { - return len(bp.proxies) -} - -func (bp *baseProvider) Touch() { - bp.healthCheck.touch() -} - -func (bp *baseProvider) HealthCheckURL() string { - return bp.healthCheck.url -} - -func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { - bp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval) -} - -func (bp *baseProvider) setProxies(proxies []C.Proxy) { - bp.proxies = proxies - bp.version += 1 - bp.healthCheck.setProxy(proxies) - if bp.healthCheck.auto() { - go bp.healthCheck.check() - } -} - -func (bp *baseProvider) Close() error { - bp.healthCheck.close() - return nil -} - -// ProxySetProvider for auto gc -type ProxySetProvider struct { - *proxySetProvider -} - -type proxySetProvider struct { - baseProvider - *resource.Fetcher[[]C.Proxy] - subscriptionInfo *SubscriptionInfo -} - -func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { - return json.Marshal(providerForApi{ - Name: pp.Name(), - Type: pp.Type().String(), - VehicleType: pp.VehicleType().String(), - Proxies: pp.Proxies(), - TestUrl: pp.healthCheck.url, - ExpectedStatus: pp.healthCheck.expectedStatus.String(), - UpdatedAt: pp.UpdatedAt(), - SubscriptionInfo: pp.subscriptionInfo, - }) -} - -func (pp *proxySetProvider) Name() string { - return pp.Fetcher.Name() -} - -func (pp *proxySetProvider) Update() error { - _, _, err := pp.Fetcher.Update() - return err -} - -func (pp *proxySetProvider) Initial() error { - if pp.healthCheck.auto() { - go pp.healthCheck.process() - } - _, err := pp.Fetcher.Initial() - if err != nil { - return err - } - if subscriptionInfo := cachefile.Cache().GetSubscriptionInfo(pp.Name()); subscriptionInfo != "" { - pp.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo) - } - pp.closeAllConnections() - return nil -} - -func (pp *proxySetProvider) closeAllConnections() { - statistic.DefaultManager.Range(func(c statistic.Tracker) bool { - for _, chain := range c.Chains() { - if chain == pp.Name() { - _ = c.Close() - break - } - } - return true - }) -} - -func (pp *proxySetProvider) Close() error { - _ = pp.baseProvider.Close() - return pp.Fetcher.Close() -} - -func NewProxySetProvider(name string, interval time.Duration, payload []map[string]any, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { - pd := &proxySetProvider{ - baseProvider: baseProvider{ - name: name, - proxies: []C.Proxy{}, - healthCheck: hc, - }, - } - - if len(payload) > 0 { // using as fallback proxies - ps := ProxySchema{Proxies: payload} - buf, err := yaml.Marshal(ps) - if err != nil { - return nil, err - } - proxies, err := parser(buf) - if err != nil { - return nil, err - } - pd.proxies = proxies - } - - fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies) - pd.Fetcher = fetcher - if httpVehicle, ok := vehicle.(*resource.HTTPVehicle); ok { - httpVehicle.SetInRead(func(resp *http.Response) { - if subscriptionInfo := resp.Header.Get("subscription-userinfo"); subscriptionInfo != "" { - cachefile.Cache().SetSubscriptionInfo(name, subscriptionInfo) - pd.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo) - } - }) - } - - wrapper := &ProxySetProvider{pd} - runtime.SetFinalizer(wrapper, (*ProxySetProvider).Close) - return wrapper, nil -} - -func (pp *ProxySetProvider) Close() error { - runtime.SetFinalizer(pp, nil) - return pp.proxySetProvider.Close() -} - -// InlineProvider for auto gc -type InlineProvider struct { - *inlineProvider -} - -type inlineProvider struct { - baseProvider - updateAt time.Time -} - -func (ip *inlineProvider) MarshalJSON() ([]byte, error) { - return json.Marshal(providerForApi{ - Name: ip.Name(), - Type: ip.Type().String(), - VehicleType: ip.VehicleType().String(), - Proxies: ip.Proxies(), - TestUrl: ip.healthCheck.url, - ExpectedStatus: ip.healthCheck.expectedStatus.String(), - UpdatedAt: ip.updateAt, - }) -} - -func (ip *inlineProvider) VehicleType() types.VehicleType { - return types.Inline -} - -func (ip *inlineProvider) Initial() error { - if ip.healthCheck.auto() { - go ip.healthCheck.process() - } - return nil -} - -func (ip *inlineProvider) Update() error { - // make api update happy - ip.updateAt = time.Now() - return nil -} - -func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) { - ps := ProxySchema{Proxies: payload} - buf, err := yaml.Marshal(ps) - if err != nil { - return nil, err - } - proxies, err := parser(buf) - if err != nil { - return nil, err - } - - ip := &inlineProvider{ - baseProvider: baseProvider{ - name: name, - proxies: proxies, - healthCheck: hc, - }, - updateAt: time.Now(), - } - wrapper := &InlineProvider{ip} - runtime.SetFinalizer(wrapper, (*InlineProvider).Close) - return wrapper, nil -} - -func (ip *InlineProvider) Close() error { - runtime.SetFinalizer(ip, nil) - return ip.baseProvider.Close() -} - -// CompatibleProvider for auto gc -type CompatibleProvider struct { - *compatibleProvider -} - -type compatibleProvider struct { - baseProvider -} - -func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { - return json.Marshal(providerForApi{ - Name: cp.Name(), - Type: cp.Type().String(), - VehicleType: cp.VehicleType().String(), - Proxies: cp.Proxies(), - TestUrl: cp.healthCheck.url, - ExpectedStatus: cp.healthCheck.expectedStatus.String(), - }) -} - -func (cp *compatibleProvider) Update() error { - return nil -} - -func (cp *compatibleProvider) Initial() error { - if cp.healthCheck.auto() { - go cp.healthCheck.process() - } - return nil -} - -func (cp *compatibleProvider) VehicleType() types.VehicleType { - return types.Compatible -} - -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") - } - - pd := &compatibleProvider{ - baseProvider: baseProvider{ - name: name, - proxies: proxies, - healthCheck: hc, - }, - } - - wrapper := &CompatibleProvider{pd} - runtime.SetFinalizer(wrapper, (*CompatibleProvider).Close) - return wrapper, nil -} - -func (cp *CompatibleProvider) Close() error { - runtime.SetFinalizer(cp, nil) - return cp.compatibleProvider.Close() -} - -func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) { - excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None) - if err != nil { - return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) - } - var excludeTypeArray []string - if excludeType != "" { - excludeTypeArray = strings.Split(excludeType, "|") - } - - var filterRegs []*regexp2.Regexp - for _, filter := range strings.Split(filter, "`") { - filterReg, err := regexp2.Compile(filter, regexp2.None) - if err != nil { - return nil, fmt.Errorf("invalid filter regex: %w", err) - } - filterRegs = append(filterRegs, filterReg) - } - - return func(buf []byte) ([]C.Proxy, error) { - schema := &ProxySchema{} - - if err := yaml.Unmarshal(buf, schema); err != nil { - proxies, err1 := convert.ConvertsV2Ray(buf) - if err1 != nil { - return nil, fmt.Errorf("%w, %w", err, err1) - } - schema.Proxies = proxies - } - - if schema.Proxies == nil { - return nil, errors.New("file must have a `proxies` field") - } - - proxies := []C.Proxy{} - proxiesSet := map[string]struct{}{} - for _, filterReg := range filterRegs { - for idx, mapping := range schema.Proxies { - if nil != excludeTypeArray && len(excludeTypeArray) > 0 { - mType, ok := mapping["type"] - if !ok { - continue - } - pType, ok := mType.(string) - if !ok { - continue - } - flag := false - for i := range excludeTypeArray { - if strings.EqualFold(pType, excludeTypeArray[i]) { - flag = true - break - } - - } - if flag { - continue - } - - } - mName, ok := mapping["name"] - if !ok { - continue - } - name, ok := mName.(string) - if !ok { - continue - } - if len(excludeFilter) > 0 { - if mat, _ := excludeFilterReg.MatchString(name); mat { - continue - } - } - if len(filter) > 0 { - if mat, _ := filterReg.MatchString(name); !mat { - continue - } - } - if _, ok := proxiesSet[name]; ok { - continue - } - - if len(dialerProxy) > 0 { - mapping["dialer-proxy"] = dialerProxy - } - - val := reflect.ValueOf(override) - for i := 0; i < val.NumField(); i++ { - field := val.Field(i) - if field.IsNil() { - continue - } - fieldName := strings.Split(val.Type().Field(i).Tag.Get("provider"), ",")[0] - switch fieldName { - case "additional-prefix": - name := mapping["name"].(string) - mapping["name"] = *field.Interface().(*string) + name - case "additional-suffix": - name := mapping["name"].(string) - mapping["name"] = name + *field.Interface().(*string) - case "proxy-name": - // Iterate through all naming replacement rules and perform the replacements. - for _, expr := range override.ProxyName { - name := mapping["name"].(string) - newName, err := expr.Pattern.Replace(name, expr.Target, 0, -1) - if err != nil { - return nil, fmt.Errorf("proxy name replace error: %w", err) - } - mapping["name"] = newName - } - default: - mapping[fieldName] = field.Elem().Interface() - } - } - - proxy, err := adapter.ParseProxy(mapping) - if err != nil { - return nil, fmt.Errorf("proxy %d error: %w", idx, err) - } - - proxiesSet[name] = struct{}{} - 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 - }, nil -} diff --git a/adapter/provider/subscription_info.go b/adapter/provider/subscription_info.go deleted file mode 100644 index 2ec8537d7d..0000000000 --- a/adapter/provider/subscription_info.go +++ /dev/null @@ -1,58 +0,0 @@ -package provider - -import ( - "fmt" - "strconv" - "strings" - - "github.com/metacubex/mihomo/log" -) - -type SubscriptionInfo struct { - Upload int64 - Download int64 - Total int64 - Expire int64 -} - -func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo) { - userinfo = strings.ReplaceAll(strings.ToLower(userinfo), " ", "") - si = new(SubscriptionInfo) - - for _, field := range strings.Split(userinfo, ";") { - name, value, ok := strings.Cut(field, "=") - if !ok { - continue - } - - intValue, err := parseValue(value) - if err != nil { - log.Warnln("[Provider] get subscription-userinfo: %e", err) - continue - } - - switch name { - case "upload": - si.Upload = intValue - case "download": - si.Download = intValue - case "total": - si.Total = intValue - case "expire": - si.Expire = intValue - } - } - return si -} - -func parseValue(value string) (int64, error) { - if intValue, err := strconv.ParseInt(value, 10, 64); err == nil { - return intValue, nil - } - - if floatValue, err := strconv.ParseFloat(value, 64); err == nil { - return int64(floatValue), nil - } - - return 0, fmt.Errorf("failed to parse value '%s'", value) -} diff --git a/android_tz.go b/android_tz.go deleted file mode 100644 index 82fc38e315..0000000000 --- a/android_tz.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2014 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89 - -package main - -// #include -import "C" -import "time" - -func init() { - var currentT C.time_t - var currentTM C.struct_tm - C.time(¤tT) - C.localtime_r(¤tT, ¤tTM) - tzOffset := int(currentTM.tm_gmtoff) - tz := C.GoString(currentTM.tm_zone) - time.Local = time.FixedZone(tz, tzOffset) -} diff --git a/check_amd64.sh b/check_amd64.sh deleted file mode 100644 index 068296d33e..0000000000 --- a/check_amd64.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -flags=$(grep '^flags\b' = a.b2.Len() { - d = 1 - } else { - d = a.b2.Len() / a.b1.Len() - } - a.p = min(a.p+d, a.c) - - a.replace(ent) - ent.setMRU(a.t2) - case ent.ll == a.b2: - // Case III - // Cache Miss in t1 and t2 - - // Adaptation - var d int - if a.b2.Len() >= a.b1.Len() { - d = 1 - } else { - d = a.b1.Len() / a.b2.Len() - } - a.p = max(a.p-d, 0) - - a.replace(ent) - ent.setMRU(a.t2) - case ent.ll == nil && a.t1.Len()+a.b1.Len() == a.c: - // Case IV A - if a.t1.Len() < a.c { - a.delLRU(a.b1) - a.replace(ent) - } else { - a.delLRU(a.t1) - } - ent.setMRU(a.t1) - case ent.ll == nil && a.t1.Len()+a.b1.Len() < a.c: - // Case IV B - if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() >= a.c { - if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() == 2*a.c { - a.delLRU(a.b2) - } - a.replace(ent) - } - ent.setMRU(a.t1) - case ent.ll == nil: - // Case IV, not A nor B - ent.setMRU(a.t1) - } -} - -func (a *ARC[K, V]) delLRU(list *list.List[*entry[K, V]]) { - lru := list.Back() - list.Remove(lru) - a.len-- - delete(a.cache, lru.Value.key) -} - -func (a *ARC[K, V]) replace(ent *entry[K, V]) { - if a.t1.Len() > 0 && ((a.t1.Len() > a.p) || (ent.ll == a.b2 && a.t1.Len() == a.p)) { - lru := a.t1.Back().Value - lru.value = lo.Empty[V]() - lru.ghost = true - a.len-- - lru.setMRU(a.b1) - } else { - lru := a.t2.Back().Value - lru.value = lo.Empty[V]() - lru.ghost = true - a.len-- - lru.setMRU(a.b2) - } -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func max(a int, b int) int { - if a < b { - return b - } - return a -} diff --git a/common/arc/arc_test.go b/common/arc/arc_test.go deleted file mode 100644 index a9d8a0c1bc..0000000000 --- a/common/arc/arc_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package arc - -import ( - "testing" -) - -func TestInsertion(t *testing.T) { - cache := New[string, string](WithSize[string, string](3)) - if got, want := cache.Len(), 0; got != want { - t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want) - } - - const ( - k1 = "Hello" - k2 = "Hallo" - k3 = "Ciao" - k4 = "Salut" - - v1 = "World" - v2 = "Worlds" - v3 = "Welt" - ) - - // Insert the first value - cache.Set(k1, v1) - if got, want := cache.Len(), 1; got != want { - t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) - } - if got, ok := cache.Get(k1); !ok || got != v1 { - t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v1) - } - - // Replace existing value for a given key - cache.Set(k1, v2) - if got, want := cache.Len(), 1; got != want { - t.Errorf("re-insertion: cache.Len(): got %d want %d", cache.Len(), want) - } - if got, ok := cache.Get(k1); !ok || got != v2 { - t.Errorf("re-insertion: cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2) - } - - // Add a second different key - cache.Set(k2, v3) - if got, want := cache.Len(), 2; got != want { - t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) - } - if got, ok := cache.Get(k1); !ok || got != v2 { - t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2) - } - if got, ok := cache.Get(k2); !ok || got != v3 { - t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k2, got, ok, v3) - } - - // Fill cache - cache.Set(k3, v1) - if got, want := cache.Len(), 3; got != want { - t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) - } - - // Exceed size, this should not exceed size: - cache.Set(k4, v1) - if got, want := cache.Len(), 3; got != want { - t.Errorf("insertion of key out of size: cache.Len(): got %d want %d", cache.Len(), want) - } -} - -func TestEviction(t *testing.T) { - size := 3 - cache := New[string, string](WithSize[string, string](size)) - if got, want := cache.Len(), 0; got != want { - t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want) - } - - tests := []struct { - k, v string - }{ - {"k1", "v1"}, - {"k2", "v2"}, - {"k3", "v3"}, - {"k4", "v4"}, - } - for i, tt := range tests[:size] { - cache.Set(tt.k, tt.v) - if got, want := cache.Len(), i+1; got != want { - t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) - } - } - - // Exceed size and check we don't outgrow it: - cache.Set(tests[size].k, tests[size].v) - if got := cache.Len(); got != size { - t.Errorf("insertion of overflow key #%d: cache.Len(): got %d want %d", 4, cache.Len(), size) - } - - // Check that LRU got evicted: - if got, ok := cache.Get(tests[0].k); ok || got != "" { - t.Errorf("cache.Get(%v): got (%v,%t) want (,true)", tests[0].k, got, ok) - } - - for _, tt := range tests[1:] { - if got, ok := cache.Get(tt.k); !ok || got != tt.v { - t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", tt.k, got, ok, tt.v) - } - } -} diff --git a/common/arc/entry.go b/common/arc/entry.go deleted file mode 100644 index 679cbc1c3c..0000000000 --- a/common/arc/entry.go +++ /dev/null @@ -1,32 +0,0 @@ -package arc - -import ( - list "github.com/bahlo/generic-list-go" -) - -type entry[K comparable, V any] struct { - key K - value V - ll *list.List[*entry[K, V]] - el *list.Element[*entry[K, V]] - ghost bool - expires int64 -} - -func (e *entry[K, V]) setLRU(list *list.List[*entry[K, V]]) { - e.detach() - e.ll = list - e.el = e.ll.PushBack(e) -} - -func (e *entry[K, V]) setMRU(list *list.List[*entry[K, V]]) { - e.detach() - e.ll = list - e.el = e.ll.PushFront(e) -} - -func (e *entry[K, V]) detach() { - if e.ll != nil { - e.ll.Remove(e.el) - } -} diff --git a/common/atomic/type.go b/common/atomic/type.go deleted file mode 100644 index 71695c63be..0000000000 --- a/common/atomic/type.go +++ /dev/null @@ -1,198 +0,0 @@ -package atomic - -import ( - "encoding/json" - "fmt" - "strconv" - "sync/atomic" -) - -type Bool struct { - atomic.Bool -} - -func NewBool(val bool) (i Bool) { - i.Store(val) - return -} - -func (i *Bool) MarshalJSON() ([]byte, error) { - return json.Marshal(i.Load()) -} - -func (i *Bool) UnmarshalJSON(b []byte) error { - var v bool - if err := json.Unmarshal(b, &v); err != nil { - return err - } - i.Store(v) - return nil -} - -func (i *Bool) String() string { - v := i.Load() - return strconv.FormatBool(v) -} - -type Pointer[T any] struct { - atomic.Pointer[T] -} - -func NewPointer[T any](v *T) (p Pointer[T]) { - if v != nil { - p.Store(v) - } - return -} - -func (p *Pointer[T]) MarshalJSON() ([]byte, error) { - return json.Marshal(p.Load()) -} - -func (p *Pointer[T]) UnmarshalJSON(b []byte) error { - var v *T - if err := json.Unmarshal(b, &v); err != nil { - return err - } - p.Store(v) - return nil -} - -func (p *Pointer[T]) String() string { - return fmt.Sprint(p.Load()) -} - -type Int32 struct { - atomic.Int32 -} - -func NewInt32(val int32) (i Int32) { - i.Store(val) - return -} - -func (i *Int32) MarshalJSON() ([]byte, error) { - return json.Marshal(i.Load()) -} - -func (i *Int32) UnmarshalJSON(b []byte) error { - var v int32 - if err := json.Unmarshal(b, &v); err != nil { - return err - } - i.Store(v) - return nil -} - -func (i *Int32) String() string { - v := i.Load() - return strconv.FormatInt(int64(v), 10) -} - -type Int64 struct { - atomic.Int64 -} - -func NewInt64(val int64) (i Int64) { - i.Store(val) - return -} - -func (i *Int64) MarshalJSON() ([]byte, error) { - return json.Marshal(i.Load()) -} - -func (i *Int64) UnmarshalJSON(b []byte) error { - var v int64 - if err := json.Unmarshal(b, &v); err != nil { - return err - } - i.Store(v) - return nil -} - -func (i *Int64) String() string { - v := i.Load() - return strconv.FormatInt(int64(v), 10) -} - -type Uint32 struct { - atomic.Uint32 -} - -func NewUint32(val uint32) (i Uint32) { - i.Store(val) - return -} - -func (i *Uint32) MarshalJSON() ([]byte, error) { - return json.Marshal(i.Load()) -} - -func (i *Uint32) UnmarshalJSON(b []byte) error { - var v uint32 - if err := json.Unmarshal(b, &v); err != nil { - return err - } - i.Store(v) - return nil -} - -func (i *Uint32) String() string { - v := i.Load() - return strconv.FormatUint(uint64(v), 10) -} - -type Uint64 struct { - atomic.Uint64 -} - -func NewUint64(val uint64) (i Uint64) { - i.Store(val) - return -} - -func (i *Uint64) MarshalJSON() ([]byte, error) { - return json.Marshal(i.Load()) -} - -func (i *Uint64) UnmarshalJSON(b []byte) error { - var v uint64 - if err := json.Unmarshal(b, &v); err != nil { - return err - } - i.Store(v) - return nil -} - -func (i *Uint64) String() string { - v := i.Load() - return strconv.FormatUint(uint64(v), 10) -} - -type Uintptr struct { - atomic.Uintptr -} - -func NewUintptr(val uintptr) (i Uintptr) { - i.Store(val) - return -} - -func (i *Uintptr) MarshalJSON() ([]byte, error) { - return json.Marshal(i.Load()) -} - -func (i *Uintptr) UnmarshalJSON(b []byte) error { - var v uintptr - if err := json.Unmarshal(b, &v); err != nil { - return err - } - i.Store(v) - return nil -} - -func (i *Uintptr) String() string { - v := i.Load() - return strconv.FormatUint(uint64(v), 10) -} diff --git a/common/atomic/value.go b/common/atomic/value.go deleted file mode 100644 index 82d400766b..0000000000 --- a/common/atomic/value.go +++ /dev/null @@ -1,75 +0,0 @@ -package atomic - -import ( - "encoding/json" - "sync/atomic" -) - -func DefaultValue[T any]() T { - var defaultValue T - return defaultValue -} - -type TypedValue[T any] struct { - _ noCopy - value atomic.Value -} - -// tValue is a struct with determined type to resolve atomic.Value usages with interface types -// https://github.com/golang/go/issues/22550 -// -// The intention to have an atomic value store for errors. However, running this code panics: -// panic: sync/atomic: store of inconsistently typed value into Value -// This is because atomic.Value requires that the underlying concrete type be the same (which is a reasonable expectation for its implementation). -// When going through the atomic.Value.Store method call, the fact that both these are of the error interface is lost. -type tValue[T any] struct { - value T -} - -func (t *TypedValue[T]) Load() T { - value := t.value.Load() - if value == nil { - return DefaultValue[T]() - } - return value.(tValue[T]).value -} - -func (t *TypedValue[T]) Store(value T) { - t.value.Store(tValue[T]{value}) -} - -func (t *TypedValue[T]) Swap(new T) T { - old := t.value.Swap(tValue[T]{new}) - if old == nil { - return DefaultValue[T]() - } - return old.(tValue[T]).value -} - -func (t *TypedValue[T]) CompareAndSwap(old, new T) bool { - return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) -} - -func (t *TypedValue[T]) MarshalJSON() ([]byte, error) { - return json.Marshal(t.Load()) -} - -func (t *TypedValue[T]) UnmarshalJSON(b []byte) error { - var v T - if err := json.Unmarshal(b, &v); err != nil { - return err - } - t.Store(v) - return nil -} - -func NewTypedValue[T any](t T) (v TypedValue[T]) { - v.Store(t) - return -} - -type noCopy struct{} - -// Lock is a no-op used by -copylocks checker from `go vet`. -func (*noCopy) Lock() {} -func (*noCopy) Unlock() {} diff --git a/common/batch/batch.go b/common/batch/batch.go deleted file mode 100644 index 5a6360e56f..0000000000 --- a/common/batch/batch.go +++ /dev/null @@ -1,105 +0,0 @@ -package batch - -import ( - "context" - "sync" -) - -type Option[T any] func(b *Batch[T]) - -type Result[T any] struct { - Value T - Err error -} - -type Error struct { - Key string - Err error -} - -func WithConcurrencyNum[T any](n int) Option[T] { - return func(b *Batch[T]) { - q := make(chan struct{}, n) - for i := 0; i < n; i++ { - q <- struct{}{} - } - b.queue = q - } -} - -// Batch similar to errgroup, but can control the maximum number of concurrent -type Batch[T any] struct { - result map[string]Result[T] - queue chan struct{} - wg sync.WaitGroup - mux sync.Mutex - err *Error - once sync.Once - cancel func() -} - -func (b *Batch[T]) Go(key string, fn func() (T, 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[T]{value, err} - b.mux.Lock() - defer b.mux.Unlock() - b.result[key] = ret - }() -} - -func (b *Batch[T]) Wait() *Error { - b.wg.Wait() - if b.cancel != nil { - b.cancel() - } - return b.err -} - -func (b *Batch[T]) WaitAndGetResult() (map[string]Result[T], *Error) { - err := b.Wait() - return b.Result(), err -} - -func (b *Batch[T]) Result() map[string]Result[T] { - b.mux.Lock() - defer b.mux.Unlock() - copyM := map[string]Result[T]{} - for k, v := range b.result { - copyM[k] = v - } - return copyM -} - -func New[T any](ctx context.Context, opts ...Option[T]) (*Batch[T], context.Context) { - ctx, cancel := context.WithCancel(ctx) - - b := &Batch[T]{ - result: map[string]Result[T]{}, - } - - 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 deleted file mode 100644 index 73350fd375..0000000000 --- a/common/batch/batch_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package batch - -import ( - "context" - "errors" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestBatch(t *testing.T) { - b, _ := New[string](context.Background()) - - now := time.Now() - b.Go("foo", func() (string, error) { - time.Sleep(time.Millisecond * 100) - return "foo", nil - }) - b.Go("bar", func() (string, 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) - } -} - -func TestBatchWithConcurrencyNum(t *testing.T) { - b, _ := New[string]( - context.Background(), - WithConcurrencyNum[string](3), - ) - - now := time.Now() - for i := 0; i < 7; i++ { - idx := i - b.Go(strconv.Itoa(idx), func() (string, 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) - } -} - -func TestBatchContext(t *testing.T) { - b, ctx := New[string](context.Background()) - - b.Go("error", func() (string, error) { - time.Sleep(time.Millisecond * 100) - return "", errors.New("test error") - }) - - b.Go("ctx", func() (string, error) { - <-ctx.Done() - return "", 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/buf/sing.go b/common/buf/sing.go deleted file mode 100644 index 59c650adeb..0000000000 --- a/common/buf/sing.go +++ /dev/null @@ -1,21 +0,0 @@ -package buf - -import ( - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/buf" -) - -const BufferSize = buf.BufferSize - -type Buffer = buf.Buffer - -var New = buf.New -var NewPacket = buf.NewPacket -var NewSize = buf.NewSize -var With = buf.With -var As = buf.As - -var ( - Must = common.Must - Error = common.Error -) diff --git a/common/callback/callback.go b/common/callback/callback.go deleted file mode 100644 index 9ae0f94a38..0000000000 --- a/common/callback/callback.go +++ /dev/null @@ -1,55 +0,0 @@ -package callback - -import ( - "github.com/metacubex/mihomo/common/buf" - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" -) - -type firstWriteCallBackConn struct { - C.Conn - callback func(error) - written bool -} - -func (c *firstWriteCallBackConn) Write(b []byte) (n int, err error) { - defer func() { - if !c.written { - c.written = true - c.callback(err) - } - }() - return c.Conn.Write(b) -} - -func (c *firstWriteCallBackConn) WriteBuffer(buffer *buf.Buffer) (err error) { - defer func() { - if !c.written { - c.written = true - c.callback(err) - } - }() - return c.Conn.WriteBuffer(buffer) -} - -func (c *firstWriteCallBackConn) Upstream() any { - return c.Conn -} - -func (c *firstWriteCallBackConn) WriterReplaceable() bool { - return c.written -} - -func (c *firstWriteCallBackConn) ReaderReplaceable() bool { - return true -} - -var _ N.ExtendedConn = (*firstWriteCallBackConn)(nil) - -func NewFirstWriteCallBackConn(c C.Conn, callback func(error)) C.Conn { - return &firstWriteCallBackConn{ - Conn: c, - callback: callback, - written: false, - } -} diff --git a/common/callback/close_callback.go b/common/callback/close_callback.go deleted file mode 100644 index 630ee5d7f7..0000000000 --- a/common/callback/close_callback.go +++ /dev/null @@ -1,61 +0,0 @@ -package callback - -import ( - "sync" - - C "github.com/metacubex/mihomo/constant" -) - -type closeCallbackConn struct { - C.Conn - closeFunc func() - closeOnce sync.Once -} - -func (w *closeCallbackConn) Close() error { - w.closeOnce.Do(w.closeFunc) - return w.Conn.Close() -} - -func (w *closeCallbackConn) ReaderReplaceable() bool { - return true -} - -func (w *closeCallbackConn) WriterReplaceable() bool { - return true -} - -func (w *closeCallbackConn) Upstream() any { - return w.Conn -} - -func NewCloseCallbackConn(conn C.Conn, callback func()) C.Conn { - return &closeCallbackConn{Conn: conn, closeFunc: callback} -} - -type closeCallbackPacketConn struct { - C.PacketConn - closeFunc func() - closeOnce sync.Once -} - -func (w *closeCallbackPacketConn) Close() error { - w.closeOnce.Do(w.closeFunc) - return w.PacketConn.Close() -} - -func (w *closeCallbackPacketConn) ReaderReplaceable() bool { - return true -} - -func (w *closeCallbackPacketConn) WriterReplaceable() bool { - return true -} - -func (w *closeCallbackPacketConn) Upstream() any { - return w.PacketConn -} - -func NewCloseCallbackPacketConn(conn C.PacketConn, callback func()) C.PacketConn { - return &closeCallbackPacketConn{PacketConn: conn, closeFunc: callback} -} diff --git a/common/cmd/cmd.go b/common/cmd/cmd.go deleted file mode 100644 index e015dd2e7e..0000000000 --- a/common/cmd/cmd.go +++ /dev/null @@ -1,36 +0,0 @@ -package cmd - -import ( - "fmt" - "os/exec" - "strings" -) - -func ExecCmd(cmdStr string) (string, error) { - args := splitArgs(cmdStr) - - var cmd *exec.Cmd - if len(args) == 1 { - cmd = exec.Command(args[0]) - } else { - cmd = exec.Command(args[0], args[1:]...) - - } - prepareBackgroundCommand(cmd) - out, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("%v, %s", err, string(out)) - } - return string(out), nil -} - -func splitArgs(cmd string) []string { - args := strings.Split(cmd, " ") - - // use in pipeline - if len(args) > 2 && strings.ContainsAny(cmd, "|") { - suffix := strings.Join(args[2:], " ") - args = append(args[:2], suffix) - } - return args -} diff --git a/common/cmd/cmd_other.go b/common/cmd/cmd_other.go deleted file mode 100644 index c6bc4ac478..0000000000 --- a/common/cmd/cmd_other.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !windows - -package cmd - -import ( - "os/exec" -) - -func prepareBackgroundCommand(cmd *exec.Cmd) { - -} diff --git a/common/cmd/cmd_test.go b/common/cmd/cmd_test.go deleted file mode 100644 index b124a22dcf..0000000000 --- a/common/cmd/cmd_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package cmd - -import ( - "runtime" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSplitArgs(t *testing.T) { - args := splitArgs("ls") - args1 := splitArgs("ls -la") - args2 := splitArgs("bash -c ls") - args3 := splitArgs("bash -c ls -lahF | grep 'cmd'") - - assert.Equal(t, 1, len(args)) - assert.Equal(t, 2, len(args1)) - assert.Equal(t, 3, len(args2)) - assert.Equal(t, 3, len(args3)) -} - -func TestExecCmd(t *testing.T) { - if runtime.GOOS == "windows" { - _, err := ExecCmd("cmd -c 'dir'") - assert.Nil(t, err) - return - } - - _, err := ExecCmd("ls") - _, err1 := ExecCmd("ls -la") - _, err2 := ExecCmd("bash -c ls") - _, err3 := ExecCmd("bash -c ls -la") - _, err4 := ExecCmd("bash -c ls -la | grep 'cmd'") - - assert.Nil(t, err) - assert.Nil(t, err1) - assert.Nil(t, err2) - assert.Nil(t, err3) - assert.Nil(t, err4) -} diff --git a/common/cmd/cmd_windows.go b/common/cmd/cmd_windows.go deleted file mode 100644 index 5d7fd9d387..0000000000 --- a/common/cmd/cmd_windows.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build windows - -package cmd - -import ( - "os/exec" - "syscall" -) - -func prepareBackgroundCommand(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} -} diff --git a/common/contextutils/afterfunc_compact.go b/common/contextutils/afterfunc_compact.go deleted file mode 100644 index 2400ddb31a..0000000000 --- a/common/contextutils/afterfunc_compact.go +++ /dev/null @@ -1,31 +0,0 @@ -package contextutils - -import ( - "context" - "sync" -) - -func afterFunc(ctx context.Context, f func()) (stop func() bool) { - stopc := make(chan struct{}) - once := sync.Once{} // either starts running f or stops f from running - if ctx.Done() != nil { - go func() { - select { - case <-ctx.Done(): - once.Do(func() { - go f() - }) - case <-stopc: - } - }() - } - - return func() bool { - stopped := false - once.Do(func() { - stopped = true - close(stopc) - }) - return stopped - } -} diff --git a/common/contextutils/afterfunc_go120.go b/common/contextutils/afterfunc_go120.go deleted file mode 100644 index 6ff22bda6d..0000000000 --- a/common/contextutils/afterfunc_go120.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !go1.21 - -package contextutils - -import ( - "context" -) - -func AfterFunc(ctx context.Context, f func()) (stop func() bool) { - return afterFunc(ctx, f) -} diff --git a/common/contextutils/afterfunc_go121.go b/common/contextutils/afterfunc_go121.go deleted file mode 100644 index b9d4c1fa2b..0000000000 --- a/common/contextutils/afterfunc_go121.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build go1.21 - -package contextutils - -import "context" - -func AfterFunc(ctx context.Context, f func()) (stop func() bool) { - return context.AfterFunc(ctx, f) -} diff --git a/common/contextutils/afterfunc_test.go b/common/contextutils/afterfunc_test.go deleted file mode 100644 index 05f6a05584..0000000000 --- a/common/contextutils/afterfunc_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package contextutils - -import ( - "context" - "testing" - "time" -) - -const ( - shortDuration = 1 * time.Millisecond // a reasonable duration to block in a test - veryLongDuration = 1000 * time.Hour // an arbitrary upper bound on the test's running time -) - -func TestAfterFuncCalledAfterCancel(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - donec := make(chan struct{}) - stop := afterFunc(ctx, func() { - close(donec) - }) - select { - case <-donec: - t.Fatalf("AfterFunc called before context is done") - case <-time.After(shortDuration): - } - cancel() - select { - case <-donec: - case <-time.After(veryLongDuration): - t.Fatalf("AfterFunc not called after context is canceled") - } - if stop() { - t.Fatalf("stop() = true, want false") - } -} - -func TestAfterFuncCalledAfterTimeout(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), shortDuration) - defer cancel() - donec := make(chan struct{}) - afterFunc(ctx, func() { - close(donec) - }) - select { - case <-donec: - case <-time.After(veryLongDuration): - t.Fatalf("AfterFunc not called after context is canceled") - } -} - -func TestAfterFuncCalledImmediately(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - donec := make(chan struct{}) - afterFunc(ctx, func() { - close(donec) - }) - select { - case <-donec: - case <-time.After(veryLongDuration): - t.Fatalf("AfterFunc not called for already-canceled context") - } -} - -func TestAfterFuncNotCalledAfterStop(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - donec := make(chan struct{}) - stop := afterFunc(ctx, func() { - close(donec) - }) - if !stop() { - t.Fatalf("stop() = false, want true") - } - cancel() - select { - case <-donec: - t.Fatalf("AfterFunc called for already-canceled context") - case <-time.After(shortDuration): - } - if stop() { - t.Fatalf("stop() = true, want false") - } -} - -// This test verifies that canceling a context does not block waiting for AfterFuncs to finish. -func TestAfterFuncCalledAsynchronously(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - donec := make(chan struct{}) - stop := afterFunc(ctx, func() { - // The channel send blocks until donec is read from. - donec <- struct{}{} - }) - defer stop() - cancel() - // After cancel returns, read from donec and unblock the AfterFunc. - select { - case <-donec: - case <-time.After(veryLongDuration): - t.Fatalf("AfterFunc not called after context is canceled") - } -} diff --git a/common/convert/base64.go b/common/convert/base64.go deleted file mode 100644 index 21d818f361..0000000000 --- a/common/convert/base64.go +++ /dev/null @@ -1,45 +0,0 @@ -package convert - -import ( - "encoding/base64" - "strings" -) - -var ( - encRaw = base64.RawStdEncoding - enc = base64.StdEncoding -) - -// DecodeBase64 try to decode content from the given bytes, -// which can be in base64.RawStdEncoding, base64.StdEncoding or just plaintext. -func DecodeBase64(buf []byte) []byte { - result, err := tryDecodeBase64(buf) - if err != nil { - return buf - } - return result -} - -func tryDecodeBase64(buf []byte) ([]byte, error) { - dBuf := make([]byte, encRaw.DecodedLen(len(buf))) - n, err := encRaw.Decode(dBuf, buf) - if err != nil { - n, err = enc.Decode(dBuf, buf) - if err != nil { - return nil, err - } - } - return dBuf[:n], nil -} - -func urlSafe(data string) string { - return strings.NewReplacer("+", "-", "/", "_").Replace(data) -} - -func decodeUrlSafe(data string) string { - dcBuf, err := base64.RawURLEncoding.DecodeString(data) - if err != nil { - return "" - } - return string(dcBuf) -} diff --git a/common/convert/converter.go b/common/convert/converter.go deleted file mode 100644 index acf6abbccc..0000000000 --- a/common/convert/converter.go +++ /dev/null @@ -1,536 +0,0 @@ -package convert - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "net/url" - "strconv" - "strings" - - "github.com/metacubex/mihomo/log" -) - -// ConvertsV2Ray convert V2Ray subscribe proxies data to mihomo proxies config -func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { - data := DecodeBase64(buf) - - arr := strings.Split(string(data), "\n") - - proxies := make([]map[string]any, 0, len(arr)) - names := make(map[string]int, 200) - - for _, line := range arr { - line = strings.TrimRight(line, " \r") - if line == "" { - continue - } - - scheme, body, found := strings.Cut(line, "://") - if !found { - continue - } - - scheme = strings.ToLower(scheme) - switch scheme { - case "hysteria": - urlHysteria, err := url.Parse(line) - if err != nil { - continue - } - - query := urlHysteria.Query() - name := uniqueName(names, urlHysteria.Fragment) - hysteria := make(map[string]any, 20) - - hysteria["name"] = name - hysteria["type"] = scheme - hysteria["server"] = urlHysteria.Hostname() - hysteria["port"] = urlHysteria.Port() - hysteria["sni"] = query.Get("peer") - hysteria["obfs"] = query.Get("obfs") - if alpn := query.Get("alpn"); alpn != "" { - hysteria["alpn"] = strings.Split(alpn, ",") - } - hysteria["auth_str"] = query.Get("auth") - hysteria["protocol"] = query.Get("protocol") - up := query.Get("up") - down := query.Get("down") - if up == "" { - up = query.Get("upmbps") - } - if down == "" { - down = query.Get("downmbps") - } - hysteria["down"] = down - hysteria["up"] = up - hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure")) - - proxies = append(proxies, hysteria) - - case "hysteria2", "hy2": - urlHysteria2, err := url.Parse(line) - if err != nil { - continue - } - - query := urlHysteria2.Query() - name := uniqueName(names, urlHysteria2.Fragment) - hysteria2 := make(map[string]any, 20) - - hysteria2["name"] = name - hysteria2["type"] = "hysteria2" - hysteria2["server"] = urlHysteria2.Hostname() - if port := urlHysteria2.Port(); port != "" { - hysteria2["port"] = port - } else { - hysteria2["port"] = "443" - } - hysteria2["obfs"] = query.Get("obfs") - hysteria2["obfs-password"] = query.Get("obfs-password") - hysteria2["sni"] = query.Get("sni") - hysteria2["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure")) - if alpn := query.Get("alpn"); alpn != "" { - hysteria2["alpn"] = strings.Split(alpn, ",") - } - if auth := urlHysteria2.User.String(); auth != "" { - hysteria2["password"] = auth - } - hysteria2["fingerprint"] = query.Get("pinSHA256") - hysteria2["down"] = query.Get("down") - hysteria2["up"] = query.Get("up") - - proxies = append(proxies, hysteria2) - - case "tuic": - // A temporary unofficial TUIC share link standard - // Modified from https://github.com/daeuniverse/dae/discussions/182 - // Changes: - // 1. Support TUICv4, just replace uuid:password with token - // 2. Remove `allow_insecure` field - urlTUIC, err := url.Parse(line) - if err != nil { - continue - } - query := urlTUIC.Query() - - tuic := make(map[string]any, 20) - tuic["name"] = uniqueName(names, urlTUIC.Fragment) - tuic["type"] = scheme - tuic["server"] = urlTUIC.Hostname() - tuic["port"] = urlTUIC.Port() - tuic["udp"] = true - password, v5 := urlTUIC.User.Password() - if v5 { - tuic["uuid"] = urlTUIC.User.Username() - tuic["password"] = password - } else { - tuic["token"] = urlTUIC.User.Username() - } - if cc := query.Get("congestion_control"); cc != "" { - tuic["congestion-controller"] = cc - } - if alpn := query.Get("alpn"); alpn != "" { - tuic["alpn"] = strings.Split(alpn, ",") - } - if sni := query.Get("sni"); sni != "" { - tuic["sni"] = sni - } - if query.Get("disable_sni") == "1" { - tuic["disable-sni"] = true - } - if udpRelayMode := query.Get("udp_relay_mode"); udpRelayMode != "" { - tuic["udp-relay-mode"] = udpRelayMode - } - - proxies = append(proxies, tuic) - - case "trojan": - urlTrojan, err := url.Parse(line) - if err != nil { - continue - } - - query := urlTrojan.Query() - - name := uniqueName(names, urlTrojan.Fragment) - trojan := make(map[string]any, 20) - - trojan["name"] = name - trojan["type"] = scheme - trojan["server"] = urlTrojan.Hostname() - trojan["port"] = urlTrojan.Port() - trojan["password"] = urlTrojan.User.Username() - trojan["udp"] = true - trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure")) - - if sni := query.Get("sni"); sni != "" { - trojan["sni"] = sni - } - if alpn := query.Get("alpn"); alpn != "" { - trojan["alpn"] = strings.Split(alpn, ",") - } - - network := strings.ToLower(query.Get("type")) - if network != "" { - trojan["network"] = network - } - - switch network { - case "ws": - headers := make(map[string]any) - wsOpts := make(map[string]any) - - headers["User-Agent"] = RandUserAgent() - - wsOpts["path"] = query.Get("path") - wsOpts["headers"] = headers - - trojan["ws-opts"] = wsOpts - - case "grpc": - grpcOpts := make(map[string]any) - grpcOpts["grpc-service-name"] = query.Get("serviceName") - trojan["grpc-opts"] = grpcOpts - } - - if fingerprint := query.Get("fp"); fingerprint == "" { - trojan["client-fingerprint"] = "chrome" - } else { - trojan["client-fingerprint"] = fingerprint - } - - proxies = append(proxies, trojan) - - case "vless": - urlVLess, err := url.Parse(line) - if err != nil { - continue - } - query := urlVLess.Query() - vless := make(map[string]any, 20) - err = handleVShareLink(names, urlVLess, scheme, vless) - if err != nil { - log.Warnln("error:%s line:%s", err.Error(), line) - continue - } - if flow := query.Get("flow"); flow != "" { - vless["flow"] = strings.ToLower(flow) - } - proxies = append(proxies, vless) - - case "vmess": - // V2RayN-styled share link - // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) - dcBuf, err := tryDecodeBase64([]byte(body)) - if err != nil { - // Xray VMessAEAD share link - urlVMess, err := url.Parse(line) - if err != nil { - continue - } - query := urlVMess.Query() - vmess := make(map[string]any, 20) - err = handleVShareLink(names, urlVMess, scheme, vmess) - if err != nil { - log.Warnln("error:%s line:%s", err.Error(), line) - continue - } - vmess["alterId"] = 0 - vmess["cipher"] = "auto" - if encryption := query.Get("encryption"); encryption != "" { - vmess["cipher"] = encryption - } - proxies = append(proxies, vmess) - continue - } - - jsonDc := json.NewDecoder(bytes.NewReader(dcBuf)) - values := make(map[string]any, 20) - - if jsonDc.Decode(&values) != nil { - continue - } - tempName, ok := values["ps"].(string) - if !ok { - continue - } - name := uniqueName(names, tempName) - vmess := make(map[string]any, 20) - - vmess["name"] = name - vmess["type"] = scheme - vmess["server"] = values["add"] - vmess["port"] = values["port"] - vmess["uuid"] = values["id"] - if alterId, ok := values["aid"]; ok { - vmess["alterId"] = alterId - } else { - vmess["alterId"] = 0 - } - vmess["udp"] = true - vmess["xudp"] = true - vmess["tls"] = false - vmess["skip-cert-verify"] = false - - vmess["cipher"] = "auto" - if cipher, ok := values["scy"].(string); ok && cipher != "" { - vmess["cipher"] = cipher - } - - if sni, ok := values["sni"].(string); ok && sni != "" { - vmess["servername"] = sni - } - - network, ok := values["net"].(string) - if ok { - network = strings.ToLower(network) - if values["type"] == "http" { - network = "http" - } else if network == "http" { - network = "h2" - } - vmess["network"] = network - } - - tls, ok := values["tls"].(string) - if ok { - tls = strings.ToLower(tls) - if strings.HasSuffix(tls, "tls") { - vmess["tls"] = true - } - if alpn, ok := values["alpn"].(string); ok { - vmess["alpn"] = strings.Split(alpn, ",") - } - } - - switch network { - case "http": - headers := make(map[string]any) - httpOpts := make(map[string]any) - if host, ok := values["host"].(string); ok && host != "" { - headers["Host"] = []string{host} - } - httpOpts["path"] = []string{"/"} - if path, ok := values["path"].(string); ok && path != "" { - httpOpts["path"] = []string{path} - } - httpOpts["headers"] = headers - - vmess["http-opts"] = httpOpts - - case "h2": - headers := make(map[string]any) - h2Opts := make(map[string]any) - if host, ok := values["host"].(string); ok && host != "" { - headers["Host"] = []string{host} - } - - h2Opts["path"] = values["path"] - h2Opts["headers"] = headers - - vmess["h2-opts"] = h2Opts - - case "ws", "httpupgrade": - headers := make(map[string]any) - wsOpts := make(map[string]any) - wsOpts["path"] = "/" - if host, ok := values["host"].(string); ok && host != "" { - headers["Host"] = host - } - if path, ok := values["path"].(string); ok && path != "" { - path := path - pathURL, err := url.Parse(path) - if err == nil { - query := pathURL.Query() - if earlyData := query.Get("ed"); earlyData != "" { - med, err := strconv.Atoi(earlyData) - if err == nil { - switch network { - case "ws": - wsOpts["max-early-data"] = med - wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol" - case "httpupgrade": - wsOpts["v2ray-http-upgrade-fast-open"] = true - } - query.Del("ed") - pathURL.RawQuery = query.Encode() - path = pathURL.String() - } - } - if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" { - wsOpts["early-data-header-name"] = earlyDataHeader - } - } - wsOpts["path"] = path - } - wsOpts["headers"] = headers - vmess["ws-opts"] = wsOpts - - case "grpc": - grpcOpts := make(map[string]any) - grpcOpts["grpc-service-name"] = values["path"] - vmess["grpc-opts"] = grpcOpts - } - - proxies = append(proxies, vmess) - - case "ss": - urlSS, err := url.Parse(line) - if err != nil { - continue - } - - name := uniqueName(names, urlSS.Fragment) - port := urlSS.Port() - - if port == "" { - dcBuf, err := encRaw.DecodeString(urlSS.Host) - if err != nil { - continue - } - - urlSS, err = url.Parse("ss://" + string(dcBuf)) - if err != nil { - continue - } - } - - var ( - cipherRaw = urlSS.User.Username() - cipher string - password string - ) - cipher = cipherRaw - if password, found = urlSS.User.Password(); !found { - dcBuf, err := base64.RawURLEncoding.DecodeString(cipherRaw) - if err != nil { - dcBuf, _ = enc.DecodeString(cipherRaw) - } - cipher, password, found = strings.Cut(string(dcBuf), ":") - if !found { - continue - } - err = VerifyMethod(cipher, password) - if err != nil { - dcBuf, _ = encRaw.DecodeString(cipherRaw) - cipher, password, found = strings.Cut(string(dcBuf), ":") - } - } - - ss := make(map[string]any, 10) - - ss["name"] = name - ss["type"] = scheme - ss["server"] = urlSS.Hostname() - ss["port"] = urlSS.Port() - ss["cipher"] = cipher - ss["password"] = password - query := urlSS.Query() - ss["udp"] = true - if query.Get("udp-over-tcp") == "true" || query.Get("uot") == "1" { - ss["udp-over-tcp"] = true - } - plugin := query.Get("plugin") - if strings.Contains(plugin, ";") { - pluginInfo, _ := url.ParseQuery("pluginName=" + strings.ReplaceAll(plugin, ";", "&")) - pluginName := pluginInfo.Get("pluginName") - if strings.Contains(pluginName, "obfs") { - ss["plugin"] = "obfs" - ss["plugin-opts"] = map[string]any{ - "mode": pluginInfo.Get("obfs"), - "host": pluginInfo.Get("obfs-host"), - } - } else if strings.Contains(pluginName, "v2ray-plugin") { - ss["plugin"] = "v2ray-plugin" - ss["plugin-opts"] = map[string]any{ - "mode": pluginInfo.Get("mode"), - "host": pluginInfo.Get("host"), - "path": pluginInfo.Get("path"), - "tls": strings.Contains(plugin, "tls"), - } - } - } - - proxies = append(proxies, ss) - - case "ssr": - dcBuf, err := encRaw.DecodeString(body) - if err != nil { - continue - } - - // ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1 - - before, after, ok := strings.Cut(string(dcBuf), "/?") - if !ok { - continue - } - - beforeArr := strings.Split(before, ":") - - if len(beforeArr) != 6 { - continue - } - - host := beforeArr[0] - port := beforeArr[1] - protocol := beforeArr[2] - method := beforeArr[3] - obfs := beforeArr[4] - password := decodeUrlSafe(urlSafe(beforeArr[5])) - - query, err := url.ParseQuery(urlSafe(after)) - if err != nil { - continue - } - - remarks := decodeUrlSafe(query.Get("remarks")) - name := uniqueName(names, remarks) - - obfsParam := decodeUrlSafe(query.Get("obfsparam")) - protocolParam := query.Get("protoparam") - - ssr := make(map[string]any, 20) - - ssr["name"] = name - ssr["type"] = scheme - ssr["server"] = host - ssr["port"] = port - ssr["cipher"] = method - ssr["password"] = password - ssr["obfs"] = obfs - ssr["protocol"] = protocol - ssr["udp"] = true - - if obfsParam != "" { - ssr["obfs-param"] = obfsParam - } - - if protocolParam != "" { - ssr["protocol-param"] = protocolParam - } - - proxies = append(proxies, ssr) - } - } - - if len(proxies) == 0 { - return nil, fmt.Errorf("convert v2ray subscribe error: format invalid") - } - - return proxies, nil -} - -func uniqueName(names map[string]int, name string) string { - if index, ok := names[name]; ok { - index++ - names[name] = index - name = fmt.Sprintf("%s-%02d", name, index) - } else { - index = 0 - names[name] = index - } - return name -} diff --git a/common/convert/converter_test.go b/common/convert/converter_test.go deleted file mode 100644 index 83b41c4c8c..0000000000 --- a/common/convert/converter_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package convert - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -// https://v2.hysteria.network/zh/docs/developers/URI-Scheme/ -func TestConvertsV2Ray_normal(t *testing.T) { - hy2test := "hysteria2://letmein@example.com:8443/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com&up=114&down=514&alpn=h3,h4#hy2test" - - expected := []map[string]interface{}{ - { - "name": "hy2test", - "type": "hysteria2", - "server": "example.com", - "port": "8443", - "sni": "real.example.com", - "obfs": "salamander", - "obfs-password": "gawrgura", - "alpn": []string{"h3", "h4"}, - "password": "letmein", - "up": "114", - "down": "514", - "skip-cert-verify": true, - "fingerprint": "deadbeef", - }, - } - - proxies, err := ConvertsV2Ray([]byte(hy2test)) - - assert.Nil(t, err) - assert.Equal(t, expected, proxies) -} diff --git a/common/convert/util.go b/common/convert/util.go deleted file mode 100644 index ab00637493..0000000000 --- a/common/convert/util.go +++ /dev/null @@ -1,323 +0,0 @@ -package convert - -import ( - "encoding/base64" - "net/http" - "strings" - "time" - - "github.com/metacubex/mihomo/common/utils" - - "github.com/metacubex/randv2" - "github.com/metacubex/sing-shadowsocks/shadowimpl" -) - -var hostsSuffix = []string{ - "-cdn.aliyuncs.com", - ".alicdn.com", - ".pan.baidu.com", - ".tbcache.com", - ".aliyuncdn.com", - ".vod.miguvideo.com", - ".cibntv.net", - ".myqcloud.com", - ".smtcdns.com", - ".alikunlun.com", - ".smtcdns.net", - ".apcdns.net", - ".cdn-go.cn", - ".cdntip.com", - ".cdntips.com", - ".alidayu.com", - ".alidns.com", - ".cdngslb.com", - ".mxhichina.com", - ".alibabadns.com", -} - -var userAgents = []string{ - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", - "Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", - "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", - "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36", - "Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", - "Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", - "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", - "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0", - "Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36", - "Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36", - "Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", - "Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24", - "Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", - "Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", - "Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", - "Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1", - "Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", - "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", - "Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", - "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36", - "Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36", - "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", -} - -var ( - hostsLen = len(hostsSuffix) - uaLen = len(userAgents) -) - -func RandHost() string { - base := strings.ToLower(base64.RawURLEncoding.EncodeToString(utils.NewUUIDV4().Bytes())) - base = strings.ReplaceAll(base, "-", "") - base = strings.ReplaceAll(base, "_", "") - buf := []byte(base) - prefix := string(buf[:3]) + "---" - prefix += string(buf[6:8]) + "-" - prefix += string(buf[len(buf)-8:]) - - return prefix + hostsSuffix[randv2.IntN(hostsLen)] -} - -func RandUserAgent() string { - return userAgents[randv2.IntN(uaLen)] -} - -func SetUserAgent(header http.Header) { - if header.Get("User-Agent") != "" { - return - } - userAgent := RandUserAgent() - header.Set("User-Agent", userAgent) -} - -func VerifyMethod(cipher, password string) (err error) { - _, err = shadowimpl.FetchMethod(cipher, password, time.Now) - return -} diff --git a/common/convert/v.go b/common/convert/v.go deleted file mode 100644 index 4102ab75e4..0000000000 --- a/common/convert/v.go +++ /dev/null @@ -1,136 +0,0 @@ -package convert - -import ( - "errors" - "fmt" - "net/url" - "strconv" - "strings" -) - -func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy map[string]any) error { - // Xray VMessAEAD / VLESS share link standard - // https://github.com/XTLS/Xray-core/discussions/716 - query := url.Query() - proxy["name"] = uniqueName(names, url.Fragment) - if url.Hostname() == "" { - return errors.New("url.Hostname() is empty") - } - if url.Port() == "" { - return errors.New("url.Port() is empty") - } - proxy["type"] = scheme - proxy["server"] = url.Hostname() - proxy["port"] = url.Port() - proxy["uuid"] = url.User.Username() - proxy["udp"] = true - tls := strings.ToLower(query.Get("security")) - if strings.HasSuffix(tls, "tls") || tls == "reality" { - proxy["tls"] = true - if fingerprint := query.Get("fp"); fingerprint == "" { - proxy["client-fingerprint"] = "chrome" - } else { - proxy["client-fingerprint"] = fingerprint - } - if alpn := query.Get("alpn"); alpn != "" { - proxy["alpn"] = strings.Split(alpn, ",") - } - } - if sni := query.Get("sni"); sni != "" { - proxy["servername"] = sni - } - if realityPublicKey := query.Get("pbk"); realityPublicKey != "" { - proxy["reality-opts"] = map[string]any{ - "public-key": realityPublicKey, - "short-id": query.Get("sid"), - } - } - - switch query.Get("packetEncoding") { - case "none": - case "packet": - proxy["packet-addr"] = true - default: - proxy["xudp"] = true - } - - network := strings.ToLower(query.Get("type")) - if network == "" { - network = "tcp" - } - fakeType := strings.ToLower(query.Get("headerType")) - if fakeType == "http" { - network = "http" - } else if network == "http" { - network = "h2" - } - proxy["network"] = network - switch network { - case "tcp": - if fakeType != "none" { - headers := make(map[string]any) - httpOpts := make(map[string]any) - httpOpts["path"] = []string{"/"} - - if host := query.Get("host"); host != "" { - headers["Host"] = []string{host} - } - - if method := query.Get("method"); method != "" { - httpOpts["method"] = method - } - - if path := query.Get("path"); path != "" { - httpOpts["path"] = []string{path} - } - httpOpts["headers"] = headers - proxy["http-opts"] = httpOpts - } - - case "http": - headers := make(map[string]any) - h2Opts := make(map[string]any) - h2Opts["path"] = []string{"/"} - if path := query.Get("path"); path != "" { - h2Opts["path"] = []string{path} - } - if host := query.Get("host"); host != "" { - h2Opts["host"] = []string{host} - } - h2Opts["headers"] = headers - proxy["h2-opts"] = h2Opts - - case "ws", "httpupgrade": - headers := make(map[string]any) - wsOpts := make(map[string]any) - headers["User-Agent"] = RandUserAgent() - headers["Host"] = query.Get("host") - wsOpts["path"] = query.Get("path") - wsOpts["headers"] = headers - - if earlyData := query.Get("ed"); earlyData != "" { - med, err := strconv.Atoi(earlyData) - if err != nil { - return fmt.Errorf("bad WebSocket max early data size: %v", err) - } - switch network { - case "ws": - wsOpts["max-early-data"] = med - wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol" - case "httpupgrade": - wsOpts["v2ray-http-upgrade-fast-open"] = true - } - } - if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" { - wsOpts["early-data-header-name"] = earlyDataHeader - } - - proxy["ws-opts"] = wsOpts - - case "grpc": - grpcOpts := make(map[string]any) - grpcOpts["grpc-service-name"] = query.Get("serviceName") - proxy["grpc-opts"] = grpcOpts - } - return nil -} diff --git a/common/lru/lrucache.go b/common/lru/lrucache.go deleted file mode 100644 index 4afe8e0c35..0000000000 --- a/common/lru/lrucache.go +++ /dev/null @@ -1,291 +0,0 @@ -package lru - -// Modified by https://github.com/die-net/lrucache - -import ( - "sync" - "time" - - list "github.com/bahlo/generic-list-go" - "github.com/samber/lo" -) - -// Option is part of Functional Options Pattern -type Option[K comparable, V any] func(*LruCache[K, V]) - -// EvictCallback is used to get a callback when a cache entry is evicted -type EvictCallback[K comparable, V any] func(key K, value V) - -// WithEvict set the evict callback -func WithEvict[K comparable, V any](cb EvictCallback[K, V]) Option[K, V] { - return func(l *LruCache[K, V]) { - l.onEvict = cb - } -} - -// WithUpdateAgeOnGet update expires when Get element -func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] { - return func(l *LruCache[K, V]) { - l.updateAgeOnGet = true - } -} - -// WithAge defined element max age (second) -func WithAge[K comparable, V any](maxAge int64) Option[K, V] { - return func(l *LruCache[K, V]) { - l.maxAge = maxAge - } -} - -// WithSize defined max length of LruCache -func WithSize[K comparable, V any](maxSize int) Option[K, V] { - return func(l *LruCache[K, V]) { - l.maxSize = maxSize - } -} - -// WithStale decide whether Stale return is enabled. -// If this feature is enabled, element will not get Evicted according to `WithAge`. -func WithStale[K comparable, V any](stale bool) Option[K, V] { - return func(l *LruCache[K, V]) { - 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[K comparable, V any] struct { - maxAge int64 - maxSize int - mu sync.Mutex - cache map[K]*list.Element[*entry[K, V]] - lru *list.List[*entry[K, V]] // Front is least-recent - updateAgeOnGet bool - staleReturn bool - onEvict EvictCallback[K, V] -} - -// New creates an LruCache -func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { - lc := &LruCache[K, V]{} - lc.Clear() - - for _, option := range options { - option(lc) - } - - return lc -} - -func (c *LruCache[K, V]) Clear() { - c.mu.Lock() - defer c.mu.Unlock() - - c.lru = list.New[*entry[K, V]]() - c.cache = make(map[K]*list.Element[*entry[K, V]]) -} - -// Get returns any representation of a cached response and a bool -// set to true if the key was found. -func (c *LruCache[K, V]) Get(key K) (V, bool) { - c.mu.Lock() - defer c.mu.Unlock() - - el := c.get(key) - if el == nil { - return lo.Empty[V](), false - } - value := el.value - - return value, true -} - -func (c *LruCache[K, V]) GetOrStore(key K, constructor func() V) (V, bool) { - c.mu.Lock() - defer c.mu.Unlock() - - el := c.get(key) - if el == nil { - value := constructor() - c.set(key, value) - return value, false - } - value := el.value - - return value, true -} - -// GetWithExpire returns 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[K, V]) GetWithExpire(key K) (V, time.Time, bool) { - c.mu.Lock() - defer c.mu.Unlock() - - el := c.get(key) - if el == nil { - return lo.Empty[V](), time.Time{}, false - } - - return el.value, time.Unix(el.expires, 0), true -} - -// Exist returns if key exist in cache but not put item to the head of linked list -func (c *LruCache[K, V]) Exist(key K) bool { - c.mu.Lock() - defer c.mu.Unlock() - - _, ok := c.cache[key] - return ok -} - -// Set stores any representation of a response for a given key. -func (c *LruCache[K, V]) Set(key K, value V) { - c.mu.Lock() - defer c.mu.Unlock() - - c.set(key, value) -} - -func (c *LruCache[K, V]) set(key K, value V) { - expires := int64(0) - if c.maxAge > 0 { - expires = time.Now().Unix() + c.maxAge - } - c.setWithExpire(key, value, time.Unix(expires, 0)) -} - -// SetWithExpire stores any representation of a response for a given key and given expires. -// The expires time will round to second. -func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) { - c.mu.Lock() - defer c.mu.Unlock() - - c.setWithExpire(key, value, expires) -} - -func (c *LruCache[K, V]) setWithExpire(key K, value V, expires time.Time) { - if le, ok := c.cache[key]; ok { - c.lru.MoveToBack(le) - e := le.Value - e.value = value - e.expires = expires.Unix() - } else { - e := &entry[K, V]{key: key, value: value, expires: expires.Unix()} - c.cache[key] = c.lru.PushBack(e) - - if c.maxSize > 0 { - if elLen := c.lru.Len(); elLen > c.maxSize { - c.deleteElement(c.lru.Front()) - } - } - } - - c.maybeDeleteOldest() -} - -// CloneTo clone and overwrite elements to another LruCache -func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) { - c.mu.Lock() - defer c.mu.Unlock() - - n.mu.Lock() - defer n.mu.Unlock() - - n.lru = list.New[*entry[K, V]]() - n.cache = make(map[K]*list.Element[*entry[K, V]]) - - for e := c.lru.Front(); e != nil; e = e.Next() { - elm := e.Value - n.cache[elm.key] = n.lru.PushBack(elm) - } -} - -func (c *LruCache[K, V]) get(key K) *entry[K, V] { - le, ok := c.cache[key] - if !ok { - return nil - } - - if !c.staleReturn && c.maxAge > 0 && le.Value.expires <= time.Now().Unix() { - c.deleteElement(le) - c.maybeDeleteOldest() - - return nil - } - - c.lru.MoveToBack(le) - el := le.Value - if c.maxAge > 0 && c.updateAgeOnGet { - el.expires = time.Now().Unix() + c.maxAge - } - return el -} - -// Delete removes the value associated with a key. -func (c *LruCache[K, V]) Delete(key K) { - c.mu.Lock() - defer c.mu.Unlock() - - c.delete(key) -} - -func (c *LruCache[K, V]) delete(key K) { - if le, ok := c.cache[key]; ok { - c.deleteElement(le) - } -} - -func (c *LruCache[K, V]) maybeDeleteOldest() { - if !c.staleReturn && c.maxAge > 0 { - now := time.Now().Unix() - for le := c.lru.Front(); le != nil && le.Value.expires <= now; le = c.lru.Front() { - c.deleteElement(le) - } - } -} - -func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) { - c.lru.Remove(le) - e := le.Value - delete(c.cache, e.key) - if c.onEvict != nil { - c.onEvict(e.key, e.value) - } -} - -// Compute either sets the computed new value for the key or deletes -// the value for the key. When the delete result of the valueFn function -// is set to true, the value will be deleted, if it exists. When delete -// is set to false, the value is updated to the newValue. -// The ok result indicates whether value was computed and stored, thus, is -// present in the map. The actual result contains the new value in cases where -// the value was computed and stored. -func (c *LruCache[K, V]) Compute( - key K, - valueFn func(oldValue V, loaded bool) (newValue V, delete bool), -) (actual V, ok bool) { - c.mu.Lock() - defer c.mu.Unlock() - - if el := c.get(key); el != nil { - actual, ok = el.value, true - } - if newValue, del := valueFn(actual, ok); del { - if ok { // data not in cache, so needn't delete - c.delete(key) - } - return lo.Empty[V](), false - } else { - c.set(key, newValue) - return newValue, true - } -} - -type entry[K comparable, V any] struct { - key K - value V - expires int64 -} diff --git a/common/lru/lrucache_test.go b/common/lru/lrucache_test.go deleted file mode 100644 index 340b3da3df..0000000000 --- a/common/lru/lrucache_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package lru - -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[string, string]() - - 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) - } - } - - for _, e := range entries { - c.Delete(e.key) - - _, ok := c.Get(e.key) - assert.False(t, ok) - } -} - -func TestLRUMaxAge(t *testing.T) { - c := New[string, string](WithAge[string, string](86400)) - - now := time.Now().Unix() - expected := now + 86400 - - // Add one expired entry - c.Set("foo", "bar") - c.lru.Back().Value.expires = now - - // Reset - c.Set("foo", "bar") - e := c.lru.Back().Value - assert.True(t, e.expires >= now) - c.lru.Back().Value.expires = now - - // Set a few and verify expiration times - for _, s := range entries { - c.Set(s.key, s.value) - e := c.lru.Back().Value - 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.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[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]()) - - now := time.Now().Unix() - expires := now + 86400/2 - - // Add one expired entry - c.Set("foo", "bar") - c.lru.Back().Value.expires = expires - - _, ok := c.Get("foo") - assert.True(t, ok) - assert.True(t, c.lru.Back().Value.expires > expires) -} - -func TestMaxSize(t *testing.T) { - c := New[string, string](WithSize[string, string](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[int, int](WithSize[int, int](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 int, value int) { - temp = key + value - } - - c := New[int, int](WithEvict[int, int](evict), WithSize[int, int](1)) - c.Set(1, 2) - c.Set(2, 3) - - assert.Equal(t, temp, 3) -} - -func TestSetWithExpire(t *testing.T) { - c := New[int, *struct{}](WithAge[int, *struct{}](1)) - now := time.Now().Unix() - - tenSecBefore := time.Unix(now-10, 0) - c.SetWithExpire(1, &struct{}{}, tenSecBefore) - - // res is expected not to exist, and expires should be empty time.Time - res, expires, exist := c.GetWithExpire(1) - - assert.True(t, nil == res) - assert.Equal(t, time.Time{}, expires) - assert.Equal(t, false, exist) -} - -func TestStale(t *testing.T) { - c := New[int, int](WithAge[int, int](1), WithStale[int, int](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[string, int](WithSize[string, int](10)) - o.Set("1", 1) - o.Set("2", 2) - - n := New[string, int](WithSize[string, int](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 deleted file mode 100644 index f4470290b3..0000000000 --- a/common/murmur3/murmur.go +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index e52b7937d6..0000000000 --- a/common/murmur3/murmur32.go +++ /dev/null @@ -1,144 +0,0 @@ -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/addr.go b/common/net/addr.go deleted file mode 100644 index 4efaefcd38..0000000000 --- a/common/net/addr.go +++ /dev/null @@ -1,36 +0,0 @@ -package net - -import ( - "net" -) - -type CustomAddr interface { - net.Addr - RawAddr() net.Addr -} - -type customAddr struct { - networkStr string - addrStr string - rawAddr net.Addr -} - -func (a customAddr) Network() string { - return a.networkStr -} - -func (a customAddr) String() string { - return a.addrStr -} - -func (a customAddr) RawAddr() net.Addr { - return a.rawAddr -} - -func NewCustomAddr(networkStr string, addrStr string, rawAddr net.Addr) CustomAddr { - return customAddr{ - networkStr: networkStr, - addrStr: addrStr, - rawAddr: rawAddr, - } -} diff --git a/common/net/bind.go b/common/net/bind.go deleted file mode 100644 index 231c24c2b4..0000000000 --- a/common/net/bind.go +++ /dev/null @@ -1,45 +0,0 @@ -package net - -import "net" - -type bindPacketConn struct { - EnhancePacketConn - rAddr net.Addr -} - -func (c *bindPacketConn) Read(b []byte) (n int, err error) { - n, _, err = c.EnhancePacketConn.ReadFrom(b) - return n, err -} - -func (c *bindPacketConn) WaitRead() (data []byte, put func(), err error) { - data, put, _, err = c.EnhancePacketConn.WaitReadFrom() - return -} - -func (c *bindPacketConn) Write(b []byte) (n int, err error) { - return c.EnhancePacketConn.WriteTo(b, c.rAddr) -} - -func (c *bindPacketConn) RemoteAddr() net.Addr { - return c.rAddr -} - -func (c *bindPacketConn) LocalAddr() net.Addr { - if c.EnhancePacketConn.LocalAddr() == nil { - return &net.UDPAddr{IP: net.IPv4zero, Port: 0} - } else { - return c.EnhancePacketConn.LocalAddr() - } -} - -func (c *bindPacketConn) Upstream() any { - return c.EnhancePacketConn -} - -func NewBindPacketConn(pc net.PacketConn, rAddr net.Addr) net.Conn { - return &bindPacketConn{ - EnhancePacketConn: NewEnhancePacketConn(pc), - rAddr: rAddr, - } -} diff --git a/common/net/bufconn.go b/common/net/bufconn.go deleted file mode 100644 index b7e98e049e..0000000000 --- a/common/net/bufconn.go +++ /dev/null @@ -1,106 +0,0 @@ -package net - -import ( - "bufio" - "net" - - "github.com/metacubex/mihomo/common/buf" -) - -var _ ExtendedConn = (*BufferedConn)(nil) - -type BufferedConn struct { - r *bufio.Reader - ExtendedConn - peeked bool -} - -func NewBufferedConn(c net.Conn) *BufferedConn { - if bc, ok := c.(*BufferedConn); ok { - return bc - } - return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false} -} - -func WarpConnWithBioReader(c net.Conn, br *bufio.Reader) net.Conn { - if br != nil && br.Buffered() > 0 { - if bc, ok := c.(*BufferedConn); ok && bc.r == br { - return bc - } - return &BufferedConn{br, NewExtendedConn(c), true} - } - return c -} - -// Reader returns the internal bufio.Reader. -func (c *BufferedConn) Reader() *bufio.Reader { - return c.r -} - -func (c *BufferedConn) ResetPeeked() { - c.peeked = false -} - -func (c *BufferedConn) Peeked() bool { - return c.peeked -} - -// Peek returns the next n bytes without advancing the reader. -func (c *BufferedConn) Peek(n int) ([]byte, error) { - c.peeked = true - return c.r.Peek(n) -} - -func (c *BufferedConn) Discard(n int) (discarded int, err error) { - return c.r.Discard(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() -} - -func (c *BufferedConn) ReadBuffer(buffer *buf.Buffer) (err error) { - if c.r != nil && c.r.Buffered() > 0 { - _, err = buffer.ReadOnceFrom(c.r) - return - } - return c.ExtendedConn.ReadBuffer(buffer) -} - -func (c *BufferedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy - if c.r != nil && c.r.Buffered() > 0 { - length := c.r.Buffered() - b, _ := c.r.Peek(length) - _, _ = c.r.Discard(length) - return buf.As(b) - } - c.r = nil // drop bufio.Reader to let gc can clean up its internal buf - return nil -} - -func (c *BufferedConn) Upstream() any { - return c.ExtendedConn -} - -func (c *BufferedConn) ReaderReplaceable() bool { - if c.r != nil && c.r.Buffered() > 0 { - return false - } - return true -} - -func (c *BufferedConn) WriterReplaceable() bool { - return true -} diff --git a/common/net/bufconn_unsafe.go b/common/net/bufconn_unsafe.go deleted file mode 100644 index 349321dfd9..0000000000 --- a/common/net/bufconn_unsafe.go +++ /dev/null @@ -1,34 +0,0 @@ -package net - -import ( - "io" - "unsafe" -) - -// bufioReader copy from stdlib bufio/bufio.go -// This structure has remained unchanged from go1.5 to go1.21. -type bufioReader struct { - buf []byte - rd io.Reader // reader provided by the client - r, w int // buf read and write positions - err error - lastByte int // last byte read for UnreadByte; -1 means invalid - lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid -} - -func (c *BufferedConn) AppendData(buf []byte) (ok bool) { - b := (*bufioReader)(unsafe.Pointer(c.r)) - pos := len(b.buf) - b.w - len(buf) - if pos >= -b.r { // len(b.buf)-(b.w - b.r) >= len(buf) - if pos < 0 { // len(b.buf)-b.w < len(buf) - // Slide existing data to beginning. - copy(b.buf, b.buf[b.r:b.w]) - b.w -= b.r - b.r = 0 - } - - b.w += copy(b.buf[b.w:], buf) - return true - } - return false -} diff --git a/common/net/cached.go b/common/net/cached.go deleted file mode 100644 index fb605b742e..0000000000 --- a/common/net/cached.go +++ /dev/null @@ -1,49 +0,0 @@ -package net - -import ( - "net" - - "github.com/metacubex/mihomo/common/buf" -) - -var _ ExtendedConn = (*CachedConn)(nil) - -type CachedConn struct { - ExtendedConn - data []byte -} - -func NewCachedConn(c net.Conn, data []byte) *CachedConn { - return &CachedConn{NewExtendedConn(c), data} -} - -func (c *CachedConn) Read(b []byte) (n int, err error) { - if len(c.data) > 0 { - n = copy(b, c.data) - c.data = c.data[n:] - return - } - return c.ExtendedConn.Read(b) -} - -func (c *CachedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy - if len(c.data) > 0 { - return buf.As(c.data) - } - return nil -} - -func (c *CachedConn) Upstream() any { - return c.ExtendedConn -} - -func (c *CachedConn) ReaderReplaceable() bool { - if len(c.data) > 0 { - return false - } - return true -} - -func (c *CachedConn) WriterReplaceable() bool { - return true -} diff --git a/common/net/context.go b/common/net/context.go deleted file mode 100644 index b170516ec9..0000000000 --- a/common/net/context.go +++ /dev/null @@ -1,39 +0,0 @@ -package net - -import ( - "context" - "net" - - "github.com/metacubex/mihomo/common/contextutils" -) - -// SetupContextForConn is a helper function that starts connection I/O interrupter. -// if ctx be canceled before done called, it will close the connection. -// should use like this: -// -// func streamConn(ctx context.Context, conn net.Conn) (_ net.Conn, err error) { -// if ctx.Done() != nil { -// done := N.SetupContextForConn(ctx, conn) -// defer done(&err) -// } -// conn, err := xxx -// return conn, err -// } -func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) { - stopc := make(chan struct{}) - stop := contextutils.AfterFunc(ctx, func() { - // Close the connection, discarding the error - _ = conn.Close() - close(stopc) - }) - return func(inputErr *error) { - if !stop() { - // The AfterFunc was started, wait for it to complete. - <-stopc - if ctxErr := ctx.Err(); ctxErr != nil && inputErr != nil { - // Return context error to user. - *inputErr = ctxErr - } - } - } -} diff --git a/common/net/context_test.go b/common/net/context_test.go deleted file mode 100644 index 8e4c4ad13a..0000000000 --- a/common/net/context_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package net_test - -import ( - "context" - "errors" - "net" - "testing" - "time" - - N "github.com/metacubex/mihomo/common/net" - - "github.com/stretchr/testify/assert" -) - -func testRead(ctx context.Context, conn net.Conn) (err error) { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, conn) - defer done(&err) - } - _, err = conn.Read(make([]byte, 1)) - return err -} - -func TestSetupContextForConnWithCancel(t *testing.T) { - t.Parallel() - c1, c2 := N.Pipe() - defer c1.Close() - defer c2.Close() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - errc := make(chan error) - go func() { - errc <- testRead(ctx, c1) - }() - - select { - case <-errc: - t.Fatal("conn closed before cancel") - case <-time.After(100 * time.Millisecond): - cancel() - } - - select { - case err := <-errc: - assert.ErrorIs(t, err, context.Canceled) - case <-time.After(100 * time.Millisecond): - t.Fatal("conn not be canceled") - } -} - -func TestSetupContextForConnWithTimeout1(t *testing.T) { - t.Parallel() - c1, c2 := N.Pipe() - defer c1.Close() - defer c2.Close() - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() - - errc := make(chan error) - go func() { - errc <- testRead(ctx, c1) - }() - - select { - case err := <-errc: - if !errors.Is(ctx.Err(), context.DeadlineExceeded) { - t.Fatal("conn closed before timeout") - } - assert.ErrorIs(t, err, context.DeadlineExceeded) - case <-time.After(200 * time.Millisecond): - t.Fatal("conn not be canceled") - } -} - -func TestSetupContextForConnWithTimeout2(t *testing.T) { - t.Parallel() - c1, c2 := N.Pipe() - defer c1.Close() - defer c2.Close() - ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) - defer cancel() - - errc := make(chan error) - go func() { - errc <- testRead(ctx, c1) - }() - - select { - case <-errc: - t.Fatal("conn closed before cancel") - case <-time.After(100 * time.Millisecond): - c2.Write(make([]byte, 1)) - } - - select { - case err := <-errc: - assert.Nil(t, ctx.Err()) - assert.Nil(t, err) - case <-time.After(200 * time.Millisecond): - t.Fatal("conn not be canceled") - } -} diff --git a/common/net/deadline/conn.go b/common/net/deadline/conn.go deleted file mode 100644 index 03ee7f6a6a..0000000000 --- a/common/net/deadline/conn.go +++ /dev/null @@ -1,154 +0,0 @@ -package deadline - -import ( - "net" - "os" - "time" - - "github.com/metacubex/mihomo/common/atomic" - - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - "github.com/metacubex/sing/common/network" -) - -type connReadResult struct { - buffer []byte - err error -} - -type Conn struct { - network.ExtendedConn - deadline atomic.TypedValue[time.Time] - pipeDeadline pipeDeadline - disablePipe atomic.Bool - inRead atomic.Bool - resultCh chan *connReadResult -} - -func IsConn(conn any) bool { - _, ok := conn.(*Conn) - return ok -} - -func NewConn(conn net.Conn) *Conn { - c := &Conn{ - ExtendedConn: bufio.NewExtendedConn(conn), - pipeDeadline: makePipeDeadline(), - resultCh: make(chan *connReadResult, 1), - } - c.resultCh <- nil - return c -} - -func (c *Conn) Read(p []byte) (n int, err error) { - select { - case result := <-c.resultCh: - if result != nil { - n = copy(p, result.buffer) - err = result.err - if n >= len(result.buffer) { - c.resultCh <- nil // finish cache read - } else { - result.buffer = result.buffer[n:] - c.resultCh <- result // push back for next call - } - return - } else { - c.resultCh <- nil - break - } - case <-c.pipeDeadline.wait(): - return 0, os.ErrDeadlineExceeded - } - - if c.disablePipe.Load() { - return c.ExtendedConn.Read(p) - } else if c.deadline.Load().IsZero() { - c.inRead.Store(true) - defer c.inRead.Store(false) - return c.ExtendedConn.Read(p) - } - - <-c.resultCh - go c.pipeRead(len(p)) - - return c.Read(p) -} - -func (c *Conn) pipeRead(size int) { - buffer := make([]byte, size) - n, err := c.ExtendedConn.Read(buffer) - buffer = buffer[:n] - c.resultCh <- &connReadResult{ - buffer: buffer, - err: err, - } -} - -func (c *Conn) ReadBuffer(buffer *buf.Buffer) (err error) { - select { - case result := <-c.resultCh: - if result != nil { - n, _ := buffer.Write(result.buffer) - err = result.err - - if n >= len(result.buffer) { - c.resultCh <- nil // finish cache read - } else { - result.buffer = result.buffer[n:] - c.resultCh <- result // push back for next call - } - return - } else { - c.resultCh <- nil - break - } - case <-c.pipeDeadline.wait(): - return os.ErrDeadlineExceeded - } - - if c.disablePipe.Load() { - return c.ExtendedConn.ReadBuffer(buffer) - } else if c.deadline.Load().IsZero() { - c.inRead.Store(true) - defer c.inRead.Store(false) - return c.ExtendedConn.ReadBuffer(buffer) - } - - <-c.resultCh - go c.pipeRead(buffer.FreeLen()) - - return c.ReadBuffer(buffer) -} - -func (c *Conn) SetReadDeadline(t time.Time) error { - if c.disablePipe.Load() { - return c.ExtendedConn.SetReadDeadline(t) - } else if c.inRead.Load() { - c.disablePipe.Store(true) - return c.ExtendedConn.SetReadDeadline(t) - } - c.deadline.Store(t) - c.pipeDeadline.set(t) - return nil -} - -func (c *Conn) ReaderReplaceable() bool { - select { - case result := <-c.resultCh: - c.resultCh <- result - if result != nil { - return false // cache reading - } else { - break - } - default: - return false // pipe reading - } - return c.disablePipe.Load() || c.deadline.Load().IsZero() -} - -func (c *Conn) Upstream() any { - return c.ExtendedConn -} diff --git a/common/net/deadline/packet.go b/common/net/deadline/packet.go deleted file mode 100644 index 670431987e..0000000000 --- a/common/net/deadline/packet.go +++ /dev/null @@ -1,154 +0,0 @@ -package deadline - -import ( - "net" - "os" - "runtime" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/net/packet" -) - -type readResult struct { - data []byte - addr net.Addr - err error -} - -type NetPacketConn struct { - net.PacketConn - deadline atomic.TypedValue[time.Time] - pipeDeadline pipeDeadline - disablePipe atomic.Bool - inRead atomic.Bool - resultCh chan any -} - -func NewNetPacketConn(pc net.PacketConn) net.PacketConn { - npc := &NetPacketConn{ - PacketConn: pc, - pipeDeadline: makePipeDeadline(), - resultCh: make(chan any, 1), - } - npc.resultCh <- nil - if enhancePC, isEnhance := pc.(packet.EnhancePacketConn); isEnhance { - epc := &EnhancePacketConn{ - NetPacketConn: npc, - enhancePacketConn: enhancePacketConn{ - netPacketConn: npc, - enhancePacketConn: enhancePC, - }, - } - if singPC, isSingPC := pc.(packet.SingPacketConn); isSingPC { - return &EnhanceSingPacketConn{ - EnhancePacketConn: epc, - singPacketConn: singPacketConn{ - netPacketConn: npc, - singPacketConn: singPC, - }, - } - } - return epc - } - if singPC, isSingPC := pc.(packet.SingPacketConn); isSingPC { - return &SingPacketConn{ - NetPacketConn: npc, - singPacketConn: singPacketConn{ - netPacketConn: npc, - singPacketConn: singPC, - }, - } - } - return npc -} - -func (c *NetPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { -FOR: - for { - select { - case result := <-c.resultCh: - if result != nil { - if result, ok := result.(*readResult); ok { - n = copy(p, result.data) - addr = result.addr - err = result.err - c.resultCh <- nil // finish cache read - return - } - c.resultCh <- result // another type of read - runtime.Gosched() // allowing other goroutines to run - continue FOR - } else { - c.resultCh <- nil - break FOR - } - case <-c.pipeDeadline.wait(): - return 0, nil, os.ErrDeadlineExceeded - } - } - - if c.disablePipe.Load() { - return c.PacketConn.ReadFrom(p) - } else if c.deadline.Load().IsZero() { - c.inRead.Store(true) - defer c.inRead.Store(false) - n, addr, err = c.PacketConn.ReadFrom(p) - return - } - - <-c.resultCh - go c.pipeReadFrom(len(p)) - - return c.ReadFrom(p) -} - -func (c *NetPacketConn) pipeReadFrom(size int) { - buffer := make([]byte, size) - n, addr, err := c.PacketConn.ReadFrom(buffer) - buffer = buffer[:n] - result := &readResult{} - result.data = buffer - result.addr = addr - result.err = err - c.resultCh <- result -} - -func (c *NetPacketConn) SetReadDeadline(t time.Time) error { - if c.disablePipe.Load() { - return c.PacketConn.SetReadDeadline(t) - } else if c.inRead.Load() { - c.disablePipe.Store(true) - return c.PacketConn.SetReadDeadline(t) - } - c.deadline.Store(t) - c.pipeDeadline.set(t) - return nil -} - -func (c *NetPacketConn) ReaderReplaceable() bool { - select { - case result := <-c.resultCh: - c.resultCh <- result - if result != nil { - return false // cache reading - } else { - break - } - default: - return false // pipe reading - } - return c.disablePipe.Load() || c.deadline.Load().IsZero() -} - -func (c *NetPacketConn) WriterReplaceable() bool { - return true -} - -func (c *NetPacketConn) Upstream() any { - return c.PacketConn -} - -func (c *NetPacketConn) NeedAdditionalReadDeadline() bool { - return false -} diff --git a/common/net/deadline/packet_enhance.go b/common/net/deadline/packet_enhance.go deleted file mode 100644 index 3e314fb8db..0000000000 --- a/common/net/deadline/packet_enhance.go +++ /dev/null @@ -1,83 +0,0 @@ -package deadline - -import ( - "net" - "os" - "runtime" - - "github.com/metacubex/mihomo/common/net/packet" -) - -type EnhancePacketConn struct { - *NetPacketConn - enhancePacketConn -} - -var _ packet.EnhancePacketConn = (*EnhancePacketConn)(nil) - -func NewEnhancePacketConn(pc packet.EnhancePacketConn) packet.EnhancePacketConn { - return NewNetPacketConn(pc).(packet.EnhancePacketConn) -} - -type enhanceReadResult struct { - data []byte - put func() - addr net.Addr - err error -} - -type enhancePacketConn struct { - netPacketConn *NetPacketConn - enhancePacketConn packet.EnhancePacketConn -} - -func (c *enhancePacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { -FOR: - for { - select { - case result := <-c.netPacketConn.resultCh: - if result != nil { - if result, ok := result.(*enhanceReadResult); ok { - data = result.data - put = result.put - addr = result.addr - err = result.err - c.netPacketConn.resultCh <- nil // finish cache read - return - } - c.netPacketConn.resultCh <- result // another type of read - runtime.Gosched() // allowing other goroutines to run - continue FOR - } else { - c.netPacketConn.resultCh <- nil - break FOR - } - case <-c.netPacketConn.pipeDeadline.wait(): - return nil, nil, nil, os.ErrDeadlineExceeded - } - } - - if c.netPacketConn.disablePipe.Load() { - return c.enhancePacketConn.WaitReadFrom() - } else if c.netPacketConn.deadline.Load().IsZero() { - c.netPacketConn.inRead.Store(true) - defer c.netPacketConn.inRead.Store(false) - data, put, addr, err = c.enhancePacketConn.WaitReadFrom() - return - } - - <-c.netPacketConn.resultCh - go c.pipeWaitReadFrom() - - return c.WaitReadFrom() -} - -func (c *enhancePacketConn) pipeWaitReadFrom() { - data, put, addr, err := c.enhancePacketConn.WaitReadFrom() - result := &enhanceReadResult{} - result.data = data - result.put = put - result.addr = addr - result.err = err - c.netPacketConn.resultCh <- result -} diff --git a/common/net/deadline/packet_sing.go b/common/net/deadline/packet_sing.go deleted file mode 100644 index 71a1c51515..0000000000 --- a/common/net/deadline/packet_sing.go +++ /dev/null @@ -1,179 +0,0 @@ -package deadline - -import ( - "os" - "runtime" - - "github.com/metacubex/mihomo/common/net/packet" - - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" -) - -type SingPacketConn struct { - *NetPacketConn - singPacketConn -} - -var _ packet.SingPacketConn = (*SingPacketConn)(nil) - -func NewSingPacketConn(pc packet.SingPacketConn) packet.SingPacketConn { - return NewNetPacketConn(pc).(packet.SingPacketConn) -} - -type EnhanceSingPacketConn struct { - *EnhancePacketConn - singPacketConn -} - -func NewEnhanceSingPacketConn(pc packet.EnhanceSingPacketConn) packet.EnhanceSingPacketConn { - return NewNetPacketConn(pc).(packet.EnhanceSingPacketConn) -} - -var _ packet.EnhanceSingPacketConn = (*EnhanceSingPacketConn)(nil) - -type singReadResult struct { - buffer *buf.Buffer - destination M.Socksaddr - err error -} - -type singPacketConn struct { - netPacketConn *NetPacketConn - singPacketConn packet.SingPacketConn -} - -func (c *singPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { -FOR: - for { - select { - case result := <-c.netPacketConn.resultCh: - if result != nil { - if result, ok := result.(*singReadResult); ok { - destination = result.destination - err = result.err - n, _ := buffer.Write(result.buffer.Bytes()) - result.buffer.Advance(n) - if result.buffer.IsEmpty() { - result.buffer.Release() - } - c.netPacketConn.resultCh <- nil // finish cache read - return - } - c.netPacketConn.resultCh <- result // another type of read - runtime.Gosched() // allowing other goroutines to run - continue FOR - } else { - c.netPacketConn.resultCh <- nil - break FOR - } - case <-c.netPacketConn.pipeDeadline.wait(): - return M.Socksaddr{}, os.ErrDeadlineExceeded - } - } - - if c.netPacketConn.disablePipe.Load() { - return c.singPacketConn.ReadPacket(buffer) - } else if c.netPacketConn.deadline.Load().IsZero() { - c.netPacketConn.inRead.Store(true) - defer c.netPacketConn.inRead.Store(false) - destination, err = c.singPacketConn.ReadPacket(buffer) - return - } - - <-c.netPacketConn.resultCh - go c.pipeReadPacket(buffer.FreeLen()) - - return c.ReadPacket(buffer) -} - -func (c *singPacketConn) pipeReadPacket(pLen int) { - buffer := buf.NewSize(pLen) - destination, err := c.singPacketConn.ReadPacket(buffer) - result := &singReadResult{} - result.destination = destination - result.err = err - c.netPacketConn.resultCh <- result -} - -func (c *singPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - return c.singPacketConn.WritePacket(buffer, destination) -} - -func (c *singPacketConn) CreateReadWaiter() (N.PacketReadWaiter, bool) { - prw, isReadWaiter := bufio.CreatePacketReadWaiter(c.singPacketConn) - if isReadWaiter { - return &singPacketReadWaiter{ - netPacketConn: c.netPacketConn, - packetReadWaiter: prw, - }, true - } - return nil, false -} - -var _ N.PacketReadWaiter = (*singPacketReadWaiter)(nil) - -type singPacketReadWaiter struct { - netPacketConn *NetPacketConn - packetReadWaiter N.PacketReadWaiter -} - -type singWaitReadResult singReadResult - -func (c *singPacketReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - return c.packetReadWaiter.InitializeReadWaiter(options) -} - -func (c *singPacketReadWaiter) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { -FOR: - for { - select { - case result := <-c.netPacketConn.resultCh: - if result != nil { - if result, ok := result.(*singWaitReadResult); ok { - buffer = result.buffer - destination = result.destination - err = result.err - c.netPacketConn.resultCh <- nil // finish cache read - return - } - c.netPacketConn.resultCh <- result // another type of read - runtime.Gosched() // allowing other goroutines to run - continue FOR - } else { - c.netPacketConn.resultCh <- nil - break FOR - } - case <-c.netPacketConn.pipeDeadline.wait(): - return nil, M.Socksaddr{}, os.ErrDeadlineExceeded - } - } - - if c.netPacketConn.disablePipe.Load() { - return c.packetReadWaiter.WaitReadPacket() - } else if c.netPacketConn.deadline.Load().IsZero() { - c.netPacketConn.inRead.Store(true) - defer c.netPacketConn.inRead.Store(false) - return c.packetReadWaiter.WaitReadPacket() - } - - <-c.netPacketConn.resultCh - go c.pipeWaitReadPacket() - - return c.WaitReadPacket() -} - -func (c *singPacketReadWaiter) pipeWaitReadPacket() { - buffer, destination, err := c.packetReadWaiter.WaitReadPacket() - result := &singWaitReadResult{} - result.buffer = buffer - result.destination = destination - result.err = err - c.netPacketConn.resultCh <- result -} - -func (c *singPacketReadWaiter) Upstream() any { - return c.packetReadWaiter -} diff --git a/common/net/deadline/pipe.go b/common/net/deadline/pipe.go deleted file mode 100644 index 2cccfb42cc..0000000000 --- a/common/net/deadline/pipe.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package deadline - -import ( - "sync" - "time" -) - -// pipeDeadline is an abstraction for handling timeouts. -type pipeDeadline struct { - mu sync.Mutex // Guards timer and cancel - timer *time.Timer - cancel chan struct{} // Must be non-nil -} - -func makePipeDeadline() pipeDeadline { - return pipeDeadline{cancel: make(chan struct{})} -} - -// set sets the point in time when the deadline will time out. -// A timeout event is signaled by closing the channel returned by waiter. -// Once a timeout has occurred, the deadline can be refreshed by specifying a -// t value in the future. -// -// A zero value for t prevents timeout. -func (d *pipeDeadline) set(t time.Time) { - d.mu.Lock() - defer d.mu.Unlock() - - if d.timer != nil && !d.timer.Stop() { - <-d.cancel // Wait for the timer callback to finish and close cancel - } - d.timer = nil - - // Time is zero, then there is no deadline. - closed := isClosedChan(d.cancel) - if t.IsZero() { - if closed { - d.cancel = make(chan struct{}) - } - return - } - - // Time in the future, setup a timer to cancel in the future. - if dur := time.Until(t); dur > 0 { - if closed { - d.cancel = make(chan struct{}) - } - d.timer = time.AfterFunc(dur, func() { - close(d.cancel) - }) - return - } - - // Time in the past, so close immediately. - if !closed { - close(d.cancel) - } -} - -// wait returns a channel that is closed when the deadline is exceeded. -func (d *pipeDeadline) wait() chan struct{} { - d.mu.Lock() - defer d.mu.Unlock() - return d.cancel -} - -func isClosedChan(c <-chan struct{}) bool { - select { - case <-c: - return true - default: - return false - } -} - -func makeFilledChan() chan struct{} { - ch := make(chan struct{}, 1) - ch <- struct{}{} - return ch -} diff --git a/common/net/deadline/pipe_sing.go b/common/net/deadline/pipe_sing.go deleted file mode 100644 index e39bde7519..0000000000 --- a/common/net/deadline/pipe_sing.go +++ /dev/null @@ -1,222 +0,0 @@ -package deadline - -import ( - "io" - "net" - "os" - "sync" - "time" - - "github.com/metacubex/sing/common/buf" - N "github.com/metacubex/sing/common/network" -) - -type pipeAddr struct{} - -func (pipeAddr) Network() string { return "pipe" } -func (pipeAddr) String() string { return "pipe" } - -type pipe struct { - wrMu sync.Mutex // Serialize Write operations - - // Used by local Read to interact with remote Write. - // Successful receive on rdRx is always followed by send on rdTx. - rdRx <-chan []byte - rdTx chan<- int - - // Used by local Write to interact with remote Read. - // Successful send on wrTx is always followed by receive on wrRx. - wrTx chan<- []byte - wrRx <-chan int - - once sync.Once // Protects closing localDone - localDone chan struct{} - remoteDone <-chan struct{} - - readDeadline pipeDeadline - writeDeadline pipeDeadline - - readWaitOptions N.ReadWaitOptions -} - -// Pipe creates a synchronous, in-memory, full duplex -// network connection; both ends implement the Conn interface. -// Reads on one end are matched with writes on the other, -// copying data directly between the two; there is no internal -// buffering. -func Pipe() (net.Conn, net.Conn) { - cb1 := make(chan []byte) - cb2 := make(chan []byte) - cn1 := make(chan int) - cn2 := make(chan int) - done1 := make(chan struct{}) - done2 := make(chan struct{}) - - p1 := &pipe{ - rdRx: cb1, rdTx: cn1, - wrTx: cb2, wrRx: cn2, - localDone: done1, remoteDone: done2, - readDeadline: makePipeDeadline(), - writeDeadline: makePipeDeadline(), - } - p2 := &pipe{ - rdRx: cb2, rdTx: cn2, - wrTx: cb1, wrRx: cn1, - localDone: done2, remoteDone: done1, - readDeadline: makePipeDeadline(), - writeDeadline: makePipeDeadline(), - } - return p1, p2 -} - -func (*pipe) LocalAddr() net.Addr { return pipeAddr{} } -func (*pipe) RemoteAddr() net.Addr { return pipeAddr{} } - -func (p *pipe) Read(b []byte) (int, error) { - n, err := p.read(b) - if err != nil && err != io.EOF && err != io.ErrClosedPipe { - err = &net.OpError{Op: "read", Net: "pipe", Err: err} - } - return n, err -} - -func (p *pipe) read(b []byte) (n int, err error) { - switch { - case isClosedChan(p.localDone): - return 0, io.ErrClosedPipe - case isClosedChan(p.remoteDone): - return 0, io.EOF - case isClosedChan(p.readDeadline.wait()): - return 0, os.ErrDeadlineExceeded - } - - select { - case bw := <-p.rdRx: - nr := copy(b, bw) - p.rdTx <- nr - return nr, nil - case <-p.localDone: - return 0, io.ErrClosedPipe - case <-p.remoteDone: - return 0, io.EOF - case <-p.readDeadline.wait(): - return 0, os.ErrDeadlineExceeded - } -} - -func (p *pipe) Write(b []byte) (int, error) { - n, err := p.write(b) - if err != nil && err != io.ErrClosedPipe { - err = &net.OpError{Op: "write", Net: "pipe", Err: err} - } - return n, err -} - -func (p *pipe) write(b []byte) (n int, err error) { - switch { - case isClosedChan(p.localDone): - return 0, io.ErrClosedPipe - case isClosedChan(p.remoteDone): - return 0, io.ErrClosedPipe - case isClosedChan(p.writeDeadline.wait()): - return 0, os.ErrDeadlineExceeded - } - - p.wrMu.Lock() // Ensure entirety of b is written together - defer p.wrMu.Unlock() - for once := true; once || len(b) > 0; once = false { - select { - case p.wrTx <- b: - nw := <-p.wrRx - b = b[nw:] - n += nw - case <-p.localDone: - return n, io.ErrClosedPipe - case <-p.remoteDone: - return n, io.ErrClosedPipe - case <-p.writeDeadline.wait(): - return n, os.ErrDeadlineExceeded - } - } - return n, nil -} - -func (p *pipe) SetDeadline(t time.Time) error { - if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) { - return io.ErrClosedPipe - } - p.readDeadline.set(t) - p.writeDeadline.set(t) - return nil -} - -func (p *pipe) SetReadDeadline(t time.Time) error { - if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) { - return io.ErrClosedPipe - } - p.readDeadline.set(t) - return nil -} - -func (p *pipe) SetWriteDeadline(t time.Time) error { - if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) { - return io.ErrClosedPipe - } - p.writeDeadline.set(t) - return nil -} - -func (p *pipe) Close() error { - p.once.Do(func() { close(p.localDone) }) - return nil -} - -var _ N.ReadWaiter = (*pipe)(nil) - -func (p *pipe) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - p.readWaitOptions = options - return false -} - -func (p *pipe) WaitReadBuffer() (buffer *buf.Buffer, err error) { - buffer, err = p.waitReadBuffer() - if err != nil && err != io.EOF && err != io.ErrClosedPipe { - err = &net.OpError{Op: "read", Net: "pipe", Err: err} - } - return -} - -func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) { - switch { - case isClosedChan(p.localDone): - return nil, io.ErrClosedPipe - case isClosedChan(p.remoteDone): - return nil, io.EOF - case isClosedChan(p.readDeadline.wait()): - return nil, os.ErrDeadlineExceeded - } - select { - case bw := <-p.rdRx: - buffer = p.readWaitOptions.NewBuffer() - var nr int - nr, err = buffer.Write(bw) - if err != nil { - buffer.Release() - return - } - p.readWaitOptions.PostReturn(buffer) - p.rdTx <- nr - return - case <-p.localDone: - return nil, io.ErrClosedPipe - case <-p.remoteDone: - return nil, io.EOF - case <-p.readDeadline.wait(): - return nil, os.ErrDeadlineExceeded - } -} - -func IsPipe(conn any) bool { - _, ok := conn.(*pipe) - return ok -} diff --git a/common/net/earlyconn.go b/common/net/earlyconn.go deleted file mode 100644 index 82d392eeb0..0000000000 --- a/common/net/earlyconn.go +++ /dev/null @@ -1,65 +0,0 @@ -package net - -import ( - "net" - "sync" - - "github.com/metacubex/mihomo/common/buf" - "github.com/metacubex/mihomo/common/once" -) - -type earlyConn struct { - ExtendedConn // only expose standard N.ExtendedConn function to outside - resFunc func() error - resOnce sync.Once - resErr error -} - -func (conn *earlyConn) Response() error { - conn.resOnce.Do(func() { - conn.resErr = conn.resFunc() - }) - return conn.resErr -} - -func (conn *earlyConn) Read(b []byte) (n int, err error) { - err = conn.Response() - if err != nil { - return 0, err - } - return conn.ExtendedConn.Read(b) -} - -func (conn *earlyConn) ReadBuffer(buffer *buf.Buffer) (err error) { - err = conn.Response() - if err != nil { - return err - } - return conn.ExtendedConn.ReadBuffer(buffer) -} - -func (conn *earlyConn) Upstream() any { - return conn.ExtendedConn -} - -func (conn *earlyConn) Success() bool { - return once.Done(&conn.resOnce) && conn.resErr == nil -} - -func (conn *earlyConn) ReaderReplaceable() bool { - return conn.Success() -} - -func (conn *earlyConn) ReaderPossiblyReplaceable() bool { - return !conn.Success() -} - -func (conn *earlyConn) WriterReplaceable() bool { - return true -} - -var _ ExtendedConn = (*earlyConn)(nil) - -func NewEarlyConn(c net.Conn, f func() error) net.Conn { - return &earlyConn{ExtendedConn: NewExtendedConn(c), resFunc: f} -} diff --git a/common/net/io.go b/common/net/io.go deleted file mode 100644 index 5bb4f0023b..0000000000 --- a/common/net/io.go +++ /dev/null @@ -1,11 +0,0 @@ -package net - -import "io" - -type ReadOnlyReader struct { - io.Reader -} - -type WriteOnlyWriter struct { - io.Writer -} diff --git a/common/net/listener.go b/common/net/listener.go deleted file mode 100644 index 0621b6c221..0000000000 --- a/common/net/listener.go +++ /dev/null @@ -1,90 +0,0 @@ -package net - -import ( - "context" - "net" - "sync" -) - -type handleContextListener struct { - net.Listener - ctx context.Context - cancel context.CancelFunc - conns chan net.Conn - err error - once sync.Once - handle func(context.Context, net.Conn) (net.Conn, error) - panicLog func(any) -} - -func (l *handleContextListener) init() { - go func() { - for { - c, err := l.Listener.Accept() - if err != nil { - l.err = err - close(l.conns) - return - } - go func() { - defer func() { - if r := recover(); r != nil { - if l.panicLog != nil { - l.panicLog(r) - } - } - }() - if c, err := l.handle(l.ctx, c); err == nil { - l.conns <- c - } else { - // handle failed, close the underlying connection. - _ = c.Close() - } - }() - } - }() -} - -func (l *handleContextListener) Accept() (net.Conn, error) { - l.once.Do(l.init) - if c, ok := <-l.conns; ok { - return c, nil - } - return nil, l.err -} - -func (l *handleContextListener) Close() error { - l.cancel() - l.once.Do(func() { // l.init has not been called yet, so close related resources directly. - l.err = net.ErrClosed - close(l.conns) - }) - defer func() { - // at here, listener has been closed, so we should close all connections in the channel - for c := range l.conns { - go func(c net.Conn) { - defer func() { - if r := recover(); r != nil { - if l.panicLog != nil { - l.panicLog(r) - } - } - }() - _ = c.Close() - }(c) - } - }() - return l.Listener.Close() -} - -func NewHandleContextListener(ctx context.Context, l net.Listener, handle func(context.Context, net.Conn) (net.Conn, error), panicLog func(any)) net.Listener { - ctx, cancel := context.WithCancel(ctx) - return &handleContextListener{ - Listener: l, - ctx: ctx, - cancel: cancel, - conns: make(chan net.Conn), - handle: handle, - panicLog: panicLog, - } -} diff --git a/common/net/packet.go b/common/net/packet.go deleted file mode 100644 index fd03b4f80d..0000000000 --- a/common/net/packet.go +++ /dev/null @@ -1,18 +0,0 @@ -package net - -import ( - "github.com/metacubex/mihomo/common/net/deadline" - "github.com/metacubex/mihomo/common/net/packet" -) - -type EnhancePacketConn = packet.EnhancePacketConn -type WaitReadFrom = packet.WaitReadFrom - -var NewEnhancePacketConn = packet.NewEnhancePacketConn -var NewThreadSafePacketConn = packet.NewThreadSafePacketConn -var NewRefPacketConn = packet.NewRefPacketConn - -var NewDeadlineNetPacketConn = deadline.NewNetPacketConn -var NewDeadlineEnhancePacketConn = deadline.NewEnhancePacketConn -var NewDeadlineSingPacketConn = deadline.NewSingPacketConn -var NewDeadlineEnhanceSingPacketConn = deadline.NewEnhanceSingPacketConn diff --git a/common/net/packet/packet.go b/common/net/packet/packet.go deleted file mode 100644 index 0cdbccaeed..0000000000 --- a/common/net/packet/packet.go +++ /dev/null @@ -1,77 +0,0 @@ -package packet - -import ( - "net" - - "github.com/metacubex/mihomo/common/pool" -) - -type WaitReadFrom interface { - WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) -} - -type EnhancePacketConn interface { - net.PacketConn - WaitReadFrom -} - -func NewEnhancePacketConn(pc net.PacketConn) EnhancePacketConn { - if udpConn, isUDPConn := pc.(*net.UDPConn); isUDPConn { - return &enhanceUDPConn{UDPConn: udpConn} - } - if enhancePC, isEnhancePC := pc.(EnhancePacketConn); isEnhancePC { - return enhancePC - } - if singPC, isSingPC := pc.(SingPacketConn); isSingPC { - return newEnhanceSingPacketConn(singPC) - } - return &enhancePacketConn{PacketConn: pc} -} - -type enhancePacketConn struct { - net.PacketConn -} - -func (c *enhancePacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - return waitReadFrom(c.PacketConn) -} - -func (c *enhancePacketConn) Upstream() any { - return c.PacketConn -} - -func (c *enhancePacketConn) WriterReplaceable() bool { - return true -} - -func (c *enhancePacketConn) ReaderReplaceable() bool { - return true -} - -func (c *enhanceUDPConn) Upstream() any { - return c.UDPConn -} - -func (c *enhanceUDPConn) WriterReplaceable() bool { - return true -} - -func (c *enhanceUDPConn) ReaderReplaceable() bool { - return true -} - -func waitReadFrom(pc net.PacketConn) (data []byte, put func(), addr net.Addr, err error) { - readBuf := pool.Get(pool.UDPBufferSize) - put = func() { - _ = pool.Put(readBuf) - } - var readN int - readN, addr, err = pc.ReadFrom(readBuf) - if readN > 0 { - data = readBuf[:readN] - } else { - put() - put = nil - } - return -} diff --git a/common/net/packet/packet_posix.go b/common/net/packet/packet_posix.go deleted file mode 100644 index 8a39288716..0000000000 --- a/common/net/packet/packet_posix.go +++ /dev/null @@ -1,69 +0,0 @@ -//go:build !windows - -package packet - -import ( - "net" - "strconv" - "syscall" - - "github.com/metacubex/mihomo/common/pool" -) - -type enhanceUDPConn struct { - *net.UDPConn - rawConn syscall.RawConn -} - -func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - if c.rawConn == nil { - c.rawConn, _ = c.UDPConn.SyscallConn() - } - var readErr error - err = c.rawConn.Read(func(fd uintptr) (done bool) { - readBuf := pool.Get(pool.UDPBufferSize) - put = func() { - _ = pool.Put(readBuf) - } - var readFrom syscall.Sockaddr - var readN int - readN, _, _, readFrom, readErr = syscall.Recvmsg(int(fd), readBuf, nil, 0) - if readN > 0 { - data = readBuf[:readN] - } else { - put() - put = nil - data = nil - } - if readErr == syscall.EAGAIN { - return false - } - if readFrom != nil { - switch from := readFrom.(type) { - case *syscall.SockaddrInet4: - ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 4 bytes - addr = &net.UDPAddr{IP: ip[:], Port: from.Port} - case *syscall.SockaddrInet6: - ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes - zone := "" - if from.ZoneId != 0 { - zone = strconv.FormatInt(int64(from.ZoneId), 10) - } - addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone} - } - } - // udp should not convert readN == 0 to io.EOF - //if readN == 0 { - // readErr = io.EOF - //} - return true - }) - if err != nil { - return - } - if readErr != nil { - err = readErr - return - } - return -} diff --git a/common/net/packet/packet_sing.go b/common/net/packet/packet_sing.go deleted file mode 100644 index fac7c0d9a2..0000000000 --- a/common/net/packet/packet_sing.go +++ /dev/null @@ -1,77 +0,0 @@ -package packet - -import ( - "net" - - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" -) - -type SingPacketConn = N.NetPacketConn - -type EnhanceSingPacketConn interface { - SingPacketConn - EnhancePacketConn -} - -type enhanceSingPacketConn struct { - SingPacketConn - packetReadWaiter N.PacketReadWaiter -} - -func (c *enhanceSingPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - var buff *buf.Buffer - var dest M.Socksaddr - rwOptions := N.ReadWaitOptions{} - if c.packetReadWaiter != nil { - c.packetReadWaiter.InitializeReadWaiter(rwOptions) - buff, dest, err = c.packetReadWaiter.WaitReadPacket() - } else { - buff = rwOptions.NewPacketBuffer() - dest, err = c.SingPacketConn.ReadPacket(buff) - if buff != nil { - rwOptions.PostReturn(buff) - } - } - if dest.IsFqdn() { - addr = dest - } else { - addr = dest.UDPAddr() - } - if err != nil { - buff.Release() - return - } - if buff == nil { - return - } - if buff.IsEmpty() { - buff.Release() - return - } - data = buff.Bytes() - put = buff.Release - return -} - -func (c *enhanceSingPacketConn) Upstream() any { - return c.SingPacketConn -} - -func (c *enhanceSingPacketConn) WriterReplaceable() bool { - return true -} - -func (c *enhanceSingPacketConn) ReaderReplaceable() bool { - return true -} - -func newEnhanceSingPacketConn(conn SingPacketConn) *enhanceSingPacketConn { - epc := &enhanceSingPacketConn{SingPacketConn: conn} - if readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn); isReadWaiter { - epc.packetReadWaiter = readWaiter - } - return epc -} diff --git a/common/net/packet/packet_windows.go b/common/net/packet/packet_windows.go deleted file mode 100644 index e6743ee517..0000000000 --- a/common/net/packet/packet_windows.go +++ /dev/null @@ -1,79 +0,0 @@ -//go:build windows - -package packet - -import ( - "net" - "strconv" - "syscall" - - "github.com/metacubex/mihomo/common/pool" - - "golang.org/x/sys/windows" -) - -type enhanceUDPConn struct { - *net.UDPConn - rawConn syscall.RawConn -} - -func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - if c.rawConn == nil { - c.rawConn, _ = c.UDPConn.SyscallConn() - } - var readErr error - hasData := false - err = c.rawConn.Read(func(fd uintptr) (done bool) { - if !hasData { - hasData = true - // golang's internal/poll.FD.RawRead will Use a zero-byte read as a way to get notified when this - // socket is readable if we return false. So the `recvfrom` syscall will not block the system thread. - return false - } - readBuf := pool.Get(pool.UDPBufferSize) - put = func() { - _ = pool.Put(readBuf) - } - var readFrom windows.Sockaddr - var readN int - readN, readFrom, readErr = windows.Recvfrom(windows.Handle(fd), readBuf, 0) - if readN > 0 { - data = readBuf[:readN] - } else { - put() - put = nil - data = nil - } - if readErr == windows.WSAEWOULDBLOCK { - return false - } - if readFrom != nil { - switch from := readFrom.(type) { - case *windows.SockaddrInet4: - ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 4 bytes - addr = &net.UDPAddr{IP: ip[:], Port: from.Port} - case *windows.SockaddrInet6: - ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes - zone := "" - if from.ZoneId != 0 { - zone = strconv.FormatInt(int64(from.ZoneId), 10) - } - addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone} - } - } - // udp should not convert readN == 0 to io.EOF - //if readN == 0 { - // readErr = io.EOF - //} - hasData = false - return true - }) - if err != nil { - return - } - if readErr != nil { - err = readErr - return - } - return -} diff --git a/common/net/packet/ref.go b/common/net/packet/ref.go deleted file mode 100644 index a562b2e217..0000000000 --- a/common/net/packet/ref.go +++ /dev/null @@ -1,75 +0,0 @@ -package packet - -import ( - "net" - "runtime" - "time" -) - -type refPacketConn struct { - pc EnhancePacketConn - ref any -} - -func (c *refPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - defer runtime.KeepAlive(c.ref) - return c.pc.WaitReadFrom() -} - -func (c *refPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - defer runtime.KeepAlive(c.ref) - return c.pc.ReadFrom(p) -} - -func (c *refPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - defer runtime.KeepAlive(c.ref) - return c.pc.WriteTo(p, addr) -} - -func (c *refPacketConn) Close() error { - defer runtime.KeepAlive(c.ref) - return c.pc.Close() -} - -func (c *refPacketConn) LocalAddr() net.Addr { - defer runtime.KeepAlive(c.ref) - return c.pc.LocalAddr() -} - -func (c *refPacketConn) SetDeadline(t time.Time) error { - defer runtime.KeepAlive(c.ref) - return c.pc.SetDeadline(t) -} - -func (c *refPacketConn) SetReadDeadline(t time.Time) error { - defer runtime.KeepAlive(c.ref) - return c.pc.SetReadDeadline(t) -} - -func (c *refPacketConn) SetWriteDeadline(t time.Time) error { - defer runtime.KeepAlive(c.ref) - return c.pc.SetWriteDeadline(t) -} - -func (c *refPacketConn) Upstream() any { - return c.pc -} - -func (c *refPacketConn) ReaderReplaceable() bool { // Relay() will handle reference - return true -} - -func (c *refPacketConn) WriterReplaceable() bool { // Relay() will handle reference - return true -} - -func NewRefPacketConn(pc net.PacketConn, ref any) EnhancePacketConn { - rPC := &refPacketConn{pc: NewEnhancePacketConn(pc), ref: ref} - if singPC, isSingPC := pc.(SingPacketConn); isSingPC { - return &refSingPacketConn{ - refPacketConn: rPC, - singPacketConn: singPC, - } - } - return rPC -} diff --git a/common/net/packet/ref_sing.go b/common/net/packet/ref_sing.go deleted file mode 100644 index 851694b289..0000000000 --- a/common/net/packet/ref_sing.go +++ /dev/null @@ -1,26 +0,0 @@ -package packet - -import ( - "runtime" - - "github.com/metacubex/sing/common/buf" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" -) - -type refSingPacketConn struct { - *refPacketConn - singPacketConn SingPacketConn -} - -var _ N.NetPacketConn = (*refSingPacketConn)(nil) - -func (c *refSingPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - defer runtime.KeepAlive(c.ref) - return c.singPacketConn.WritePacket(buffer, destination) -} - -func (c *refSingPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { - defer runtime.KeepAlive(c.ref) - return c.singPacketConn.ReadPacket(buffer) -} diff --git a/common/net/packet/thread.go b/common/net/packet/thread.go deleted file mode 100644 index 14d6423398..0000000000 --- a/common/net/packet/thread.go +++ /dev/null @@ -1,36 +0,0 @@ -package packet - -import ( - "net" - "sync" -) - -type threadSafePacketConn struct { - EnhancePacketConn - access sync.Mutex -} - -func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { - c.access.Lock() - defer c.access.Unlock() - return c.EnhancePacketConn.WriteTo(b, addr) -} - -func (c *threadSafePacketConn) Upstream() any { - return c.EnhancePacketConn -} - -func (c *threadSafePacketConn) ReaderReplaceable() bool { - return true -} - -func NewThreadSafePacketConn(pc net.PacketConn) EnhancePacketConn { - tsPC := &threadSafePacketConn{EnhancePacketConn: NewEnhancePacketConn(pc)} - if singPC, isSingPC := pc.(SingPacketConn); isSingPC { - return &threadSafeSingPacketConn{ - threadSafePacketConn: tsPC, - singPacketConn: singPC, - } - } - return tsPC -} diff --git a/common/net/packet/thread_sing.go b/common/net/packet/thread_sing.go deleted file mode 100644 index 53e7a48de2..0000000000 --- a/common/net/packet/thread_sing.go +++ /dev/null @@ -1,24 +0,0 @@ -package packet - -import ( - "github.com/metacubex/sing/common/buf" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" -) - -type threadSafeSingPacketConn struct { - *threadSafePacketConn - singPacketConn SingPacketConn -} - -var _ N.NetPacketConn = (*threadSafeSingPacketConn)(nil) - -func (c *threadSafeSingPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - c.access.Lock() - defer c.access.Unlock() - return c.singPacketConn.WritePacket(buffer, destination) -} - -func (c *threadSafeSingPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { - return c.singPacketConn.ReadPacket(buffer) -} diff --git a/common/net/refconn.go b/common/net/refconn.go deleted file mode 100644 index b1e6c9fc32..0000000000 --- a/common/net/refconn.go +++ /dev/null @@ -1,82 +0,0 @@ -package net - -import ( - "net" - "runtime" - "time" - - "github.com/metacubex/mihomo/common/buf" -) - -type refConn struct { - conn ExtendedConn - ref any -} - -func (c *refConn) Read(b []byte) (n int, err error) { - defer runtime.KeepAlive(c.ref) - return c.conn.Read(b) -} - -func (c *refConn) Write(b []byte) (n int, err error) { - defer runtime.KeepAlive(c.ref) - return c.conn.Write(b) -} - -func (c *refConn) Close() error { - defer runtime.KeepAlive(c.ref) - return c.conn.Close() -} - -func (c *refConn) LocalAddr() net.Addr { - defer runtime.KeepAlive(c.ref) - return c.conn.LocalAddr() -} - -func (c *refConn) RemoteAddr() net.Addr { - defer runtime.KeepAlive(c.ref) - return c.conn.RemoteAddr() -} - -func (c *refConn) SetDeadline(t time.Time) error { - defer runtime.KeepAlive(c.ref) - return c.conn.SetDeadline(t) -} - -func (c *refConn) SetReadDeadline(t time.Time) error { - defer runtime.KeepAlive(c.ref) - return c.conn.SetReadDeadline(t) -} - -func (c *refConn) SetWriteDeadline(t time.Time) error { - defer runtime.KeepAlive(c.ref) - return c.conn.SetWriteDeadline(t) -} - -func (c *refConn) Upstream() any { - return c.conn -} - -func (c *refConn) ReadBuffer(buffer *buf.Buffer) error { - defer runtime.KeepAlive(c.ref) - return c.conn.ReadBuffer(buffer) -} - -func (c *refConn) WriteBuffer(buffer *buf.Buffer) error { - defer runtime.KeepAlive(c.ref) - return c.conn.WriteBuffer(buffer) -} - -func (c *refConn) ReaderReplaceable() bool { // Relay() will handle reference - return true -} - -func (c *refConn) WriterReplaceable() bool { // Relay() will handle reference - return true -} - -var _ ExtendedConn = (*refConn)(nil) - -func NewRefConn(conn net.Conn, ref any) ExtendedConn { - return &refConn{conn: NewExtendedConn(conn), ref: ref} -} diff --git a/common/net/relay.go b/common/net/relay.go deleted file mode 100644 index f2a1b1463a..0000000000 --- a/common/net/relay.go +++ /dev/null @@ -1,24 +0,0 @@ -package net - -//import ( -// "io" -// "net" -// "time" -//) -// -//// Relay copies between left and right bidirectionally. -//func Relay(leftConn, rightConn net.Conn) { -// ch := make(chan error) -// -// go func() { -// // Wrapping to avoid using *net.TCPConn.(ReadFrom) -// // See also https://github.com/metacubex/mihomo/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/net/sing.go b/common/net/sing.go deleted file mode 100644 index 2cd1d726af..0000000000 --- a/common/net/sing.go +++ /dev/null @@ -1,51 +0,0 @@ -package net - -import ( - "context" - "net" - "runtime" - - "github.com/metacubex/mihomo/common/net/deadline" - - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/bufio" - "github.com/metacubex/sing/common/network" -) - -var NewExtendedConn = bufio.NewExtendedConn -var NewExtendedWriter = bufio.NewExtendedWriter -var NewExtendedReader = bufio.NewExtendedReader - -type ExtendedConn = network.ExtendedConn -type ExtendedWriter = network.ExtendedWriter -type ExtendedReader = network.ExtendedReader - -var WriteBuffer = bufio.WriteBuffer - -func NewDeadlineConn(conn net.Conn) ExtendedConn { - if deadline.IsPipe(conn) || deadline.IsPipe(network.UnwrapReader(conn)) { - return NewExtendedConn(conn) // pipe always have correctly deadline implement - } - if deadline.IsConn(conn) || deadline.IsConn(network.UnwrapReader(conn)) { - return NewExtendedConn(conn) // was a *deadline.Conn - } - return deadline.NewConn(conn) -} - -func NeedHandshake(conn any) bool { - if earlyConn, isEarlyConn := common.Cast[network.EarlyConn](conn); isEarlyConn && earlyConn.NeedHandshake() { - return true - } - return false -} - -type CountFunc = network.CountFunc - -var Pipe = deadline.Pipe - -// Relay copies between left and right bidirectionally. -func Relay(leftConn, rightConn net.Conn) { - defer runtime.KeepAlive(leftConn) - defer runtime.KeepAlive(rightConn) - _ = bufio.CopyConn(context.TODO(), leftConn, rightConn) -} diff --git a/common/net/tcpip.go b/common/net/tcpip.go deleted file mode 100644 index a84e7e4c4f..0000000000 --- a/common/net/tcpip.go +++ /dev/null @@ -1,46 +0,0 @@ -package net - -import ( - "fmt" - "net" - "strings" -) - -func SplitNetworkType(s string) (string, string, error) { - var ( - shecme string - hostPort string - ) - result := strings.Split(s, "://") - if len(result) == 2 { - shecme = result[0] - hostPort = result[1] - } else if len(result) == 1 { - hostPort = result[0] - } else { - return "", "", fmt.Errorf("tcp/udp style error") - } - - if len(shecme) == 0 { - shecme = "udp" - } - - if shecme != "tcp" && shecme != "udp" { - return "", "", fmt.Errorf("scheme should be tcp:// or udp://") - } else { - return shecme, hostPort, nil - } -} - -func SplitHostPort(s string) (host, port string, hasPort bool, err error) { - temp := s - hasPort = true - - if !strings.Contains(s, ":") && !strings.Contains(s, "]:") { - temp += ":0" - hasPort = false - } - - host, port, err = net.SplitHostPort(temp) - return -} diff --git a/common/net/websocket.go b/common/net/websocket.go deleted file mode 100644 index b002310a94..0000000000 --- a/common/net/websocket.go +++ /dev/null @@ -1,131 +0,0 @@ -package net - -import ( - "encoding/binary" - "math/bits" -) - -// kanged from https://github.com/nhooyr/websocket/blob/master/frame.go -// License: MIT - -// MaskWebSocket applies the WebSocket masking algorithm to p -// with the given key. -// See https://tools.ietf.org/html/rfc6455#section-5.3 -// -// The returned value is the correctly rotated key to -// to continue to mask/unmask the message. -// -// It is optimized for LittleEndian and expects the key -// to be in little endian. -// -// See https://github.com/golang/go/issues/31586 -func MaskWebSocket(key uint32, b []byte) uint32 { - if len(b) >= 8 { - key64 := uint64(key)<<32 | uint64(key) - - // At some point in the future we can clean these unrolled loops up. - // See https://github.com/golang/go/issues/31586#issuecomment-487436401 - - // Then we xor until b is less than 128 bytes. - for len(b) >= 128 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - v = binary.LittleEndian.Uint64(b[64:72]) - binary.LittleEndian.PutUint64(b[64:72], v^key64) - v = binary.LittleEndian.Uint64(b[72:80]) - binary.LittleEndian.PutUint64(b[72:80], v^key64) - v = binary.LittleEndian.Uint64(b[80:88]) - binary.LittleEndian.PutUint64(b[80:88], v^key64) - v = binary.LittleEndian.Uint64(b[88:96]) - binary.LittleEndian.PutUint64(b[88:96], v^key64) - v = binary.LittleEndian.Uint64(b[96:104]) - binary.LittleEndian.PutUint64(b[96:104], v^key64) - v = binary.LittleEndian.Uint64(b[104:112]) - binary.LittleEndian.PutUint64(b[104:112], v^key64) - v = binary.LittleEndian.Uint64(b[112:120]) - binary.LittleEndian.PutUint64(b[112:120], v^key64) - v = binary.LittleEndian.Uint64(b[120:128]) - binary.LittleEndian.PutUint64(b[120:128], v^key64) - b = b[128:] - } - - // Then we xor until b is less than 64 bytes. - for len(b) >= 64 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - b = b[64:] - } - - // Then we xor until b is less than 32 bytes. - for len(b) >= 32 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - b = b[32:] - } - - // Then we xor until b is less than 16 bytes. - for len(b) >= 16 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - b = b[16:] - } - - // Then we xor until b is less than 8 bytes. - for len(b) >= 8 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - b = b[8:] - } - } - - // Then we xor until b is less than 4 bytes. - for len(b) >= 4 { - v := binary.LittleEndian.Uint32(b) - binary.LittleEndian.PutUint32(b, v^key) - b = b[4:] - } - - // xor remaining bytes. - for i := range b { - b[i] ^= byte(key) - key = bits.RotateLeft32(key, -8) - } - - return key -} diff --git a/common/observable/iterable.go b/common/observable/iterable.go deleted file mode 100644 index c78b49a30e..0000000000 --- a/common/observable/iterable.go +++ /dev/null @@ -1,3 +0,0 @@ -package observable - -type Iterable[T any] <-chan T diff --git a/common/observable/observable.go b/common/observable/observable.go deleted file mode 100644 index 1e45cc66e1..0000000000 --- a/common/observable/observable.go +++ /dev/null @@ -1,68 +0,0 @@ -package observable - -import ( - "errors" - "sync" -) - -type Observable[T any] struct { - iterable Iterable[T] - listener map[Subscription[T]]*Subscriber[T] - mux sync.Mutex - done bool - stopCh chan struct{} -} - -func (o *Observable[T]) 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[T]) close() { - o.mux.Lock() - defer o.mux.Unlock() - - o.done = true - for _, sub := range o.listener { - sub.Close() - } - close(o.stopCh) -} - -func (o *Observable[T]) Subscribe() (Subscription[T], error) { - o.mux.Lock() - defer o.mux.Unlock() - if o.done { - return nil, errors.New("observable is closed") - } - subscriber := newSubscriber[T]() - o.listener[subscriber.Out()] = subscriber - return subscriber.Out(), nil -} - -func (o *Observable[T]) UnSubscribe(sub Subscription[T]) { - o.mux.Lock() - defer o.mux.Unlock() - subscriber, exist := o.listener[sub] - if !exist { - return - } - delete(o.listener, sub) - subscriber.Close() -} - -func NewObservable[T any](iter Iterable[T]) *Observable[T] { - observable := &Observable[T]{ - iterable: iter, - listener: map[Subscription[T]]*Subscriber[T]{}, - stopCh: make(chan struct{}), - } - go observable.process() - return observable -} diff --git a/common/observable/observable_test.go b/common/observable/observable_test.go deleted file mode 100644 index 6be7f3aacc..0000000000 --- a/common/observable/observable_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package observable - -import ( - "sync" - "testing" - "time" - - "github.com/metacubex/mihomo/common/atomic" - - "github.com/stretchr/testify/assert" -) - -func iterator[T any](item []T) chan T { - ch := make(chan T) - go func() { - time.Sleep(100 * time.Millisecond) - for _, elm := range item { - ch <- elm - } - close(ch) - }() - return ch -} - -func TestObservable(t *testing.T) { - iter := iterator[int]([]int{1, 2, 3, 4, 5}) - src := NewObservable[int](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[int]([]int{1, 2, 3, 4, 5}) - src := NewObservable[int](iter) - ch1, _ := src.Subscribe() - ch2, _ := src.Subscribe() - count := atomic.NewInt32(0) - - var wg sync.WaitGroup - wg.Add(2) - waitCh := func(ch <-chan int) { - for range ch { - count.Add(1) - } - 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[int]([]int{1, 2, 3, 4, 5}) - src := NewObservable[int](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[int]([]int{1}) - src := NewObservable[int](iter) - data, _ := src.Subscribe() - <-data - select { - case <-src.stopCh: - case <-time.After(time.Second): - assert.Fail(t, "timeout not stop") - } -} - -func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) { - sub := Subscription[int](make(chan int)) - iter := iterator[int]([]int{1}) - src := NewObservable[int](iter) - src.UnSubscribe(sub) -} - -func TestObservable_SubscribeGoroutineLeak(t *testing.T) { - iter := iterator[int]([]int{1, 2, 3, 4, 5}) - src := NewObservable[int](iter) - max := 100 - - var list []Subscription[int] - for i := 0; i < max; i++ { - ch, _ := src.Subscribe() - list = append(list, ch) - } - - var wg sync.WaitGroup - wg.Add(max) - waitCh := func(ch <-chan int) { - 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 int) - o := NewObservable[int](ch) - num := 1000 - - subs := []Subscription[int]{} - 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[int]) { - 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 deleted file mode 100644 index b7df4caea5..0000000000 --- a/common/observable/subscriber.go +++ /dev/null @@ -1,33 +0,0 @@ -package observable - -import ( - "sync" -) - -type Subscription[T any] <-chan T - -type Subscriber[T any] struct { - buffer chan T - once sync.Once -} - -func (s *Subscriber[T]) Emit(item T) { - s.buffer <- item -} - -func (s *Subscriber[T]) Out() Subscription[T] { - return s.buffer -} - -func (s *Subscriber[T]) Close() { - s.once.Do(func() { - close(s.buffer) - }) -} - -func newSubscriber[T any]() *Subscriber[T] { - sub := &Subscriber[T]{ - buffer: make(chan T, 200), - } - return sub -} diff --git a/common/once/once_go120.go b/common/once/once_go120.go deleted file mode 100644 index 51578a2da2..0000000000 --- a/common/once/once_go120.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !go1.22 - -package once - -import ( - "sync" - "sync/atomic" - "unsafe" -) - -type Once struct { - done uint32 - m sync.Mutex -} - -func Done(once *sync.Once) bool { - // atomic visit sync.Once.done - return atomic.LoadUint32((*uint32)(unsafe.Pointer(once))) == 1 -} - -func Reset(once *sync.Once) { - o := (*Once)(unsafe.Pointer(once)) - o.m.Lock() - defer o.m.Unlock() - atomic.StoreUint32(&o.done, 0) -} diff --git a/common/once/once_go122.go b/common/once/once_go122.go deleted file mode 100644 index 5ff8461d6e..0000000000 --- a/common/once/once_go122.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build go1.22 - -package once - -import ( - "sync" - "sync/atomic" - "unsafe" -) - -type Once struct { - done atomic.Uint32 - m sync.Mutex -} - -func Done(once *sync.Once) bool { - // atomic visit sync.Once.done - return (*atomic.Uint32)(unsafe.Pointer(once)).Load() == 1 -} - -func Reset(once *sync.Once) { - o := (*Once)(unsafe.Pointer(once)) - o.m.Lock() - defer o.m.Unlock() - o.done.Store(0) -} diff --git a/common/once/oncefunc.go b/common/once/oncefunc.go deleted file mode 100644 index 80c00f8802..0000000000 --- a/common/once/oncefunc.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package once - -import "sync" - -// OnceFunc returns a function that invokes f only once. The returned function -// may be called concurrently. -// -// If f panics, the returned function will panic with the same value on every call. -func OnceFunc(f func()) func() { - var ( - once sync.Once - valid bool - p any - ) - // Construct the inner closure just once to reduce costs on the fast path. - g := func() { - defer func() { - p = recover() - if !valid { - // Re-panic immediately so on the first call the user gets a - // complete stack trace into f. - panic(p) - } - }() - f() - f = nil // Do not keep f alive after invoking it. - valid = true // Set only if f does not panic. - } - return func() { - once.Do(g) - if !valid { - panic(p) - } - } -} - -// OnceValue returns a function that invokes f only once and returns the value -// returned by f. The returned function may be called concurrently. -// -// If f panics, the returned function will panic with the same value on every call. -func OnceValue[T any](f func() T) func() T { - var ( - once sync.Once - valid bool - p any - result T - ) - g := func() { - defer func() { - p = recover() - if !valid { - panic(p) - } - }() - result = f() - f = nil - valid = true - } - return func() T { - once.Do(g) - if !valid { - panic(p) - } - return result - } -} - -// OnceValues returns a function that invokes f only once and returns the values -// returned by f. The returned function may be called concurrently. -// -// If f panics, the returned function will panic with the same value on every call. -func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) { - var ( - once sync.Once - valid bool - p any - r1 T1 - r2 T2 - ) - g := func() { - defer func() { - p = recover() - if !valid { - panic(p) - } - }() - r1, r2 = f() - f = nil - valid = true - } - return func() (T1, T2) { - once.Do(g) - if !valid { - panic(p) - } - return r1, r2 - } -} diff --git a/common/picker/picker.go b/common/picker/picker.go deleted file mode 100644 index 3a7688cafc..0000000000 --- a/common/picker/picker.go +++ /dev/null @@ -1,92 +0,0 @@ -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[T any] struct { - ctx context.Context - cancel func() - - wg sync.WaitGroup - - once sync.Once - errOnce sync.Once - result T - err error -} - -func newPicker[T any](ctx context.Context, cancel func()) *Picker[T] { - return &Picker[T]{ - ctx: ctx, - cancel: cancel, - } -} - -// WithContext returns a new Picker and an associated Context derived from ctx. -// and cancel when first element return. -func WithContext[T any](ctx context.Context) (*Picker[T], context.Context) { - ctx, cancel := context.WithCancel(ctx) - return newPicker[T](ctx, cancel), ctx -} - -// WithTimeout returns a new Picker and an associated Context derived from ctx with timeout. -func WithTimeout[T any](ctx context.Context, timeout time.Duration) (*Picker[T], context.Context) { - ctx, cancel := context.WithTimeout(ctx, timeout) - return newPicker[T](ctx, cancel), ctx -} - -// Wait blocks until all function calls from the Go method have returned, -// then returns the first nil error result (if any) from them. -func (p *Picker[T]) Wait() T { - p.wg.Wait() - if p.cancel != nil { - p.cancel() - p.cancel = nil - } - return p.result -} - -// Error return the first error (if all success return nil) -func (p *Picker[T]) Error() error { - return p.err -} - -// Go calls the given function in a new goroutine. -// The first call to return a nil error cancels the group; its result will be returned by Wait. -func (p *Picker[T]) Go(f func() (T, 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() - p.cancel = nil - } - }) - } else { - p.errOnce.Do(func() { - p.err = err - }) - } - }() -} - -// Close cancels the picker context and releases resources associated with it. -// If Wait has been called, then there is no need to call Close. -func (p *Picker[T]) Close() error { - if p.cancel != nil { - p.cancel() - p.cancel = nil - } - return nil -} diff --git a/common/picker/picker_test.go b/common/picker/picker_test.go deleted file mode 100644 index 357c40019b..0000000000 --- a/common/picker/picker_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package picker - -import ( - "context" - "testing" - "time" - - "github.com/samber/lo" - "github.com/stretchr/testify/assert" -) - -func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, error) { - return func() (T, error) { - timer := time.NewTimer(time.Millisecond * time.Duration(delay)) - select { - case <-timer.C: - return input, nil - case <-ctx.Done(): - return lo.Empty[T](), ctx.Err() - } - } -} - -func TestPicker_Basic(t *testing.T) { - t.Parallel() - picker, ctx := WithContext[int](context.Background()) - picker.Go(sleepAndSend(ctx, 200, 2)) - picker.Go(sleepAndSend(ctx, 100, 1)) - - number := picker.Wait() - assert.NotNil(t, number) - assert.Equal(t, number, 1) -} - -func TestPicker_Timeout(t *testing.T) { - t.Parallel() - picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5) - picker.Go(sleepAndSend(ctx, 100, 1)) - - number := picker.Wait() - assert.Equal(t, number, lo.Empty[int]()) - assert.NotNil(t, picker.Error()) -} diff --git a/common/pool/alloc.go b/common/pool/alloc.go deleted file mode 100644 index 80927a2c0a..0000000000 --- a/common/pool/alloc.go +++ /dev/null @@ -1,139 +0,0 @@ -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 [11]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 { - return &Allocator{ - buffers: [...]sync.Pool{ // 64B -> 64K - {New: func() any { return new([1 << 6]byte) }}, - {New: func() any { return new([1 << 7]byte) }}, - {New: func() any { return new([1 << 8]byte) }}, - {New: func() any { return new([1 << 9]byte) }}, - {New: func() any { return new([1 << 10]byte) }}, - {New: func() any { return new([1 << 11]byte) }}, - {New: func() any { return new([1 << 12]byte) }}, - {New: func() any { return new([1 << 13]byte) }}, - {New: func() any { return new([1 << 14]byte) }}, - {New: func() any { return new([1 << 15]byte) }}, - {New: func() any { return new([1 << 16]byte) }}, - }, - } -} - -// 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: - var index uint16 - if size > 64 { - index = msb(size) - if size != 1< 65536 { - return nil - } - - bits := msb(cap(buf)) - if cap(buf) != 1<= 0 { - stack = stack[line+1:] - } - return &panicError{value: v, stack: stack} -} - -// call is an in-flight or completed singleflight.Do call -type call[T any] struct { - wg sync.WaitGroup - - // These fields are written once before the WaitGroup is done - // and are only read after the WaitGroup is done. - val T - err error - - // These fields are read and written with the singleflight - // mutex held before the WaitGroup is done, and are read but - // not written after the WaitGroup is done. - dups int - chans []chan<- Result[T] -} - -// Group represents a class of work and forms a namespace in -// which units of work can be executed with duplicate suppression. -type Group[T any] struct { - mu sync.Mutex // protects m - m map[string]*call[T] // lazily initialized - - StoreResult bool -} - -// Result holds the results of Do, so they can be passed -// on a channel. -type Result[T any] struct { - Val T - Err error - Shared bool -} - -// Do executes and returns the results of the given function, making -// sure that only one execution is in-flight for a given key at a -// time. If a duplicate comes in, the duplicate caller waits for the -// original to complete and receives the same results. -// The return value shared indicates whether v was given to multiple callers. -func (g *Group[T]) Do(key string, fn func() (T, error)) (v T, err error, shared bool) { - g.mu.Lock() - if g.m == nil { - g.m = make(map[string]*call[T]) - } - if c, ok := g.m[key]; ok { - c.dups++ - g.mu.Unlock() - c.wg.Wait() - - if e, ok := c.err.(*panicError); ok { - panic(e) - } else if c.err == errGoexit { - runtime.Goexit() - } - return c.val, c.err, true - } - c := new(call[T]) - c.wg.Add(1) - g.m[key] = c - g.mu.Unlock() - - g.doCall(c, key, fn) - return c.val, c.err, c.dups > 0 -} - -// DoChan is like Do but returns a channel that will receive the -// results when they are ready. -// -// The returned channel will not be closed. -func (g *Group[T]) DoChan(key string, fn func() (T, error)) <-chan Result[T] { - ch := make(chan Result[T], 1) - g.mu.Lock() - if g.m == nil { - g.m = make(map[string]*call[T]) - } - if c, ok := g.m[key]; ok { - c.dups++ - c.chans = append(c.chans, ch) - g.mu.Unlock() - return ch - } - c := &call[T]{chans: []chan<- Result[T]{ch}} - c.wg.Add(1) - g.m[key] = c - g.mu.Unlock() - - go g.doCall(c, key, fn) - - return ch -} - -// doCall handles the single call for a key. -func (g *Group[T]) doCall(c *call[T], key string, fn func() (T, error)) { - normalReturn := false - recovered := false - - // use double-defer to distinguish panic from runtime.Goexit, - // more details see https://golang.org/cl/134395 - defer func() { - // the given function invoked runtime.Goexit - if !normalReturn && !recovered { - c.err = errGoexit - } - - g.mu.Lock() - defer g.mu.Unlock() - c.wg.Done() - if g.m[key] == c && !g.StoreResult { - delete(g.m, key) - } - - if e, ok := c.err.(*panicError); ok { - // In order to prevent the waiting channels from being blocked forever, - // needs to ensure that this panic cannot be recovered. - if len(c.chans) > 0 { - go panic(e) - select {} // Keep this goroutine around so that it will appear in the crash dump. - } else { - panic(e) - } - } else if c.err == errGoexit { - // Already in the process of goexit, no need to call again - } else { - // Normal return - for _, ch := range c.chans { - ch <- Result[T]{c.val, c.err, c.dups > 0} - } - } - }() - - func() { - defer func() { - if !normalReturn { - // Ideally, we would wait to take a stack trace until we've determined - // whether this is a panic or a runtime.Goexit. - // - // Unfortunately, the only way we can distinguish the two is to see - // whether the recover stopped the goroutine from terminating, and by - // the time we know that, the part of the stack trace relevant to the - // panic has been discarded. - if r := recover(); r != nil { - c.err = newPanicError(r) - } - } - }() - - c.val, c.err = fn() - normalReturn = true - }() - - if !normalReturn { - recovered = true - } -} - -// Forget tells the singleflight to forget about a key. Future calls -// to Do for this key will call the function rather than waiting for -// an earlier call to complete. -func (g *Group[T]) Forget(key string) { - g.mu.Lock() - delete(g.m, key) - g.mu.Unlock() -} - -func (g *Group[T]) Reset() { - g.mu.Lock() - g.m = nil - g.mu.Unlock() -} diff --git a/common/sockopt/reuse_common.go b/common/sockopt/reuse_common.go deleted file mode 100644 index 907697c32c..0000000000 --- a/common/sockopt/reuse_common.go +++ /dev/null @@ -1,30 +0,0 @@ -package sockopt - -import ( - "net" - "syscall" -) - -func RawConnReuseaddr(rc syscall.RawConn) (err error) { - var innerErr error - err = rc.Control(func(fd uintptr) { - innerErr = reuseControl(fd) - }) - - if innerErr != nil { - err = innerErr - } - return -} - -func UDPReuseaddr(c net.PacketConn) error { - if c, ok := c.(syscall.Conn); ok { - rc, err := c.SyscallConn() - if err != nil { - return err - } - - return RawConnReuseaddr(rc) - } - return nil -} diff --git a/common/sockopt/reuse_other.go b/common/sockopt/reuse_other.go deleted file mode 100644 index 39d901a050..0000000000 --- a/common/sockopt/reuse_other.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows - -package sockopt - -func reuseControl(fd uintptr) error { return nil } diff --git a/common/sockopt/reuse_unix.go b/common/sockopt/reuse_unix.go deleted file mode 100644 index e000b9e89c..0000000000 --- a/common/sockopt/reuse_unix.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris - -package sockopt - -import ( - "golang.org/x/sys/unix" -) - -func reuseControl(fd uintptr) error { - e1 := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) - e2 := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) - - if e1 != nil { - return e1 - } - - if e2 != nil { - return e2 - } - - return nil -} diff --git a/common/sockopt/reuse_windows.go b/common/sockopt/reuse_windows.go deleted file mode 100644 index 79305d02b0..0000000000 --- a/common/sockopt/reuse_windows.go +++ /dev/null @@ -1,9 +0,0 @@ -package sockopt - -import ( - "golang.org/x/sys/windows" -) - -func reuseControl(fd uintptr) error { - return windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) -} diff --git a/common/structure/structure.go b/common/structure/structure.go deleted file mode 100644 index 5bafc178e0..0000000000 --- a/common/structure/structure.go +++ /dev/null @@ -1,604 +0,0 @@ -package structure - -// references: https://github.com/mitchellh/mapstructure - -import ( - "encoding" - "encoding/base64" - "fmt" - "reflect" - "strconv" - "strings" -) - -// Option is the configuration that is used to create a new decoder -type Option struct { - TagName string - WeaklyTypedInput bool - KeyReplacer *strings.Replacer -} - -var DefaultKeyReplacer = strings.NewReplacer("_", "-") - -// 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 { - if d.option.KeyReplacer != nil { - key = d.option.KeyReplacer.Replace(key) - } - - for _strKey := range src { - strKey := _strKey - if d.option.KeyReplacer != nil { - strKey = d.option.KeyReplacer.Replace(strKey) - } - if strings.EqualFold(key, strKey) { - value = src[_strKey] - ok = true - break - } - } - } - 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 -} - -// isNil returns true if the input is nil or a typed nil pointer. -func isNil(input any) bool { - if input == nil { - return true - } - val := reflect.ValueOf(input) - return val.Kind() == reflect.Pointer && val.IsNil() -} - -func (d *Decoder) decode(name string, data any, val reflect.Value) error { - if isNil(data) { - // If the data is nil, then we don't set anything - // Maybe we should set to zero value? - return nil - } - if !reflect.ValueOf(data).IsValid() { - // If the input value is invalid, then we just set the value - // to be the zero value. - val.Set(reflect.Zero(val.Type())) - return nil - } - for { - kind := val.Kind() - if kind == reflect.Pointer && val.IsNil() { - val.Set(reflect.New(val.Type().Elem())) - } - if ok, err := d.decodeTextUnmarshaller(name, data, val); ok { - return err - } - switch { - case isInt(kind): - return d.decodeInt(name, data, val) - case isUint(kind): - return d.decodeUint(name, data, val) - case isFloat(kind): - return d.decodeFloat(name, data, val) - } - switch kind { - case reflect.Pointer: - val = val.Elem() - continue - 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 isInt(kind reflect.Kind) bool { - switch kind { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return true - default: - return false - } -} - -func isUint(kind reflect.Kind) bool { - switch kind { - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return true - default: - return false - } -} - -func isFloat(kind reflect.Kind) bool { - switch kind { - case reflect.Float32, reflect.Float64: - return true - default: - return false - } -} - -func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) { - dataVal := reflect.ValueOf(data) - kind := dataVal.Kind() - switch { - case isInt(kind): - val.SetInt(dataVal.Int()) - case isUint(kind) && d.option.WeaklyTypedInput: - val.SetInt(int64(dataVal.Uint())) - case isFloat(kind) && 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) decodeUint(name string, data any, val reflect.Value) (err error) { - dataVal := reflect.ValueOf(data) - kind := dataVal.Kind() - switch { - case isUint(kind): - val.SetUint(dataVal.Uint()) - case isInt(kind) && d.option.WeaklyTypedInput: - val.SetUint(uint64(dataVal.Int())) - case isFloat(kind) && d.option.WeaklyTypedInput: - val.SetUint(uint64(dataVal.Float())) - case kind == reflect.String && d.option.WeaklyTypedInput: - var i uint64 - i, err = strconv.ParseUint(dataVal.String(), 0, val.Type().Bits()) - if err == nil { - val.SetUint(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) decodeFloat(name string, data any, val reflect.Value) (err error) { - dataVal := reflect.ValueOf(data) - kind := dataVal.Kind() - switch { - case isFloat(kind): - val.SetFloat(dataVal.Float()) - case isUint(kind): - val.SetFloat(float64(dataVal.Uint())) - case isInt(kind) && d.option.WeaklyTypedInput: - val.SetFloat(float64(dataVal.Int())) - case kind == reflect.String && d.option.WeaklyTypedInput: - var i float64 - i, err = strconv.ParseFloat(dataVal.String(), val.Type().Bits()) - if err == nil { - val.SetFloat(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 isInt(kind) && d.option.WeaklyTypedInput: - val.SetString(strconv.FormatInt(dataVal.Int(), 10)) - case isUint(kind) && d.option.WeaklyTypedInput: - val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) - case isFloat(kind) && d.option.WeaklyTypedInput: - val.SetString(strconv.FormatFloat(dataVal.Float(), 'E', -1, dataVal.Type().Bits())) - 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 isInt(kind) && d.option.WeaklyTypedInput: - val.SetBool(dataVal.Int() != 0) - case isUint(kind) && d.option.WeaklyTypedInput: - val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) - 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.String && valElemType.Kind() == reflect.Uint8 { // from encoding/json - s := []byte(dataVal.String()) - b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) - n, err := base64.StdEncoding.Decode(b, s) - if err != nil { - return fmt.Errorf("try decode '%s' by base64 error: %w", name, err) - } - val.SetBytes(b[:n]) - return nil - } - - if dataVal.Kind() != reflect.Slice { - return fmt.Errorf("'%s' is not a slice", name) - } - - valSlice := val - // make a new slice with cap(val)==cap(dataVal) - // the caller can determine whether the original configuration contains this item by judging whether the value is nil. - valSlice = reflect.MakeSlice(valType, 0, dataVal.Len()) - 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 - } - var 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. - if d.option.KeyReplacer != nil { - fieldName = d.option.KeyReplacer.Replace(fieldName) - } - for dataValKey := range dataValKeys { - mK, ok := dataValKey.Interface().(string) - if !ok { - // Not a string key - continue - } - if d.option.KeyReplacer != nil { - mK = d.option.KeyReplacer.Replace(mK) - } - - 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 -} - -func (d *Decoder) decodeTextUnmarshaller(name string, data any, val reflect.Value) (bool, error) { - if !val.CanAddr() { - return false, nil - } - valAddr := val.Addr() - if !valAddr.CanInterface() { - return false, nil - } - unmarshaller, ok := valAddr.Interface().(encoding.TextUnmarshaler) - if !ok { - return false, nil - } - var str string - if err := d.decodeString(name, data, reflect.Indirect(reflect.ValueOf(&str))); err != nil { - return false, err - } - if err := unmarshaller.UnmarshalText([]byte(str)); err != nil { - return true, fmt.Errorf("cannot parse '%s' as %s: %s", name, val.Type(), err) - } - return true, nil -} diff --git a/common/structure/structure_test.go b/common/structure/structure_test.go deleted file mode 100644 index 266b828fd4..0000000000 --- a/common/structure/structure_test.go +++ /dev/null @@ -1,287 +0,0 @@ -package structure - -import ( - "strconv" - "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) -} - -func TestStructure_SliceCap(t *testing.T) { - rawMap := map[string]any{ - "foo": []string{}, - } - - s := &struct { - Foo []string `test:"foo,omitempty"` - Bar []string `test:"bar,omitempty"` - }{} - - err := decoder.Decode(rawMap, s) - assert.Nil(t, err) - assert.NotNil(t, s.Foo) // structure's Decode will ensure value not nil when input has value even it was set an empty array - assert.Nil(t, s.Bar) -} - -func TestStructure_Base64(t *testing.T) { - rawMap := map[string]any{ - "foo": "AQID", - } - - s := &struct { - Foo []byte `test:"foo"` - }{} - - err := decoder.Decode(rawMap, s) - assert.Nil(t, err) - assert.Equal(t, []byte{1, 2, 3}, s.Foo) -} - -func TestStructure_Pointer(t *testing.T) { - rawMap := map[string]any{ - "foo": "foo", - } - - s := &struct { - Foo *string `test:"foo,omitempty"` - Bar *string `test:"bar,omitempty"` - }{} - - err := decoder.Decode(rawMap, s) - assert.Nil(t, err) - assert.NotNil(t, s.Foo) - assert.Equal(t, "foo", *s.Foo) - assert.Nil(t, s.Bar) -} - -type num struct { - a int -} - -func (n *num) UnmarshalText(text []byte) (err error) { - n.a, err = strconv.Atoi(string(text)) - return -} - -func TestStructure_TextUnmarshaller(t *testing.T) { - rawMap := map[string]any{ - "num": "255", - "num_p": "127", - } - - s := &struct { - Num num `test:"num"` - NumP *num `test:"num_p"` - }{} - - err := decoder.Decode(rawMap, s) - assert.Nil(t, err) - assert.Equal(t, 255, s.Num.a) - assert.NotNil(t, s.NumP) - assert.Equal(t, s.NumP.a, 127) - - // test WeaklyTypedInput - rawMap["num"] = 256 - err = decoder.Decode(rawMap, s) - assert.NotNilf(t, err, "should throw error: %#v", s) - err = weakTypeDecoder.Decode(rawMap, s) - assert.Nil(t, err) - assert.Equal(t, 256, s.Num.a) - - // test invalid input - rawMap["num_p"] = "abc" - err = decoder.Decode(rawMap, s) - assert.NotNilf(t, err, "should throw error: %#v", s) -} - -func TestStructure_Null(t *testing.T) { - rawMap := map[string]any{ - "opt": map[string]any{ - "bar": nil, - }, - } - - s := struct { - Opt struct { - Bar string `test:"bar,optional"` - } `test:"opt,optional"` - }{} - - err := decoder.Decode(rawMap, &s) - assert.Nil(t, err) - assert.Equal(t, s.Opt.Bar, "") -} diff --git a/common/utils/callback.go b/common/utils/callback.go deleted file mode 100644 index ad734c0fd6..0000000000 --- a/common/utils/callback.go +++ /dev/null @@ -1,50 +0,0 @@ -package utils - -import ( - "io" - "sync" - - list "github.com/bahlo/generic-list-go" -) - -type Callback[T any] struct { - list list.List[func(T)] - mutex sync.RWMutex -} - -func NewCallback[T any]() *Callback[T] { - return &Callback[T]{} -} - -func (c *Callback[T]) Register(item func(T)) io.Closer { - c.mutex.Lock() - defer c.mutex.Unlock() - element := c.list.PushBack(item) - return &callbackCloser[T]{ - element: element, - callback: c, - } -} - -func (c *Callback[T]) Emit(item T) { - c.mutex.RLock() - defer c.mutex.RUnlock() - for element := c.list.Front(); element != nil; element = element.Next() { - go element.Value(item) - } -} - -type callbackCloser[T any] struct { - element *list.Element[func(T)] - callback *Callback[T] - once sync.Once -} - -func (c *callbackCloser[T]) Close() error { - c.once.Do(func() { - c.callback.mutex.Lock() - defer c.callback.mutex.Unlock() - c.callback.list.Remove(c.element) - }) - return nil -} diff --git a/common/utils/global_id.go b/common/utils/global_id.go deleted file mode 100644 index 68b4613794..0000000000 --- a/common/utils/global_id.go +++ /dev/null @@ -1,17 +0,0 @@ -package utils - -import ( - "hash/maphash" - "unsafe" -) - -var globalSeed = maphash.MakeSeed() - -func GlobalID(material string) (id [8]byte) { - *(*uint64)(unsafe.Pointer(&id[0])) = maphash.String(globalSeed, material) - return -} - -func MapHash(material string) uint64 { - return maphash.String(globalSeed, material) -} diff --git a/common/utils/hash.go b/common/utils/hash.go deleted file mode 100644 index 6957e2c3ec..0000000000 --- a/common/utils/hash.go +++ /dev/null @@ -1,62 +0,0 @@ -package utils - -import ( - "crypto/md5" - "encoding/hex" - "errors" -) - -// HashType warps hash array inside struct -// someday can change to other hash algorithm simply -type HashType struct { - md5 [md5.Size]byte // MD5 -} - -func MakeHash(data []byte) HashType { - return HashType{md5.Sum(data)} -} - -func (h HashType) Equal(hash HashType) bool { - return h.md5 == hash.md5 -} - -func (h HashType) Bytes() []byte { - return h.md5[:] -} - -func (h HashType) String() string { - return hex.EncodeToString(h.Bytes()) -} - -func (h HashType) MarshalText() ([]byte, error) { - return []byte(h.String()), nil -} - -func (h *HashType) UnmarshalText(data []byte) error { - if hex.DecodedLen(len(data)) != md5.Size { - return errors.New("invalid hash length") - } - _, err := hex.Decode(h.md5[:], data) - return err -} - -func (h HashType) MarshalBinary() ([]byte, error) { - return h.md5[:], nil -} - -func (h *HashType) UnmarshalBinary(data []byte) error { - if len(data) != md5.Size { - return errors.New("invalid hash length") - } - copy(h.md5[:], data) - return nil -} - -func (h HashType) Len() int { - return len(h.md5) -} - -func (h HashType) IsValid() bool { - var zero HashType - return h != zero -} diff --git a/common/utils/manipulation.go b/common/utils/manipulation.go deleted file mode 100644 index 6c98a7295d..0000000000 --- a/common/utils/manipulation.go +++ /dev/null @@ -1,8 +0,0 @@ -package utils - -import "github.com/samber/lo" - -func EmptyOr[T comparable](v T, def T) T { - ret, _ := lo.Coalesce(v, def) - return ret -} diff --git a/common/utils/must.go b/common/utils/must.go deleted file mode 100644 index 2dd5ff5ee3..0000000000 --- a/common/utils/must.go +++ /dev/null @@ -1,8 +0,0 @@ -package utils - -func MustOK[T any](result T, ok bool) T { - if ok { - return result - } - panic("operation failed") -} diff --git a/common/utils/range.go b/common/utils/range.go deleted file mode 100644 index 7b4a235ca5..0000000000 --- a/common/utils/range.go +++ /dev/null @@ -1,44 +0,0 @@ -package utils - -import ( - "golang.org/x/exp/constraints" -) - -type Range[T constraints.Ordered] struct { - start T - end T -} - -func NewRange[T constraints.Ordered](start, end T) Range[T] { - if start > end { - return Range[T]{ - start: end, - end: start, - } - } - - return Range[T]{ - start: start, - end: end, - } -} - -func (r Range[T]) Contains(t T) bool { - return t >= r.start && t <= r.end -} - -func (r Range[T]) LeftContains(t T) bool { - return t >= r.start && t < r.end -} - -func (r Range[T]) RightContains(t T) bool { - return t > r.start && t <= r.end -} - -func (r Range[T]) Start() T { - return r.start -} - -func (r Range[T]) End() T { - return r.end -} diff --git a/common/utils/ranges.go b/common/utils/ranges.go deleted file mode 100644 index 21eacda534..0000000000 --- a/common/utils/ranges.go +++ /dev/null @@ -1,173 +0,0 @@ -package utils - -import ( - "errors" - "fmt" - "sort" - "strconv" - "strings" - - "golang.org/x/exp/constraints" -) - -type IntRanges[T constraints.Integer] []Range[T] - -var errIntRanges = errors.New("intRanges error") - -func newIntRanges[T constraints.Integer](expected string, parseFn func(string) (T, error)) (IntRanges[T], error) { - // example: 200 or 200/302 or 200-400 or 200/204/401-429/501-503 - expected = strings.TrimSpace(expected) - if len(expected) == 0 || expected == "*" { - return nil, nil - } - - // support: 200,302 or 200,204,401-429,501-503 - expected = strings.ReplaceAll(expected, ",", "/") - list := strings.Split(expected, "/") - if len(list) > 28 { - return nil, fmt.Errorf("%w, too many ranges to use, maximum support 28 ranges", errIntRanges) - } - - return newIntRangesFromList[T](list, parseFn) -} - -func newIntRangesFromList[T constraints.Integer](list []string, parseFn func(string) (T, error)) (IntRanges[T], error) { - var ranges IntRanges[T] - for _, s := range list { - if s == "" { - continue - } - - status := strings.Split(s, "-") - statusLen := len(status) - if statusLen > 2 { - return nil, errIntRanges - } - - start, err := parseFn(strings.Trim(status[0], "[ ]")) - if err != nil { - return nil, errIntRanges - } - - switch statusLen { - case 1: // Port range - ranges = append(ranges, NewRange(T(start), T(start))) - case 2: // Single port - end, err := parseFn(strings.Trim(status[1], "[ ]")) - if err != nil { - return nil, errIntRanges - } - - ranges = append(ranges, NewRange(T(start), T(end))) - } - } - - return ranges, nil -} - -func parseUnsigned[T constraints.Unsigned](s string) (T, error) { - if val, err := strconv.ParseUint(s, 10, 64); err == nil { - return T(val), nil - } else { - return 0, err - } -} - -func NewUnsignedRanges[T constraints.Unsigned](expected string) (IntRanges[T], error) { - return newIntRanges(expected, parseUnsigned[T]) -} - -func NewUnsignedRangesFromList[T constraints.Unsigned](list []string) (IntRanges[T], error) { - return newIntRangesFromList(list, parseUnsigned[T]) -} - -func parseSigned[T constraints.Signed](s string) (T, error) { - if val, err := strconv.ParseInt(s, 10, 64); err == nil { - return T(val), nil - } else { - return 0, err - } -} - -func NewSignedRanges[T constraints.Signed](expected string) (IntRanges[T], error) { - return newIntRanges(expected, parseSigned[T]) -} - -func NewSignedRangesFromList[T constraints.Signed](list []string) (IntRanges[T], error) { - return newIntRangesFromList(list, parseSigned[T]) -} - -func (ranges IntRanges[T]) Check(status T) bool { - if len(ranges) == 0 { - return true - } - - for _, segment := range ranges { - if segment.Contains(status) { - return true - } - } - - return false -} - -func (ranges IntRanges[T]) String() string { - if len(ranges) == 0 { - return "*" - } - - terms := make([]string, len(ranges)) - for i, r := range ranges { - start := r.Start() - end := r.End() - - var term string - if start == end { - term = strconv.Itoa(int(start)) - } else { - term = strconv.Itoa(int(start)) + "-" + strconv.Itoa(int(end)) - } - - terms[i] = term - } - - return strings.Join(terms, "/") -} - -func (ranges IntRanges[T]) Range(f func(t T) bool) { - if len(ranges) == 0 { - return - } - - for _, r := range ranges { - for i := r.Start(); i <= r.End() && i >= r.Start(); i++ { - if !f(i) { - return - } - if i+1 < i { // integer overflow - break - } - } - } -} - -func (ranges IntRanges[T]) Merge() (mergedRanges IntRanges[T]) { - if len(ranges) == 0 { - return - } - sort.Slice(ranges, func(i, j int) bool { - return ranges[i].Start() < ranges[j].Start() - }) - mergedRanges = ranges[:1] - var rangeIndex int - for _, r := range ranges[1:] { - if mergedRanges[rangeIndex].End()+1 > mergedRanges[rangeIndex].End() && // integer overflow - r.Start() > mergedRanges[rangeIndex].End()+1 { - mergedRanges = append(mergedRanges, r) - rangeIndex++ - } else if r.End() > mergedRanges[rangeIndex].End() { - mergedRanges[rangeIndex].end = r.End() - } - } - return -} diff --git a/common/utils/ranges_test.go b/common/utils/ranges_test.go deleted file mode 100644 index 3ae829d18a..0000000000 --- a/common/utils/ranges_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package utils - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestMergeRanges(t *testing.T) { - t.Parallel() - for _, testRange := range []struct { - ranges IntRanges[uint16] - expected IntRanges[uint16] - }{ - { - ranges: IntRanges[uint16]{ - NewRange[uint16](0, 1), - NewRange[uint16](1, 2), - }, - expected: IntRanges[uint16]{ - NewRange[uint16](0, 2), - }, - }, - { - ranges: IntRanges[uint16]{ - NewRange[uint16](0, 3), - NewRange[uint16](5, 7), - NewRange[uint16](8, 9), - NewRange[uint16](10, 10), - }, - expected: IntRanges[uint16]{ - NewRange[uint16](0, 3), - NewRange[uint16](5, 10), - }, - }, - { - ranges: IntRanges[uint16]{ - NewRange[uint16](1, 3), - NewRange[uint16](2, 6), - NewRange[uint16](8, 10), - NewRange[uint16](15, 18), - }, - expected: IntRanges[uint16]{ - NewRange[uint16](1, 6), - NewRange[uint16](8, 10), - NewRange[uint16](15, 18), - }, - }, - { - ranges: IntRanges[uint16]{ - NewRange[uint16](1, 3), - NewRange[uint16](2, 7), - NewRange[uint16](2, 6), - }, - expected: IntRanges[uint16]{ - NewRange[uint16](1, 7), - }, - }, - { - ranges: IntRanges[uint16]{ - NewRange[uint16](1, 3), - NewRange[uint16](2, 6), - NewRange[uint16](2, 7), - }, - expected: IntRanges[uint16]{ - NewRange[uint16](1, 7), - }, - }, - { - ranges: IntRanges[uint16]{ - NewRange[uint16](1, 3), - NewRange[uint16](2, 65535), - NewRange[uint16](2, 7), - NewRange[uint16](3, 16), - }, - expected: IntRanges[uint16]{ - NewRange[uint16](1, 65535), - }, - }, - } { - assert.Equal(t, testRange.expected, testRange.ranges.Merge()) - } -} diff --git a/common/utils/slice.go b/common/utils/slice.go deleted file mode 100644 index 1cd0334708..0000000000 --- a/common/utils/slice.go +++ /dev/null @@ -1,45 +0,0 @@ -package utils - -import ( - "errors" - "fmt" - "reflect" -) - -func Filter[T comparable](tSlice []T, filter func(t T) bool) []T { - result := make([]T, 0) - for _, t := range tSlice { - if filter(t) { - result = append(result, t) - } - } - return result -} - -func Map[T any, N any](arr []T, block func(it T) N) []N { - if arr == nil { // keep nil - return nil - } - retArr := make([]N, 0, len(arr)) - for index := range arr { - retArr = append(retArr, block(arr[index])) - } - return retArr -} - -func ToStringSlice(value any) ([]string, error) { - strArr := make([]string, 0) - switch reflect.TypeOf(value).Kind() { - case reflect.Slice, reflect.Array: - origin := reflect.ValueOf(value) - for i := 0; i < origin.Len(); i++ { - item := fmt.Sprintf("%v", origin.Index(i)) - strArr = append(strArr, item) - } - case reflect.String: - strArr = append(strArr, fmt.Sprintf("%v", value)) - default: - return nil, errors.New("value format error, must be string or array") - } - return strArr, nil -} diff --git a/common/utils/string_unsafe.go b/common/utils/string_unsafe.go deleted file mode 100644 index e427d29969..0000000000 --- a/common/utils/string_unsafe.go +++ /dev/null @@ -1,21 +0,0 @@ -package utils - -import "unsafe" - -// ImmutableBytesFromString is equivalent to []byte(s), except that it uses the -// same memory backing s instead of making a heap-allocated copy. This is only -// valid if the returned slice is never mutated. -func ImmutableBytesFromString(s string) []byte { - b := unsafe.StringData(s) - return unsafe.Slice(b, len(s)) -} - -// StringFromImmutableBytes is equivalent to string(bs), except that it uses -// the same memory backing bs instead of making a heap-allocated copy. This is -// only valid if bs is never mutated after StringFromImmutableBytes returns. -func StringFromImmutableBytes(bs []byte) string { - if len(bs) == 0 { - return "" - } - return unsafe.String(&bs[0], len(bs)) -} diff --git a/common/utils/strings.go b/common/utils/strings.go deleted file mode 100644 index 5d5ae59657..0000000000 --- a/common/utils/strings.go +++ /dev/null @@ -1,9 +0,0 @@ -package utils - -func Reverse(s string) string { - a := []rune(s) - for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { - a[i], a[j] = a[j], a[i] - } - return string(a) -} diff --git a/common/utils/uuid.go b/common/utils/uuid.go deleted file mode 100644 index d9a8b78948..0000000000 --- a/common/utils/uuid.go +++ /dev/null @@ -1,71 +0,0 @@ -package utils - -import ( - "github.com/gofrs/uuid/v5" - "github.com/metacubex/randv2" -) - -type unsafeRandReader struct{} - -func (r unsafeRandReader) Read(p []byte) (n int, err error) { - // modify from https://github.com/golang/go/blob/587c3847da81aa7cfc3b3db2677c8586c94df13a/src/runtime/rand.go#L70-L89 - // Inspired by wyrand. - n = len(p) - v := randv2.Uint64() - for len(p) > 0 { - v ^= 0xa0761d6478bd642f - v *= 0xe7037ed1a0b428db - size := 8 - if len(p) < 8 { - size = len(p) - } - for i := 0; i < size; i++ { - p[i] ^= byte(v >> (8 * i)) - } - p = p[size:] - v = v>>32 | v<<32 - } - - return -} - -var UnsafeRandReader = unsafeRandReader{} - -var UnsafeUUIDGenerator = uuid.NewGenWithOptions(uuid.WithRandomReader(UnsafeRandReader)) - -func NewUUIDV1() uuid.UUID { - u, _ := UnsafeUUIDGenerator.NewV1() // unsafeRandReader wouldn't cause error, so ignore err is safe - return u -} - -func NewUUIDV3(ns uuid.UUID, name string) uuid.UUID { - return UnsafeUUIDGenerator.NewV3(ns, name) -} - -func NewUUIDV4() uuid.UUID { - u, _ := UnsafeUUIDGenerator.NewV4() // unsafeRandReader wouldn't cause error, so ignore err is safe - return u -} - -func NewUUIDV5(ns uuid.UUID, name string) uuid.UUID { - return UnsafeUUIDGenerator.NewV5(ns, name) -} - -func NewUUIDV6() uuid.UUID { - u, _ := UnsafeUUIDGenerator.NewV6() // unsafeRandReader wouldn't cause error, so ignore err is safe - return u -} - -func NewUUIDV7() uuid.UUID { - u, _ := UnsafeUUIDGenerator.NewV7() // unsafeRandReader wouldn't cause error, so ignore err is safe - return u -} - -// UUIDMap https://github.com/XTLS/Xray-core/issues/158#issue-783294090 -func UUIDMap(str string) (uuid.UUID, error) { - u, err := uuid.FromString(str) - if err != nil { - return NewUUIDV5(uuid.Nil, str), nil - } - return u, nil -} diff --git a/common/utils/uuid_test.go b/common/utils/uuid_test.go deleted file mode 100644 index 3e0507d81d..0000000000 --- a/common/utils/uuid_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package utils - -import ( - "github.com/gofrs/uuid/v5" - "reflect" - "testing" -) - -func TestUUIDMap(t *testing.T) { - type args struct { - str string - } - - tests := []struct { - name string - args args - want uuid.UUID - wantErr bool - }{ - { - name: "uuid-test-1", - args: args{ - str: "82410302-039e-41b6-98b0-d964084b4170", - }, - want: uuid.FromStringOrNil("82410302-039e-41b6-98b0-d964084b4170"), - wantErr: false, - }, - { - name: "uuid-test-2", - args: args{ - str: "88c502e6-d7eb-4c8e-8259-94cb13d83c77", - }, - want: uuid.FromStringOrNil("88c502e6-d7eb-4c8e-8259-94cb13d83c77"), - wantErr: false, - }, - { - name: "uuid-map-1", - args: args{ - str: "123456", - }, - want: uuid.FromStringOrNil("f8598425-92f2-5508-a071-4fc67f9040ac"), - wantErr: false, - }, - // GENERATED BY 'xray uuid -i' - { - name: "uuid-map-2", - args: args{ - str: "a9dk23bz0", - }, - want: uuid.FromStringOrNil("c91481b6-fc0f-5d9e-b166-5ddf07b9c3c5"), - wantErr: false, - }, - { - name: "uuid-map-2", - args: args{ - str: "中文123", - }, - want: uuid.FromStringOrNil("145c544c-2229-59e5-8dbb-3f33b7610d26"), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := UUIDMap(tt.args.str) - if (err != nil) != tt.wantErr { - t.Errorf("UUIDMap() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("UUIDMap() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/component/auth/auth.go b/component/auth/auth.go deleted file mode 100644 index 176b21d793..0000000000 --- a/component/auth/auth.go +++ /dev/null @@ -1,43 +0,0 @@ -package auth - -type Authenticator interface { - Verify(user string, pass string) bool - Users() []string -} - -type AuthStore interface { - Authenticator() Authenticator - SetAuthenticator(Authenticator) -} - -type AuthUser struct { - User string - Pass string -} - -type inMemoryAuthenticator struct { - storage map[string]string - usernames []string -} - -func (au *inMemoryAuthenticator) Verify(user string, pass string) bool { - realPass, ok := au.storage[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: make(map[string]string), - usernames: make([]string, 0, len(users)), - } - for _, user := range users { - au.storage[user.User] = user.Pass - au.usernames = append(au.usernames, user.User) - } - return au -} diff --git a/component/ca/ca-certificates.crt b/component/ca/ca-certificates.crt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/component/ca/config.go b/component/ca/config.go deleted file mode 100644 index f4d3ae7510..0000000000 --- a/component/ca/config.go +++ /dev/null @@ -1,134 +0,0 @@ -package ca - -import ( - "crypto/tls" - "crypto/x509" - _ "embed" - "errors" - "fmt" - "os" - "strconv" - "sync" - - C "github.com/metacubex/mihomo/constant" -) - -var globalCertPool *x509.CertPool -var mutex sync.RWMutex -var errNotMatch = errors.New("certificate fingerprints do not match") - -//go:embed ca-certificates.crt -var _CaCertificates []byte -var DisableEmbedCa, _ = strconv.ParseBool(os.Getenv("DISABLE_EMBED_CA")) -var DisableSystemCa, _ = strconv.ParseBool(os.Getenv("DISABLE_SYSTEM_CA")) - -func AddCertificate(certificate string) error { - mutex.Lock() - defer mutex.Unlock() - - if certificate == "" { - return fmt.Errorf("certificate is empty") - } - - if globalCertPool == nil { - initializeCertPool() - } - - if globalCertPool.AppendCertsFromPEM([]byte(certificate)) { - return nil - } else if cert, err := x509.ParseCertificate([]byte(certificate)); err == nil { - globalCertPool.AddCert(cert) - return nil - } else { - return fmt.Errorf("add certificate failed") - } -} - -func initializeCertPool() { - var err error - if DisableSystemCa { - globalCertPool = x509.NewCertPool() - } else { - globalCertPool, err = x509.SystemCertPool() - if err != nil { - globalCertPool = x509.NewCertPool() - } - } - if !DisableEmbedCa { - globalCertPool.AppendCertsFromPEM(_CaCertificates) - } -} - -func ResetCertificate() { - mutex.Lock() - defer mutex.Unlock() - initializeCertPool() -} - -func getCertPool() *x509.CertPool { - if globalCertPool == nil { - mutex.Lock() - defer mutex.Unlock() - if globalCertPool != nil { - return globalCertPool - } - initializeCertPool() - } - return globalCertPool -} - -func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error) { - var certificate []byte - var err error - if len(customCA) > 0 { - path := C.Path.Resolve(customCA) - if !C.Path.IsSafePath(path) { - return nil, C.Path.ErrNotSafePath(path) - } - certificate, err = os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("load ca error: %w", err) - } - } else if customCAString != "" { - certificate = []byte(customCAString) - } - if len(certificate) > 0 { - certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM(certificate) { - return nil, fmt.Errorf("failed to parse certificate:\n\n %s", certificate) - } - return certPool, nil - } else { - return getCertPool(), nil - } -} - -// GetTLSConfig specified fingerprint, customCA and customCAString -func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, customCAString string) (_ *tls.Config, err error) { - if tlsConfig == nil { - tlsConfig = &tls.Config{} - } - tlsConfig.RootCAs, err = GetCertPool(customCA, customCAString) - if err != nil { - return nil, err - } - - if len(fingerprint) > 0 { - tlsConfig.VerifyPeerCertificate, err = NewFingerprintVerifier(fingerprint) - if err != nil { - return nil, err - } - tlsConfig.InsecureSkipVerify = true - } - return tlsConfig, nil -} - -// GetSpecifiedFingerprintTLSConfig specified fingerprint -func GetSpecifiedFingerprintTLSConfig(tlsConfig *tls.Config, fingerprint string) (*tls.Config, error) { - return GetTLSConfig(tlsConfig, fingerprint, "", "") -} - -func GetGlobalTLSConfig(tlsConfig *tls.Config) *tls.Config { - tlsConfig, _ = GetTLSConfig(tlsConfig, "", "", "") - return tlsConfig -} diff --git a/component/ca/fingerprint.go b/component/ca/fingerprint.go deleted file mode 100644 index 5c44a31f32..0000000000 --- a/component/ca/fingerprint.go +++ /dev/null @@ -1,44 +0,0 @@ -package ca - -import ( - "bytes" - "crypto/sha256" - "crypto/x509" - "encoding/hex" - "fmt" - "strings" -) - -// NewFingerprintVerifier returns a function that verifies whether a certificate's SHA-256 fingerprint matches the given one. -func NewFingerprintVerifier(fingerprint string) (func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error, error) { - switch fingerprint { - case "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random", "randomized": // WTF??? - return nil, fmt.Errorf("`fingerprint` is used for TLS certificate pinning. If you need to specify the browser fingerprint, use `client-fingerprint`") - } - fingerprint = strings.TrimSpace(strings.Replace(fingerprint, ":", "", -1)) - fpByte, err := hex.DecodeString(fingerprint) - if err != nil { - return nil, fmt.Errorf("fingerprint string decode error: %w", err) - } - - if len(fpByte) != 32 { - return nil, fmt.Errorf("fingerprint string length error,need sha256 fingerprint") - } - - return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - // ssl pining - for _, rawCert := range rawCerts { - hash := sha256.Sum256(rawCert) - if bytes.Equal(fpByte, hash[:]) { - return nil - } - } - return errNotMatch - }, nil -} - -// CalculateFingerprint computes the SHA-256 fingerprint of the given DER-encoded certificate and returns it as a hex string. -func CalculateFingerprint(certDER []byte) string { - hash := sha256.Sum256(certDER) - return hex.EncodeToString(hash[:]) -} diff --git a/component/ca/fix_windows.go b/component/ca/fix_windows.go deleted file mode 100644 index 42511791ef..0000000000 --- a/component/ca/fix_windows.go +++ /dev/null @@ -1,14 +0,0 @@ -package ca - -import ( - "github.com/metacubex/mihomo/constant/features" -) - -func init() { - // crypto/x509: certificate validation in Windows fails to validate IP in SAN - // https://github.com/golang/go/issues/37176 - // As far as I can tell this is still the case on most older versions of Windows (but seems to be fixed in 10) - if features.WindowsMajorVersion < 10 && len(_CaCertificates) > 0 { - DisableSystemCa = true - } -} diff --git a/component/ca/keypair.go b/component/ca/keypair.go deleted file mode 100644 index 7fa6c21feb..0000000000 --- a/component/ca/keypair.go +++ /dev/null @@ -1,101 +0,0 @@ -package ca - -import ( - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "fmt" - "math/big" -) - -type Path interface { - Resolve(path string) string - IsSafePath(path string) bool - ErrNotSafePath(path string) error -} - -// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution. -// Returns a tls.Certificate and an error, where the error indicates issues during parsing or file loading. -// If both certificate and privateKey are empty, generates a random TLS RSA key pair. -// Accepts a Path interface for resolving file paths when necessary. -func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate, error) { - if certificate == "" && privateKey == "" { - var err error - certificate, privateKey, _, err = NewRandomTLSKeyPair(KeyPairTypeRSA) - if err != nil { - return tls.Certificate{}, err - } - } - cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) - if painTextErr == nil { - return cert, nil - } - if path == nil { - return tls.Certificate{}, painTextErr - } - - certificate = path.Resolve(certificate) - privateKey = path.Resolve(privateKey) - var loadErr error - if !path.IsSafePath(certificate) { - loadErr = path.ErrNotSafePath(certificate) - } else if !path.IsSafePath(privateKey) { - loadErr = path.ErrNotSafePath(privateKey) - } else { - cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey) - } - if loadErr != nil { - return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) - } - return cert, nil -} - -type KeyPairType string - -const ( - KeyPairTypeRSA KeyPairType = "rsa" - KeyPairTypeP256 KeyPairType = "p256" - KeyPairTypeP384 KeyPairType = "p384" - KeyPairTypeEd25519 KeyPairType = "ed25519" -) - -// NewRandomTLSKeyPair generates a random TLS key pair based on the specified KeyPairType and returns it with a SHA256 fingerprint. -// Note: Most browsers do not support KeyPairTypeEd25519 type of certificate, and utls.UConn will also reject this type of certificate. -func NewRandomTLSKeyPair(keyPairType KeyPairType) (certificate string, privateKey string, fingerprint string, err error) { - var key crypto.Signer - switch keyPairType { - case KeyPairTypeRSA: - key, err = rsa.GenerateKey(rand.Reader, 2048) - case KeyPairTypeP256: - key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - case KeyPairTypeP384: - key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case KeyPairTypeEd25519: - _, key, err = ed25519.GenerateKey(rand.Reader) - default: // fallback to KeyPairTypeRSA - key, err = rsa.GenerateKey(rand.Reader, 2048) - } - if err != nil { - return - } - - template := x509.Certificate{SerialNumber: big.NewInt(1)} - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) - if err != nil { - return - } - privBytes, err := x509.MarshalPKCS8PrivateKey(key) - if err != nil { - return - } - fingerprint = CalculateFingerprint(certDER) - privateKey = string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})) - certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})) - return -} diff --git a/component/cidr/ipcidr_set.go b/component/cidr/ipcidr_set.go deleted file mode 100644 index 4bde96711e..0000000000 --- a/component/cidr/ipcidr_set.go +++ /dev/null @@ -1,90 +0,0 @@ -package cidr - -import ( - "fmt" - "net/netip" - "unsafe" - - "go4.org/netipx" -) - -type IpCidrSet struct { - // must same with netipx.IPSet - rr []netipx.IPRange -} - -func NewIpCidrSet() *IpCidrSet { - return &IpCidrSet{} -} - -func (set *IpCidrSet) AddIpCidrForString(ipCidr string) error { - prefix, err := netip.ParsePrefix(ipCidr) - if err != nil { - return err - } - return set.AddIpCidr(prefix) -} - -func (set *IpCidrSet) AddIpCidr(ipCidr netip.Prefix) (err error) { - if r := netipx.RangeOfPrefix(ipCidr); r.IsValid() { - set.rr = append(set.rr, r) - } else { - err = fmt.Errorf("not valid ipcidr range: %s", ipCidr) - } - return -} - -func (set *IpCidrSet) IsContainForString(ipString string) bool { - ip, err := netip.ParseAddr(ipString) - if err != nil { - return false - } - return set.IsContain(ip) -} - -func (set *IpCidrSet) IsContain(ip netip.Addr) bool { - return set.ToIPSet().Contains(ip.WithZone("")) -} - -// MatchIp implements C.IpMatcher -func (set *IpCidrSet) MatchIp(ip netip.Addr) bool { - if set.IsEmpty() { - return false - } - return set.IsContain(ip) -} - -func (set *IpCidrSet) Merge() error { - var b netipx.IPSetBuilder - b.AddSet(set.ToIPSet()) - i, err := b.IPSet() - if err != nil { - return err - } - set.fromIPSet(i) - return nil -} - -func (set *IpCidrSet) IsEmpty() bool { - return set == nil || len(set.rr) == 0 -} - -func (set *IpCidrSet) Foreach(f func(prefix netip.Prefix) bool) { - for _, r := range set.rr { - for _, prefix := range r.Prefixes() { - if !f(prefix) { - return - } - } - } -} - -// ToIPSet not safe convert to *netipx.IPSet -// be careful, must be used after Merge -func (set *IpCidrSet) ToIPSet() *netipx.IPSet { - return (*netipx.IPSet)(unsafe.Pointer(set)) -} - -func (set *IpCidrSet) fromIPSet(i *netipx.IPSet) { - *set = *(*IpCidrSet)(unsafe.Pointer(i)) -} diff --git a/component/cidr/ipcidr_set_bin.go b/component/cidr/ipcidr_set_bin.go deleted file mode 100644 index f6a0348856..0000000000 --- a/component/cidr/ipcidr_set_bin.go +++ /dev/null @@ -1,77 +0,0 @@ -package cidr - -import ( - "encoding/binary" - "errors" - "io" - "net/netip" - - "go4.org/netipx" -) - -func (ss *IpCidrSet) WriteBin(w io.Writer) (err error) { - // version - _, err = w.Write([]byte{1}) - if err != nil { - return err - } - - // rr - err = binary.Write(w, binary.BigEndian, int64(len(ss.rr))) - if err != nil { - return err - } - for _, r := range ss.rr { - err = binary.Write(w, binary.BigEndian, r.From().As16()) - if err != nil { - return err - } - err = binary.Write(w, binary.BigEndian, r.To().As16()) - if err != nil { - return err - } - } - - return nil -} - -func ReadIpCidrSet(r io.Reader) (ss *IpCidrSet, err error) { - // version - version := make([]byte, 1) - _, err = io.ReadFull(r, version) - if err != nil { - return nil, err - } - if version[0] != 1 { - return nil, errors.New("version is invalid") - } - - ss = NewIpCidrSet() - var length int64 - - // rr - err = binary.Read(r, binary.BigEndian, &length) - if err != nil { - return nil, err - } - if length < 1 { - return nil, errors.New("length is invalid") - } - ss.rr = make([]netipx.IPRange, length) - for i := int64(0); i < length; i++ { - var a16 [16]byte - err = binary.Read(r, binary.BigEndian, &a16) - if err != nil { - return nil, err - } - from := netip.AddrFrom16(a16).Unmap() - err = binary.Read(r, binary.BigEndian, &a16) - if err != nil { - return nil, err - } - to := netip.AddrFrom16(a16).Unmap() - ss.rr[i] = netipx.IPRangeFrom(from, to) - } - - return ss, nil -} diff --git a/component/cidr/ipcidr_set_test.go b/component/cidr/ipcidr_set_test.go deleted file mode 100644 index b229aa2b35..0000000000 --- a/component/cidr/ipcidr_set_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package cidr - -import ( - "testing" -) - -func TestIpv4(t *testing.T) { - tests := []struct { - name string - ipCidr string - ip string - expected bool - }{ - { - name: "Test Case 1", - ipCidr: "149.154.160.0/20", - ip: "149.154.160.0", - expected: true, - }, - { - name: "Test Case 2", - ipCidr: "192.168.0.0/16", - ip: "10.0.0.1", - expected: false, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - set := &IpCidrSet{} - set.AddIpCidrForString(test.ipCidr) - - result := set.IsContainForString(test.ip) - if result != test.expected { - t.Errorf("Expected result: %v, got: %v", test.expected, result) - } - }) - } -} - -func TestIpv6(t *testing.T) { - tests := []struct { - name string - ipCidr string - ip string - expected bool - }{ - { - name: "Test Case 1", - ipCidr: "2409:8000::/20", - ip: "2409:8087:1e03:21::27", - expected: true, - }, - { - name: "Test Case 2", - ipCidr: "240e::/16", - ip: "240e:964:ea02:100:1800::71", - expected: true, - }, - } - // Add more test cases as needed - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - set := &IpCidrSet{} - set.AddIpCidrForString(test.ipCidr) - - result := set.IsContainForString(test.ip) - if result != test.expected { - t.Errorf("Expected result: %v, got: %v", test.expected, result) - } - }) - } -} - -func TestMerge(t *testing.T) { - tests := []struct { - name string - ipCidr1 string - ipCidr2 string - ipCidr3 string - expectedLen int - }{ - { - name: "Test Case 1", - ipCidr1: "2409:8000::/20", - ipCidr2: "2409:8000::/21", - ipCidr3: "2409:8000::/48", - expectedLen: 1, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - set := &IpCidrSet{} - set.AddIpCidrForString(test.ipCidr1) - set.AddIpCidrForString(test.ipCidr2) - set.Merge() - - rangesLen := len(set.rr) - - if rangesLen != test.expectedLen { - t.Errorf("Expected len: %v, got: %v", test.expectedLen, rangesLen) - } - }) - } -} diff --git a/component/dhcp/conn.go b/component/dhcp/conn.go deleted file mode 100644 index 61c801bd3e..0000000000 --- a/component/dhcp/conn.go +++ /dev/null @@ -1,29 +0,0 @@ -package dhcp - -import ( - "context" - "net" - "net/netip" - "runtime" - - "github.com/metacubex/mihomo/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, netip.AddrPortFrom(netip.AddrFrom4([4]byte{255, 255, 255, 255}), 67), options...) -} diff --git a/component/dhcp/dhcp.go b/component/dhcp/dhcp.go deleted file mode 100644 index 50f91c39f9..0000000000 --- a/component/dhcp/dhcp.go +++ /dev/null @@ -1,99 +0,0 @@ -package dhcp - -import ( - "context" - "errors" - "net" - "net/netip" - - "github.com/metacubex/mihomo/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) ([]netip.Addr, error) { - conn, err := ListenDHCPClient(context, ifaceName) - if err != nil { - return nil, err - } - defer func() { - _ = conn.Close() - }() - - result := make(chan []netip.Addr, 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<- []netip.Addr) { - 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() - l := len(dns) - if l == 0 { - return - } - - results := make([]netip.Addr, 0, len(dns)) - for _, ip := range dns { - if addr, ok := netip.AddrFromSlice(ip); ok { - results = append(results, addr.Unmap()) - } - } - - result <- results - - return - } -} diff --git a/component/dialer/bind.go b/component/dialer/bind.go deleted file mode 100644 index de6aacd3bd..0000000000 --- a/component/dialer/bind.go +++ /dev/null @@ -1,102 +0,0 @@ -package dialer - -import ( - "net" - "net/netip" - "strconv" - "strings" - - "github.com/metacubex/mihomo/component/iface" -) - -func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination netip.Addr, port int) (net.Addr, error) { - ifaceObj, err := iface.ResolveInterface(ifaceName) - if err != nil { - return nil, err - } - destination = destination.Unmap() - - var addr netip.Prefix - switch network { - case "udp4", "tcp4": - addr, err = ifaceObj.PickIPv4Addr(destination) - case "tcp6", "udp6": - addr, err = ifaceObj.PickIPv6Addr(destination) - default: - if destination.IsValid() { - if destination.Is4() { - 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.Addr().AsSlice(), - Port: port, - }, nil - } else if strings.HasPrefix(network, "udp") { - return &net.UDPAddr{ - IP: addr.Addr().AsSlice(), - Port: port, - }, nil - } - - return nil, iface.ErrAddrNotFound -} - -func fallbackBindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) 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 := LookupLocalAddrFromIfaceName(ifaceName, network, destination, int(local)) - if err != nil { - return err - } - - dialer.LocalAddr = addr - - return nil -} - -func fallbackBindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string, rAddrPort netip.AddrPort) (string, error) { - _, port, err := net.SplitHostPort(address) - if err != nil { - port = "0" - } - - local, _ := strconv.ParseUint(port, 10, 16) - - addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, netip.Addr{}, int(local)) - if err != nil { - return "", err - } - - return addr.String(), nil -} - -func fallbackParseNetwork(network string, addr netip.Addr) string { - // fix fallbackBindIfaceToListenConfig() force bind to an ipv4 address - if !strings.HasSuffix(network, "4") && - !strings.HasSuffix(network, "6") && - addr.Unmap().Is6() { - network += "6" - } - return network -} diff --git a/component/dialer/bind_darwin.go b/component/dialer/bind_darwin.go deleted file mode 100644 index fdea24bfdc..0000000000 --- a/component/dialer/bind_darwin.go +++ /dev/null @@ -1,61 +0,0 @@ -package dialer - -import ( - "context" - "net" - "net/netip" - "syscall" - - "github.com/metacubex/mihomo/component/iface" - - "golang.org/x/sys/unix" -) - -func bindControl(ifaceIdx int) controlFn { - return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { - addrPort, err := netip.ParseAddrPort(address) - if err == nil && !addrPort.Addr().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, _ netip.Addr) error { - ifaceObj, err := iface.ResolveInterface(ifaceName) - if err != nil { - return err - } - - addControlToDialer(dialer, bindControl(ifaceObj.Index)) - return nil -} - -func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { - ifaceObj, err := iface.ResolveInterface(ifaceName) - if err != nil { - return "", err - } - - addControlToListenConfig(lc, bindControl(ifaceObj.Index)) - return address, nil -} - -func ParseNetwork(network string, addr netip.Addr) string { - return network -} diff --git a/component/dialer/bind_linux.go b/component/dialer/bind_linux.go deleted file mode 100644 index 79cf735b73..0000000000 --- a/component/dialer/bind_linux.go +++ /dev/null @@ -1,46 +0,0 @@ -package dialer - -import ( - "context" - "net" - "net/netip" - "syscall" - - "golang.org/x/sys/unix" -) - -func bindControl(ifaceName string) controlFn { - return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { - addrPort, err := netip.ParseAddrPort(address) - if err == nil && !addrPort.Addr().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, _ netip.Addr) error { - addControlToDialer(dialer, bindControl(ifaceName)) - - return nil -} - -func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { - addControlToListenConfig(lc, bindControl(ifaceName)) - - return address, nil -} - -func ParseNetwork(network string, addr netip.Addr) string { - return network -} diff --git a/component/dialer/bind_others.go b/component/dialer/bind_others.go deleted file mode 100644 index b7db962a6c..0000000000 --- a/component/dialer/bind_others.go +++ /dev/null @@ -1,20 +0,0 @@ -//go:build !linux && !darwin && !windows - -package dialer - -import ( - "net" - "net/netip" -) - -func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error { - return fallbackBindIfaceToDialer(ifaceName, dialer, network, destination) -} - -func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, network, address string, rAddrPort netip.AddrPort) (string, error) { - return fallbackBindIfaceToListenConfig(ifaceName, lc, network, address, rAddrPort) -} - -func ParseNetwork(network string, addr netip.Addr) string { - return fallbackParseNetwork(network, addr) -} diff --git a/component/dialer/bind_windows.go b/component/dialer/bind_windows.go deleted file mode 100644 index 98a076d049..0000000000 --- a/component/dialer/bind_windows.go +++ /dev/null @@ -1,101 +0,0 @@ -package dialer - -import ( - "context" - "encoding/binary" - "fmt" - "net" - "net/netip" - "syscall" - "unsafe" - - "github.com/metacubex/mihomo/component/iface" -) - -const ( - IP_UNICAST_IF = 31 - IPV6_UNICAST_IF = 31 -) - -func bind4(handle syscall.Handle, ifaceIdx int) error { - var bytes [4]byte - binary.BigEndian.PutUint32(bytes[:], uint32(ifaceIdx)) - idx := *(*uint32)(unsafe.Pointer(&bytes[0])) - err := syscall.SetsockoptInt(handle, syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx)) - if err != nil { - err = fmt.Errorf("bind4: %w", err) - } - return err -} - -func bind6(handle syscall.Handle, ifaceIdx int) error { - err := syscall.SetsockoptInt(handle, syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, ifaceIdx) - if err != nil { - err = fmt.Errorf("bind6: %w", err) - } - return err -} - -func bindControl(ifaceIdx int, rAddrPort netip.AddrPort) controlFn { - return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { - addrPort, err := netip.ParseAddrPort(address) - if err == nil && !addrPort.Addr().IsGlobalUnicast() { - return - } - - var innerErr error - err = c.Control(func(fd uintptr) { - handle := syscall.Handle(fd) - bind6err := bind6(handle, ifaceIdx) - bind4err := bind4(handle, ifaceIdx) - switch network { - case "ip6", "tcp6": - innerErr = bind6err - case "ip4", "tcp4", "udp4": - innerErr = bind4err - case "udp6": - // golang will set network to udp6 when listenUDP on wildcard ip (eg: ":0", "") - if (!addrPort.Addr().IsValid() || addrPort.Addr().IsUnspecified()) && bind6err != nil && rAddrPort.Addr().Unmap().Is4() { - // try bind ipv6, if failed, ignore. it's a workaround for windows disable interface ipv6 - if bind4err != nil { - innerErr = fmt.Errorf("%w (%s)", bind6err, bind4err) - } else { - innerErr = nil - } - } else { - innerErr = bind6err - } - } - }) - - if innerErr != nil { - err = innerErr - } - - return - } -} - -func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, destination netip.Addr) error { - ifaceObj, err := iface.ResolveInterface(ifaceName) - if err != nil { - return err - } - - addControlToDialer(dialer, bindControl(ifaceObj.Index, netip.AddrPortFrom(destination, 0))) - return nil -} - -func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { - ifaceObj, err := iface.ResolveInterface(ifaceName) - if err != nil { - return "", err - } - - addControlToListenConfig(lc, bindControl(ifaceObj.Index, rAddrPort)) - return address, nil -} - -func ParseNetwork(network string, addr netip.Addr) string { - return network -} diff --git a/component/dialer/control.go b/component/dialer/control.go deleted file mode 100644 index 18ed84e420..0000000000 --- a/component/dialer/control.go +++ /dev/null @@ -1,39 +0,0 @@ -package dialer - -import ( - "context" - "net" - "syscall" -) - -type controlFn = func(ctx context.Context, network, address string, c syscall.RawConn) error - -func addControlToListenConfig(lc *net.ListenConfig, fn controlFn) { - llc := *lc - lc.Control = func(network, address string, c syscall.RawConn) (err error) { - switch { - case llc.Control != nil: - if err = llc.Control(network, address, c); err != nil { - return - } - } - return fn(context.Background(), network, address, c) - } -} - -func addControlToDialer(d *net.Dialer, fn controlFn) { - ld := *d - d.ControlContext = func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { - switch { - case ld.ControlContext != nil: - if err = ld.ControlContext(ctx, network, address, c); err != nil { - return - } - case ld.Control != nil: - if err = ld.Control(network, address, c); err != nil { - return - } - } - return fn(ctx, network, address, c) - } -} diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go deleted file mode 100644 index d7e7072aa5..0000000000 --- a/component/dialer/dialer.go +++ /dev/null @@ -1,386 +0,0 @@ -package dialer - -import ( - "context" - "errors" - "fmt" - "net" - "net/netip" - "os" - "strings" - "sync" - "time" - - "github.com/metacubex/mihomo/component/keepalive" - "github.com/metacubex/mihomo/component/resolver" -) - -const ( - DefaultTCPTimeout = 5 * time.Second - DefaultUDPTimeout = DefaultTCPTimeout -) - -type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) - -var ( - dialMux sync.Mutex - actualSingleStackDialContext = serialSingleStackDialContext - actualDualStackDialContext = serialDualStackDialContext - tcpConcurrent = false - fallbackTimeout = 300 * time.Millisecond -) - -func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) { - opt := applyOptions(options...) - - if opt.network == 4 || opt.network == 6 { - if strings.Contains(network, "tcp") { - network = "tcp" - } else { - network = "udp" - } - - network = fmt.Sprintf("%s%d", network, opt.network) - } - - ips, port, err := parseAddr(ctx, network, address, opt.resolver) - if err != nil { - return nil, err - } - - switch network { - case "tcp4", "tcp6", "udp4", "udp6": - return actualSingleStackDialContext(ctx, network, ips, port, opt) - case "tcp", "udp": - return actualDualStackDialContext(ctx, network, ips, port, opt) - default: - return nil, ErrorInvalidedNetworkStack - } -} - -func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort, options ...Option) (net.PacketConn, error) { - opt := applyOptions(options...) - - lc := &net.ListenConfig{} - if opt.addrReuse { - addrReuseToListenConfig(lc) - } - if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA) - socketHookToListenConfig(lc) - } else { - if opt.interfaceName == "" { - opt.interfaceName = DefaultInterface.Load() - } - if opt.interfaceName == "" { - if finder := DefaultInterfaceFinder.Load(); finder != nil { - opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr()) - } - } - if rAddrPort.Addr().Unmap().IsLoopback() { - // avoid "The requested address is not valid in its context." - opt.interfaceName = "" - } - if opt.interfaceName != "" { - bind := bindIfaceToListenConfig - if opt.fallbackBind { - bind = fallbackBindIfaceToListenConfig - } - addr, err := bind(opt.interfaceName, lc, network, address, rAddrPort) - if err != nil { - return nil, err - } - address = addr - } - if opt.routingMark == 0 { - opt.routingMark = int(DefaultRoutingMark.Load()) - } - if opt.routingMark != 0 { - bindMarkToListenConfig(opt.routingMark, lc, network, address) - } - } - - return lc.ListenPacket(ctx, network, address) -} - -func SetTcpConcurrent(concurrent bool) { - dialMux.Lock() - defer dialMux.Unlock() - tcpConcurrent = concurrent - if concurrent { - actualSingleStackDialContext = concurrentSingleStackDialContext - actualDualStackDialContext = concurrentDualStackDialContext - } else { - actualSingleStackDialContext = serialSingleStackDialContext - actualDualStackDialContext = serialDualStackDialContext - } -} - -func GetTcpConcurrent() bool { - dialMux.Lock() - defer dialMux.Unlock() - return tcpConcurrent -} - -func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt option) (net.Conn, error) { - var address string - destination, port = resolver.LookupIP4P(destination, port) - address = net.JoinHostPort(destination.String(), port) - - netDialer := opt.netDialer - switch netDialer.(type) { - case nil: - netDialer = &net.Dialer{} - case *net.Dialer: - _netDialer := *netDialer.(*net.Dialer) - netDialer = &_netDialer // make a copy - default: - return netDialer.DialContext(ctx, network, address) - } - - dialer := netDialer.(*net.Dialer) - keepalive.SetNetDialer(dialer) - if opt.mpTcp { - setMultiPathTCP(dialer) - } - - if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA) - socketHookToToDialer(dialer) - } else { - if opt.interfaceName == "" { - opt.interfaceName = DefaultInterface.Load() - } - if opt.interfaceName == "" { - if finder := DefaultInterfaceFinder.Load(); finder != nil { - opt.interfaceName = finder.FindInterfaceName(destination) - } - } - if opt.interfaceName != "" { - bind := bindIfaceToDialer - if opt.fallbackBind { - bind = fallbackBindIfaceToDialer - } - if err := bind(opt.interfaceName, dialer, network, destination); err != nil { - return nil, err - } - } - if opt.routingMark == 0 { - opt.routingMark = int(DefaultRoutingMark.Load()) - } - if opt.routingMark != 0 { - bindMarkToDialer(opt.routingMark, dialer, network, destination) - } - if opt.tfo && !DisableTFO { - return dialTFO(ctx, *dialer, network, address) - } - } - - return dialer.DialContext(ctx, network, address) -} - -func serialSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { - return serialDialContext(ctx, network, ips, port, opt) -} - -func serialDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { - return dualStackDialContext(ctx, serialDialContext, network, ips, port, opt) -} - -func concurrentSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { - return parallelDialContext(ctx, network, ips, port, opt) -} - -func concurrentDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { - if opt.prefer != 4 && opt.prefer != 6 { - return parallelDialContext(ctx, network, ips, port, opt) - } - return dualStackDialContext(ctx, parallelDialContext, network, ips, port, opt) -} - -func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { - ipv4s, ipv6s := resolver.SortationAddr(ips) - if len(ipv4s) == 0 && len(ipv6s) == 0 { - return nil, ErrorNoIpAddress - } - - preferIPVersion := opt.prefer - fallbackTicker := time.NewTicker(fallbackTimeout) - defer fallbackTicker.Stop() - - results := make(chan dialResult) - returned := make(chan struct{}) - defer close(returned) - - var wg sync.WaitGroup - - racer := func(ips []netip.Addr, isPrimary bool) { - defer wg.Done() - result := dialResult{isPrimary: isPrimary} - defer func() { - select { - case results <- result: - case <-returned: - if result.Conn != nil && result.error == nil { - _ = result.Conn.Close() - } - } - }() - result.Conn, result.error = dialFn(ctx, network, ips, port, opt) - } - - if len(ipv4s) != 0 { - wg.Add(1) - go racer(ipv4s, preferIPVersion != 6) - } - - if len(ipv6s) != 0 { - wg.Add(1) - go racer(ipv6s, preferIPVersion != 4) - } - - go func() { - wg.Wait() - close(results) - }() - - var fallback dialResult - var errs []error - -loop: - for { - select { - case <-fallbackTicker.C: - if fallback.error == nil && fallback.Conn != nil { - return fallback.Conn, nil - } - case res, ok := <-results: - if !ok { - break loop - } - if res.error == nil { - if res.isPrimary { - return res.Conn, nil - } - fallback = res - } else { - if res.isPrimary { - errs = append([]error{fmt.Errorf("connect failed: %w", res.error)}, errs...) - } else { - errs = append(errs, fmt.Errorf("connect failed: %w", res.error)) - } - } - } - } - - if fallback.error == nil && fallback.Conn != nil { - return fallback.Conn, nil - } - return nil, errors.Join(errs...) -} - -func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { - if len(ips) == 0 { - return nil, ErrorNoIpAddress - } - results := make(chan dialResult) - returned := make(chan struct{}) - defer close(returned) - racer := func(ctx context.Context, ip netip.Addr) { - result := dialResult{isPrimary: true, ip: ip} - defer func() { - select { - case results <- result: - case <-returned: - if result.Conn != nil && result.error == nil { - _ = result.Conn.Close() - } - } - }() - result.Conn, result.error = dialContext(ctx, network, ip, port, opt) - } - - for _, ip := range ips { - go racer(ctx, ip) - } - var errs []error - for i := 0; i < len(ips); i++ { - res := <-results - if res.error == nil { - return res.Conn, nil - } - errs = append(errs, res.error) - } - - if len(errs) > 0 { - return nil, errors.Join(errs...) - } - return nil, os.ErrDeadlineExceeded -} - -func serialDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { - if len(ips) == 0 { - return nil, ErrorNoIpAddress - } - var errs []error - for _, ip := range ips { - if conn, err := dialContext(ctx, network, ip, port, opt); err == nil { - return conn, nil - } else { - errs = append(errs, err) - } - } - return nil, errors.Join(errs...) -} - -type dialResult struct { - ip netip.Addr - net.Conn - error - isPrimary bool -} - -func parseAddr(ctx context.Context, network, address string, preferResolver resolver.Resolver) ([]netip.Addr, string, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, "-1", err - } - - if preferResolver == nil { - preferResolver = resolver.ProxyServerHostResolver - } - - var ips []netip.Addr - switch network { - case "tcp4", "udp4": - ips, err = resolver.LookupIPv4WithResolver(ctx, host, preferResolver) - case "tcp6", "udp6": - ips, err = resolver.LookupIPv6WithResolver(ctx, host, preferResolver) - default: - ips, err = resolver.LookupIPWithResolver(ctx, host, preferResolver) - } - if err != nil { - return nil, "-1", fmt.Errorf("dns resolve failed: %w", err) - } - for i, ip := range ips { - if ip.Is4In6() { - ips[i] = ip.Unmap() - } - } - return ips, port, nil -} - -type Dialer struct { - Opt option -} - -func (d Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - return DialContext(ctx, network, address, WithOption(d.Opt)) -} - -func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { - return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, rAddrPort, WithOption(d.Opt)) -} - -func NewDialer(options ...Option) Dialer { - opt := applyOptions(options...) - return Dialer{Opt: opt} -} diff --git a/component/dialer/error.go b/component/dialer/error.go deleted file mode 100644 index 035baa0323..0000000000 --- a/component/dialer/error.go +++ /dev/null @@ -1,10 +0,0 @@ -package dialer - -import ( - "errors" -) - -var ( - ErrorNoIpAddress = errors.New("no ip address") - ErrorInvalidedNetworkStack = errors.New("invalided network stack") -) diff --git a/component/dialer/mark_linux.go b/component/dialer/mark_linux.go deleted file mode 100644 index 996c3dea65..0000000000 --- a/component/dialer/mark_linux.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build linux - -package dialer - -import ( - "context" - "net" - "net/netip" - "syscall" -) - -func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) { - addControlToDialer(dialer, bindMarkToControl(mark)) -} - -func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) { - addControlToListenConfig(lc, bindMarkToControl(mark)) -} - -func bindMarkToControl(mark int) controlFn { - return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { - - addrPort, err := netip.ParseAddrPort(address) - if err == nil && !addrPort.Addr().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 deleted file mode 100644 index 64e5878429..0000000000 --- a/component/dialer/mark_nonlinux.go +++ /dev/null @@ -1,27 +0,0 @@ -//go:build !linux - -package dialer - -import ( - "net" - "net/netip" - "sync" - - "github.com/metacubex/mihomo/log" -) - -var printMarkWarnOnce sync.Once - -func printMarkWarn() { - printMarkWarnOnce.Do(func() { - log.Warnln("Routing mark on socket is not supported on current platform") - }) -} - -func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) { - printMarkWarn() -} - -func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) { - printMarkWarn() -} diff --git a/component/dialer/mptcp_go120.go b/component/dialer/mptcp_go120.go deleted file mode 100644 index 6e564673b4..0000000000 --- a/component/dialer/mptcp_go120.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !go1.21 - -package dialer - -import ( - "net" -) - -const multipathTCPAvailable = false - -func setMultiPathTCP(dialer *net.Dialer) { -} diff --git a/component/dialer/mptcp_go121.go b/component/dialer/mptcp_go121.go deleted file mode 100644 index 360826c88d..0000000000 --- a/component/dialer/mptcp_go121.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build go1.21 - -package dialer - -import "net" - -const multipathTCPAvailable = true - -func setMultiPathTCP(dialer *net.Dialer) { - dialer.SetMultipathTCP(true) -} diff --git a/component/dialer/options.go b/component/dialer/options.go deleted file mode 100644 index bb978cdba6..0000000000 --- a/component/dialer/options.go +++ /dev/null @@ -1,128 +0,0 @@ -package dialer - -import ( - "context" - "net" - "net/netip" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/component/resolver" -) - -var ( - DefaultInterface = atomic.NewTypedValue[string]("") - DefaultRoutingMark = atomic.NewInt32(0) - - DefaultInterfaceFinder = atomic.NewTypedValue[InterfaceFinder](nil) -) - -type InterfaceFinder interface { - FindInterfaceName(destination netip.Addr) string -} - -type NetDialer interface { - DialContext(ctx context.Context, network, address string) (net.Conn, error) -} - -type option struct { - interfaceName string - fallbackBind bool - addrReuse bool - routingMark int - network int - prefer int - tfo bool - mpTcp bool - resolver resolver.Resolver - netDialer NetDialer -} - -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 - } -} - -func WithResolver(r resolver.Resolver) Option { - return func(opt *option) { - opt.resolver = r - } -} - -func WithPreferIPv4() Option { - return func(opt *option) { - opt.prefer = 4 - } -} - -func WithPreferIPv6() Option { - return func(opt *option) { - opt.prefer = 6 - } -} - -func WithOnlySingleStack(isIPv4 bool) Option { - return func(opt *option) { - if isIPv4 { - opt.network = 4 - } else { - opt.network = 6 - } - } -} - -func WithTFO(tfo bool) Option { - return func(opt *option) { - opt.tfo = tfo - } -} - -func WithMPTCP(mpTcp bool) Option { - return func(opt *option) { - opt.mpTcp = mpTcp - } -} - -func WithNetDialer(netDialer NetDialer) Option { - return func(opt *option) { - opt.netDialer = netDialer - } -} - -func WithOption(o option) Option { - return func(opt *option) { - *opt = o - } -} - -func IsZeroOptions(opts []Option) bool { - return applyOptions(opts...) == option{} -} - -func applyOptions(options ...Option) option { - opt := option{} - for _, o := range options { - o(&opt) - } - return opt -} diff --git a/component/dialer/reuse.go b/component/dialer/reuse.go deleted file mode 100644 index 4a46e572fd..0000000000 --- a/component/dialer/reuse.go +++ /dev/null @@ -1,15 +0,0 @@ -package dialer - -import ( - "context" - "net" - "syscall" - - "github.com/metacubex/mihomo/common/sockopt" -) - -func addrReuseToListenConfig(lc *net.ListenConfig) { - addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error { - return sockopt.RawConnReuseaddr(c) - }) -} diff --git a/component/dialer/socket_hook.go b/component/dialer/socket_hook.go deleted file mode 100644 index f860552579..0000000000 --- a/component/dialer/socket_hook.go +++ /dev/null @@ -1,27 +0,0 @@ -package dialer - -import ( - "context" - "net" - "syscall" -) - -// SocketControl -// never change type traits because it's used in CMFA -type SocketControl func(network, address string, conn syscall.RawConn) error - -// DefaultSocketHook -// never change type traits because it's used in CMFA -var DefaultSocketHook SocketControl - -func socketHookToToDialer(dialer *net.Dialer) { - addControlToDialer(dialer, func(ctx context.Context, network, address string, c syscall.RawConn) error { - return DefaultSocketHook(network, address, c) - }) -} - -func socketHookToListenConfig(lc *net.ListenConfig) { - addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error { - return DefaultSocketHook(network, address, c) - }) -} diff --git a/component/dialer/tfo.go b/component/dialer/tfo.go deleted file mode 100644 index 76fe94d021..0000000000 --- a/component/dialer/tfo.go +++ /dev/null @@ -1,139 +0,0 @@ -package dialer - -import ( - "context" - "io" - "net" - "time" - - "github.com/metacubex/tfo-go" -) - -var DisableTFO = false - -type tfoConn struct { - net.Conn - closed bool - dialed chan bool - cancel context.CancelFunc - ctx context.Context - dialFn func(ctx context.Context, earlyData []byte) (net.Conn, error) -} - -func (c *tfoConn) Dial(earlyData []byte) (err error) { - conn, err := c.dialFn(c.ctx, earlyData) - if err != nil { - return - } - c.Conn = conn - c.dialed <- true - return err -} - -func (c *tfoConn) Read(b []byte) (n int, err error) { - if c.closed { - return 0, io.ErrClosedPipe - } - if c.Conn == nil { - select { - case <-c.ctx.Done(): - return 0, io.ErrUnexpectedEOF - case <-c.dialed: - } - } - return c.Conn.Read(b) -} - -func (c *tfoConn) Write(b []byte) (n int, err error) { - if c.closed { - return 0, io.ErrClosedPipe - } - if c.Conn == nil { - if err := c.Dial(b); err != nil { - return 0, err - } - return len(b), nil - } - - return c.Conn.Write(b) -} - -func (c *tfoConn) Close() error { - c.closed = true - c.cancel() - if c.Conn == nil { - return nil - } - return c.Conn.Close() -} - -func (c *tfoConn) LocalAddr() net.Addr { - if c.Conn == nil { - return &net.TCPAddr{} - } - return c.Conn.LocalAddr() -} - -func (c *tfoConn) RemoteAddr() net.Addr { - if c.Conn == nil { - return &net.TCPAddr{} - } - return c.Conn.RemoteAddr() -} - -func (c *tfoConn) SetDeadline(t time.Time) error { - if err := c.SetReadDeadline(t); err != nil { - return err - } - return c.SetWriteDeadline(t) -} - -func (c *tfoConn) SetReadDeadline(t time.Time) error { - if c.Conn == nil { - return nil - } - return c.Conn.SetReadDeadline(t) -} - -func (c *tfoConn) SetWriteDeadline(t time.Time) error { - if c.Conn == nil { - return nil - } - return c.Conn.SetWriteDeadline(t) -} - -func (c *tfoConn) Upstream() any { - if c.Conn == nil { // ensure return a nil interface not an interface with nil value - return nil - } - return c.Conn -} - -func (c *tfoConn) NeedAdditionalReadDeadline() bool { - return c.Conn == nil -} - -func (c *tfoConn) NeedHandshake() bool { - return c.Conn == nil -} - -func (c *tfoConn) ReaderReplaceable() bool { - return c.Conn != nil -} - -func (c *tfoConn) WriterReplaceable() bool { - return c.Conn != nil -} - -func dialTFO(ctx context.Context, netDialer net.Dialer, network, address string) (net.Conn, error) { - ctx, cancel := context.WithTimeout(context.Background(), DefaultTCPTimeout) - dialer := tfo.Dialer{Dialer: netDialer, DisableTFO: false} - return &tfoConn{ - dialed: make(chan bool, 1), - cancel: cancel, - ctx: ctx, - dialFn: func(ctx context.Context, earlyData []byte) (net.Conn, error) { - return dialer.DialContext(ctx, network, address, earlyData) - }, - }, nil -} diff --git a/component/dialer/tfo_windows.go b/component/dialer/tfo_windows.go deleted file mode 100644 index 632661186c..0000000000 --- a/component/dialer/tfo_windows.go +++ /dev/null @@ -1,11 +0,0 @@ -package dialer - -import "github.com/metacubex/mihomo/constant/features" - -func init() { - // According to MSDN, this option is available since Windows 10, 1607 - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms738596(v=vs.85).aspx - if features.WindowsMajorVersion < 10 || (features.WindowsMajorVersion == 10 && features.WindowsBuildNumber < 14393) { - DisableTFO = true - } -} diff --git a/component/fakeip/cachefile.go b/component/fakeip/cachefile.go deleted file mode 100644 index 92d09721d8..0000000000 --- a/component/fakeip/cachefile.go +++ /dev/null @@ -1,55 +0,0 @@ -package fakeip - -import ( - "net/netip" - - "github.com/metacubex/mihomo/component/profile/cachefile" -) - -type cachefileStore struct { - cache *cachefile.FakeIpStore -} - -// GetByHost implements store.GetByHost -func (c *cachefileStore) GetByHost(host string) (netip.Addr, bool) { - return c.cache.GetByHost(host) -} - -// PutByHost implements store.PutByHost -func (c *cachefileStore) PutByHost(host string, ip netip.Addr) { - c.cache.PutByHost(host, ip) -} - -// GetByIP implements store.GetByIP -func (c *cachefileStore) GetByIP(ip netip.Addr) (string, bool) { - return c.cache.GetByIP(ip) -} - -// PutByIP implements store.PutByIP -func (c *cachefileStore) PutByIP(ip netip.Addr, host string) { - c.cache.PutByIP(ip, host) -} - -// DelByIP implements store.DelByIP -func (c *cachefileStore) DelByIP(ip netip.Addr) { - c.cache.DelByIP(ip) -} - -// Exist implements store.Exist -func (c *cachefileStore) Exist(ip netip.Addr) bool { - _, exist := c.GetByIP(ip) - return exist -} - -// CloneTo implements store.CloneTo -// already persistence -func (c *cachefileStore) CloneTo(store store) {} - -// FlushFakeIP implements store.FlushFakeIP -func (c *cachefileStore) FlushFakeIP() error { - return c.cache.FlushFakeIP() -} - -func newCachefileStore(cache *cachefile.CacheFile) *cachefileStore { - return &cachefileStore{cache.FakeIpStore()} -} diff --git a/component/fakeip/memory.go b/component/fakeip/memory.go deleted file mode 100644 index 0a8492f8aa..0000000000 --- a/component/fakeip/memory.go +++ /dev/null @@ -1,80 +0,0 @@ -package fakeip - -import ( - "net/netip" - - "github.com/metacubex/mihomo/common/lru" -) - -type memoryStore struct { - cacheIP *lru.LruCache[string, netip.Addr] - cacheHost *lru.LruCache[netip.Addr, string] -} - -// GetByHost implements store.GetByHost -func (m *memoryStore) GetByHost(host string) (netip.Addr, bool) { - if ip, exist := m.cacheIP.Get(host); exist { - // ensure ip --> host on head of linked list - m.cacheHost.Get(ip) - return ip, true - } - - return netip.Addr{}, false -} - -// PutByHost implements store.PutByHost -func (m *memoryStore) PutByHost(host string, ip netip.Addr) { - m.cacheIP.Set(host, ip) -} - -// GetByIP implements store.GetByIP -func (m *memoryStore) GetByIP(ip netip.Addr) (string, bool) { - if host, exist := m.cacheHost.Get(ip); exist { - // ensure host --> ip on head of linked list - m.cacheIP.Get(host) - return host, true - } - - return "", false -} - -// PutByIP implements store.PutByIP -func (m *memoryStore) PutByIP(ip netip.Addr, host string) { - m.cacheHost.Set(ip, host) -} - -// DelByIP implements store.DelByIP -func (m *memoryStore) DelByIP(ip netip.Addr) { - if host, exist := m.cacheHost.Get(ip); exist { - m.cacheIP.Delete(host) - } - m.cacheHost.Delete(ip) -} - -// Exist implements store.Exist -func (m *memoryStore) Exist(ip netip.Addr) bool { - return m.cacheHost.Exist(ip) -} - -// CloneTo implements store.CloneTo -// only for memoryStore to memoryStore -func (m *memoryStore) CloneTo(store store) { - if ms, ok := store.(*memoryStore); ok { - m.cacheIP.CloneTo(ms.cacheIP) - m.cacheHost.CloneTo(ms.cacheHost) - } -} - -// FlushFakeIP implements store.FlushFakeIP -func (m *memoryStore) FlushFakeIP() error { - m.cacheIP.Clear() - m.cacheHost.Clear() - return nil -} - -func newMemoryStore(size int) *memoryStore { - return &memoryStore{ - cacheIP: lru.New[string, netip.Addr](lru.WithSize[string, netip.Addr](size)), - cacheHost: lru.New[netip.Addr, string](lru.WithSize[netip.Addr, string](size)), - } -} diff --git a/component/fakeip/pool.go b/component/fakeip/pool.go deleted file mode 100644 index 895fb6587d..0000000000 --- a/component/fakeip/pool.go +++ /dev/null @@ -1,213 +0,0 @@ -package fakeip - -import ( - "errors" - "net/netip" - "strings" - "sync" - - "github.com/metacubex/mihomo/component/profile/cachefile" - C "github.com/metacubex/mihomo/constant" - - "go4.org/netipx" -) - -const ( - offsetKey = "key-offset-fake-ip" - cycleKey = "key-cycle-fake-ip" -) - -type store interface { - GetByHost(host string) (netip.Addr, bool) - PutByHost(host string, ip netip.Addr) - GetByIP(ip netip.Addr) (string, bool) - PutByIP(ip netip.Addr, host string) - DelByIP(ip netip.Addr) - Exist(ip netip.Addr) bool - CloneTo(store) - FlushFakeIP() error -} - -// Pool is an implementation about fake ip generator without storage -type Pool struct { - gateway netip.Addr - first netip.Addr - last netip.Addr - offset netip.Addr - cycle bool - mux sync.Mutex - host []C.DomainMatcher - mode C.FilterMode - ipnet netip.Prefix - store store -} - -// Lookup return a fake ip with host -func (p *Pool) Lookup(host string) netip.Addr { - 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 netip.Addr) (string, bool) { - p.mux.Lock() - defer p.mux.Unlock() - - return p.store.GetByIP(ip) -} - -// ShouldSkipped return if domain should be skipped -func (p *Pool) ShouldSkipped(domain string) bool { - should := p.shouldSkipped(domain) - if p.mode == C.FilterWhiteList { - return !should - } - return should -} - -func (p *Pool) shouldSkipped(domain string) bool { - for _, matcher := range p.host { - if matcher.MatchDomain(domain) { - return true - } - } - return false -} - -// Exist returns if given ip exists in fake-ip pool -func (p *Pool) Exist(ip netip.Addr) bool { - p.mux.Lock() - defer p.mux.Unlock() - - return p.store.Exist(ip) -} - -// Gateway return gateway ip -func (p *Pool) Gateway() netip.Addr { - return p.gateway -} - -// Broadcast return the last ip -func (p *Pool) Broadcast() netip.Addr { - return p.last -} - -// IPNet return raw ipnet -func (p *Pool) IPNet() netip.Prefix { - 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) netip.Addr { - p.offset = p.offset.Next() - - if !p.offset.Less(p.last) { - p.cycle = true - p.offset = p.first - } - - if p.cycle || p.store.Exist(p.offset) { - p.store.DelByIP(p.offset) - } - - p.store.PutByIP(p.offset, host) - return p.offset -} - -func (p *Pool) FlushFakeIP() error { - err := p.store.FlushFakeIP() - if err == nil { - p.cycle = false - p.offset = p.first.Prev() - } - return err -} - -func (p *Pool) StoreState() { - if s, ok := p.store.(*cachefileStore); ok { - s.PutByHost(offsetKey, p.offset) - if p.cycle { - s.PutByHost(cycleKey, p.offset) - } - } -} - -func (p *Pool) restoreState() { - if s, ok := p.store.(*cachefileStore); ok { - if _, exist := s.GetByHost(cycleKey); exist { - p.cycle = true - } - - if offset, exist := s.GetByHost(offsetKey); exist { - if p.ipnet.Contains(offset) { - p.offset = offset - } else { - _ = p.FlushFakeIP() - } - } else if s.Exist(p.first) { - _ = p.FlushFakeIP() - } - } -} - -type Options struct { - IPNet netip.Prefix - Host []C.DomainMatcher - Mode C.FilterMode - - // 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) { - var ( - hostAddr = options.IPNet.Masked().Addr() - gateway = hostAddr.Next() - first = gateway.Next().Next().Next() // default start with 198.18.0.4 - last = netipx.PrefixLastIP(options.IPNet) - ) - - if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) { - return nil, errors.New("ipnet don't have valid ip") - } - - pool := &Pool{ - gateway: gateway, - first: first, - last: last, - offset: first.Prev(), - cycle: false, - host: options.Host, - mode: options.Mode, - ipnet: options.IPNet, - } - if options.Persistence { - pool.store = newCachefileStore(cachefile.Cache()) - } else { - pool.store = newMemoryStore(options.Size) - } - - pool.restoreState() - - return pool, nil -} diff --git a/component/fakeip/pool_test.go b/component/fakeip/pool_test.go deleted file mode 100644 index be78b87c71..0000000000 --- a/component/fakeip/pool_test.go +++ /dev/null @@ -1,326 +0,0 @@ -package fakeip - -import ( - "fmt" - "net/netip" - "os" - "testing" - "time" - - "github.com/metacubex/mihomo/component/profile/cachefile" - "github.com/metacubex/mihomo/component/trie" - C "github.com/metacubex/mihomo/constant" - - "github.com/metacubex/bbolt" - "github.com/stretchr/testify/assert" -) - -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("", "mihomo") - 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 = newCachefileStore(&cachefile.CacheFile{DB: db}) - return pool, f.Name(), nil -} - -func TestPool_Basic(t *testing.T) { - ipnet := netip.MustParsePrefix("192.168.0.0/28") - 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.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4})) - assert.Equal(t, pool.Lookup("foo.com"), netip.AddrFrom4([4]byte{192, 168, 0, 4})) - assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5})) - assert.True(t, exist) - assert.Equal(t, bar, "bar.com") - assert.Equal(t, pool.Gateway(), netip.AddrFrom4([4]byte{192, 168, 0, 1})) - assert.Equal(t, pool.Broadcast(), netip.AddrFrom4([4]byte{192, 168, 0, 15})) - assert.Equal(t, pool.IPNet().String(), ipnet.String()) - assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5}))) - assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 6}))) - assert.False(t, pool.Exist(netip.MustParseAddr("::1"))) - } -} - -func TestPool_BasicV6(t *testing.T) { - ipnet := netip.MustParsePrefix("2001:4860:4860::8888/118") - pools, tempfile, err := createPools(Options{ - IPNet: ipnet, - Size: 10, - }) - assert.Nil(t, err) - defer os.Remove(tempfile) - - for _, pool := range pools { - first := pool.Lookup("foo.com") - last := pool.Lookup("bar.com") - bar, exist := pool.LookBack(last) - - assert.Equal(t, first, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) - assert.Equal(t, pool.Lookup("foo.com"), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) - assert.Equal(t, last, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")) - assert.True(t, exist) - assert.Equal(t, bar, "bar.com") - assert.Equal(t, pool.Gateway(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801")) - assert.Equal(t, pool.Broadcast(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff")) - assert.Equal(t, pool.IPNet().String(), ipnet.String()) - assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805"))) - assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8806"))) - assert.False(t, pool.Exist(netip.MustParseAddr("127.0.0.1"))) - } -} - -func TestPool_Case_Insensitive(t *testing.T) { - ipnet := netip.MustParsePrefix("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.Equal(t, first, 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 := netip.MustParsePrefix("192.168.0.16/28") - pools, tempfile, err := createPools(Options{ - IPNet: ipnet, - Size: 10, - }) - assert.Nil(t, err) - defer os.Remove(tempfile) - - for _, pool := range pools { - foo := pool.Lookup("foo.com") - bar := pool.Lookup("bar.com") - for i := 0; i < 9; i++ { - pool.Lookup(fmt.Sprintf("%d.com", i)) - } - baz := pool.Lookup("baz.com") - next := pool.Lookup("foo.com") - assert.Equal(t, foo, baz) - assert.Equal(t, next, bar) - } -} - -func TestPool_Skip(t *testing.T) { - ipnet := netip.MustParsePrefix("192.168.0.1/29") - tree := trie.New[struct{}]() - assert.NoError(t, tree.Insert("example.com", struct{}{})) - assert.False(t, tree.IsEmpty()) - pools, tempfile, err := createPools(Options{ - IPNet: ipnet, - Size: 10, - Host: []C.DomainMatcher{tree.NewDomainSet()}, - }) - 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")) - assert.False(t, pool.shouldSkipped("baz.com")) - } -} - -func TestPool_SkipWhiteList(t *testing.T) { - ipnet := netip.MustParsePrefix("192.168.0.1/29") - tree := trie.New[struct{}]() - assert.NoError(t, tree.Insert("example.com", struct{}{})) - assert.False(t, tree.IsEmpty()) - pools, tempfile, err := createPools(Options{ - IPNet: ipnet, - Size: 10, - Host: []C.DomainMatcher{tree.NewDomainSet()}, - Mode: C.FilterWhiteList, - }) - assert.Nil(t, err) - defer os.Remove(tempfile) - - for _, pool := range pools { - assert.False(t, pool.ShouldSkipped("example.com")) - assert.True(t, pool.ShouldSkipped("foo.com")) - assert.True(t, pool.ShouldSkipped("baz.com")) - } -} - -func TestPool_MaxCacheSize(t *testing.T) { - ipnet := netip.MustParsePrefix("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.NotEqual(t, first, next) -} - -func TestPool_DoubleMapping(t *testing.T) { - ipnet := netip.MustParsePrefix("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.NotEqual(t, bazIP, newBazIP) -} - -func TestPool_Clone(t *testing.T) { - ipnet := netip.MustParsePrefix("192.168.0.1/24") - pool, _ := New(Options{ - IPNet: ipnet, - Size: 2, - }) - - first := pool.Lookup("foo.com") - last := pool.Lookup("bar.com") - assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4})) - assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5})) - - 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 := netip.MustParsePrefix("192.168.0.1/31") - _, err := New(Options{ - IPNet: ipnet, - Size: 10, - }) - - assert.Error(t, err) -} - -func TestPool_FlushFileCache(t *testing.T) { - ipnet := netip.MustParsePrefix("192.168.0.1/28") - pools, tempfile, err := createPools(Options{ - IPNet: ipnet, - Size: 10, - }) - assert.Nil(t, err) - defer os.Remove(tempfile) - - for _, pool := range pools { - foo := pool.Lookup("foo.com") - bar := pool.Lookup("baz.com") - bax := pool.Lookup("baz.com") - fox := pool.Lookup("foo.com") - - err = pool.FlushFakeIP() - assert.Nil(t, err) - - next := pool.Lookup("baz.com") - baz := pool.Lookup("foo.com") - nero := pool.Lookup("foo.com") - - assert.Equal(t, foo, fox) - assert.Equal(t, foo, next) - assert.NotEqual(t, foo, baz) - assert.Equal(t, bar, bax) - assert.Equal(t, bar, baz) - assert.NotEqual(t, bar, next) - assert.Equal(t, baz, nero) - } -} - -func TestPool_FlushMemoryCache(t *testing.T) { - ipnet := netip.MustParsePrefix("192.168.0.1/28") - pool, _ := New(Options{ - IPNet: ipnet, - Size: 10, - }) - - foo := pool.Lookup("foo.com") - bar := pool.Lookup("baz.com") - bax := pool.Lookup("baz.com") - fox := pool.Lookup("foo.com") - - err := pool.FlushFakeIP() - assert.Nil(t, err) - - next := pool.Lookup("baz.com") - baz := pool.Lookup("foo.com") - nero := pool.Lookup("foo.com") - - assert.Equal(t, foo, fox) - assert.Equal(t, foo, next) - assert.NotEqual(t, foo, baz) - assert.Equal(t, bar, bax) - assert.Equal(t, bar, baz) - assert.NotEqual(t, bar, next) - assert.Equal(t, baz, nero) -} diff --git a/component/generater/cmd.go b/component/generater/cmd.go deleted file mode 100644 index 9d2c3d976b..0000000000 --- a/component/generater/cmd.go +++ /dev/null @@ -1,37 +0,0 @@ -package generater - -import ( - "encoding/base64" - "fmt" - - "github.com/gofrs/uuid/v5" -) - -func Main(args []string) { - if len(args) < 1 { - panic("Using: generate uuid/reality-keypair/wg-keypair") - } - switch args[0] { - case "uuid": - newUUID, err := uuid.NewV4() - if err != nil { - panic(err) - } - fmt.Println(newUUID.String()) - case "reality-keypair": - privateKey, err := GeneratePrivateKey() - if err != nil { - panic(err) - } - publicKey := privateKey.PublicKey() - fmt.Println("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:])) - fmt.Println("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:])) - case "wg-keypair": - privateKey, err := GeneratePrivateKey() - if err != nil { - panic(err) - } - fmt.Println("PrivateKey: " + privateKey.String()) - fmt.Println("PublicKey: " + privateKey.PublicKey().String()) - } -} diff --git a/component/generater/types.go b/component/generater/types.go deleted file mode 100644 index 06f59e9468..0000000000 --- a/component/generater/types.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copy from https://github.com/WireGuard/wgctrl-go/blob/a9ab2273dd1075ea74b88c76f8757f8b4003fcbf/wgtypes/types.go#L71-L155 - -package generater - -import ( - "crypto/rand" - "encoding/base64" - "fmt" - - "golang.org/x/crypto/curve25519" -) - -// KeyLen is the expected key length for a WireGuard key. -const KeyLen = 32 // wgh.KeyLen - -// A Key is a public, private, or pre-shared secret key. The Key constructor -// functions in this package can be used to create Keys suitable for each of -// these applications. -type Key [KeyLen]byte - -// GenerateKey generates a Key suitable for use as a pre-shared secret key from -// a cryptographically safe source. -// -// The output Key should not be used as a private key; use GeneratePrivateKey -// instead. -func GenerateKey() (Key, error) { - b := make([]byte, KeyLen) - if _, err := rand.Read(b); err != nil { - return Key{}, fmt.Errorf("wgtypes: failed to read random bytes: %v", err) - } - - return NewKey(b) -} - -// GeneratePrivateKey generates a Key suitable for use as a private key from a -// cryptographically safe source. -func GeneratePrivateKey() (Key, error) { - key, err := GenerateKey() - if err != nil { - return Key{}, err - } - - // Modify random bytes using algorithm described at: - // https://cr.yp.to/ecdh.html. - key[0] &= 248 - key[31] &= 127 - key[31] |= 64 - - return key, nil -} - -// NewKey creates a Key from an existing byte slice. The byte slice must be -// exactly 32 bytes in length. -func NewKey(b []byte) (Key, error) { - if len(b) != KeyLen { - return Key{}, fmt.Errorf("wgtypes: incorrect key size: %d", len(b)) - } - - var k Key - copy(k[:], b) - - return k, nil -} - -// ParseKey parses a Key from a base64-encoded string, as produced by the -// Key.String method. -func ParseKey(s string) (Key, error) { - b, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return Key{}, fmt.Errorf("wgtypes: failed to parse base64-encoded key: %v", err) - } - - return NewKey(b) -} - -// PublicKey computes a public key from the private key k. -// -// PublicKey should only be called when k is a private key. -func (k Key) PublicKey() Key { - var ( - pub [KeyLen]byte - priv = [KeyLen]byte(k) - ) - - // ScalarBaseMult uses the correct base value per https://cr.yp.to/ecdh.html, - // so no need to specify it. - curve25519.ScalarBaseMult(&pub, &priv) - - return Key(pub) -} - -// String returns the base64-encoded string representation of a Key. -// -// ParseKey can be used to produce a new Key from this string. -func (k Key) String() string { - return base64.StdEncoding.EncodeToString(k[:]) -} diff --git a/component/geodata/attr.go b/component/geodata/attr.go deleted file mode 100644 index 2fd41ad6c0..0000000000 --- a/component/geodata/attr.go +++ /dev/null @@ -1,59 +0,0 @@ -package geodata - -import ( - "strings" - - "github.com/metacubex/mihomo/component/geodata/router" -) - -type AttributeList struct { - matcher []BooleanMatcher -} - -func (al *AttributeList) Match(domain *router.Domain) bool { - for _, matcher := range al.matcher { - if !matcher.Match(domain) { - return false - } - } - return true -} - -func (al *AttributeList) IsEmpty() bool { - return len(al.matcher) == 0 -} - -func (al *AttributeList) String() string { - matcher := make([]string, len(al.matcher)) - for i, match := range al.matcher { - matcher[i] = string(match) - } - return strings.Join(matcher, ",") -} - -func parseAttrs(attrs []string) *AttributeList { - al := new(AttributeList) - for _, attr := range attrs { - trimmedAttr := strings.ToLower(strings.TrimSpace(attr)) - if len(trimmedAttr) == 0 { - continue - } - al.matcher = append(al.matcher, BooleanMatcher(trimmedAttr)) - } - return al -} - -type AttributeMatcher interface { - Match(*router.Domain) bool -} - -type BooleanMatcher string - -func (m BooleanMatcher) Match(domain *router.Domain) bool { - for _, attr := range domain.Attribute { - if strings.EqualFold(attr.GetKey(), string(m)) { - return true - } - } - return false -} diff --git a/component/geodata/geodata.go b/component/geodata/geodata.go deleted file mode 100644 index a6ef146acd..0000000000 --- a/component/geodata/geodata.go +++ /dev/null @@ -1,44 +0,0 @@ -package geodata - -import ( - "fmt" - - "github.com/metacubex/mihomo/component/geodata/router" - C "github.com/metacubex/mihomo/constant" -) - -type loader struct { - LoaderImplementation -} - -func (l *loader) LoadGeoSite(list string) ([]*router.Domain, error) { - return l.LoadSiteByPath(C.GeositeName, list) -} - -func (l *loader) LoadGeoIP(country string) ([]*router.CIDR, error) { - return l.LoadIPByPath(C.GeoipName, country) -} - -var loaders map[string]func() LoaderImplementation - -func RegisterGeoDataLoaderImplementationCreator(name string, loader func() LoaderImplementation) { - if loaders == nil { - loaders = map[string]func() LoaderImplementation{} - } - loaders[name] = loader -} - -func getGeoDataLoaderImplementation(name string) (LoaderImplementation, error) { - if geoLoader, ok := loaders[name]; ok { - return geoLoader(), nil - } - return nil, fmt.Errorf("unable to locate GeoData loader %s", name) -} - -func GetGeoDataLoader(name string) (Loader, error) { - loadImpl, err := getGeoDataLoaderImplementation(name) - if err == nil { - return &loader{loadImpl}, nil - } - return nil, err -} diff --git a/component/geodata/geodataproto.go b/component/geodata/geodataproto.go deleted file mode 100644 index 0f1ce4d272..0000000000 --- a/component/geodata/geodataproto.go +++ /dev/null @@ -1,18 +0,0 @@ -package geodata - -import ( - "github.com/metacubex/mihomo/component/geodata/router" -) - -type LoaderImplementation interface { - LoadSiteByPath(filename, list string) ([]*router.Domain, error) - LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) - LoadIPByPath(filename, country string) ([]*router.CIDR, error) - LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) -} - -type Loader interface { - LoaderImplementation - LoadGeoSite(list string) ([]*router.Domain, error) - LoadGeoIP(country string) ([]*router.CIDR, error) -} diff --git a/component/geodata/init.go b/component/geodata/init.go deleted file mode 100644 index 08ec1b948d..0000000000 --- a/component/geodata/init.go +++ /dev/null @@ -1,204 +0,0 @@ -package geodata - -import ( - "context" - "fmt" - "io" - "net/http" - "os" - "sync" - "time" - - "github.com/metacubex/mihomo/common/atomic" - mihomoHttp "github.com/metacubex/mihomo/component/http" - "github.com/metacubex/mihomo/component/mmdb" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -var ( - initGeoSite bool - initGeoIP int - initASN bool - - initGeoSiteMutex sync.Mutex - initGeoIPMutex sync.Mutex - initASNMutex sync.Mutex - - geoIpEnable atomic.Bool - geoSiteEnable atomic.Bool - asnEnable atomic.Bool - - geoIpUrl string - mmdbUrl string - geoSiteUrl string - asnUrl string -) - -func GeoIpUrl() string { - return geoIpUrl -} - -func SetGeoIpUrl(url string) { - geoIpUrl = url -} - -func MmdbUrl() string { - return mmdbUrl -} - -func SetMmdbUrl(url string) { - mmdbUrl = url -} - -func GeoSiteUrl() string { - return geoSiteUrl -} - -func SetGeoSiteUrl(url string) { - geoSiteUrl = url -} - -func ASNUrl() string { - return asnUrl -} - -func SetASNUrl(url string) { - asnUrl = url -} - -func downloadToPath(url string, path string) (err error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil) - 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 InitGeoSite() error { - geoSiteEnable.Store(true) - initGeoSiteMutex.Lock() - defer initGeoSiteMutex.Unlock() - if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { - log.Infoln("Can't find GeoSite.dat, start download") - if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil { - return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) - } - log.Infoln("Download GeoSite.dat finish") - initGeoSite = false - } - if !initGeoSite { - if err := Verify(C.GeositeName); err != nil { - log.Warnln("GeoSite.dat invalid, remove and download: %s", err) - if err := os.Remove(C.Path.GeoSite()); err != nil { - return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error()) - } - if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil { - return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) - } - } - initGeoSite = true - } - return nil -} - -func InitGeoIP() error { - geoIpEnable.Store(true) - initGeoIPMutex.Lock() - defer initGeoIPMutex.Unlock() - if GeodataMode() { - if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) { - log.Infoln("Can't find GeoIP.dat, start download") - if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil { - return fmt.Errorf("can't download GeoIP.dat: %s", err.Error()) - } - log.Infoln("Download GeoIP.dat finish") - initGeoIP = 0 - } - - if initGeoIP != 1 { - if err := Verify(C.GeoipName); err != nil { - log.Warnln("GeoIP.dat invalid, remove and download: %s", err) - if err := os.Remove(C.Path.GeoIP()); err != nil { - return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error()) - } - if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil { - return fmt.Errorf("can't download GeoIP.dat: %s", err.Error()) - } - } - initGeoIP = 1 - } - return nil - } - - if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) { - log.Infoln("Can't find MMDB, start download") - if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil { - return fmt.Errorf("can't download MMDB: %s", err.Error()) - } - } - - if initGeoIP != 2 { - if !mmdb.Verify(C.Path.MMDB()) { - 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 := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil { - return fmt.Errorf("can't download MMDB: %s", err.Error()) - } - } - initGeoIP = 2 - } - return nil -} - -func InitASN() error { - asnEnable.Store(true) - initASNMutex.Lock() - defer initASNMutex.Unlock() - if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) { - log.Infoln("Can't find ASN.mmdb, start download") - if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil { - return fmt.Errorf("can't download ASN.mmdb: %s", err.Error()) - } - log.Infoln("Download ASN.mmdb finish") - initASN = false - } - if !initASN { - if !mmdb.Verify(C.Path.ASN()) { - log.Warnln("ASN invalid, remove and download") - if err := os.Remove(C.Path.ASN()); err != nil { - return fmt.Errorf("can't remove invalid ASN: %s", err.Error()) - } - if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil { - return fmt.Errorf("can't download ASN: %s", err.Error()) - } - } - initASN = true - } - return nil -} - -func GeoIpEnable() bool { - return geoIpEnable.Load() -} - -func GeoSiteEnable() bool { - return geoSiteEnable.Load() -} - -func ASNEnable() bool { - return asnEnable.Load() -} diff --git a/component/geodata/memconservative/cache.go b/component/geodata/memconservative/cache.go deleted file mode 100644 index ef76a42c70..0000000000 --- a/component/geodata/memconservative/cache.go +++ /dev/null @@ -1,142 +0,0 @@ -package memconservative - -import ( - "fmt" - "os" - "strings" - - "github.com/metacubex/mihomo/component/geodata/router" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - "google.golang.org/protobuf/proto" -) - -type GeoIPCache map[string]*router.GeoIP - -func (g GeoIPCache) Has(key string) bool { - return !(g.Get(key) == nil) -} - -func (g GeoIPCache) Get(key string) *router.GeoIP { - if g == nil { - return nil - } - return g[key] -} - -func (g GeoIPCache) Set(key string, value *router.GeoIP) { - if g == nil { - g = make(map[string]*router.GeoIP) - } - g[key] = value -} - -func (g GeoIPCache) Unmarshal(filename, code string) (*router.GeoIP, error) { - asset := C.Path.GetAssetLocation(filename) - idx := strings.ToLower(asset + ":" + code) - if g.Has(idx) { - return g.Get(idx), nil - } - - geoipBytes, err := Decode(asset, code) - switch err { - case nil: - var geoip router.GeoIP - if err := proto.Unmarshal(geoipBytes, &geoip); err != nil { - return nil, err - } - g.Set(idx, &geoip) - return &geoip, nil - - case errCodeNotFound: - return nil, fmt.Errorf("country code %s%s%s", code, " not found in ", filename) - - case errFailedToReadBytes, errFailedToReadExpectedLenBytes, - errInvalidGeodataFile, errInvalidGeodataVarintLength: - log.Warnln("failed to decode geoip file: %s%s", filename, ", fallback to the original ReadFile method") - geoipBytes, err = os.ReadFile(asset) - if err != nil { - return nil, err - } - var geoipList router.GeoIPList - if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { - return nil, err - } - for _, geoip := range geoipList.GetEntry() { - if strings.EqualFold(code, geoip.GetCountryCode()) { - g.Set(idx, geoip) - return geoip, nil - } - } - - default: - return nil, err - } - - return nil, fmt.Errorf("country code %s%s%s", code, " not found in ", filename) -} - -type GeoSiteCache map[string]*router.GeoSite - -func (g GeoSiteCache) Has(key string) bool { - return !(g.Get(key) == nil) -} - -func (g GeoSiteCache) Get(key string) *router.GeoSite { - if g == nil { - return nil - } - return g[key] -} - -func (g GeoSiteCache) Set(key string, value *router.GeoSite) { - if g == nil { - g = make(map[string]*router.GeoSite) - } - g[key] = value -} - -func (g GeoSiteCache) Unmarshal(filename, code string) (*router.GeoSite, error) { - asset := C.Path.GetAssetLocation(filename) - idx := strings.ToLower(asset + ":" + code) - if g.Has(idx) { - return g.Get(idx), nil - } - - geositeBytes, err := Decode(asset, code) - switch err { - case nil: - var geosite router.GeoSite - if err := proto.Unmarshal(geositeBytes, &geosite); err != nil { - return nil, err - } - g.Set(idx, &geosite) - return &geosite, nil - - case errCodeNotFound: - return nil, fmt.Errorf("list %s%s%s", code, " not found in ", filename) - - case errFailedToReadBytes, errFailedToReadExpectedLenBytes, - errInvalidGeodataFile, errInvalidGeodataVarintLength: - log.Warnln("failed to decode geosite file: %s%s", filename, ", fallback to the original ReadFile method") - geositeBytes, err = os.ReadFile(asset) - if err != nil { - return nil, err - } - var geositeList router.GeoSiteList - if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { - return nil, err - } - for _, geosite := range geositeList.GetEntry() { - if strings.EqualFold(code, geosite.GetCountryCode()) { - g.Set(idx, geosite) - return geosite, nil - } - } - - default: - return nil, err - } - - return nil, fmt.Errorf("list %s%s%s", code, " not found in ", filename) -} diff --git a/component/geodata/memconservative/decode.go b/component/geodata/memconservative/decode.go deleted file mode 100644 index 7eb86400d6..0000000000 --- a/component/geodata/memconservative/decode.go +++ /dev/null @@ -1,107 +0,0 @@ -package memconservative - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - - "google.golang.org/protobuf/encoding/protowire" -) - -var ( - errFailedToReadBytes = errors.New("failed to read bytes") - errFailedToReadExpectedLenBytes = errors.New("failed to read expected length of bytes") - errInvalidGeodataFile = errors.New("invalid geodata file") - errInvalidGeodataVarintLength = errors.New("invalid geodata varint length") - errCodeNotFound = errors.New("code not found") -) - -func emitBytes(f io.ReadSeeker, code string) ([]byte, error) { - count := 1 - isInner := false - tempContainer := make([]byte, 0, 5) - - var result []byte - var advancedN uint64 = 1 - var geoDataVarintLength, codeVarintLength, varintLenByteLen uint64 = 0, 0, 0 - -Loop: - for { - container := make([]byte, advancedN) - bytesRead, err := f.Read(container) - if err == io.EOF { - return nil, errCodeNotFound - } - if err != nil { - return nil, errFailedToReadBytes - } - if bytesRead != len(container) { - return nil, errFailedToReadExpectedLenBytes - } - - switch count { - case 1, 3: // data type ((field_number << 3) | wire_type) - if container[0] != 10 { // byte `0A` equals to `10` in decimal - return nil, errInvalidGeodataFile - } - advancedN = 1 - count++ - case 2, 4: // data length - tempContainer = append(tempContainer, container...) - if container[0] > 127 { // max one-byte-length byte `7F`(0FFF FFFF) equals to `127` in decimal - advancedN = 1 - goto Loop - } - lenVarint, n := protowire.ConsumeVarint(tempContainer) - if n < 0 { - return nil, errInvalidGeodataVarintLength - } - tempContainer = nil - if !isInner { - isInner = true - geoDataVarintLength = lenVarint - advancedN = 1 - } else { - isInner = false - codeVarintLength = lenVarint - varintLenByteLen = uint64(n) - advancedN = codeVarintLength - } - count++ - case 5: // data value - if strings.EqualFold(string(container), code) { - count++ - offset := -(1 + int64(varintLenByteLen) + int64(codeVarintLength)) - _, _ = f.Seek(offset, 1) // back to the start of GeoIP or GeoSite varint - advancedN = geoDataVarintLength // the number of bytes to be read in next round - } else { - count = 1 - offset := int64(geoDataVarintLength) - int64(codeVarintLength) - int64(varintLenByteLen) - 1 - _, _ = f.Seek(offset, 1) // skip the unmatched GeoIP or GeoSite varint - advancedN = 1 // the next round will be the start of another GeoIPList or GeoSiteList - } - case 6: // matched GeoIP or GeoSite varint - result = container - break Loop - } - } - return result, nil -} - -func Decode(filename, code string) ([]byte, error) { - f, err := os.Open(filename) - if err != nil { - return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error()) - } - defer func(f *os.File) { - _ = f.Close() - }(f) - - geoBytes, err := emitBytes(f, code) - if err != nil { - return nil, err - } - return geoBytes, nil -} diff --git a/component/geodata/memconservative/memc.go b/component/geodata/memconservative/memc.go deleted file mode 100644 index 30d89f1023..0000000000 --- a/component/geodata/memconservative/memc.go +++ /dev/null @@ -1,49 +0,0 @@ -package memconservative - -import ( - "errors" - "fmt" - "runtime" - - "github.com/metacubex/mihomo/component/geodata" - "github.com/metacubex/mihomo/component/geodata/router" -) - -type memConservativeLoader struct { - geoipcache GeoIPCache - geositecache GeoSiteCache -} - -func (m *memConservativeLoader) LoadIPByPath(filename, country string) ([]*router.CIDR, error) { - defer runtime.GC() - geoip, err := m.geoipcache.Unmarshal(filename, country) - if err != nil { - return nil, fmt.Errorf("failed to decode geodata file: %s, base error: %s", filename, err.Error()) - } - return geoip.Cidr, nil -} - -func (m *memConservativeLoader) LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) { - return nil, errors.New("memConservative do not support LoadIPByBytes") -} - -func (m *memConservativeLoader) LoadSiteByPath(filename, list string) ([]*router.Domain, error) { - defer runtime.GC() - geosite, err := m.geositecache.Unmarshal(filename, list) - if err != nil { - return nil, fmt.Errorf("failed to decode geodata file: %s, base error: %s", filename, err.Error()) - } - return geosite.Domain, nil -} - -func (m *memConservativeLoader) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) { - return nil, errors.New("memConservative do not support LoadSiteByBytes") -} - -func newMemConservativeLoader() geodata.LoaderImplementation { - return &memConservativeLoader{make(map[string]*router.GeoIP), make(map[string]*router.GeoSite)} -} - -func init() { - geodata.RegisterGeoDataLoaderImplementationCreator("memconservative", newMemConservativeLoader) -} diff --git a/component/geodata/package_info.go b/component/geodata/package_info.go deleted file mode 100644 index 4644ccc2dc..0000000000 --- a/component/geodata/package_info.go +++ /dev/null @@ -1,4 +0,0 @@ -// Modified from: https://github.com/v2fly/v2ray-core/tree/master/infra/conf/geodata -// License: MIT - -package geodata diff --git a/component/geodata/router/condition.go b/component/geodata/router/condition.go deleted file mode 100644 index fb47e4a40c..0000000000 --- a/component/geodata/router/condition.go +++ /dev/null @@ -1,189 +0,0 @@ -package router - -import ( - "fmt" - "net/netip" - "strings" - - "github.com/metacubex/mihomo/component/cidr" - "github.com/metacubex/mihomo/component/geodata/strmatcher" - "github.com/metacubex/mihomo/component/trie" -) - -var matcherTypeMap = map[Domain_Type]strmatcher.Type{ - Domain_Plain: strmatcher.Substr, - Domain_Regex: strmatcher.Regex, - Domain_Domain: strmatcher.Domain, - Domain_Full: strmatcher.Full, -} - -func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) { - matcherType, f := matcherTypeMap[domain.Type] - if !f { - return nil, fmt.Errorf("unsupported domain type %v", domain.Type) - } - - matcher, err := matcherType.New(domain.Value) - if err != nil { - return nil, fmt.Errorf("failed to create domain matcher, base error: %s", err.Error()) - } - - return matcher, nil -} - -type DomainMatcher interface { - ApplyDomain(string) bool - Count() int -} - -type succinctDomainMatcher struct { - set *trie.DomainSet - otherMatchers []strmatcher.Matcher - count int -} - -func (m *succinctDomainMatcher) ApplyDomain(domain string) bool { - isMatched := m.set.Has(domain) - if !isMatched { - for _, matcher := range m.otherMatchers { - isMatched = matcher.Match(domain) - if isMatched { - break - } - } - } - return isMatched -} - -func (m *succinctDomainMatcher) Count() int { - return m.count -} - -func NewSuccinctMatcherGroup(domains []*Domain) (DomainMatcher, error) { - t := trie.New[struct{}]() - m := &succinctDomainMatcher{ - count: len(domains), - } - for _, d := range domains { - switch d.Type { - case Domain_Plain, Domain_Regex: - matcher, err := matcherTypeMap[d.Type].New(d.Value) - if err != nil { - return nil, err - } - m.otherMatchers = append(m.otherMatchers, matcher) - - case Domain_Domain: - err := t.Insert("+."+d.Value, struct{}{}) - if err != nil { - return nil, err - } - - case Domain_Full: - err := t.Insert(d.Value, struct{}{}) - if err != nil { - return nil, err - } - } - } - m.set = t.NewDomainSet() - return m, nil -} - -type v2rayDomainMatcher struct { - matchers strmatcher.IndexMatcher - count int -} - -func NewMphMatcherGroup(domains []*Domain) (DomainMatcher, error) { - g := strmatcher.NewMphMatcherGroup() - for _, d := range domains { - matcherType, f := matcherTypeMap[d.Type] - if !f { - return nil, fmt.Errorf("unsupported domain type %v", d.Type) - } - _, err := g.AddPattern(d.Value, matcherType) - if err != nil { - return nil, err - } - } - g.Build() - return &v2rayDomainMatcher{ - matchers: g, - count: len(domains), - }, nil -} - -func (m *v2rayDomainMatcher) ApplyDomain(domain string) bool { - return len(m.matchers.Match(strings.ToLower(domain))) > 0 -} - -func (m *v2rayDomainMatcher) Count() int { - return m.count -} - -type notDomainMatcher struct { - DomainMatcher -} - -func (m notDomainMatcher) ApplyDomain(domain string) bool { - return !m.DomainMatcher.ApplyDomain(domain) -} - -func NewNotDomainMatcherGroup(matcher DomainMatcher) DomainMatcher { - return notDomainMatcher{matcher} -} - -type IPMatcher interface { - Match(ip netip.Addr) bool - Count() int -} - -type geoIPMatcher struct { - cidrSet *cidr.IpCidrSet - count int -} - -// Match returns true if the given ip is included by the GeoIP. -func (m *geoIPMatcher) Match(ip netip.Addr) bool { - return m.cidrSet.IsContain(ip) -} - -func (m *geoIPMatcher) Count() int { - return m.count -} - -func NewGeoIPMatcher(cidrList []*CIDR) (IPMatcher, error) { - m := &geoIPMatcher{ - cidrSet: cidr.NewIpCidrSet(), - count: len(cidrList), - } - for _, cidr := range cidrList { - addr, ok := netip.AddrFromSlice(cidr.Ip) - if !ok { - return nil, fmt.Errorf("error when loading GeoIP: invalid IP: %s", cidr.Ip) - } - err := m.cidrSet.AddIpCidr(netip.PrefixFrom(addr, int(cidr.Prefix))) - if err != nil { - return nil, fmt.Errorf("error when loading GeoIP: %w", err) - } - } - err := m.cidrSet.Merge() - if err != nil { - return nil, err - } - - return m, nil -} - -type notIPMatcher struct { - IPMatcher -} - -func (m notIPMatcher) Match(ip netip.Addr) bool { - return !m.IPMatcher.Match(ip) -} - -func NewNotIpMatcherGroup(matcher IPMatcher) IPMatcher { - return notIPMatcher{matcher} -} diff --git a/component/geodata/router/config.pb.go b/component/geodata/router/config.pb.go deleted file mode 100644 index 59d90c7a9a..0000000000 --- a/component/geodata/router/config.pb.go +++ /dev/null @@ -1,725 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.28.0 -// protoc v3.19.1 -// source: component/geodata/router/config.proto - -package router - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// Type of domain value. -type Domain_Type int32 - -const ( - // The value is used as is. - Domain_Plain Domain_Type = 0 - // The value is used as a regular expression. - Domain_Regex Domain_Type = 1 - // The value is a root domain. - Domain_Domain Domain_Type = 2 - // The value is a domain. - Domain_Full Domain_Type = 3 -) - -// Enum value maps for Domain_Type. -var ( - Domain_Type_name = map[int32]string{ - 0: "Plain", - 1: "Regex", - 2: "Domain", - 3: "Full", - } - Domain_Type_value = map[string]int32{ - "Plain": 0, - "Regex": 1, - "Domain": 2, - "Full": 3, - } -) - -func (x Domain_Type) Enum() *Domain_Type { - p := new(Domain_Type) - *p = x - return p -} - -func (x Domain_Type) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Domain_Type) Descriptor() protoreflect.EnumDescriptor { - return file_component_geodata_router_config_proto_enumTypes[0].Descriptor() -} - -func (Domain_Type) Type() protoreflect.EnumType { - return &file_component_geodata_router_config_proto_enumTypes[0] -} - -func (x Domain_Type) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Domain_Type.Descriptor instead. -func (Domain_Type) EnumDescriptor() ([]byte, []int) { - return file_component_geodata_router_config_proto_rawDescGZIP(), []int{0, 0} -} - -// Domain for routing decision. -type Domain struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Domain matching type. - Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=mihomo.component.geodata.router.Domain_Type" json:"type,omitempty"` - // Domain value. - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - // Attributes of this domain. May be used for filtering. - Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"` -} - -func (x *Domain) Reset() { - *x = Domain{} - if protoimpl.UnsafeEnabled { - mi := &file_component_geodata_router_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Domain) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Domain) ProtoMessage() {} - -func (x *Domain) ProtoReflect() protoreflect.Message { - mi := &file_component_geodata_router_config_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Domain.ProtoReflect.Descriptor instead. -func (*Domain) Descriptor() ([]byte, []int) { - return file_component_geodata_router_config_proto_rawDescGZIP(), []int{0} -} - -func (x *Domain) GetType() Domain_Type { - if x != nil { - return x.Type - } - return Domain_Plain -} - -func (x *Domain) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -func (x *Domain) GetAttribute() []*Domain_Attribute { - if x != nil { - return x.Attribute - } - return nil -} - -// IP for routing decision, in CIDR form. -type CIDR struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // IP address, should be either 4 or 16 bytes. - Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` - // Number of leading ones in the network mask. - Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"` -} - -func (x *CIDR) Reset() { - *x = CIDR{} - if protoimpl.UnsafeEnabled { - mi := &file_component_geodata_router_config_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *CIDR) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CIDR) ProtoMessage() {} - -func (x *CIDR) ProtoReflect() protoreflect.Message { - mi := &file_component_geodata_router_config_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CIDR.ProtoReflect.Descriptor instead. -func (*CIDR) Descriptor() ([]byte, []int) { - return file_component_geodata_router_config_proto_rawDescGZIP(), []int{1} -} - -func (x *CIDR) GetIp() []byte { - if x != nil { - return x.Ip - } - return nil -} - -func (x *CIDR) GetPrefix() uint32 { - if x != nil { - return x.Prefix - } - return 0 -} - -type GeoIP struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` - Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"` - ReverseMatch bool `protobuf:"varint,3,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"` -} - -func (x *GeoIP) Reset() { - *x = GeoIP{} - if protoimpl.UnsafeEnabled { - mi := &file_component_geodata_router_config_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GeoIP) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GeoIP) ProtoMessage() {} - -func (x *GeoIP) ProtoReflect() protoreflect.Message { - mi := &file_component_geodata_router_config_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead. -func (*GeoIP) Descriptor() ([]byte, []int) { - return file_component_geodata_router_config_proto_rawDescGZIP(), []int{2} -} - -func (x *GeoIP) GetCountryCode() string { - if x != nil { - return x.CountryCode - } - return "" -} - -func (x *GeoIP) GetCidr() []*CIDR { - if x != nil { - return x.Cidr - } - return nil -} - -func (x *GeoIP) GetReverseMatch() bool { - if x != nil { - return x.ReverseMatch - } - return false -} - -type GeoIPList struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` -} - -func (x *GeoIPList) Reset() { - *x = GeoIPList{} - if protoimpl.UnsafeEnabled { - mi := &file_component_geodata_router_config_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GeoIPList) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GeoIPList) ProtoMessage() {} - -func (x *GeoIPList) ProtoReflect() protoreflect.Message { - mi := &file_component_geodata_router_config_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead. -func (*GeoIPList) Descriptor() ([]byte, []int) { - return file_component_geodata_router_config_proto_rawDescGZIP(), []int{3} -} - -func (x *GeoIPList) GetEntry() []*GeoIP { - if x != nil { - return x.Entry - } - return nil -} - -type GeoSite struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` - Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` -} - -func (x *GeoSite) Reset() { - *x = GeoSite{} - if protoimpl.UnsafeEnabled { - mi := &file_component_geodata_router_config_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GeoSite) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GeoSite) ProtoMessage() {} - -func (x *GeoSite) ProtoReflect() protoreflect.Message { - mi := &file_component_geodata_router_config_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GeoSite.ProtoReflect.Descriptor instead. -func (*GeoSite) Descriptor() ([]byte, []int) { - return file_component_geodata_router_config_proto_rawDescGZIP(), []int{4} -} - -func (x *GeoSite) GetCountryCode() string { - if x != nil { - return x.CountryCode - } - return "" -} - -func (x *GeoSite) GetDomain() []*Domain { - if x != nil { - return x.Domain - } - return nil -} - -type GeoSiteList struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` -} - -func (x *GeoSiteList) Reset() { - *x = GeoSiteList{} - if protoimpl.UnsafeEnabled { - mi := &file_component_geodata_router_config_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GeoSiteList) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GeoSiteList) ProtoMessage() {} - -func (x *GeoSiteList) ProtoReflect() protoreflect.Message { - mi := &file_component_geodata_router_config_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead. -func (*GeoSiteList) Descriptor() ([]byte, []int) { - return file_component_geodata_router_config_proto_rawDescGZIP(), []int{5} -} - -func (x *GeoSiteList) GetEntry() []*GeoSite { - if x != nil { - return x.Entry - } - return nil -} - -type Domain_Attribute struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - // Types that are assignable to TypedValue: - // *Domain_Attribute_BoolValue - // *Domain_Attribute_IntValue - TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"` -} - -func (x *Domain_Attribute) Reset() { - *x = Domain_Attribute{} - if protoimpl.UnsafeEnabled { - mi := &file_component_geodata_router_config_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Domain_Attribute) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Domain_Attribute) ProtoMessage() {} - -func (x *Domain_Attribute) ProtoReflect() protoreflect.Message { - mi := &file_component_geodata_router_config_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead. -func (*Domain_Attribute) Descriptor() ([]byte, []int) { - return file_component_geodata_router_config_proto_rawDescGZIP(), []int{0, 0} -} - -func (x *Domain_Attribute) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -func (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue { - if m != nil { - return m.TypedValue - } - return nil -} - -func (x *Domain_Attribute) GetBoolValue() bool { - if x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok { - return x.BoolValue - } - return false -} - -func (x *Domain_Attribute) GetIntValue() int64 { - if x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok { - return x.IntValue - } - return 0 -} - -type isDomain_Attribute_TypedValue interface { - isDomain_Attribute_TypedValue() -} - -type Domain_Attribute_BoolValue struct { - BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"` -} - -type Domain_Attribute_IntValue struct { - IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"` -} - -func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {} - -func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {} - -var File_component_geodata_router_config_proto protoreflect.FileDescriptor - -var file_component_geodata_router_config_proto_rawDesc = []byte{ - 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2f, 0x67, 0x65, 0x6f, 0x64, - 0x61, 0x74, 0x61, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, - 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, - 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x22, 0xd1, 0x02, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x12, 0x3f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x2b, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, - 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x72, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4e, 0x0a, 0x09, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, - 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, - 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x44, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x1a, 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, - 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, - 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, - 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x32, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, - 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, - 0x02, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, - 0x49, 0x44, 0x52, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x02, 0x69, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0x89, 0x01, 0x0a, 0x05, - 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, - 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x38, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, - 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, - 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, - 0x64, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, - 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, 0x76, 0x65, 0x72, - 0x73, 0x65, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x22, 0x48, 0x0a, 0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, - 0x4c, 0x69, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, - 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x22, 0x6c, 0x0a, 0x07, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, - 0x3e, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x26, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, - 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, - 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, - 0x4c, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x3d, - 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, - 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, - 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x47, - 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x7c, 0x0a, - 0x22, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, - 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x44, 0x72, 0x65, 0x61, 0x6d, 0x61, 0x63, 0x72, 0x6f, 0x2f, 0x63, 0x6c, 0x61, 0x73, - 0x68, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2f, 0x67, 0x65, 0x6f, 0x64, - 0x61, 0x74, 0x61, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0xaa, 0x02, 0x1e, 0x43, 0x6c, 0x61, - 0x73, 0x68, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x6f, - 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, -} - -var ( - file_component_geodata_router_config_proto_rawDescOnce sync.Once - file_component_geodata_router_config_proto_rawDescData = file_component_geodata_router_config_proto_rawDesc -) - -func file_component_geodata_router_config_proto_rawDescGZIP() []byte { - file_component_geodata_router_config_proto_rawDescOnce.Do(func() { - file_component_geodata_router_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_component_geodata_router_config_proto_rawDescData) - }) - return file_component_geodata_router_config_proto_rawDescData -} - -var file_component_geodata_router_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_component_geodata_router_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7) -var file_component_geodata_router_config_proto_goTypes = []interface{}{ - (Domain_Type)(0), // 0: mihomo.component.geodata.router.Domain.Type - (*Domain)(nil), // 1: mihomo.component.geodata.router.Domain - (*CIDR)(nil), // 2: mihomo.component.geodata.router.CIDR - (*GeoIP)(nil), // 3: mihomo.component.geodata.router.GeoIP - (*GeoIPList)(nil), // 4: mihomo.component.geodata.router.GeoIPList - (*GeoSite)(nil), // 5: mihomo.component.geodata.router.GeoSite - (*GeoSiteList)(nil), // 6: mihomo.component.geodata.router.GeoSiteList - (*Domain_Attribute)(nil), // 7: mihomo.component.geodata.router.Domain.Attribute -} -var file_component_geodata_router_config_proto_depIdxs = []int32{ - 0, // 0: mihomo.component.geodata.router.Domain.type:type_name -> mihomo.component.geodata.router.Domain.Type - 7, // 1: mihomo.component.geodata.router.Domain.attribute:type_name -> mihomo.component.geodata.router.Domain.Attribute - 2, // 2: mihomo.component.geodata.router.GeoIP.cidr:type_name -> mihomo.component.geodata.router.CIDR - 3, // 3: mihomo.component.geodata.router.GeoIPList.entry:type_name -> mihomo.component.geodata.router.GeoIP - 1, // 4: mihomo.component.geodata.router.GeoSite.domain:type_name -> mihomo.component.geodata.router.Domain - 5, // 5: mihomo.component.geodata.router.GeoSiteList.entry:type_name -> mihomo.component.geodata.router.GeoSite - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name -} - -func init() { file_component_geodata_router_config_proto_init() } -func file_component_geodata_router_config_proto_init() { - if File_component_geodata_router_config_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_component_geodata_router_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Domain); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_component_geodata_router_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CIDR); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_component_geodata_router_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GeoIP); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_component_geodata_router_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GeoIPList); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_component_geodata_router_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GeoSite); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_component_geodata_router_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GeoSiteList); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_component_geodata_router_config_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Domain_Attribute); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_component_geodata_router_config_proto_msgTypes[6].OneofWrappers = []interface{}{ - (*Domain_Attribute_BoolValue)(nil), - (*Domain_Attribute_IntValue)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_component_geodata_router_config_proto_rawDesc, - NumEnums: 1, - NumMessages: 7, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_component_geodata_router_config_proto_goTypes, - DependencyIndexes: file_component_geodata_router_config_proto_depIdxs, - EnumInfos: file_component_geodata_router_config_proto_enumTypes, - MessageInfos: file_component_geodata_router_config_proto_msgTypes, - }.Build() - File_component_geodata_router_config_proto = out.File - file_component_geodata_router_config_proto_rawDesc = nil - file_component_geodata_router_config_proto_goTypes = nil - file_component_geodata_router_config_proto_depIdxs = nil -} diff --git a/component/geodata/router/config.proto b/component/geodata/router/config.proto deleted file mode 100644 index 98795740da..0000000000 --- a/component/geodata/router/config.proto +++ /dev/null @@ -1,68 +0,0 @@ -syntax = "proto3"; - -package mihomo.component.geodata.router; -option csharp_namespace = "Mihomo.Component.Geodata.Router"; -option go_package = "github.com/metacubex/mihomo/component/geodata/router"; -option java_package = "com.mihomo.component.geodata.router"; -option java_multiple_files = true; - -// Domain for routing decision. -message Domain { - // Type of domain value. - enum Type { - // The value is used as is. - Plain = 0; - // The value is used as a regular expression. - Regex = 1; - // The value is a root domain. - Domain = 2; - // The value is a domain. - Full = 3; - } - - // Domain matching type. - Type type = 1; - - // Domain value. - string value = 2; - - message Attribute { - string key = 1; - - oneof typed_value { - bool bool_value = 2; - int64 int_value = 3; - } - } - - // Attributes of this domain. May be used for filtering. - repeated Attribute attribute = 3; -} - -// IP for routing decision, in CIDR form. -message CIDR { - // IP address, should be either 4 or 16 bytes. - bytes ip = 1; - - // Number of leading ones in the network mask. - uint32 prefix = 2; -} - -message GeoIP { - string country_code = 1; - repeated CIDR cidr = 2; - bool reverse_match = 3; -} - -message GeoIPList { - repeated GeoIP entry = 1; -} - -message GeoSite { - string country_code = 1; - repeated Domain domain = 2; -} - -message GeoSiteList { - repeated GeoSite entry = 1; -} diff --git a/component/geodata/standard/standard.go b/component/geodata/standard/standard.go deleted file mode 100644 index bfaae5ece7..0000000000 --- a/component/geodata/standard/standard.go +++ /dev/null @@ -1,92 +0,0 @@ -package standard - -import ( - "fmt" - "io" - "os" - "strings" - - "github.com/metacubex/mihomo/component/geodata" - "github.com/metacubex/mihomo/component/geodata/router" - C "github.com/metacubex/mihomo/constant" - - "google.golang.org/protobuf/proto" -) - -func ReadFile(path string) ([]byte, error) { - reader, err := os.Open(path) - if err != nil { - return nil, err - } - defer func(reader *os.File) { - _ = reader.Close() - }(reader) - - return io.ReadAll(reader) -} - -func ReadAsset(file string) ([]byte, error) { - return ReadFile(C.Path.GetAssetLocation(file)) -} - -func loadIP(geoipBytes []byte, country string) ([]*router.CIDR, error) { - var geoipList router.GeoIPList - if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { - return nil, err - } - - for _, geoip := range geoipList.Entry { - if strings.EqualFold(geoip.CountryCode, country) { - return geoip.Cidr, nil - } - } - - return nil, fmt.Errorf("country %s not found", country) -} - -func loadSite(geositeBytes []byte, list string) ([]*router.Domain, error) { - var geositeList router.GeoSiteList - if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { - return nil, err - } - - for _, site := range geositeList.Entry { - if strings.EqualFold(site.CountryCode, list) { - return site.Domain, nil - } - } - - return nil, fmt.Errorf("list %s not found", list) -} - -type standardLoader struct{} - -func (d standardLoader) LoadSiteByPath(filename, list string) ([]*router.Domain, error) { - geositeBytes, err := ReadAsset(filename) - if err != nil { - return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error()) - } - return loadSite(geositeBytes, list) -} - -func (d standardLoader) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) { - return loadSite(geositeBytes, list) -} - -func (d standardLoader) LoadIPByPath(filename, country string) ([]*router.CIDR, error) { - geoipBytes, err := ReadAsset(filename) - if err != nil { - return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error()) - } - return loadIP(geoipBytes, country) -} - -func (d standardLoader) LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) { - return loadIP(geoipBytes, country) -} - -func init() { - geodata.RegisterGeoDataLoaderImplementationCreator("standard", func() geodata.LoaderImplementation { - return standardLoader{} - }) -} diff --git a/component/geodata/strmatcher/ac_automaton_matcher.go b/component/geodata/strmatcher/ac_automaton_matcher.go deleted file mode 100644 index fd02d51175..0000000000 --- a/component/geodata/strmatcher/ac_automaton_matcher.go +++ /dev/null @@ -1,241 +0,0 @@ -package strmatcher - -import ( - list "github.com/bahlo/generic-list-go" -) - -const validCharCount = 53 - -type MatchType struct { - matchType Type - exist bool -} - -const ( - TrieEdge bool = true - FailEdge bool = false -) - -type Edge struct { - edgeType bool - nextNode int -} - -type ACAutomaton struct { - trie [][validCharCount]Edge - fail []int - exists []MatchType - count int -} - -func newNode() [validCharCount]Edge { - var s [validCharCount]Edge - for i := range s { - s[i] = Edge{ - edgeType: FailEdge, - nextNode: 0, - } - } - return s -} - -var char2Index = [...]int{ - 'A': 0, - 'a': 0, - 'B': 1, - 'b': 1, - 'C': 2, - 'c': 2, - 'D': 3, - 'd': 3, - 'E': 4, - 'e': 4, - 'F': 5, - 'f': 5, - 'G': 6, - 'g': 6, - 'H': 7, - 'h': 7, - 'I': 8, - 'i': 8, - 'J': 9, - 'j': 9, - 'K': 10, - 'k': 10, - 'L': 11, - 'l': 11, - 'M': 12, - 'm': 12, - 'N': 13, - 'n': 13, - 'O': 14, - 'o': 14, - 'P': 15, - 'p': 15, - 'Q': 16, - 'q': 16, - 'R': 17, - 'r': 17, - 'S': 18, - 's': 18, - 'T': 19, - 't': 19, - 'U': 20, - 'u': 20, - 'V': 21, - 'v': 21, - 'W': 22, - 'w': 22, - 'X': 23, - 'x': 23, - 'Y': 24, - 'y': 24, - 'Z': 25, - 'z': 25, - '!': 26, - '$': 27, - '&': 28, - '\'': 29, - '(': 30, - ')': 31, - '*': 32, - '+': 33, - ',': 34, - ';': 35, - '=': 36, - ':': 37, - '%': 38, - '-': 39, - '.': 40, - '_': 41, - '~': 42, - '0': 43, - '1': 44, - '2': 45, - '3': 46, - '4': 47, - '5': 48, - '6': 49, - '7': 50, - '8': 51, - '9': 52, -} - -func NewACAutomaton() *ACAutomaton { - ac := new(ACAutomaton) - ac.trie = append(ac.trie, newNode()) - ac.fail = append(ac.fail, 0) - ac.exists = append(ac.exists, MatchType{ - matchType: Full, - exist: false, - }) - return ac -} - -func (ac *ACAutomaton) Add(domain string, t Type) { - node := 0 - for i := len(domain) - 1; i >= 0; i-- { - idx := char2Index[domain[i]] - if ac.trie[node][idx].nextNode == 0 { - ac.count++ - if len(ac.trie) < ac.count+1 { - ac.trie = append(ac.trie, newNode()) - ac.fail = append(ac.fail, 0) - ac.exists = append(ac.exists, MatchType{ - matchType: Full, - exist: false, - }) - } - ac.trie[node][idx] = Edge{ - edgeType: TrieEdge, - nextNode: ac.count, - } - } - node = ac.trie[node][idx].nextNode - } - ac.exists[node] = MatchType{ - matchType: t, - exist: true, - } - switch t { - case Domain: - ac.exists[node] = MatchType{ - matchType: Full, - exist: true, - } - idx := char2Index['.'] - if ac.trie[node][idx].nextNode == 0 { - ac.count++ - if len(ac.trie) < ac.count+1 { - ac.trie = append(ac.trie, newNode()) - ac.fail = append(ac.fail, 0) - ac.exists = append(ac.exists, MatchType{ - matchType: Full, - exist: false, - }) - } - ac.trie[node][idx] = Edge{ - edgeType: TrieEdge, - nextNode: ac.count, - } - } - node = ac.trie[node][idx].nextNode - ac.exists[node] = MatchType{ - matchType: t, - exist: true, - } - default: - break - } -} - -func (ac *ACAutomaton) Build() { - queue := list.New[Edge]() - for i := 0; i < validCharCount; i++ { - if ac.trie[0][i].nextNode != 0 { - queue.PushBack(ac.trie[0][i]) - } - } - for { - front := queue.Front() - if front == nil { - break - } else { - node := front.Value.nextNode - queue.Remove(front) - for i := 0; i < validCharCount; i++ { - if ac.trie[node][i].nextNode != 0 { - ac.fail[ac.trie[node][i].nextNode] = ac.trie[ac.fail[node]][i].nextNode - queue.PushBack(ac.trie[node][i]) - } else { - ac.trie[node][i] = Edge{ - edgeType: FailEdge, - nextNode: ac.trie[ac.fail[node]][i].nextNode, - } - } - } - } - } -} - -func (ac *ACAutomaton) Match(s string) bool { - node := 0 - fullMatch := true - // 1. the match string is all through trie edge. FULL MATCH or DOMAIN - // 2. the match string is through a fail edge. NOT FULL MATCH - // 2.1 Through a fail edge, but there exists a valid node. SUBSTR - for i := len(s) - 1; i >= 0; i-- { - idx := char2Index[s[i]] - fullMatch = fullMatch && ac.trie[node][idx].edgeType - node = ac.trie[node][idx].nextNode - switch ac.exists[node].matchType { - case Substr: - return true - case Domain: - if fullMatch { - return true - } - } - } - return fullMatch && ac.exists[node].exist -} diff --git a/component/geodata/strmatcher/matchers.go b/component/geodata/strmatcher/matchers.go deleted file mode 100644 index b5ab09c4cb..0000000000 --- a/component/geodata/strmatcher/matchers.go +++ /dev/null @@ -1,52 +0,0 @@ -package strmatcher - -import ( - "regexp" - "strings" -) - -type fullMatcher string - -func (m fullMatcher) Match(s string) bool { - return string(m) == s -} - -func (m fullMatcher) String() string { - return "full:" + string(m) -} - -type substrMatcher string - -func (m substrMatcher) Match(s string) bool { - return strings.Contains(s, string(m)) -} - -func (m substrMatcher) String() string { - return "keyword:" + string(m) -} - -type domainMatcher string - -func (m domainMatcher) Match(s string) bool { - pattern := string(m) - if !strings.HasSuffix(s, pattern) { - return false - } - return len(s) == len(pattern) || s[len(s)-len(pattern)-1] == '.' -} - -func (m domainMatcher) String() string { - return "domain:" + string(m) -} - -type regexMatcher struct { - pattern *regexp.Regexp -} - -func (m *regexMatcher) Match(s string) bool { - return m.pattern.MatchString(s) -} - -func (m *regexMatcher) String() string { - return "regexp:" + m.pattern.String() -} diff --git a/component/geodata/strmatcher/mph_matcher.go b/component/geodata/strmatcher/mph_matcher.go deleted file mode 100644 index 7c1b4062cf..0000000000 --- a/component/geodata/strmatcher/mph_matcher.go +++ /dev/null @@ -1,296 +0,0 @@ -package strmatcher - -import ( - "math/bits" - "regexp" - "sort" - "strings" - "unsafe" -) - -// PrimeRK is the prime base used in Rabin-Karp algorithm. -const PrimeRK = 16777619 - -// calculate the rolling murmurHash of given string -func RollingHash(s string) uint32 { - h := uint32(0) - for i := len(s) - 1; i >= 0; i-- { - h = h*PrimeRK + uint32(s[i]) - } - return h -} - -// A MphMatcherGroup is divided into three parts: -// 1. `full` and `domain` patterns are matched by Rabin-Karp algorithm and minimal perfect hash table; -// 2. `substr` patterns are matched by ac automaton; -// 3. `regex` patterns are matched with the regex library. -type MphMatcherGroup struct { - ac *ACAutomaton - otherMatchers []matcherEntry - rules []string - level0 []uint32 - level0Mask int - level1 []uint32 - level1Mask int - count uint32 - ruleMap *map[string]uint32 -} - -func (g *MphMatcherGroup) AddFullOrDomainPattern(pattern string, t Type) { - h := RollingHash(pattern) - switch t { - case Domain: - (*g.ruleMap)["."+pattern] = h*PrimeRK + uint32('.') - fallthrough - case Full: - (*g.ruleMap)[pattern] = h - default: - } -} - -func NewMphMatcherGroup() *MphMatcherGroup { - return &MphMatcherGroup{ - ac: nil, - otherMatchers: nil, - rules: nil, - level0: nil, - level0Mask: 0, - level1: nil, - level1Mask: 0, - count: 1, - ruleMap: &map[string]uint32{}, - } -} - -// AddPattern adds a pattern to MphMatcherGroup -func (g *MphMatcherGroup) AddPattern(pattern string, t Type) (uint32, error) { - switch t { - case Substr: - if g.ac == nil { - g.ac = NewACAutomaton() - } - g.ac.Add(pattern, t) - case Full, Domain: - pattern = strings.ToLower(pattern) - g.AddFullOrDomainPattern(pattern, t) - case Regex: - r, err := regexp.Compile(pattern) - if err != nil { - return 0, err - } - g.otherMatchers = append(g.otherMatchers, matcherEntry{ - m: ®exMatcher{pattern: r}, - id: g.count, - }) - default: - panic("Unknown type") - } - return g.count, nil -} - -// Build builds a minimal perfect hash table and ac automaton from insert rules -func (g *MphMatcherGroup) Build() { - if g.ac != nil { - g.ac.Build() - } - keyLen := len(*g.ruleMap) - if keyLen == 0 { - keyLen = 1 - (*g.ruleMap)["empty___"] = RollingHash("empty___") - } - g.level0 = make([]uint32, nextPow2(keyLen/4)) - g.level0Mask = len(g.level0) - 1 - g.level1 = make([]uint32, nextPow2(keyLen)) - g.level1Mask = len(g.level1) - 1 - sparseBuckets := make([][]int, len(g.level0)) - var ruleIdx int - for rule, hash := range *g.ruleMap { - n := int(hash) & g.level0Mask - g.rules = append(g.rules, rule) - sparseBuckets[n] = append(sparseBuckets[n], ruleIdx) - ruleIdx++ - } - g.ruleMap = nil - var buckets []indexBucket - for n, vals := range sparseBuckets { - if len(vals) > 0 { - buckets = append(buckets, indexBucket{n, vals}) - } - } - sort.Sort(bySize(buckets)) - - occ := make([]bool, len(g.level1)) - var tmpOcc []int - for _, bucket := range buckets { - seed := uint32(0) - for { - findSeed := true - tmpOcc = tmpOcc[:0] - for _, i := range bucket.vals { - n := int(strhashFallback(unsafe.Pointer(&g.rules[i]), uintptr(seed))) & g.level1Mask - if occ[n] { - for _, n := range tmpOcc { - occ[n] = false - } - seed++ - findSeed = false - break - } - occ[n] = true - tmpOcc = append(tmpOcc, n) - g.level1[n] = uint32(i) - } - if findSeed { - g.level0[bucket.n] = seed - break - } - } - } -} - -func nextPow2(v int) int { - if v <= 1 { - return 1 - } - const MaxUInt = ^uint(0) - n := (MaxUInt >> bits.LeadingZeros(uint(v))) + 1 - return int(n) -} - -// Lookup searches for s in t and returns its index and whether it was found. -func (g *MphMatcherGroup) Lookup(h uint32, s string) bool { - i0 := int(h) & g.level0Mask - seed := g.level0[i0] - i1 := int(strhashFallback(unsafe.Pointer(&s), uintptr(seed))) & g.level1Mask - n := g.level1[i1] - return s == g.rules[int(n)] -} - -// Match implements IndexMatcher.Match. -func (g *MphMatcherGroup) Match(pattern string) []uint32 { - result := []uint32{} - hash := uint32(0) - for i := len(pattern) - 1; i >= 0; i-- { - hash = hash*PrimeRK + uint32(pattern[i]) - if pattern[i] == '.' { - if g.Lookup(hash, pattern[i:]) { - result = append(result, 1) - return result - } - } - } - if g.Lookup(hash, pattern) { - result = append(result, 1) - return result - } - if g.ac != nil && g.ac.Match(pattern) { - result = append(result, 1) - return result - } - for _, e := range g.otherMatchers { - if e.m.Match(pattern) { - result = append(result, e.id) - return result - } - } - return nil -} - -type indexBucket struct { - n int - vals []int -} - -type bySize []indexBucket - -func (s bySize) Len() int { return len(s) } -func (s bySize) Less(i, j int) bool { return len(s[i].vals) > len(s[j].vals) } -func (s bySize) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -type stringStruct struct { - str unsafe.Pointer - len int -} - -func strhashFallback(a unsafe.Pointer, h uintptr) uintptr { - x := (*stringStruct)(a) - return memhashFallback(x.str, h, uintptr(x.len)) -} - -const ( - // Constants for multiplication: four random odd 64-bit numbers. - m1 = 16877499708836156737 - m2 = 2820277070424839065 - m3 = 9497967016996688599 - m4 = 15839092249703872147 -) - -var hashkey = [4]uintptr{1, 1, 1, 1} - -func memhashFallback(p unsafe.Pointer, seed, s uintptr) uintptr { - h := uint64(seed + s*hashkey[0]) -tail: - switch { - case s == 0: - case s < 4: - h ^= uint64(*(*byte)(p)) - h ^= uint64(*(*byte)(unsafe.Add(p, s>>1))) << 8 - h ^= uint64(*(*byte)(unsafe.Add(p, s-1))) << 16 - h = bits.RotateLeft64(h*m1, 31) * m2 - case s <= 8: - h ^= uint64(readUnaligned32(p)) - h ^= uint64(readUnaligned32(unsafe.Add(p, s-4))) << 32 - h = bits.RotateLeft64(h*m1, 31) * m2 - case s <= 16: - h ^= readUnaligned64(p) - h = bits.RotateLeft64(h*m1, 31) * m2 - h ^= readUnaligned64(unsafe.Add(p, s-8)) - h = bits.RotateLeft64(h*m1, 31) * m2 - case s <= 32: - h ^= readUnaligned64(p) - h = bits.RotateLeft64(h*m1, 31) * m2 - h ^= readUnaligned64(unsafe.Add(p, 8)) - h = bits.RotateLeft64(h*m1, 31) * m2 - h ^= readUnaligned64(unsafe.Add(p, s-16)) - h = bits.RotateLeft64(h*m1, 31) * m2 - h ^= readUnaligned64(unsafe.Add(p, s-8)) - h = bits.RotateLeft64(h*m1, 31) * m2 - default: - v1 := h - v2 := uint64(seed * hashkey[1]) - v3 := uint64(seed * hashkey[2]) - v4 := uint64(seed * hashkey[3]) - for s >= 32 { - v1 ^= readUnaligned64(p) - v1 = bits.RotateLeft64(v1*m1, 31) * m2 - p = unsafe.Add(p, 8) - v2 ^= readUnaligned64(p) - v2 = bits.RotateLeft64(v2*m2, 31) * m3 - p = unsafe.Add(p, 8) - v3 ^= readUnaligned64(p) - v3 = bits.RotateLeft64(v3*m3, 31) * m4 - p = unsafe.Add(p, 8) - v4 ^= readUnaligned64(p) - v4 = bits.RotateLeft64(v4*m4, 31) * m1 - p = unsafe.Add(p, 8) - s -= 32 - } - h = v1 ^ v2 ^ v3 ^ v4 - goto tail - } - - h ^= h >> 29 - h *= m3 - h ^= h >> 32 - return uintptr(h) -} - -func readUnaligned32(p unsafe.Pointer) uint32 { - q := (*[4]byte)(p) - return uint32(q[0]) | uint32(q[1])<<8 | uint32(q[2])<<16 | uint32(q[3])<<24 -} - -func readUnaligned64(p unsafe.Pointer) uint64 { - q := (*[8]byte)(p) - return uint64(q[0]) | uint64(q[1])<<8 | uint64(q[2])<<16 | uint64(q[3])<<24 | uint64(q[4])<<32 | uint64(q[5])<<40 | uint64(q[6])<<48 | uint64(q[7])<<56 -} diff --git a/component/geodata/strmatcher/package_info.go b/component/geodata/strmatcher/package_info.go deleted file mode 100644 index 8d6e169f4c..0000000000 --- a/component/geodata/strmatcher/package_info.go +++ /dev/null @@ -1,4 +0,0 @@ -// Modified from: https://github.com/v2fly/v2ray-core/tree/master/common/strmatcher -// License: MIT - -package strmatcher diff --git a/component/geodata/strmatcher/strmatcher.go b/component/geodata/strmatcher/strmatcher.go deleted file mode 100644 index 6bdb8b97d6..0000000000 --- a/component/geodata/strmatcher/strmatcher.go +++ /dev/null @@ -1,60 +0,0 @@ -package strmatcher - -import ( - "regexp" -) - -// Matcher is the interface to determine a string matches a pattern. -type Matcher interface { - // Match returns true if the given string matches a predefined pattern. - Match(string) bool - String() string -} - -// Type is the type of the matcher. -type Type byte - -const ( - // Full is the type of matcher that the input string must exactly equal to the pattern. - Full Type = iota - // Substr is the type of matcher that the input string must contain the pattern as a sub-string. - Substr - // Domain is the type of matcher that the input string must be a sub-domain or itself of the pattern. - Domain - // Regex is the type of matcher that the input string must matches the regular-expression pattern. - Regex -) - -// New creates a new Matcher based on the given pattern. -func (t Type) New(pattern string) (Matcher, error) { - // 1. regex matching is case-sensitive - switch t { - case Full: - return fullMatcher(pattern), nil - case Substr: - return substrMatcher(pattern), nil - case Domain: - return domainMatcher(pattern), nil - case Regex: - r, err := regexp.Compile(pattern) - if err != nil { - return nil, err - } - return ®exMatcher{ - pattern: r, - }, nil - default: - panic("Unknown type") - } -} - -// IndexMatcher is the interface for matching with a group of matchers. -type IndexMatcher interface { - // Match returns the index of a matcher that matches the input. It returns empty array if no such matcher exists. - Match(input string) []uint32 -} - -type matcherEntry struct { - m Matcher - id uint32 -} diff --git a/component/geodata/utils.go b/component/geodata/utils.go deleted file mode 100644 index 4796624966..0000000000 --- a/component/geodata/utils.go +++ /dev/null @@ -1,203 +0,0 @@ -package geodata - -import ( - "errors" - "fmt" - "strings" - - "github.com/metacubex/mihomo/common/singleflight" - "github.com/metacubex/mihomo/component/geodata/router" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -var ( - geoMode bool - geoLoaderName = "memconservative" - geoSiteMatcher = "succinct" -) - -// geoLoaderName = "standard" - -func GeodataMode() bool { - return geoMode -} - -func LoaderName() string { - return geoLoaderName -} - -func SiteMatcherName() string { - return geoSiteMatcher -} - -func SetGeodataMode(newGeodataMode bool) { - geoMode = newGeodataMode -} - -func SetLoader(newLoader string) { - if newLoader == "memc" { - newLoader = "memconservative" - } - geoLoaderName = newLoader -} - -func SetSiteMatcher(newMatcher string) { - switch newMatcher { - case "mph", "hybrid": - geoSiteMatcher = "mph" - default: - geoSiteMatcher = "succinct" - } -} - -func Verify(name string) error { - switch name { - case C.GeositeName: - _, err := LoadGeoSiteMatcher("CN") - return err - case C.GeoipName: - _, err := LoadGeoIPMatcher("CN") - return err - default: - return fmt.Errorf("not support name") - } -} - -var loadGeoSiteMatcherListSF = singleflight.Group[[]*router.Domain]{StoreResult: true} -var loadGeoSiteMatcherSF = singleflight.Group[router.DomainMatcher]{StoreResult: true} - -func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, error) { - if countryCode == "" { - return nil, fmt.Errorf("country code could not be empty") - } - - not := false - if countryCode[0] == '!' { - not = true - countryCode = countryCode[1:] - } - countryCode = strings.ToLower(countryCode) - - parts := strings.Split(countryCode, "@") - if len(parts) == 0 { - return nil, errors.New("empty rule") - } - listName := strings.TrimSpace(parts[0]) - attrVal := parts[1:] - attrs := parseAttrs(attrVal) - - if listName == "" { - return nil, fmt.Errorf("empty listname in rule: %s", countryCode) - } - - matcherName := listName - if !attrs.IsEmpty() { - matcherName += "@" + attrs.String() - } - matcher, err, shared := loadGeoSiteMatcherSF.Do(matcherName, func() (router.DomainMatcher, error) { - log.Infoln("Load GeoSite rule: %s", matcherName) - domains, err, shared := loadGeoSiteMatcherListSF.Do(listName, func() ([]*router.Domain, error) { - geoLoader, err := GetGeoDataLoader(geoLoaderName) - if err != nil { - return nil, err - } - return geoLoader.LoadGeoSite(listName) - }) - if err != nil { - if !shared { - loadGeoSiteMatcherListSF.Forget(listName) // don't store the error result - } - return nil, err - } - - if attrs.IsEmpty() { - if strings.Contains(countryCode, "@") { - log.Warnln("empty attribute list: %s", countryCode) - } - } else { - filteredDomains := make([]*router.Domain, 0, len(domains)) - hasAttrMatched := false - for _, domain := range domains { - if attrs.Match(domain) { - hasAttrMatched = true - filteredDomains = append(filteredDomains, domain) - } - } - if !hasAttrMatched { - log.Warnln("attribute match no rule: geosite: %s", countryCode) - } - domains = filteredDomains - } - - /** - linear: linear algorithm - matcher, err := router.NewDomainMatcher(domains) - mph:minimal perfect hash algorithm - */ - if geoSiteMatcher == "mph" { - return router.NewMphMatcherGroup(domains) - } else { - return router.NewSuccinctMatcherGroup(domains) - } - }) - if err != nil { - if !shared { - loadGeoSiteMatcherSF.Forget(matcherName) // don't store the error result - } - return nil, err - } - if not { - matcher = router.NewNotDomainMatcherGroup(matcher) - } - - return matcher, nil -} - -var loadGeoIPMatcherSF = singleflight.Group[router.IPMatcher]{StoreResult: true} - -func LoadGeoIPMatcher(country string) (router.IPMatcher, error) { - if len(country) == 0 { - return nil, fmt.Errorf("country code could not be empty") - } - - not := false - if country[0] == '!' { - not = true - country = country[1:] - } - country = strings.ToLower(country) - - matcher, err, shared := loadGeoIPMatcherSF.Do(country, func() (router.IPMatcher, error) { - log.Infoln("Load GeoIP rule: %s", country) - geoLoader, err := GetGeoDataLoader(geoLoaderName) - if err != nil { - return nil, err - } - cidrList, err := geoLoader.LoadGeoIP(country) - if err != nil { - return nil, err - } - return router.NewGeoIPMatcher(cidrList) - }) - if err != nil { - if !shared { - loadGeoIPMatcherSF.Forget(country) // don't store the error result - log.Warnln("Load GeoIP rule: %s", country) - } - return nil, err - } - if not { - matcher = router.NewNotIpMatcherGroup(matcher) - } - return matcher, nil -} - -func ClearGeoSiteCache() { - loadGeoSiteMatcherListSF.Reset() - loadGeoSiteMatcherSF.Reset() -} - -func ClearGeoIPCache() { - loadGeoIPMatcherSF.Reset() -} diff --git a/component/http/http.go b/component/http/http.go deleted file mode 100644 index a2c44d85c3..0000000000 --- a/component/http/http.go +++ /dev/null @@ -1,83 +0,0 @@ -package http - -import ( - "context" - "crypto/tls" - "io" - "net" - "net/http" - URL "net/url" - "runtime" - "strings" - "time" - - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/listener/inner" -) - -var ( - ua string -) - -func UA() string { - return ua -} - -func SetUA(UA string) { - ua = UA -} - -func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) { - return HttpRequestWithProxy(ctx, url, method, header, body, "") -} - -func HttpRequestWithProxy(ctx context.Context, url, method string, header map[string][]string, body io.Reader, specialProxy string) (*http.Response, error) { - method = strings.ToUpper(method) - urlRes, err := URL.Parse(url) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(method, urlRes.String(), body) - for k, v := range header { - for _, v := range v { - req.Header.Add(k, v) - } - } - - if _, ok := header["User-Agent"]; !ok { - req.Header.Set("User-Agent", UA()) - } - - if err != nil { - return nil, err - } - - if user := urlRes.User; user != nil { - password, _ := user.Password() - req.SetBasicAuth(user.Username(), password) - } - - req = req.WithContext(ctx) - - transport := &http.Transport{ - // from http.DefaultTransport - DisableKeepAlives: runtime.GOOS == "android", - MaxIdleConns: 100, - IdleConnTimeout: 30 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - if conn, err := inner.HandleTcp(inner.GetTunnel(), address, specialProxy); err == nil { - return conn, nil - } else { - return dialer.DialContext(ctx, network, address) - } - }, - TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}), - } - - client := http.Client{Transport: transport} - return client.Do(req) -} diff --git a/component/iface/iface.go b/component/iface/iface.go deleted file mode 100644 index 92e0ccf3aa..0000000000 --- a/component/iface/iface.go +++ /dev/null @@ -1,181 +0,0 @@ -package iface - -import ( - "errors" - "net" - "net/netip" - "time" - - "github.com/metacubex/mihomo/common/singledo" - - "github.com/metacubex/bart" -) - -type Interface struct { - Index int - MTU int - Name string - HardwareAddr net.HardwareAddr - Flags net.Flags - Addresses []netip.Prefix -} - -var ( - ErrIfaceNotFound = errors.New("interface not found") - ErrAddrNotFound = errors.New("addr not found") -) - -type ifaceCache struct { - ifMap map[string]*Interface - ifTable bart.Table[*Interface] -} - -var caches = singledo.NewSingle[*ifaceCache](time.Second * 20) - -func getCache() (*ifaceCache, error) { - value, err, _ := caches.Do(func() (*ifaceCache, error) { - ifaces, err := net.Interfaces() - if err != nil { - return nil, err - } - - cache := &ifaceCache{ - ifMap: make(map[string]*Interface), - } - - for _, iface := range ifaces { - addrs, err := iface.Addrs() - if err != nil { - continue - } - - ipNets := make([]netip.Prefix, 0, len(addrs)) - for _, addr := range addrs { - var pf netip.Prefix - switch ipNet := addr.(type) { - case *net.IPNet: - ip, _ := netip.AddrFromSlice(ipNet.IP) - ones, bits := ipNet.Mask.Size() - if bits == 32 { - ip = ip.Unmap() - } - pf = netip.PrefixFrom(ip, ones) - case *net.IPAddr: - ip, _ := netip.AddrFromSlice(ipNet.IP) - ip = ip.Unmap() - pf = netip.PrefixFrom(ip, ip.BitLen()) - } - if pf.IsValid() { - ipNets = append(ipNets, pf) - } - } - - ifaceObj := &Interface{ - Index: iface.Index, - MTU: iface.MTU, - Name: iface.Name, - HardwareAddr: iface.HardwareAddr, - Flags: iface.Flags, - Addresses: ipNets, - } - cache.ifMap[iface.Name] = ifaceObj - - if iface.Flags&net.FlagUp == 0 { - continue // interface down - } - for _, prefix := range ipNets { - cache.ifTable.Insert(prefix, ifaceObj) - } - } - - return cache, nil - }) - return value, err -} - -func Interfaces() (map[string]*Interface, error) { - cache, err := getCache() - if err != nil { - return nil, err - } - return cache.ifMap, nil -} - -func ResolveInterface(name string) (*Interface, error) { - ifaces, err := Interfaces() - if err != nil { - return nil, err - } - - iface, ok := ifaces[name] - if !ok { - return nil, ErrIfaceNotFound - } - - return iface, nil -} - -func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) { - cache, err := getCache() - if err != nil { - return nil, err - } - iface, ok := cache.ifTable.Lookup(addr) - if !ok { - return nil, ErrIfaceNotFound - } - - return iface, nil -} - -func IsLocalIp(addr netip.Addr) (bool, error) { - cache, err := getCache() - if err != nil { - return false, err - } - return cache.ifTable.Contains(addr), nil -} - -func FlushCache() { - caches.Reset() -} - -func (iface *Interface) PickIPv4Addr(destination netip.Addr) (netip.Prefix, error) { - return iface.pickIPAddr(destination, func(addr netip.Prefix) bool { - return addr.Addr().Is4() - }) -} - -func (iface *Interface) PickIPv6Addr(destination netip.Addr) (netip.Prefix, error) { - return iface.pickIPAddr(destination, func(addr netip.Prefix) bool { - return addr.Addr().Is6() - }) -} - -func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr netip.Prefix) bool) (netip.Prefix, error) { - var fallback netip.Prefix - - for _, addr := range iface.Addresses { - if !accept(addr) { - continue - } - - if !fallback.IsValid() && !addr.Addr().IsLinkLocalUnicast() { - fallback = addr - - if !destination.IsValid() { - break - } - } - - if destination.IsValid() && addr.Contains(destination) { - return addr, nil - } - } - - if !fallback.IsValid() { - return netip.Prefix{}, ErrAddrNotFound - } - - return fallback, nil -} diff --git a/component/keepalive/tcp_keepalive.go b/component/keepalive/tcp_keepalive.go deleted file mode 100644 index ddb853e916..0000000000 --- a/component/keepalive/tcp_keepalive.go +++ /dev/null @@ -1,65 +0,0 @@ -package keepalive - -import ( - "net" - "runtime" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/utils" -) - -var ( - keepAliveIdle = atomic.NewTypedValue[time.Duration](0 * time.Second) - keepAliveInterval = atomic.NewTypedValue[time.Duration](0 * time.Second) - disableKeepAlive = atomic.NewBool(false) - - SetDisableKeepAliveCallback = utils.NewCallback[bool]() -) - -func SetKeepAliveIdle(t time.Duration) { - keepAliveIdle.Store(t) -} - -func SetKeepAliveInterval(t time.Duration) { - keepAliveInterval.Store(t) -} - -func KeepAliveIdle() time.Duration { - return keepAliveIdle.Load() -} - -func KeepAliveInterval() time.Duration { - return keepAliveInterval.Load() -} - -func SetDisableKeepAlive(disable bool) { - if runtime.GOOS == "android" { - setDisableKeepAlive(true) - } else { - setDisableKeepAlive(disable) - } -} - -func setDisableKeepAlive(disable bool) { - disableKeepAlive.Store(disable) - SetDisableKeepAliveCallback.Emit(disable) -} - -func DisableKeepAlive() bool { - return disableKeepAlive.Load() -} - -func SetNetDialer(dialer *net.Dialer) { - setNetDialer(dialer) -} - -func SetNetListenConfig(lc *net.ListenConfig) { - setNetListenConfig(lc) -} - -func TCPKeepAlive(c net.Conn) { - if tcp, ok := c.(TCPConn); ok && tcp != nil { - tcpKeepAlive(tcp) - } -} diff --git a/component/keepalive/tcp_keepalive_go122.go b/component/keepalive/tcp_keepalive_go122.go deleted file mode 100644 index 80a09ffa9f..0000000000 --- a/component/keepalive/tcp_keepalive_go122.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build !go1.23 - -package keepalive - -import ( - "net" - "time" -) - -type TCPConn interface { - net.Conn - SetKeepAlive(keepalive bool) error - SetKeepAlivePeriod(d time.Duration) error -} - -func tcpKeepAlive(tcp TCPConn) { - if DisableKeepAlive() { - _ = tcp.SetKeepAlive(false) - } else { - _ = tcp.SetKeepAlive(true) - _ = tcp.SetKeepAlivePeriod(KeepAliveInterval()) - } -} - -func setNetDialer(dialer *net.Dialer) { - if DisableKeepAlive() { - dialer.KeepAlive = -1 // If negative, keep-alive probes are disabled. - } else { - dialer.KeepAlive = KeepAliveInterval() - } -} - -func setNetListenConfig(lc *net.ListenConfig) { - if DisableKeepAlive() { - lc.KeepAlive = -1 // If negative, keep-alive probes are disabled. - } else { - lc.KeepAlive = KeepAliveInterval() - } -} diff --git a/component/keepalive/tcp_keepalive_go123.go b/component/keepalive/tcp_keepalive_go123.go deleted file mode 100644 index c87895760c..0000000000 --- a/component/keepalive/tcp_keepalive_go123.go +++ /dev/null @@ -1,51 +0,0 @@ -//go:build go1.23 - -package keepalive - -import "net" - -type TCPConn interface { - net.Conn - SetKeepAlive(keepalive bool) error - SetKeepAliveConfig(config net.KeepAliveConfig) error -} - -func keepAliveConfig() net.KeepAliveConfig { - config := net.KeepAliveConfig{ - Enable: true, - Idle: KeepAliveIdle(), - Interval: KeepAliveInterval(), - } - if !SupportTCPKeepAliveCount() { - // it's recommended to set both Idle and Interval to non-negative values in conjunction with a -1 - // for Count on those old Windows if you intend to customize the TCP keep-alive settings. - config.Count = -1 - } - return config -} - -func tcpKeepAlive(tcp TCPConn) { - if DisableKeepAlive() { - _ = tcp.SetKeepAlive(false) - } else { - _ = tcp.SetKeepAliveConfig(keepAliveConfig()) - } -} - -func setNetDialer(dialer *net.Dialer) { - if DisableKeepAlive() { - dialer.KeepAlive = -1 // If negative, keep-alive probes are disabled. - dialer.KeepAliveConfig.Enable = false - } else { - dialer.KeepAliveConfig = keepAliveConfig() - } -} - -func setNetListenConfig(lc *net.ListenConfig) { - if DisableKeepAlive() { - lc.KeepAlive = -1 // If negative, keep-alive probes are disabled. - lc.KeepAliveConfig.Enable = false - } else { - lc.KeepAliveConfig = keepAliveConfig() - } -} diff --git a/component/keepalive/tcp_keepalive_go123_unix.go b/component/keepalive/tcp_keepalive_go123_unix.go deleted file mode 100644 index 8033cc6c94..0000000000 --- a/component/keepalive/tcp_keepalive_go123_unix.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build go1.23 && unix - -package keepalive - -func SupportTCPKeepAliveIdle() bool { - return true -} - -func SupportTCPKeepAliveInterval() bool { - return true -} - -func SupportTCPKeepAliveCount() bool { - return true -} diff --git a/component/keepalive/tcp_keepalive_go123_windows.go b/component/keepalive/tcp_keepalive_go123_windows.go deleted file mode 100644 index 2462e80c36..0000000000 --- a/component/keepalive/tcp_keepalive_go123_windows.go +++ /dev/null @@ -1,63 +0,0 @@ -//go:build go1.23 && windows - -// copy and modify from golang1.23's internal/syscall/windows/version_windows.go - -package keepalive - -import ( - "errors" - "sync" - "syscall" - - "github.com/metacubex/mihomo/constant/features" - - "golang.org/x/sys/windows" -) - -var ( - supportTCPKeepAliveIdle bool - supportTCPKeepAliveInterval bool - supportTCPKeepAliveCount bool -) - -var initTCPKeepAlive = sync.OnceFunc(func() { - s, err := windows.WSASocket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP, nil, 0, windows.WSA_FLAG_NO_HANDLE_INHERIT) - if err != nil { - // Fallback to checking the Windows version. - major, build := features.WindowsMajorVersion, features.WindowsBuildNumber - supportTCPKeepAliveIdle = major >= 10 && build >= 16299 - supportTCPKeepAliveInterval = major >= 10 && build >= 16299 - supportTCPKeepAliveCount = major >= 10 && build >= 15063 - return - } - defer windows.Closesocket(s) - var optSupported = func(opt int) bool { - err := windows.SetsockoptInt(s, syscall.IPPROTO_TCP, opt, 1) - return !errors.Is(err, syscall.WSAENOPROTOOPT) - } - supportTCPKeepAliveIdle = optSupported(windows.TCP_KEEPIDLE) - supportTCPKeepAliveInterval = optSupported(windows.TCP_KEEPINTVL) - supportTCPKeepAliveCount = optSupported(windows.TCP_KEEPCNT) -}) - -// SupportTCPKeepAliveIdle indicates whether TCP_KEEPIDLE is supported. -// The minimal requirement is Windows 10.0.16299. -func SupportTCPKeepAliveIdle() bool { - initTCPKeepAlive() - return supportTCPKeepAliveIdle -} - -// SupportTCPKeepAliveInterval indicates whether TCP_KEEPINTVL is supported. -// The minimal requirement is Windows 10.0.16299. -func SupportTCPKeepAliveInterval() bool { - initTCPKeepAlive() - return supportTCPKeepAliveInterval -} - -// SupportTCPKeepAliveCount indicates whether TCP_KEEPCNT is supported. -// supports TCP_KEEPCNT. -// The minimal requirement is Windows 10.0.15063. -func SupportTCPKeepAliveCount() bool { - initTCPKeepAlive() - return supportTCPKeepAliveCount -} diff --git a/component/loopback/detector.go b/component/loopback/detector.go deleted file mode 100644 index c639ab2205..0000000000 --- a/component/loopback/detector.go +++ /dev/null @@ -1,115 +0,0 @@ -package loopback - -import ( - "errors" - "fmt" - "net/netip" - "os" - "strconv" - - "github.com/metacubex/mihomo/common/callback" - "github.com/metacubex/mihomo/component/iface" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" - - "github.com/puzpuzpuz/xsync/v3" -) - -var disableLoopBackDetector, _ = strconv.ParseBool(os.Getenv("DISABLE_LOOPBACK_DETECTOR")) - -func init() { - if features.CMFA { - disableLoopBackDetector = true - } -} - -var ErrReject = errors.New("reject loopback connection") - -type Detector struct { - connMap *xsync.MapOf[netip.AddrPort, struct{}] - packetConnMap *xsync.MapOf[uint16, struct{}] -} - -func NewDetector() *Detector { - if disableLoopBackDetector { - return nil - } - return &Detector{ - connMap: xsync.NewMapOf[netip.AddrPort, struct{}](), - packetConnMap: xsync.NewMapOf[uint16, struct{}](), - } -} - -func (l *Detector) NewConn(conn C.Conn) C.Conn { - if l == nil || l.connMap == nil { - return conn - } - metadata := C.Metadata{} - if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { - return conn - } - connAddr := metadata.AddrPort() - if !connAddr.IsValid() { - return conn - } - l.connMap.Store(connAddr, struct{}{}) - return callback.NewCloseCallbackConn(conn, func() { - l.connMap.Delete(connAddr) - }) -} - -func (l *Detector) NewPacketConn(conn C.PacketConn) C.PacketConn { - if l == nil || l.packetConnMap == nil { - return conn - } - metadata := C.Metadata{} - if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { - return conn - } - connAddr := metadata.AddrPort() - if !connAddr.IsValid() { - return conn - } - port := connAddr.Port() - l.packetConnMap.Store(port, struct{}{}) - return callback.NewCloseCallbackPacketConn(conn, func() { - l.packetConnMap.Delete(port) - }) -} - -func (l *Detector) CheckConn(metadata *C.Metadata) error { - if l == nil || l.connMap == nil { - return nil - } - connAddr := metadata.SourceAddrPort() - if !connAddr.IsValid() { - return nil - } - if _, ok := l.connMap.Load(connAddr); ok { - return fmt.Errorf("%w to: %s", ErrReject, metadata.RemoteAddress()) - } - return nil -} - -func (l *Detector) CheckPacketConn(metadata *C.Metadata) error { - if l == nil || l.packetConnMap == nil { - return nil - } - connAddr := metadata.SourceAddrPort() - if !connAddr.IsValid() { - return nil - } - - isLocalIp, err := iface.IsLocalIp(connAddr.Addr()) - if err != nil { - return err - } - if !isLocalIp && !connAddr.Addr().IsLoopback() { - return nil - } - - if _, ok := l.packetConnMap.Load(connAddr.Port()); ok { - return fmt.Errorf("%w to: %s", ErrReject, metadata.RemoteAddress()) - } - return nil -} diff --git a/component/mmdb/mmdb.go b/component/mmdb/mmdb.go deleted file mode 100644 index 81644b0076..0000000000 --- a/component/mmdb/mmdb.go +++ /dev/null @@ -1,96 +0,0 @@ -package mmdb - -import ( - "sync" - - mihomoOnce "github.com/metacubex/mihomo/common/once" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - "github.com/oschwald/maxminddb-golang" -) - -type databaseType = uint8 - -const ( - typeMaxmind databaseType = iota - typeSing - typeMetaV0 -) - -var ( - ipReader IPReader - asnReader ASNReader - ipOnce sync.Once - asnOnce sync.Once -) - -func LoadFromBytes(buffer []byte) { - ipOnce.Do(func() { - mmdb, err := maxminddb.FromBytes(buffer) - if err != nil { - log.Fatalln("Can't load mmdb: %s", err.Error()) - } - ipReader = IPReader{Reader: mmdb} - switch mmdb.Metadata.DatabaseType { - case "sing-geoip": - ipReader.databaseType = typeSing - case "Meta-geoip0": - ipReader.databaseType = typeMetaV0 - default: - ipReader.databaseType = typeMaxmind - } - }) -} - -func Verify(path string) bool { - instance, err := maxminddb.Open(path) - if err == nil { - instance.Close() - } - return err == nil -} - -func IPInstance() IPReader { - ipOnce.Do(func() { - mmdbPath := C.Path.MMDB() - log.Infoln("Load MMDB file: %s", mmdbPath) - mmdb, err := maxminddb.Open(mmdbPath) - if err != nil { - log.Fatalln("Can't load MMDB: %s", err.Error()) - } - ipReader = IPReader{Reader: mmdb} - switch mmdb.Metadata.DatabaseType { - case "sing-geoip": - ipReader.databaseType = typeSing - case "Meta-geoip0": - ipReader.databaseType = typeMetaV0 - default: - ipReader.databaseType = typeMaxmind - } - }) - - return ipReader -} - -func ASNInstance() ASNReader { - asnOnce.Do(func() { - ASNPath := C.Path.ASN() - log.Infoln("Load ASN file: %s", ASNPath) - asn, err := maxminddb.Open(ASNPath) - if err != nil { - log.Fatalln("Can't load ASN: %s", err.Error()) - } - asnReader = ASNReader{Reader: asn} - }) - - return asnReader -} - -func ReloadIP() { - mihomoOnce.Reset(&ipOnce) -} - -func ReloadASN() { - mihomoOnce.Reset(&asnOnce) -} diff --git a/component/mmdb/reader.go b/component/mmdb/reader.go deleted file mode 100644 index 42e500c9dc..0000000000 --- a/component/mmdb/reader.go +++ /dev/null @@ -1,89 +0,0 @@ -package mmdb - -import ( - "fmt" - "net" - "strings" - - "github.com/metacubex/mihomo/log" - "github.com/oschwald/maxminddb-golang" -) - -type geoip2Country struct { - Country struct { - IsoCode string `maxminddb:"iso_code"` - } `maxminddb:"country"` -} - -type IPReader struct { - *maxminddb.Reader - databaseType -} - -type ASNReader struct { - *maxminddb.Reader -} - -type GeoLite2 struct { - AutonomousSystemNumber uint32 `maxminddb:"autonomous_system_number"` - AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` -} - -type IPInfo struct { - ASN string `maxminddb:"asn"` - Name string `maxminddb:"name"` -} - -func (r IPReader) LookupCode(ipAddress net.IP) []string { - switch r.databaseType { - case typeMaxmind: - var country geoip2Country - _ = r.Lookup(ipAddress, &country) - if country.Country.IsoCode == "" { - return []string{} - } - return []string{strings.ToLower(country.Country.IsoCode)} - - case typeSing: - var code string - _ = r.Lookup(ipAddress, &code) - if code == "" { - return []string{} - } - return []string{code} - - case typeMetaV0: - var record any - _ = r.Lookup(ipAddress, &record) - switch record := record.(type) { - case string: - return []string{record} - case []any: // lookup returned type of slice is []any - result := make([]string, 0, len(record)) - for _, item := range record { - result = append(result, item.(string)) - } - return result - } - return []string{} - - default: - panic(fmt.Sprint("unknown geoip database type:", r.databaseType)) - } -} - -func (r ASNReader) LookupASN(ip net.IP) (string, string) { - switch r.Metadata.DatabaseType { - case "GeoLite2-ASN", "DBIP-ASN-Lite (compat=GeoLite2-ASN)": - var result GeoLite2 - _ = r.Lookup(ip, &result) - return fmt.Sprint(result.AutonomousSystemNumber), result.AutonomousSystemOrganization - case "ipinfo generic_asn_free.mmdb": - var result IPInfo - _ = r.Lookup(ip, &result) - return result.ASN[2:], result.Name - default: - log.Warnln("Unsupported ASN type: %s", r.Metadata.DatabaseType) - } - return "", "" -} diff --git a/component/nat/proxy.go b/component/nat/proxy.go deleted file mode 100644 index 66af3be28e..0000000000 --- a/component/nat/proxy.go +++ /dev/null @@ -1,26 +0,0 @@ -package nat - -import ( - "net" - - "github.com/metacubex/mihomo/common/atomic" - C "github.com/metacubex/mihomo/constant" -) - -type writeBackProxy struct { - wb atomic.TypedValue[C.WriteBack] -} - -func (w *writeBackProxy) WriteBack(b []byte, addr net.Addr) (n int, err error) { - return w.wb.Load().WriteBack(b, addr) -} - -func (w *writeBackProxy) UpdateWriteBack(wb C.WriteBack) { - w.wb.Store(wb) -} - -func NewWriteBackProxy(wb C.WriteBack) C.WriteBackProxy { - w := &writeBackProxy{} - w.UpdateWriteBack(wb) - return w -} diff --git a/component/nat/table.go b/component/nat/table.go deleted file mode 100644 index 66241fb472..0000000000 --- a/component/nat/table.go +++ /dev/null @@ -1,104 +0,0 @@ -package nat - -import ( - "net" - "sync" - - C "github.com/metacubex/mihomo/constant" - - "github.com/puzpuzpuz/xsync/v3" -) - -type Table struct { - mapping *xsync.MapOf[string, *entry] -} - -type entry struct { - PacketSender C.PacketSender - LocalUDPConnMap *xsync.MapOf[string, *net.UDPConn] - LocalLockMap *xsync.MapOf[string, *sync.Cond] -} - -func (t *Table) GetOrCreate(key string, maker func() C.PacketSender) (C.PacketSender, bool) { - item, loaded := t.mapping.LoadOrCompute(key, func() *entry { - return &entry{ - PacketSender: maker(), - LocalUDPConnMap: xsync.NewMapOf[string, *net.UDPConn](), - LocalLockMap: xsync.NewMapOf[string, *sync.Cond](), - } - }) - return item.PacketSender, loaded -} - -func (t *Table) Delete(key string) { - t.mapping.Delete(key) -} - -func (t *Table) GetForLocalConn(lAddr, rAddr string) *net.UDPConn { - entry, exist := t.getEntry(lAddr) - if !exist { - return nil - } - item, exist := entry.LocalUDPConnMap.Load(rAddr) - if !exist { - return nil - } - return item -} - -func (t *Table) AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool { - entry, exist := t.getEntry(lAddr) - if !exist { - return false - } - entry.LocalUDPConnMap.Store(rAddr, conn) - return true -} - -func (t *Table) RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool) { - entry, exist := t.getEntry(lAddr) - if !exist { - return - } - entry.LocalUDPConnMap.Range(f) -} - -func (t *Table) GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool) { - entry, loaded := t.getEntry(lAddr) - if !loaded { - return nil, false - } - item, loaded := entry.LocalLockMap.LoadOrCompute(key, makeLock) - return item, loaded -} - -func (t *Table) DeleteForLocalConn(lAddr, key string) { - entry, loaded := t.getEntry(lAddr) - if !loaded { - return - } - entry.LocalUDPConnMap.Delete(key) -} - -func (t *Table) DeleteLockForLocalConn(lAddr, key string) { - entry, loaded := t.getEntry(lAddr) - if !loaded { - return - } - entry.LocalLockMap.Delete(key) -} - -func (t *Table) getEntry(key string) (*entry, bool) { - return t.mapping.Load(key) -} - -func makeLock() *sync.Cond { - return sync.NewCond(&sync.Mutex{}) -} - -// New return *Cache -func New() *Table { - return &Table{ - mapping: xsync.NewMapOf[string, *entry](), - } -} diff --git a/component/pool/pool.go b/component/pool/pool.go deleted file mode 100644 index f8173761c9..0000000000 --- a/component/pool/pool.go +++ /dev/null @@ -1,114 +0,0 @@ -package pool - -import ( - "context" - "runtime" - "time" -) - -type Factory[T any] func(context.Context) (T, error) - -type entry[T any] struct { - elm T - time time.Time -} - -type Option[T any] func(*pool[T]) - -// WithEvict set the evict callback -func WithEvict[T any](cb func(T)) Option[T] { - return func(p *pool[T]) { - p.evict = cb - } -} - -// WithAge defined element max age (millisecond) -func WithAge[T any](maxAge int64) Option[T] { - return func(p *pool[T]) { - p.maxAge = maxAge - } -} - -// WithSize defined max size of Pool -func WithSize[T any](maxSize int) Option[T] { - return func(p *pool[T]) { - p.ch = make(chan *entry[T], maxSize) - } -} - -// Pool is for GC, see New for detail -type Pool[T any] struct { - *pool[T] -} - -type pool[T any] struct { - ch chan *entry[T] - factory Factory[T] - evict func(T) - maxAge int64 -} - -func (p *pool[T]) GetContext(ctx context.Context) (T, error) { - now := time.Now() - for { - select { - case item := <-p.ch: - elm := item - if p.maxAge != 0 && now.Sub(item.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[T]) Get() (T, error) { - return p.GetContext(context.Background()) -} - -func (p *pool[T]) Put(item T) { - e := &entry[T]{ - 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[T any](p *Pool[T]) { - for item := range p.pool.ch { - if p.pool.evict != nil { - p.pool.evict(item.elm) - } - } -} - -func New[T any](factory Factory[T], options ...Option[T]) *Pool[T] { - p := &pool[T]{ - ch: make(chan *entry[T], 10), - factory: factory, - } - - for _, option := range options { - option(p) - } - - P := &Pool[T]{p} - runtime.SetFinalizer(P, recycle[T]) - return P -} diff --git a/component/pool/pool_test.go b/component/pool/pool_test.go deleted file mode 100644 index 752aaaceb4..0000000000 --- a/component/pool/pool_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package pool - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func lg() Factory[int] { - initial := -1 - return func(context.Context) (int, error) { - initial++ - return initial, nil - } -} - -func TestPool_Basic(t *testing.T) { - g := lg() - pool := New[int](g) - - elm, _ := pool.Get() - assert.Equal(t, 0, elm) - pool.Put(elm) - elm, _ = pool.Get() - assert.Equal(t, 0, elm) - elm, _ = pool.Get() - assert.Equal(t, 1, elm) -} - -func TestPool_MaxSize(t *testing.T) { - g := lg() - size := 5 - pool := New[int](g, WithSize[int](size)) - - var items []int - - for i := 0; i < size; i++ { - item, _ := pool.Get() - items = append(items, item) - } - - extra, _ := pool.Get() - assert.Equal(t, size, extra) - - for _, item := range items { - pool.Put(item) - } - - pool.Put(extra) - - for _, item := range items { - elm, _ := pool.Get() - assert.Equal(t, item, elm) - } -} - -func TestPool_MaxAge(t *testing.T) { - g := lg() - pool := New[int](g, WithAge[int](20)) - - elm, _ := pool.Get() - pool.Put(elm) - - elm, _ = pool.Get() - assert.Equal(t, 0, elm) - pool.Put(elm) - - time.Sleep(time.Millisecond * 22) - elm, _ = pool.Get() - assert.Equal(t, 1, elm) -} diff --git a/component/power/event.go b/component/power/event.go deleted file mode 100644 index f59c2ad43e..0000000000 --- a/component/power/event.go +++ /dev/null @@ -1,22 +0,0 @@ -package power - -type Type uint8 - -const ( - SUSPEND Type = iota - RESUME - RESUMEAUTOMATIC // Because the user is not present, most applications should do nothing. -) - -func (t Type) String() string { - switch t { - case SUSPEND: - return "SUSPEND" - case RESUME: - return "RESUME" - case RESUMEAUTOMATIC: - return "RESUMEAUTOMATIC" - default: - return "" - } -} diff --git a/component/power/event_other.go b/component/power/event_other.go deleted file mode 100644 index 3a41d9e022..0000000000 --- a/component/power/event_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !windows - -package power - -import "errors" - -func NewEventListener(cb func(Type)) (func(), error) { - return nil, errors.New("not support on this platform") -} diff --git a/component/power/event_windows.go b/component/power/event_windows.go deleted file mode 100644 index a9489df417..0000000000 --- a/component/power/event_windows.go +++ /dev/null @@ -1,82 +0,0 @@ -package power - -// modify from https://github.com/golang/go/blob/b634f6fdcbebee23b7da709a243f3db217b64776/src/runtime/os_windows.go#L257 - -import ( - "runtime" - "unsafe" - - "golang.org/x/sys/windows" -) - -var ( - libPowrProf = windows.NewLazySystemDLL("powrprof.dll") - powerRegisterSuspendResumeNotification = libPowrProf.NewProc("PowerRegisterSuspendResumeNotification") - powerUnregisterSuspendResumeNotification = libPowrProf.NewProc("PowerUnregisterSuspendResumeNotification") -) - -func NewEventListener(cb func(Type)) (func(), error) { - if err := powerRegisterSuspendResumeNotification.Find(); err != nil { - return nil, err // Running on Windows 7, where we don't need it anyway. - } - if err := powerUnregisterSuspendResumeNotification.Find(); err != nil { - return nil, err // Running on Windows 7, where we don't need it anyway. - } - - // Defines the type of event - const ( - PBT_APMSUSPEND uint32 = 4 - PBT_APMRESUMESUSPEND uint32 = 7 - PBT_APMRESUMEAUTOMATIC uint32 = 18 - ) - - const ( - _DEVICE_NOTIFY_CALLBACK = 2 - ) - type _DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS struct { - callback uintptr - context uintptr - } - - var fn interface{} = func(context uintptr, changeType uint32, setting uintptr) uintptr { - switch changeType { - case PBT_APMSUSPEND: - cb(SUSPEND) - case PBT_APMRESUMESUSPEND: - cb(RESUME) - case PBT_APMRESUMEAUTOMATIC: - cb(RESUMEAUTOMATIC) - } - return 0 - } - - params := _DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS{ - callback: windows.NewCallback(fn), - } - handle := uintptr(0) - - // DWORD PowerRegisterSuspendResumeNotification( - // [in] DWORD Flags, - // [in] HANDLE Recipient, - // [out] PHPOWERNOTIFY RegistrationHandle - //); - _, _, err := powerRegisterSuspendResumeNotification.Call( - _DEVICE_NOTIFY_CALLBACK, - uintptr(unsafe.Pointer(¶ms)), - uintptr(unsafe.Pointer(&handle)), - ) - if err != nil { - return nil, err - } - - return func() { - // DWORD PowerUnregisterSuspendResumeNotification( - // [in, out] HPOWERNOTIFY RegistrationHandle - //); - _, _, _ = powerUnregisterSuspendResumeNotification.Call( - handle, - ) - runtime.KeepAlive(params) - runtime.KeepAlive(handle) - }, nil -} diff --git a/component/process/find_process_mode.go b/component/process/find_process_mode.go deleted file mode 100644 index 06618cef1b..0000000000 --- a/component/process/find_process_mode.go +++ /dev/null @@ -1,57 +0,0 @@ -package process - -import ( - "encoding/json" - "errors" - "strings" -) - -const ( - FindProcessAlways = "always" - FindProcessStrict = "strict" - FindProcessOff = "off" -) - -var ( - validModes = map[string]struct{}{ - FindProcessAlways: {}, - FindProcessOff: {}, - FindProcessStrict: {}, - } -) - -type FindProcessMode string - -func (m FindProcessMode) Always() bool { - return m == FindProcessAlways -} - -func (m FindProcessMode) Off() bool { - return m == FindProcessOff -} - -func (m *FindProcessMode) UnmarshalYAML(unmarshal func(any) error) error { - var tp string - if err := unmarshal(&tp); err != nil { - return err - } - return m.Set(tp) -} - -func (m *FindProcessMode) UnmarshalJSON(data []byte) error { - var tp string - if err := json.Unmarshal(data, &tp); err != nil { - return err - } - return m.Set(tp) -} - -func (m *FindProcessMode) Set(value string) error { - mode := strings.ToLower(value) - _, exist := validModes[mode] - if !exist { - return errors.New("invalid find process mode") - } - *m = FindProcessMode(mode) - return nil -} diff --git a/component/process/process.go b/component/process/process.go deleted file mode 100644 index 464f5a79bc..0000000000 --- a/component/process/process.go +++ /dev/null @@ -1,38 +0,0 @@ -package process - -import ( - "errors" - "net/netip" - - C "github.com/metacubex/mihomo/constant" -) - -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 FindProcessName(network string, srcIP netip.Addr, srcPort int) (uint32, string, error) { - return findProcessName(network, srcIP, srcPort) -} - -// PackageNameResolver -// never change type traits because it's used in CMFA -type PackageNameResolver func(metadata *C.Metadata) (string, error) - -// DefaultPackageNameResolver -// never change type traits because it's used in CMFA -var DefaultPackageNameResolver PackageNameResolver - -func FindPackageName(metadata *C.Metadata) (string, error) { - if resolver := DefaultPackageNameResolver; resolver != nil { - return resolver(metadata) - } - return "", ErrPlatformNotSupport -} diff --git a/component/process/process_darwin.go b/component/process/process_darwin.go deleted file mode 100644 index 93cb90ae99..0000000000 --- a/component/process/process_darwin.go +++ /dev/null @@ -1,131 +0,0 @@ -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 findProcessName(network string, ip netip.Addr, port int) (uint32, 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 0, "", ErrInvalidNetwork - } - - isIPv4 := ip.Is4() - - value, err := unix.SysctlRaw(spath) - if err != nil { - return 0, "", err - } - - buf := 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 uint16(port) != srcPort { - continue - } - - // xinpcb_n.inp_vflag - flag := buf[inp+44] - - var ( - srcIP netip.Addr - srcIsIPv4 bool - ) - switch { - case flag&0x1 > 0 && isIPv4: - // ipv4 - srcIP, _ = netip.AddrFromSlice(buf[inp+76 : inp+80]) - srcIsIPv4 = true - case flag&0x2 > 0 && !isIPv4: - // ipv6 - srcIP, _ = netip.AddrFromSlice(buf[inp+64 : inp+80]) - default: - continue - } - srcIP = srcIP.Unmap() - - if ip == srcIP { - // xsocket_n.so_last_pid - pid := readNativeUint32(buf[so+68 : so+72]) - pp, err := getExecPathFromPID(pid) - return 0, pp, err - } - - // 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 0, fallbackUDPProcess, nil - } - - return 0, "", 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_amd64.go b/component/process/process_freebsd_amd64.go deleted file mode 100644 index cb43c5ffd3..0000000000 --- a/component/process/process_freebsd_amd64.go +++ /dev/null @@ -1,235 +0,0 @@ -package process - -import ( - "encoding/binary" - "fmt" - "net/netip" - "strconv" - "strings" - "sync" - "syscall" - "unsafe" - - "github.com/metacubex/mihomo/log" -) - -// store process name for when dealing with multiple PROCESS-NAME rules -var ( - defaultSearcher *searcher - - once sync.Once -) - -func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) { - once.Do(func() { - if err := initSearcher(); err != nil { - log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error()) - log.Warnln("All PROCESS-NAME rules will be skipped") - return - } - }) - - if defaultSearcher == nil { - return 0, "", ErrPlatformNotSupport - } - - var spath string - isTCP := network == TCP - switch network { - case TCP: - spath = "net.inet.tcp.pcblist" - case UDP: - spath = "net.inet.udp.pcblist" - default: - return 0, "", ErrInvalidNetwork - } - - value, err := syscall.Sysctl(spath) - if err != nil { - return 0, "", err - } - - buf := []byte(value) - pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP) - if err != nil { - return 0, "", err - } - - pp, err := getExecPathFromPID(pid) - return 0, pp, err -} - -func getExecPathFromPID(pid uint32) (string, error) { - buf := make([]byte, 2048) - size := uint64(len(buf)) - // CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid - mib := [4]uint32{1, 14, 12, pid} - - _, _, errno := syscall.Syscall6( - syscall.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 "", errno - } - - return string(buf[:size-1]), nil -} - -func readNativeUint32(b []byte) uint32 { - return *(*uint32)(unsafe.Pointer(&b[0])) -} - -type searcher struct { - // sizeof(struct xinpgen) - headSize int - // sizeof(struct xtcpcb) - tcpItemSize int - // sizeof(struct xinpcb) - udpItemSize int - udpInpOffset int - port int - ip int - vflag int - socket int - - // sizeof(struct xfile) - fileItemSize int - data int - pid int -} - -func (s *searcher) Search(buf []byte, ip netip.Addr, port uint16, isTCP bool) (uint32, error) { - var itemSize int - var inpOffset int - - if isTCP { - // struct xtcpcb - itemSize = s.tcpItemSize - inpOffset = 8 - } else { - // struct xinpcb - itemSize = s.udpItemSize - inpOffset = s.udpInpOffset - } - - isIPv4 := ip.Is4() - // skip the first xinpgen block - for i := s.headSize; i+itemSize <= len(buf); i += itemSize { - inp := i + inpOffset - - srcPort := binary.BigEndian.Uint16(buf[inp+s.port : inp+s.port+2]) - - if port != srcPort { - continue - } - - // xinpcb.inp_vflag - flag := buf[inp+s.vflag] - - var srcIP netip.Addr - switch { - case flag&0x1 > 0 && isIPv4: - // ipv4 - srcIP, _ = netip.AddrFromSlice(buf[inp+s.ip : inp+s.ip+4]) - case flag&0x2 > 0 && !isIPv4: - // ipv6 - srcIP, _ = netip.AddrFromSlice(buf[inp+s.ip-12 : inp+s.ip+4]) - default: - continue - } - srcIP = srcIP.Unmap() - - if ip != srcIP { - continue - } - - // xsocket.xso_so, interpreted as big endian anyway since it's only used for comparison - socket := binary.BigEndian.Uint64(buf[inp+s.socket : inp+s.socket+8]) - return s.searchSocketPid(socket) - } - return 0, ErrNotFound -} - -func (s *searcher) searchSocketPid(socket uint64) (uint32, error) { - value, err := syscall.Sysctl("kern.file") - if err != nil { - return 0, err - } - - buf := []byte(value) - - // struct xfile - itemSize := s.fileItemSize - for i := 0; i+itemSize <= len(buf); i += itemSize { - // xfile.xf_data - data := binary.BigEndian.Uint64(buf[i+s.data : i+s.data+8]) - if data == socket { - // xfile.xf_pid - pid := readNativeUint32(buf[i+s.pid : i+s.pid+4]) - return pid, nil - } - } - return 0, ErrNotFound -} - -func newSearcher(major int) *searcher { - var s *searcher - switch major { - case 11: - s = &searcher{ - headSize: 32, - tcpItemSize: 1304, - udpItemSize: 632, - port: 198, - ip: 228, - vflag: 116, - socket: 88, - fileItemSize: 80, - data: 56, - pid: 8, - udpInpOffset: 8, - } - case 12: - fallthrough - case 13: - s = &searcher{ - headSize: 64, - tcpItemSize: 744, - udpItemSize: 400, - port: 254, - ip: 284, - vflag: 392, - socket: 16, - fileItemSize: 128, - data: 56, - pid: 8, - } - } - return s -} - -func initSearcher() error { - osRelease, err := syscall.Sysctl("kern.osrelease") - if err != nil { - return err - } - - dot := strings.Index(osRelease, ".") - if dot != -1 { - osRelease = osRelease[:dot] - } - major, err := strconv.Atoi(osRelease) - if err != nil { - return err - } - defaultSearcher = newSearcher(major) - if defaultSearcher == nil { - return fmt.Errorf("unsupported freebsd version %d", major) - } - return nil -} diff --git a/component/process/process_linux.go b/component/process/process_linux.go deleted file mode 100644 index 3ce45ae816..0000000000 --- a/component/process/process_linux.go +++ /dev/null @@ -1,200 +0,0 @@ -package process - -import ( - "bytes" - "encoding/binary" - "fmt" - "net/netip" - "os" - "path" - "path/filepath" - "runtime" - "strings" - "syscall" - "unicode" - "unsafe" - - "github.com/mdlayher/netlink" - "golang.org/x/sys/unix" -) - -const ( - SOCK_DIAG_BY_FAMILY = 20 - inetDiagRequestSize = int(unsafe.Sizeof(inetDiagRequest{})) - inetDiagResponseSize = int(unsafe.Sizeof(inetDiagResponse{})) -) - -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 findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) { - uid, inode, err := resolveSocketByNetlink(network, ip, srcPort) - if err != nil { - return 0, "", err - } - pp, err := resolveProcessNameByProcSearch(inode, uid) - return uid, pp, err -} - -func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { - request := &inetDiagRequest{ - States: 0xffffffff, - Cookie: [2]uint32{0xffffffff, 0xffffffff}, - } - - if ip.Is4() { - request.Family = unix.AF_INET - } else { - request.Family = unix.AF_INET6 - } - - if strings.HasPrefix(network, "tcp") { - request.Protocol = unix.IPPROTO_TCP - } else if strings.HasPrefix(network, "udp") { - request.Protocol = unix.IPPROTO_UDP - } else { - return 0, 0, ErrInvalidNetwork - } - - copy(request.Src[:], ip.AsSlice()) - - binary.BigEndian.PutUint16(request.SrcPort[:], uint16(srcPort)) - - 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: SOCK_DIAG_BY_FAMILY, - Flags: netlink.Request | netlink.Dump, - }, - Data: (*(*[inetDiagRequestSize]byte)(unsafe.Pointer(request)))[:], - } - - messages, err := conn.Execute(message) - if err != nil { - return 0, 0, err - } - - for _, msg := range messages { - if len(msg.Data) < inetDiagResponseSize { - continue - } - - response := (*inetDiagResponse)(unsafe.Pointer(&msg.Data[0])) - - return response.UID, response.INode, nil - } - - return 0, 0, ErrNotFound -} - -func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) { - files, err := os.ReadDir("/proc") - if err != nil { - return "", err - } - - buffer := make([]byte, unix.PathMax) - socket := fmt.Appendf(nil, "socket:[%d]", inode) - - for _, f := range files { - if !f.IsDir() || !isPid(f.Name()) { - continue - } - - info, err := f.Info() - if err != nil { - return "", err - } - if info.Sys().(*syscall.Stat_t).Uid != uid { - continue - } - - processPath := filepath.Join("/proc", f.Name()) - fdPath := filepath.Join(processPath, "fd") - - fds, err := os.ReadDir(fdPath) - if err != nil { - continue - } - - for _, fd := range fds { - n, err := unix.Readlink(filepath.Join(fdPath, fd.Name()), buffer) - if err != nil { - continue - } - - if runtime.GOOS == "android" { - if bytes.Equal(buffer[:n], socket) { - cmdline, err := os.ReadFile(path.Join(processPath, "cmdline")) - if err != nil { - return "", err - } - - return splitCmdline(cmdline), nil - } - } else { - if bytes.Equal(buffer[:n], socket) { - return os.Readlink(filepath.Join(processPath, "exe")) - } - } - } - } - - return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode) -} - -func splitCmdline(cmdline []byte) string { - cmdline = bytes.Trim(cmdline, " ") - - idx := bytes.IndexFunc(cmdline, func(r rune) bool { - return unicode.IsControl(r) || unicode.IsSpace(r) - }) - - if idx == -1 { - return filepath.Base(string(cmdline)) - } - return filepath.Base(string(cmdline[:idx])) -} - -func isPid(s string) bool { - return strings.IndexFunc(s, func(r rune) bool { - return !unicode.IsDigit(r) - }) == -1 -} diff --git a/component/process/process_other.go b/component/process/process_other.go deleted file mode 100644 index eea6e5fd3c..0000000000 --- a/component/process/process_other.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !darwin && !linux && !windows && (!freebsd || !amd64) - -package process - -import "net/netip" - -func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) { - return 0, "", ErrPlatformNotSupport -} - -func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { - return 0, 0, ErrPlatformNotSupport -} diff --git a/component/process/process_windows.go b/component/process/process_windows.go deleted file mode 100644 index 6ab0fef0f9..0000000000 --- a/component/process/process_windows.go +++ /dev/null @@ -1,229 +0,0 @@ -package process - -import ( - "fmt" - "net/netip" - "sync" - "syscall" - "unsafe" - - "github.com/metacubex/mihomo/log" - - "golang.org/x/sys/windows" -) - -const ( - tcpTableFunc = "GetExtendedTcpTable" - tcpTablePidConn = 4 - udpTableFunc = "GetExtendedUdpTable" - udpTablePid = 1 - queryProcNameFunc = "QueryFullProcessImageNameW" -) - -var ( - getExTCPTable uintptr - getExUDPTable uintptr - queryProcName uintptr - - once sync.Once -) - -func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) { - return 0, 0, ErrPlatformNotSupport -} - -func initWin32API() error { - h, err := windows.LoadLibrary("iphlpapi.dll") - if err != nil { - return fmt.Errorf("LoadLibrary iphlpapi.dll failed: %s", err.Error()) - } - - getExTCPTable, err = windows.GetProcAddress(h, tcpTableFunc) - if err != nil { - return fmt.Errorf("GetProcAddress of %s failed: %s", tcpTableFunc, err.Error()) - } - - getExUDPTable, err = windows.GetProcAddress(h, udpTableFunc) - if err != nil { - return fmt.Errorf("GetProcAddress of %s failed: %s", udpTableFunc, err.Error()) - } - - h, err = windows.LoadLibrary("kernel32.dll") - if err != nil { - return fmt.Errorf("LoadLibrary kernel32.dll failed: %s", err.Error()) - } - - queryProcName, err = windows.GetProcAddress(h, queryProcNameFunc) - if err != nil { - return fmt.Errorf("GetProcAddress of %s failed: %s", queryProcNameFunc, err.Error()) - } - - return nil -} - -func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) { - once.Do(func() { - err := initWin32API() - if err != nil { - log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error()) - log.Warnln("All PROCESS-NAMES rules will be skipped") - return - } - }) - family := windows.AF_INET - if ip.Is6() { - family = windows.AF_INET6 - } - - var class int - var fn uintptr - switch network { - case TCP: - fn = getExTCPTable - class = tcpTablePidConn - case UDP: - fn = getExUDPTable - class = udpTablePid - default: - return 0, "", ErrInvalidNetwork - } - - buf, err := getTransportTable(fn, family, class) - if err != nil { - return 0, "", err - } - - s := newSearcher(family == windows.AF_INET, network == TCP) - - pid, err := s.Search(buf, ip, uint16(srcPort)) - if err != nil { - return 0, "", err - } - pp, err := getExecPathFromPID(pid) - return 0, pp, err -} - -type searcher struct { - itemSize int - port int - ip int - ipSize int - pid int - tcpState int -} - -func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) { - n := int(readNativeUint32(b[:4])) - itemSize := s.itemSize - for i := 0; i < n; i++ { - row := b[4+itemSize*i : 4+itemSize*(i+1)] - - if s.tcpState >= 0 { - tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4]) - // MIB_TCP_STATE_ESTAB, only check established connections for TCP - if tcpState != 5 { - continue - } - } - - // according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian. - // this field can be illustrated as follows depends on different machine endianess: - // little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB) - // big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB) - // so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32 - srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4]))) - if srcPort != port { - continue - } - - srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize]) - srcIP = srcIP.Unmap() - // windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto - if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) { - continue - } - - pid := readNativeUint32(row[s.pid : s.pid+4]) - return pid, nil - } - return 0, ErrNotFound -} - -func newSearcher(isV4, isTCP bool) *searcher { - var itemSize, port, ip, ipSize, pid int - tcpState := -1 - switch { - case isV4 && isTCP: - // struct MIB_TCPROW_OWNER_PID - itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0 - case isV4 && !isTCP: - // struct MIB_UDPROW_OWNER_PID - itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8 - case !isV4 && isTCP: - // struct MIB_TCP6ROW_OWNER_PID - itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48 - case !isV4 && !isTCP: - // struct MIB_UDP6ROW_OWNER_PID - itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24 - } - - return &searcher{ - itemSize: itemSize, - port: port, - ip: ip, - ipSize: ipSize, - pid: pid, - tcpState: tcpState, - } -} - -func getTransportTable(fn uintptr, family int, class int) ([]byte, error) { - for size, buf := uint32(8), make([]byte, 8); ; { - ptr := unsafe.Pointer(&buf[0]) - err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0) - - switch err { - case 0: - return buf, nil - case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER): - buf = make([]byte, size) - default: - return nil, fmt.Errorf("syscall error: %d", err) - } - } -} - -func readNativeUint32(b []byte) uint32 { - return *(*uint32)(unsafe.Pointer(&b[0])) -} - -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, syscall.MAX_LONG_PATH) - size := uint32(len(buf)) - r1, _, err := syscall.Syscall6( - queryProcName, 4, - uintptr(h), - uintptr(0), - uintptr(unsafe.Pointer(&buf[0])), - uintptr(unsafe.Pointer(&size)), - 0, 0) - if r1 == 0 { - return "", err - } - return syscall.UTF16ToString(buf[:size]), nil -} diff --git a/component/profile/cachefile/cache.go b/component/profile/cachefile/cache.go deleted file mode 100644 index 7b4cdfc2a6..0000000000 --- a/component/profile/cachefile/cache.go +++ /dev/null @@ -1,104 +0,0 @@ -package cachefile - -import ( - "os" - "sync" - "time" - - "github.com/metacubex/mihomo/component/profile" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/bbolt" -) - -var ( - initOnce sync.Once - fileMode os.FileMode = 0o666 - defaultCache *CacheFile - - bucketSelected = []byte("selected") - bucketFakeip = []byte("fakeip") - bucketETag = []byte("etag") - bucketSubscriptionInfo = []byte("subscriptioninfo") -) - -// 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) Close() error { - return c.DB.Close() -} - -func initCache() { - 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()) - } - - defaultCache = &CacheFile{ - DB: db, - } -} - -// Cache return singleton of CacheFile -func Cache() *CacheFile { - initOnce.Do(initCache) - - return defaultCache -} diff --git a/component/profile/cachefile/etag.go b/component/profile/cachefile/etag.go deleted file mode 100644 index 028fe50433..0000000000 --- a/component/profile/cachefile/etag.go +++ /dev/null @@ -1,58 +0,0 @@ -package cachefile - -import ( - "time" - - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/bbolt" - "github.com/vmihailenco/msgpack/v5" -) - -type EtagWithHash struct { - Hash utils.HashType - ETag string - Time time.Time -} - -func (c *CacheFile) SetETagWithHash(url string, etagWithHash EtagWithHash) { - if c.DB == nil { - return - } - - data, err := msgpack.Marshal(etagWithHash) - if err != nil { - return // maybe panic is better - } - - err = c.DB.Batch(func(t *bbolt.Tx) error { - bucket, err := t.CreateBucketIfNotExists(bucketETag) - if err != nil { - return err - } - - return bucket.Put([]byte(url), data) - }) - if err != nil { - log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) - return - } -} -func (c *CacheFile) GetETagWithHash(key string) (etagWithHash EtagWithHash) { - if c.DB == nil { - return - } - c.DB.View(func(t *bbolt.Tx) error { - if bucket := t.Bucket(bucketETag); bucket != nil { - if v := bucket.Get([]byte(key)); v != nil { - if err := msgpack.Unmarshal(v, &etagWithHash); err != nil { - return err - } - } - } - return nil - }) - - return -} diff --git a/component/profile/cachefile/fakeip.go b/component/profile/cachefile/fakeip.go deleted file mode 100644 index 20a09f9c95..0000000000 --- a/component/profile/cachefile/fakeip.go +++ /dev/null @@ -1,115 +0,0 @@ -package cachefile - -import ( - "net/netip" - - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/bbolt" -) - -type FakeIpStore struct { - *CacheFile -} - -func (c *CacheFile) FakeIpStore() *FakeIpStore { - return &FakeIpStore{c} -} - -func (c *FakeIpStore) GetByHost(host string) (ip netip.Addr, exist bool) { - if c.DB == nil { - return - } - c.DB.View(func(t *bbolt.Tx) error { - if bucket := t.Bucket(bucketFakeip); bucket != nil { - if v := bucket.Get([]byte(host)); v != nil { - ip, exist = netip.AddrFromSlice(v) - } - } - return nil - }) - return -} - -func (c *FakeIpStore) PutByHost(host string, ip netip.Addr) { - if c.DB == nil { - return - } - err := c.DB.Batch(func(t *bbolt.Tx) error { - bucket, err := t.CreateBucketIfNotExists(bucketFakeip) - if err != nil { - return err - } - return bucket.Put([]byte(host), ip.AsSlice()) - }) - if err != nil { - log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) - } -} - -func (c *FakeIpStore) GetByIP(ip netip.Addr) (host string, exist bool) { - if c.DB == nil { - return - } - c.DB.View(func(t *bbolt.Tx) error { - if bucket := t.Bucket(bucketFakeip); bucket != nil { - if v := bucket.Get(ip.AsSlice()); v != nil { - host, exist = string(v), true - } - } - return nil - }) - return -} - -func (c *FakeIpStore) PutByIP(ip netip.Addr, host string) { - if c.DB == nil { - return - } - err := c.DB.Batch(func(t *bbolt.Tx) error { - bucket, err := t.CreateBucketIfNotExists(bucketFakeip) - if err != nil { - return err - } - return bucket.Put(ip.AsSlice(), []byte(host)) - }) - if err != nil { - log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) - } -} - -func (c *FakeIpStore) DelByIP(ip netip.Addr) { - if c.DB == nil { - return - } - - addr := ip.AsSlice() - err := c.DB.Batch(func(t *bbolt.Tx) error { - bucket, err := t.CreateBucketIfNotExists(bucketFakeip) - if err != nil { - return err - } - host := bucket.Get(addr) - err = bucket.Delete(addr) - 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()) - } -} - -func (c *FakeIpStore) FlushFakeIP() error { - err := c.DB.Batch(func(t *bbolt.Tx) error { - bucket := t.Bucket(bucketFakeip) - if bucket == nil { - return nil - } - return t.DeleteBucket(bucketFakeip) - }) - return err -} diff --git a/component/profile/cachefile/subscriptioninfo.go b/component/profile/cachefile/subscriptioninfo.go deleted file mode 100644 index c68f92ebc9..0000000000 --- a/component/profile/cachefile/subscriptioninfo.go +++ /dev/null @@ -1,41 +0,0 @@ -package cachefile - -import ( - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/bbolt" -) - -func (c *CacheFile) SetSubscriptionInfo(name string, userInfo string) { - if c.DB == nil { - return - } - - err := c.DB.Batch(func(t *bbolt.Tx) error { - bucket, err := t.CreateBucketIfNotExists(bucketSubscriptionInfo) - if err != nil { - return err - } - - return bucket.Put([]byte(name), []byte(userInfo)) - }) - if err != nil { - log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) - return - } -} -func (c *CacheFile) GetSubscriptionInfo(name string) (userInfo string) { - if c.DB == nil { - return - } - c.DB.View(func(t *bbolt.Tx) error { - if bucket := t.Bucket(bucketSubscriptionInfo); bucket != nil { - if v := bucket.Get([]byte(name)); v != nil { - userInfo = string(v) - } - } - return nil - }) - - return -} diff --git a/component/profile/profile.go b/component/profile/profile.go deleted file mode 100644 index 36db8cc39a..0000000000 --- a/component/profile/profile.go +++ /dev/null @@ -1,8 +0,0 @@ -package profile - -import ( - "github.com/metacubex/mihomo/common/atomic" -) - -// StoreSelected is a global switch for storing selected proxy to cache -var StoreSelected = atomic.NewBool(true) diff --git a/component/proxydialer/proxydialer.go b/component/proxydialer/proxydialer.go deleted file mode 100644 index e1a3cda0ae..0000000000 --- a/component/proxydialer/proxydialer.go +++ /dev/null @@ -1,93 +0,0 @@ -package proxydialer - -import ( - "context" - "errors" - "fmt" - "net" - "net/netip" - "strings" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/tunnel" - "github.com/metacubex/mihomo/tunnel/statistic" -) - -type proxyDialer struct { - proxy C.ProxyAdapter - dialer C.Dialer - statistic bool -} - -func New(proxy C.ProxyAdapter, dialer C.Dialer, statistic bool) C.Dialer { - return proxyDialer{proxy: proxy, dialer: dialer, statistic: statistic} -} - -func NewByName(proxyName string, dialer C.Dialer) (C.Dialer, error) { - proxies := tunnel.Proxies() - if proxy, ok := proxies[proxyName]; ok { - return New(proxy, dialer, true), nil - } - return nil, fmt.Errorf("proxyName[%s] not found", proxyName) -} - -func (p proxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - currentMeta := &C.Metadata{Type: C.INNER} - if err := currentMeta.SetRemoteAddress(address); err != nil { - return nil, err - } - if strings.Contains(network, "udp") { // using in wireguard outbound - if !currentMeta.Resolved() { - ip, err := resolver.ResolveIP(ctx, currentMeta.Host) - if err != nil { - return nil, errors.New("can't resolve ip") - } - currentMeta.DstIP = ip - } - pc, err := p.listenPacket(ctx, currentMeta) - if err != nil { - return nil, err - } - return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil - } - var conn C.Conn - var err error - if _, ok := p.dialer.(dialer.Dialer); ok { // first using old function to let mux work - conn, err = p.proxy.DialContext(ctx, currentMeta) - } else { - conn, err = p.proxy.DialContextWithDialer(ctx, p.dialer, currentMeta) - } - if err != nil { - return nil, err - } - if p.statistic { - conn = statistic.NewTCPTracker(conn, statistic.DefaultManager, currentMeta, nil, 0, 0, false) - } - return conn, err -} - -func (p proxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { - currentMeta := &C.Metadata{Type: C.INNER, DstIP: rAddrPort.Addr(), DstPort: rAddrPort.Port()} - return p.listenPacket(ctx, currentMeta) -} - -func (p proxyDialer) listenPacket(ctx context.Context, currentMeta *C.Metadata) (C.PacketConn, error) { - var pc C.PacketConn - var err error - currentMeta.NetWork = C.UDP - if _, ok := p.dialer.(dialer.Dialer); ok { // first using old function to let mux work - pc, err = p.proxy.ListenPacketContext(ctx, currentMeta) - } else { - pc, err = p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta) - } - if err != nil { - return nil, err - } - if p.statistic { - pc = statistic.NewUDPTracker(pc, statistic.DefaultManager, currentMeta, nil, 0, 0, false) - } - return pc, nil -} diff --git a/component/proxydialer/sing.go b/component/proxydialer/sing.go deleted file mode 100644 index 51d685e8db..0000000000 --- a/component/proxydialer/sing.go +++ /dev/null @@ -1,82 +0,0 @@ -package proxydialer - -import ( - "context" - "net" - - C "github.com/metacubex/mihomo/constant" - - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" -) - -type SingDialer interface { - N.Dialer - SetDialer(dialer C.Dialer) -} - -type singDialer proxyDialer - -var _ N.Dialer = (*singDialer)(nil) - -func (d *singDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return (*proxyDialer)(d).DialContext(ctx, network, destination.String()) -} - -func (d *singDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return (*proxyDialer)(d).ListenPacket(ctx, "udp", "", destination.AddrPort()) -} - -func (d *singDialer) SetDialer(dialer C.Dialer) { - (*proxyDialer)(d).dialer = dialer -} - -func NewSingDialer(proxy C.ProxyAdapter, dialer C.Dialer, statistic bool) SingDialer { - return (*singDialer)(&proxyDialer{ - proxy: proxy, - dialer: dialer, - statistic: statistic, - }) -} - -type byNameSingDialer struct { - dialer C.Dialer - proxyName string -} - -var _ N.Dialer = (*byNameSingDialer)(nil) - -func (d *byNameSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - var cDialer C.Dialer = d.dialer - if len(d.proxyName) > 0 { - pd, err := NewByName(d.proxyName, d.dialer) - if err != nil { - return nil, err - } - cDialer = pd - } - return cDialer.DialContext(ctx, network, destination.String()) -} - -func (d *byNameSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - var cDialer C.Dialer = d.dialer - if len(d.proxyName) > 0 { - pd, err := NewByName(d.proxyName, d.dialer) - if err != nil { - return nil, err - } - cDialer = pd - } - return cDialer.ListenPacket(ctx, "udp", "", destination.AddrPort()) -} - -func (d *byNameSingDialer) SetDialer(dialer C.Dialer) { - d.dialer = dialer -} - -func NewByNameSingDialer(proxyName string, dialer C.Dialer) SingDialer { - return &byNameSingDialer{ - dialer: dialer, - proxyName: proxyName, - } -} diff --git a/component/proxydialer/slowdown.go b/component/proxydialer/slowdown.go deleted file mode 100644 index c62fc344f5..0000000000 --- a/component/proxydialer/slowdown.go +++ /dev/null @@ -1,34 +0,0 @@ -package proxydialer - -import ( - "context" - "net" - "net/netip" - - "github.com/metacubex/mihomo/component/slowdown" - C "github.com/metacubex/mihomo/constant" -) - -type SlowDownDialer struct { - C.Dialer - Slowdown *slowdown.SlowDown -} - -func (d SlowDownDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - return slowdown.Do(d.Slowdown, ctx, func() (net.Conn, error) { - return d.Dialer.DialContext(ctx, network, address) - }) -} - -func (d SlowDownDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { - return slowdown.Do(d.Slowdown, ctx, func() (net.PacketConn, error) { - return d.Dialer.ListenPacket(ctx, network, address, rAddrPort) - }) -} - -func NewSlowDownDialer(d C.Dialer, sd *slowdown.SlowDown) SlowDownDialer { - return SlowDownDialer{ - Dialer: d, - Slowdown: sd, - } -} diff --git a/component/proxydialer/slowdown_sing.go b/component/proxydialer/slowdown_sing.go deleted file mode 100644 index c944267073..0000000000 --- a/component/proxydialer/slowdown_sing.go +++ /dev/null @@ -1,33 +0,0 @@ -package proxydialer - -import ( - "context" - "net" - - "github.com/metacubex/mihomo/component/slowdown" - M "github.com/metacubex/sing/common/metadata" -) - -type SlowDownSingDialer struct { - SingDialer - Slowdown *slowdown.SlowDown -} - -func (d SlowDownSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return slowdown.Do(d.Slowdown, ctx, func() (net.Conn, error) { - return d.SingDialer.DialContext(ctx, network, destination) - }) -} - -func (d SlowDownSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return slowdown.Do(d.Slowdown, ctx, func() (net.PacketConn, error) { - return d.SingDialer.ListenPacket(ctx, destination) - }) -} - -func NewSlowDownSingDialer(d SingDialer, sd *slowdown.SlowDown) SlowDownSingDialer { - return SlowDownSingDialer{ - SingDialer: d, - Slowdown: sd, - } -} diff --git a/component/resolver/enhancer.go b/component/resolver/enhancer.go deleted file mode 100644 index f00a13b398..0000000000 --- a/component/resolver/enhancer.go +++ /dev/null @@ -1,84 +0,0 @@ -package resolver - -import "net/netip" - -var DefaultHostMapper Enhancer - -type Enhancer interface { - FakeIPEnabled() bool - MappingEnabled() bool - IsFakeIP(netip.Addr) bool - IsFakeBroadcastIP(netip.Addr) bool - IsExistFakeIP(netip.Addr) bool - FindHostByIP(netip.Addr) (string, bool) - FlushFakeIP() error - InsertHostByIP(netip.Addr, string) - StoreFakePoolState() -} - -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 netip.Addr) bool { - if mapper := DefaultHostMapper; mapper != nil { - return mapper.IsFakeIP(ip) - } - - return false -} - -func IsFakeBroadcastIP(ip netip.Addr) bool { - if mapper := DefaultHostMapper; mapper != nil { - return mapper.IsFakeBroadcastIP(ip) - } - - return false -} - -func IsExistFakeIP(ip netip.Addr) bool { - if mapper := DefaultHostMapper; mapper != nil { - return mapper.IsExistFakeIP(ip) - } - - return false -} - -func InsertHostByIP(ip netip.Addr, host string) { - if mapper := DefaultHostMapper; mapper != nil { - mapper.InsertHostByIP(ip, host) - } -} - -func FindHostByIP(ip netip.Addr) (string, bool) { - if mapper := DefaultHostMapper; mapper != nil { - return mapper.FindHostByIP(ip) - } - - return "", false -} - -func FlushFakeIP() error { - if mapper := DefaultHostMapper; mapper != nil { - return mapper.FlushFakeIP() - } - return nil -} - -func StoreFakePoolState() { - if mapper := DefaultHostMapper; mapper != nil { - mapper.StoreFakePoolState() - } -} diff --git a/component/resolver/host.go b/component/resolver/host.go deleted file mode 100644 index 34da8e9f58..0000000000 --- a/component/resolver/host.go +++ /dev/null @@ -1,129 +0,0 @@ -package resolver - -import ( - "errors" - "net/netip" - "os" - "strconv" - "strings" - _ "unsafe" - - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/resolver/hosts" - "github.com/metacubex/mihomo/component/trie" - "github.com/metacubex/randv2" -) - -var ( - DisableSystemHosts, _ = strconv.ParseBool(os.Getenv("DISABLE_SYSTEM_HOSTS")) - UseSystemHosts bool -) - -type Hosts struct { - *trie.DomainTrie[HostValue] -} - -func NewHosts(hosts *trie.DomainTrie[HostValue]) Hosts { - return Hosts{ - hosts, - } -} - -// Return the search result and whether to match the parameter `isDomain` -func (h *Hosts) Search(domain string, isDomain bool) (*HostValue, bool) { - if value := h.DomainTrie.Search(domain); value != nil { - hostValue := value.Data() - for { - if isDomain && hostValue.IsDomain { - return &hostValue, true - } else { - if node := h.DomainTrie.Search(hostValue.Domain); node != nil { - hostValue = node.Data() - } else { - break - } - } - } - if isDomain == hostValue.IsDomain { - return &hostValue, true - } - - return &hostValue, false - } - - if !isDomain && !DisableSystemHosts && UseSystemHosts { - addr, _ := hosts.LookupStaticHost(domain) - if hostValue, err := NewHostValue(addr); err == nil { - return &hostValue, true - } - } - return nil, false -} - -type HostValue struct { - IsDomain bool - IPs []netip.Addr - Domain string -} - -func NewHostValue(value any) (HostValue, error) { - isDomain := true - ips := make([]netip.Addr, 0) - domain := "" - if valueArr, err := utils.ToStringSlice(value); err != nil { - return HostValue{}, err - } else { - if len(valueArr) > 1 { - isDomain = false - for _, str := range valueArr { - if ip, err := netip.ParseAddr(str); err == nil { - ips = append(ips, ip) - } else { - return HostValue{}, err - } - } - } else if len(valueArr) == 1 { - host := valueArr[0] - if ip, err := netip.ParseAddr(host); err == nil { - ips = append(ips, ip) - isDomain = false - } else { - domain = host - } - } - } - if isDomain { - return NewHostValueByDomain(domain) - } else { - return NewHostValueByIPs(ips) - } -} - -func NewHostValueByIPs(ips []netip.Addr) (HostValue, error) { - if len(ips) == 0 { - return HostValue{}, errors.New("ip list is empty") - } - return HostValue{ - IsDomain: false, - IPs: ips, - }, nil -} - -func NewHostValueByDomain(domain string) (HostValue, error) { - domain = strings.Trim(domain, ".") - item := strings.Split(domain, ".") - if len(item) < 2 { - return HostValue{}, errors.New("invaild domain") - } - return HostValue{ - IsDomain: true, - Domain: domain, - }, nil -} - -func (hv HostValue) RandIP() (netip.Addr, error) { - if hv.IsDomain { - return netip.Addr{}, errors.New("value type is error") - } - return hv.IPs[randv2.IntN(len(hv.IPs))], nil -} diff --git a/component/resolver/hosts/hosts.go b/component/resolver/hosts/hosts.go deleted file mode 100644 index 780ebb2479..0000000000 --- a/component/resolver/hosts/hosts.go +++ /dev/null @@ -1,309 +0,0 @@ -package hosts - -// this file copy and modify from golang's std net/hosts.go - -import ( - "errors" - "io" - "io/fs" - "net/netip" - "os" - "strings" - "sync" - "time" -) - -var hostsFilePath = "/etc/hosts" - -const cacheMaxAge = 5 * time.Second - -func parseLiteralIP(addr string) string { - ip, err := netip.ParseAddr(addr) - if err != nil { - return "" - } - return ip.String() -} - -type byName struct { - addrs []string - canonicalName string -} - -// hosts contains known host entries. -var hosts struct { - sync.Mutex - - // Key for the list of literal IP addresses must be a host - // name. It would be part of DNS labels, a FQDN or an absolute - // FQDN. - // For now the key is converted to lower case for convenience. - byName map[string]byName - - // Key for the list of host names must be a literal IP address - // including IPv6 address with zone identifier. - // We don't support old-classful IP address notation. - byAddr map[string][]string - - expire time.Time - path string - mtime time.Time - size int64 -} - -func readHosts() { - now := time.Now() - hp := hostsFilePath - - if now.Before(hosts.expire) && hosts.path == hp && len(hosts.byName) > 0 { - return - } - mtime, size, err := stat(hp) - if err == nil && hosts.path == hp && hosts.mtime.Equal(mtime) && hosts.size == size { - hosts.expire = now.Add(cacheMaxAge) - return - } - - hs := make(map[string]byName) - is := make(map[string][]string) - - file, err := open(hp) - if err != nil { - if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrPermission) { - return - } - } - - if file != nil { - defer file.close() - for line, ok := file.readLine(); ok; line, ok = file.readLine() { - if i := strings.IndexByte(line, '#'); i >= 0 { - // Discard comments. - line = line[0:i] - } - f := getFields(line) - if len(f) < 2 { - continue - } - addr := parseLiteralIP(f[0]) - if addr == "" { - continue - } - - var canonical string - for i := 1; i < len(f); i++ { - name := absDomainName(f[i]) - h := []byte(f[i]) - lowerASCIIBytes(h) - key := absDomainName(string(h)) - - if i == 1 { - canonical = key - } - - is[addr] = append(is[addr], name) - - if v, ok := hs[key]; ok { - hs[key] = byName{ - addrs: append(v.addrs, addr), - canonicalName: v.canonicalName, - } - continue - } - - hs[key] = byName{ - addrs: []string{addr}, - canonicalName: canonical, - } - } - } - } - // Update the data cache. - hosts.expire = now.Add(cacheMaxAge) - hosts.path = hp - hosts.byName = hs - hosts.byAddr = is - hosts.mtime = mtime - hosts.size = size -} - -// LookupStaticHost looks up the addresses and the canonical name for the given host from /etc/hosts. -func LookupStaticHost(host string) ([]string, string) { - hosts.Lock() - defer hosts.Unlock() - readHosts() - if len(hosts.byName) != 0 { - if hasUpperCase(host) { - lowerHost := []byte(host) - lowerASCIIBytes(lowerHost) - host = string(lowerHost) - } - if byName, ok := hosts.byName[absDomainName(host)]; ok { - ipsCp := make([]string, len(byName.addrs)) - copy(ipsCp, byName.addrs) - return ipsCp, byName.canonicalName - } - } - return nil, "" -} - -// LookupStaticAddr looks up the hosts for the given address from /etc/hosts. -func LookupStaticAddr(addr string) []string { - hosts.Lock() - defer hosts.Unlock() - readHosts() - addr = parseLiteralIP(addr) - if addr == "" { - return nil - } - if len(hosts.byAddr) != 0 { - if hosts, ok := hosts.byAddr[addr]; ok { - hostsCp := make([]string, len(hosts)) - copy(hostsCp, hosts) - return hostsCp - } - } - return nil -} - -func stat(name string) (mtime time.Time, size int64, err error) { - st, err := os.Stat(name) - if err != nil { - return time.Time{}, 0, err - } - return st.ModTime(), st.Size(), nil -} - -type file struct { - file *os.File - data []byte - atEOF bool -} - -func (f *file) close() { f.file.Close() } - -func (f *file) getLineFromData() (s string, ok bool) { - data := f.data - i := 0 - for i = 0; i < len(data); i++ { - if data[i] == '\n' { - s = string(data[0:i]) - ok = true - // move data - i++ - n := len(data) - i - copy(data[0:], data[i:]) - f.data = data[0:n] - return - } - } - if f.atEOF && len(f.data) > 0 { - // EOF, return all we have - s = string(data) - f.data = f.data[0:0] - ok = true - } - return -} - -func (f *file) readLine() (s string, ok bool) { - if s, ok = f.getLineFromData(); ok { - return - } - if len(f.data) < cap(f.data) { - ln := len(f.data) - n, err := io.ReadFull(f.file, f.data[ln:cap(f.data)]) - if n >= 0 { - f.data = f.data[0 : ln+n] - } - if err == io.EOF || err == io.ErrUnexpectedEOF { - f.atEOF = true - } - } - s, ok = f.getLineFromData() - return -} - -func (f *file) stat() (mtime time.Time, size int64, err error) { - st, err := f.file.Stat() - if err != nil { - return time.Time{}, 0, err - } - return st.ModTime(), st.Size(), nil -} - -func open(name string) (*file, error) { - fd, err := os.Open(name) - if err != nil { - return nil, err - } - return &file{fd, make([]byte, 0, 64*1024), false}, nil -} - -func getFields(s string) []string { return splitAtBytes(s, " \r\t\n") } - -// Count occurrences in s of any bytes in t. -func countAnyByte(s string, t string) int { - n := 0 - for i := 0; i < len(s); i++ { - if strings.IndexByte(t, s[i]) >= 0 { - n++ - } - } - return n -} - -// Split s at any bytes in t. -func splitAtBytes(s string, t string) []string { - a := make([]string, 1+countAnyByte(s, t)) - n := 0 - last := 0 - for i := 0; i < len(s); i++ { - if strings.IndexByte(t, s[i]) >= 0 { - if last < i { - a[n] = s[last:i] - n++ - } - last = i + 1 - } - } - if last < len(s) { - a[n] = s[last:] - n++ - } - return a[0:n] -} - -// lowerASCIIBytes makes x ASCII lowercase in-place. -func lowerASCIIBytes(x []byte) { - for i, b := range x { - if 'A' <= b && b <= 'Z' { - x[i] += 'a' - 'A' - } - } -} - -// hasUpperCase tells whether the given string contains at least one upper-case. -func hasUpperCase(s string) bool { - for i := range s { - if 'A' <= s[i] && s[i] <= 'Z' { - return true - } - } - return false -} - -// absDomainName returns an absolute domain name which ends with a -// trailing dot to match pure Go reverse resolver and all other lookup -// routines. -// See golang.org/issue/12189. -// But we don't want to add dots for local names from /etc/hosts. -// It's hard to tell so we settle on the heuristic that names without dots -// (like "localhost" or "myhost") do not get trailing dots, but any other -// names do. -func absDomainName(s string) string { - if strings.IndexByte(s, '.') != -1 && s[len(s)-1] != '.' { - s += "." - } - return s -} diff --git a/component/resolver/hosts/hosts_windows.go b/component/resolver/hosts/hosts_windows.go deleted file mode 100644 index 8ad76e0ac0..0000000000 --- a/component/resolver/hosts/hosts_windows.go +++ /dev/null @@ -1,13 +0,0 @@ -package hosts - -// this file copy and modify from golang's std net/hook_windows.go - -import ( - "golang.org/x/sys/windows" -) - -func init() { - if dir, err := windows.GetSystemDirectory(); err == nil { - hostsFilePath = dir + "/Drivers/etc/hosts" - } -} diff --git a/component/resolver/ip4p.go b/component/resolver/ip4p.go deleted file mode 100644 index e910929a3d..0000000000 --- a/component/resolver/ip4p.go +++ /dev/null @@ -1,37 +0,0 @@ -package resolver - -import ( - "net" - "net/netip" - "strconv" - - "github.com/metacubex/mihomo/log" -) - -var ( - ip4PEnable bool -) - -func GetIP4PEnable() bool { - return ip4PEnable -} - -func SetIP4PEnable(enableIP4PConvert bool) { - ip4PEnable = enableIP4PConvert -} - -// kanged from https://github.com/heiher/frp/blob/ip4p/client/ip4p.go - -func LookupIP4P(addr netip.Addr, port string) (netip.Addr, string) { - if ip4PEnable { - ip := addr.AsSlice() - if ip[0] == 0x20 && ip[1] == 0x01 && - ip[2] == 0x00 && ip[3] == 0x00 { - addr = netip.AddrFrom4([4]byte{ip[12], ip[13], ip[14], ip[15]}) - port = strconv.Itoa(int(ip[10])<<8 + int(ip[11])) - log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr.String(), port)) - return addr, port - } - } - return addr, port -} diff --git a/component/resolver/local.go b/component/resolver/local.go deleted file mode 100644 index e8505118b4..0000000000 --- a/component/resolver/local.go +++ /dev/null @@ -1,22 +0,0 @@ -package resolver - -import ( - "context" - - D "github.com/miekg/dns" -) - -var DefaultLocalServer LocalServer - -type LocalServer interface { - ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) -} - -// ServeMsg with a dns.Msg, return resolve dns.Msg -func ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { - if server := DefaultLocalServer; server != nil { - return server.ServeMsg(ctx, msg) - } - - return nil, ErrIPNotFound -} diff --git a/component/resolver/relay.go b/component/resolver/relay.go deleted file mode 100644 index 818b4152a3..0000000000 --- a/component/resolver/relay.go +++ /dev/null @@ -1,96 +0,0 @@ -package resolver - -import ( - "context" - "encoding/binary" - "io" - "net" - "time" - - "github.com/metacubex/mihomo/common/pool" - - D "github.com/miekg/dns" -) - -const DefaultDnsReadTimeout = time.Second * 10 -const DefaultDnsRelayTimeout = time.Second * 5 - -const SafeDnsPacketSize = 2 * 1024 // safe size which is 1232 from https://dnsflagday.net/2020/, so 2048 is enough - -func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration) error { - buff := pool.Get(pool.UDPBufferSize) - defer func() { - _ = pool.Put(buff) - _ = conn.Close() - }() - for { - if readTimeout > 0 { - _ = conn.SetReadDeadline(time.Now().Add(readTimeout)) - } - - length := uint16(0) - if err := binary.Read(conn, binary.BigEndian, &length); err != nil { - break - } - - if int(length) > len(buff) { - break - } - - n, err := io.ReadFull(conn, buff[:length]) - if err != nil { - break - } - - err = func() error { - ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) - defer cancel() - inData := buff[:n] - msg, err := relayDnsPacket(ctx, inData, buff, 0) - if err != nil { - return err - } - - err = binary.Write(conn, binary.BigEndian, uint16(len(msg))) - if err != nil { - return err - } - - _, err = conn.Write(msg) - if err != nil { - return err - } - return nil - }() - if err != nil { - return err - } - } - return nil -} - -func relayDnsPacket(ctx context.Context, payload []byte, target []byte, maxSize int) ([]byte, error) { - msg := &D.Msg{} - if err := msg.Unpack(payload); err != nil { - return nil, err - } - - r, err := ServeMsg(ctx, msg) - if err != nil { - m := new(D.Msg) - m.SetRcode(msg, D.RcodeServerFailure) - return m.PackBuffer(target) - } - - r.SetRcode(msg, r.Rcode) - if maxSize > 0 { - r.Truncate(maxSize) - } - r.Compress = true - return r.PackBuffer(target) -} - -// RelayDnsPacket will truncate udp message up to SafeDnsPacketSize -func RelayDnsPacket(ctx context.Context, payload []byte, target []byte) ([]byte, error) { - return relayDnsPacket(ctx, payload, target, SafeDnsPacketSize) -} diff --git a/component/resolver/resolver.go b/component/resolver/resolver.go deleted file mode 100644 index add691adf5..0000000000 --- a/component/resolver/resolver.go +++ /dev/null @@ -1,234 +0,0 @@ -package resolver - -import ( - "context" - "errors" - "fmt" - "net/netip" - "strings" - "time" - - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/trie" - - "github.com/metacubex/randv2" - "github.com/miekg/dns" -) - -var ( - // DefaultResolver aim to resolve ip - DefaultResolver Resolver - - // ProxyServerHostResolver resolve ip for proxies server host, only nil when DefaultResolver is nil - ProxyServerHostResolver Resolver - - // DirectHostResolver resolve ip for direct outbound host, only nil when DefaultResolver is nil - DirectHostResolver Resolver - - // SystemResolver always using system dns, and was init in dns module - SystemResolver Resolver - - // DisableIPv6 means don't resolve ipv6 host - // default value is true - DisableIPv6 = true - - // DefaultHosts aim to resolve hosts - DefaultHosts = NewHosts(trie.New[HostValue]()) - - // 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) (ips []netip.Addr, err error) - LookupIPv4(ctx context.Context, host string) (ips []netip.Addr, err error) - LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error) - ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error) - Invalid() bool - ClearCache() - ResetConnection() -} - -// LookupIPv4WithResolver same as LookupIPv4, but with a resolver -func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { - if node, ok := DefaultHosts.Search(host, false); ok { - if addrs := utils.Filter(node.IPs, func(ip netip.Addr) bool { - return ip.Is4() - }); len(addrs) > 0 { - return addrs, nil - } - } - - ip, err := netip.ParseAddr(host) - if err == nil { - if ip.Is4() || ip.Is4In6() { - return []netip.Addr{ip}, nil - } - return []netip.Addr{}, ErrIPVersion - } - - if r != nil && r.Invalid() { - return r.LookupIPv4(ctx, host) - } - - return SystemResolver.LookupIPv4(ctx, host) -} - -// LookupIPv4 with a host, return ipv4 list -func LookupIPv4(ctx context.Context, host string) ([]netip.Addr, error) { - return LookupIPv4WithResolver(ctx, host, DefaultResolver) -} - -// ResolveIPv4WithResolver same as ResolveIPv4, but with a resolver -func ResolveIPv4WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { - ips, err := LookupIPv4WithResolver(ctx, host, r) - if err != nil { - return netip.Addr{}, err - } else if len(ips) == 0 { - return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) - } - return ips[randv2.IntN(len(ips))], nil -} - -// ResolveIPv4 with a host, return ipv4 -func ResolveIPv4(ctx context.Context, host string) (netip.Addr, error) { - return ResolveIPv4WithResolver(ctx, host, DefaultResolver) -} - -// LookupIPv6WithResolver same as LookupIPv6, but with a resolver -func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { - if DisableIPv6 { - return nil, ErrIPv6Disabled - } - - if node, ok := DefaultHosts.Search(host, false); ok { - if addrs := utils.Filter(node.IPs, func(ip netip.Addr) bool { - return ip.Is6() - }); len(addrs) > 0 { - return addrs, nil - } - } - - if ip, err := netip.ParseAddr(host); err == nil { - if strings.Contains(host, ":") { - return []netip.Addr{ip}, nil - } - return nil, ErrIPVersion - } - - if r != nil && r.Invalid() { - return r.LookupIPv6(ctx, host) - } - - return SystemResolver.LookupIPv6(ctx, host) -} - -// LookupIPv6 with a host, return ipv6 list -func LookupIPv6(ctx context.Context, host string) ([]netip.Addr, error) { - return LookupIPv6WithResolver(ctx, host, DefaultResolver) -} - -// ResolveIPv6WithResolver same as ResolveIPv6, but with a resolver -func ResolveIPv6WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { - ips, err := LookupIPv6WithResolver(ctx, host, r) - if err != nil { - return netip.Addr{}, err - } else if len(ips) == 0 { - return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) - } - return ips[randv2.IntN(len(ips))], nil -} - -func ResolveIPv6(ctx context.Context, host string) (netip.Addr, error) { - return ResolveIPv6WithResolver(ctx, host, DefaultResolver) -} - -// LookupIPWithResolver same as LookupIP, but with a resolver -func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { - if node, ok := DefaultHosts.Search(host, false); ok { - return node.IPs, nil - } - - if r != nil && r.Invalid() { - if DisableIPv6 { - return r.LookupIPv4(ctx, host) - } - return r.LookupIP(ctx, host) - } else if DisableIPv6 { - return LookupIPv4WithResolver(ctx, host, r) - } - - if ip, err := netip.ParseAddr(host); err == nil { - return []netip.Addr{ip}, nil - } - - return SystemResolver.LookupIP(ctx, host) -} - -// LookupIP with a host, return ip -func LookupIP(ctx context.Context, host string) ([]netip.Addr, error) { - return LookupIPWithResolver(ctx, host, DefaultResolver) -} - -// ResolveIPWithResolver same as ResolveIP, but with a resolver -func ResolveIPWithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { - ips, err := LookupIPWithResolver(ctx, host, r) - if err != nil { - return netip.Addr{}, err - } else if len(ips) == 0 { - return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) - } - ipv4s, ipv6s := SortationAddr(ips) - if len(ipv4s) > 0 { - return ipv4s[randv2.IntN(len(ipv4s))], nil - } - return ipv6s[randv2.IntN(len(ipv6s))], nil -} - -// ResolveIP with a host, return ip and priority return TypeA -func ResolveIP(ctx context.Context, host string) (netip.Addr, error) { - return ResolveIPWithResolver(ctx, host, DefaultResolver) -} - -// ResolveIPPrefer6WithResolver same as ResolveIP, but with a resolver -func ResolveIPPrefer6WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) { - ips, err := LookupIPWithResolver(ctx, host, r) - if err != nil { - return netip.Addr{}, err - } else if len(ips) == 0 { - return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) - } - ipv4s, ipv6s := SortationAddr(ips) - if len(ipv6s) > 0 { - return ipv6s[randv2.IntN(len(ipv6s))], nil - } - return ipv4s[randv2.IntN(len(ipv4s))], nil -} - -// ResolveIPPrefer6 with a host, return ip and priority return TypeAAAA -func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) { - return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver) -} - -func ResetConnection() { - if DefaultResolver != nil { - go DefaultResolver.ResetConnection() - } -} - -func SortationAddr(ips []netip.Addr) (ipv4s, ipv6s []netip.Addr) { - for _, v := range ips { - if v.Unmap().Is4() { - ipv4s = append(ipv4s, v) - } else { - ipv6s = append(ipv6s, v) - } - } - return -} diff --git a/component/resolver/system.go b/component/resolver/system.go deleted file mode 100644 index a134bb05ea..0000000000 --- a/component/resolver/system.go +++ /dev/null @@ -1,39 +0,0 @@ -package resolver - -import "sync" - -var blacklist struct { - Map map[string]struct{} - Mutex sync.Mutex -} - -func init() { - blacklist.Map = make(map[string]struct{}) -} - -func AddSystemDnsBlacklist(names ...string) { - blacklist.Mutex.Lock() - defer blacklist.Mutex.Unlock() - for _, name := range names { - blacklist.Map[name] = struct{}{} - } -} - -func RemoveSystemDnsBlacklist(names ...string) { - blacklist.Mutex.Lock() - defer blacklist.Mutex.Unlock() - for _, name := range names { - delete(blacklist.Map, name) - } -} - -func IsSystemDnsBlacklisted(names ...string) bool { - blacklist.Mutex.Lock() - defer blacklist.Mutex.Unlock() - for _, name := range names { - if _, ok := blacklist.Map[name]; ok { - return true - } - } - return false -} diff --git a/component/resource/fetcher.go b/component/resource/fetcher.go deleted file mode 100644 index 3658e1a18b..0000000000 --- a/component/resource/fetcher.go +++ /dev/null @@ -1,238 +0,0 @@ -package resource - -import ( - "context" - "os" - "sync" - "time" - - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/slowdown" - types "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/fswatch" - "github.com/samber/lo" -) - -type Parser[V any] func([]byte) (V, error) - -type Fetcher[V any] struct { - ctx context.Context - ctxCancel context.CancelFunc - resourceType string - name string - vehicle types.Vehicle - updatedAt time.Time - hash utils.HashType - parser Parser[V] - interval time.Duration - onUpdate func(V) - watcher *fswatch.Watcher - loadBufMutex sync.Mutex - backoff slowdown.Backoff -} - -func (f *Fetcher[V]) Name() string { - return f.name -} - -func (f *Fetcher[V]) Vehicle() types.Vehicle { - return f.vehicle -} - -func (f *Fetcher[V]) VehicleType() types.VehicleType { - return f.vehicle.Type() -} - -func (f *Fetcher[V]) UpdatedAt() time.Time { - return f.updatedAt -} - -func (f *Fetcher[V]) Initial() (V, error) { - if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil { - // local file exists, use it first - buf, err := os.ReadFile(f.vehicle.Path()) - modTime := stat.ModTime() - contents, _, err := f.loadBuf(buf, utils.MakeHash(buf), false) - f.updatedAt = modTime // reset updatedAt to file's modTime - - if err == nil { - err = f.startPullLoop(time.Since(modTime) > f.interval) - if err != nil { - return lo.Empty[V](), err - } - return contents, nil - } - } - - // parse local file error, fallback to remote - contents, _, updateErr := f.Update() - - // start the pull loop even if f.Update() failed - err := f.startPullLoop(false) - if err != nil { - return lo.Empty[V](), err - } - - if updateErr != nil { - return lo.Empty[V](), updateErr - } - - return contents, nil -} - -func (f *Fetcher[V]) Update() (V, bool, error) { - buf, hash, err := f.vehicle.Read(f.ctx, f.hash) - if err != nil { - f.backoff.AddAttempt() // add a failed attempt to backoff - return lo.Empty[V](), false, err - } - return f.loadBuf(buf, hash, f.vehicle.Type() != types.File) -} - -func (f *Fetcher[V]) SideUpdate(buf []byte) (V, bool, error) { - return f.loadBuf(buf, utils.MakeHash(buf), true) -} - -func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) (V, bool, error) { - f.loadBufMutex.Lock() - defer f.loadBufMutex.Unlock() - - now := time.Now() - if f.hash.Equal(hash) { - if updateFile { - _ = os.Chtimes(f.vehicle.Path(), now, now) - } - f.updatedAt = now - return lo.Empty[V](), true, nil - } - - if buf == nil { // f.hash has been changed between f.vehicle.Read but should not happen (cause by concurrent) - return lo.Empty[V](), true, nil - } - - contents, err := f.parser(buf) - if err != nil { - f.backoff.AddAttempt() // add a failed attempt to backoff - return lo.Empty[V](), false, err - } - f.backoff.Reset() // no error, reset backoff - - if updateFile { - if err = f.vehicle.Write(buf); err != nil { - return lo.Empty[V](), false, err - } - } - f.updatedAt = now - f.hash = hash - - if f.onUpdate != nil { - f.onUpdate(contents) - } - - return contents, false, nil -} - -func (f *Fetcher[V]) Close() error { - f.ctxCancel() - if f.watcher != nil { - _ = f.watcher.Close() - } - return nil -} - -func (f *Fetcher[V]) pullLoop(forceUpdate bool) { - initialInterval := f.interval - time.Since(f.updatedAt) - if initialInterval > f.interval { - initialInterval = f.interval - } - - if forceUpdate { - log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name()) - f.updateWithLog() - } - if attempt := f.backoff.Attempt(); attempt > 0 { // f.Update() was failed, decrease the interval from backoff to achieve fast retry - if duration := f.backoff.ForAttempt(attempt); duration < initialInterval { - initialInterval = duration - } - } - - timer := time.NewTimer(initialInterval) - defer timer.Stop() - for { - select { - case <-timer.C: - f.updateWithLog() - interval := f.interval - if attempt := f.backoff.Attempt(); attempt > 0 { // f.Update() was failed, decrease the interval from backoff to achieve fast retry - if duration := f.backoff.ForAttempt(attempt); duration < interval { - interval = duration - } - } - timer.Reset(interval) - case <-f.ctx.Done(): - return - } - } -} - -func (f *Fetcher[V]) startPullLoop(forceUpdate bool) (err error) { - // pull contents automatically - if f.vehicle.Type() == types.File { - f.watcher, err = fswatch.NewWatcher(fswatch.Options{ - Path: []string{f.vehicle.Path()}, - Direct: true, - Callback: f.updateCallback, - }) - if err != nil { - return err - } - err = f.watcher.Start() - if err != nil { - return err - } - } else if f.interval > 0 { - go f.pullLoop(forceUpdate) - } - return -} - -func (f *Fetcher[V]) updateCallback(path string) { - f.updateWithLog() -} - -func (f *Fetcher[V]) updateWithLog() { - _, same, err := f.Update() - if err != nil { - log.Errorln("[Provider] %s pull error: %s", f.Name(), err.Error()) - return - } - - if same { - log.Debugln("[Provider] %s's content doesn't change", f.Name()) - return - } - - log.Infoln("[Provider] %s's content update", f.Name()) - return -} - -func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] { - ctx, cancel := context.WithCancel(context.Background()) - return &Fetcher[V]{ - ctx: ctx, - ctxCancel: cancel, - name: name, - vehicle: vehicle, - parser: parser, - onUpdate: onUpdate, - interval: interval, - backoff: slowdown.Backoff{ - Factor: 2, - Jitter: false, - Min: time.Second, - Max: interval, - }, - } -} diff --git a/component/resource/vehicle.go b/component/resource/vehicle.go deleted file mode 100644 index 00b3170bfd..0000000000 --- a/component/resource/vehicle.go +++ /dev/null @@ -1,183 +0,0 @@ -package resource - -import ( - "context" - "errors" - "io" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/metacubex/mihomo/common/utils" - mihomoHttp "github.com/metacubex/mihomo/component/http" - "github.com/metacubex/mihomo/component/profile/cachefile" - types "github.com/metacubex/mihomo/constant/provider" -) - -const ( - DefaultHttpTimeout = time.Second * 20 - - fileMode os.FileMode = 0o666 - dirMode os.FileMode = 0o755 -) - -var ( - etag = false -) - -func ETag() bool { - return etag -} - -func SetETag(b bool) { - etag = b -} - -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) -} - -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) Url() string { - return "file://" + f.path -} - -func (f *FileVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) { - buf, err = os.ReadFile(f.path) - if err != nil { - return - } - hash = utils.MakeHash(buf) - return -} - -func (f *FileVehicle) Proxy() string { - return "" -} - -func (f *FileVehicle) Write(buf []byte) error { - return safeWrite(f.path, buf) -} - -func NewFileVehicle(path string) *FileVehicle { - return &FileVehicle{path: path} -} - -type HTTPVehicle struct { - url string - path string - proxy string - header http.Header - timeout time.Duration - sizeLimit int64 - inRead func(response *http.Response) - provider types.ProxyProvider -} - -func (h *HTTPVehicle) Url() string { - return h.url -} - -func (h *HTTPVehicle) Type() types.VehicleType { - return types.HTTP -} - -func (h *HTTPVehicle) Path() string { - return h.path -} - -func (h *HTTPVehicle) Proxy() string { - return h.proxy -} - -func (h *HTTPVehicle) Write(buf []byte) error { - return safeWrite(h.path, buf) -} - -func (h *HTTPVehicle) SetInRead(fn func(response *http.Response)) { - h.inRead = fn -} - -func (h *HTTPVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) { - ctx, cancel := context.WithTimeout(ctx, h.timeout) - defer cancel() - header := h.header - setIfNoneMatch := false - if etag && oldHash.IsValid() { - etagWithHash := cachefile.Cache().GetETagWithHash(h.url) - if oldHash.Equal(etagWithHash.Hash) && etagWithHash.ETag != "" { - if header == nil { - header = http.Header{} - } else { - header = header.Clone() - } - header.Set("If-None-Match", etagWithHash.ETag) - setIfNoneMatch = true - } - } - resp, err := mihomoHttp.HttpRequestWithProxy(ctx, h.url, http.MethodGet, header, nil, h.proxy) - if err != nil { - return - } - defer resp.Body.Close() - - if h.inRead != nil { - h.inRead(resp) - } - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - if setIfNoneMatch && resp.StatusCode == http.StatusNotModified { - return nil, oldHash, nil - } - err = errors.New(resp.Status) - return - } - var reader io.Reader = resp.Body - if h.sizeLimit > 0 { - reader = io.LimitReader(reader, h.sizeLimit) - } - buf, err = io.ReadAll(reader) - if err != nil { - return - } - hash = utils.MakeHash(buf) - if etag { - cachefile.Cache().SetETagWithHash(h.url, cachefile.EtagWithHash{ - Hash: hash, - ETag: resp.Header.Get("ETag"), - Time: time.Now(), - }) - } - return -} - -func NewHTTPVehicle(url string, path string, proxy string, header http.Header, timeout time.Duration, sizeLimit int64) *HTTPVehicle { - return &HTTPVehicle{ - url: url, - path: path, - proxy: proxy, - header: header, - timeout: timeout, - sizeLimit: sizeLimit, - } -} diff --git a/component/slowdown/backoff.go b/component/slowdown/backoff.go deleted file mode 100644 index 1868763498..0000000000 --- a/component/slowdown/backoff.go +++ /dev/null @@ -1,107 +0,0 @@ -// modify from https://github.com/jpillora/backoff/blob/v1.0.0/backoff.go - -package slowdown - -import ( - "math" - "sync/atomic" - "time" - - "github.com/metacubex/randv2" -) - -// Backoff is a time.Duration counter, starting at Min. After every call to -// the Duration method the current timing is multiplied by Factor, but it -// never exceeds Max. -// -// Backoff is not generally concurrent-safe, but the ForAttempt method can -// be used concurrently. -type Backoff struct { - attempt atomic.Uint64 - // Factor is the multiplying factor for each increment step - Factor float64 - // Jitter eases contention by randomizing backoff steps - Jitter bool - // Min and Max are the minimum and maximum values of the counter - Min, Max time.Duration -} - -// Duration returns the duration for the current attempt before incrementing -// the attempt counter. See ForAttempt. -func (b *Backoff) Duration() time.Duration { - d := b.ForAttempt(float64(b.attempt.Add(1) - 1)) - return d -} - -const maxInt64 = float64(math.MaxInt64 - 512) - -// ForAttempt returns the duration for a specific attempt. This is useful if -// you have a large number of independent Backoffs, but don't want use -// unnecessary memory storing the Backoff parameters per Backoff. The first -// attempt should be 0. -// -// ForAttempt is concurrent-safe. -func (b *Backoff) ForAttempt(attempt float64) time.Duration { - // Zero-values are nonsensical, so we use - // them to apply defaults - min := b.Min - if min <= 0 { - min = 100 * time.Millisecond - } - max := b.Max - if max <= 0 { - max = 10 * time.Second - } - if min >= max { - // short-circuit - return max - } - factor := b.Factor - if factor <= 0 { - factor = 2 - } - //calculate this duration - minf := float64(min) - durf := minf * math.Pow(factor, attempt) - if b.Jitter { - durf = randv2.Float64()*(durf-minf) + minf - } - //ensure float64 wont overflow int64 - if durf > maxInt64 { - return max - } - dur := time.Duration(durf) - //keep within bounds - if dur < min { - return min - } - if dur > max { - return max - } - return dur -} - -// Reset restarts the current attempt counter at zero. -func (b *Backoff) Reset() { - b.attempt.Store(0) -} - -// Attempt returns the current attempt counter value. -func (b *Backoff) Attempt() float64 { - return float64(b.attempt.Load()) -} - -// AddAttempt adds one to the attempt counter. -func (b *Backoff) AddAttempt() { - b.attempt.Add(1) -} - -// Copy returns a backoff with equals constraints as the original -func (b *Backoff) Copy() *Backoff { - return &Backoff{ - Factor: b.Factor, - Jitter: b.Jitter, - Min: b.Min, - Max: b.Max, - } -} diff --git a/component/slowdown/slowdown.go b/component/slowdown/slowdown.go deleted file mode 100644 index eff4915f1d..0000000000 --- a/component/slowdown/slowdown.go +++ /dev/null @@ -1,51 +0,0 @@ -package slowdown - -import ( - "context" - "sync/atomic" - "time" -) - -type SlowDown struct { - errTimes atomic.Int64 - backoff Backoff -} - -func (s *SlowDown) Wait(ctx context.Context) (err error) { - timer := time.NewTimer(s.backoff.Duration()) - defer timer.Stop() - select { - case <-timer.C: - case <-ctx.Done(): - err = ctx.Err() - } - return -} - -func New() *SlowDown { - return &SlowDown{ - backoff: Backoff{ - Min: 10 * time.Millisecond, - Max: 1 * time.Second, - Factor: 2, - Jitter: true, - }, - } -} - -func Do[T any](s *SlowDown, ctx context.Context, fn func() (T, error)) (t T, err error) { - if s.errTimes.Load() > 10 { - err = s.Wait(ctx) - if err != nil { - return - } - } - t, err = fn() - if err != nil { - s.errTimes.Add(1) - return - } - s.errTimes.Store(0) - s.backoff.Reset() - return -} diff --git a/component/sniffer/base_sniffer.go b/component/sniffer/base_sniffer.go deleted file mode 100644 index 55f51c5003..0000000000 --- a/component/sniffer/base_sniffer.go +++ /dev/null @@ -1,48 +0,0 @@ -package sniffer - -import ( - "errors" - - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/sniffer" -) - -type SnifferConfig struct { - OverrideDest bool - Ports utils.IntRanges[uint16] -} - -type BaseSniffer struct { - ports utils.IntRanges[uint16] - supportNetworkType constant.NetWork -} - -// Protocol implements sniffer.Sniffer -func (*BaseSniffer) Protocol() string { - return "unknown" -} - -// SniffData implements sniffer.Sniffer -func (*BaseSniffer) SniffData(bytes []byte) (string, error) { - return "", errors.New("TODO") -} - -// SupportNetwork implements sniffer.Sniffer -func (bs *BaseSniffer) SupportNetwork() constant.NetWork { - return bs.supportNetworkType -} - -// SupportPort implements sniffer.Sniffer -func (bs *BaseSniffer) SupportPort(port uint16) bool { - return bs.ports.Check(port) -} - -func NewBaseSniffer(ports utils.IntRanges[uint16], networkType constant.NetWork) *BaseSniffer { - return &BaseSniffer{ - ports: ports, - supportNetworkType: networkType, - } -} - -var _ sniffer.Sniffer = (*BaseSniffer)(nil) diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go deleted file mode 100644 index 0ed3d2e637..0000000000 --- a/component/sniffer/dispatcher.go +++ /dev/null @@ -1,281 +0,0 @@ -package sniffer - -import ( - "errors" - "net" - "net/netip" - "time" - - "github.com/metacubex/mihomo/common/lru" - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/sniffer" - "github.com/metacubex/mihomo/log" -) - -var ( - ErrorUnsupportedSniffer = errors.New("unsupported sniffer") - ErrorSniffFailed = errors.New("all sniffer failed") - ErrNoClue = errors.New("not enough information for making a decision") -) - -type Dispatcher struct { - enable bool - sniffers map[sniffer.Sniffer]SnifferConfig - forceDomain []C.DomainMatcher - skipSrcAddress []C.IpMatcher - skipDstAddress []C.IpMatcher - skipDomain []C.DomainMatcher - skipList *lru.LruCache[netip.AddrPort, uint8] - forceDnsMapping bool - parsePureIp bool -} - -func (sd *Dispatcher) shouldOverride(metadata *C.Metadata) bool { - for _, matcher := range sd.skipDstAddress { - if matcher.MatchIp(metadata.DstIP) { - return false - } - } - for _, matcher := range sd.skipSrcAddress { - if matcher.MatchIp(metadata.SrcIP) { - return false - } - } - if metadata.Host == "" && sd.parsePureIp { - return true - } - if metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping { - return true - } - return sd.forceSniff(metadata) -} - -func (sd *Dispatcher) forceSniff(metadata *C.Metadata) bool { - for _, matcher := range sd.forceDomain { - if matcher.MatchDomain(metadata.Host) { - return true - } - } - return false -} - -// UDPSniff is called when a UDP NAT is created and passed the first initialization packet. -// It may return a wrapped packetSender if the sniffer process needs to wait for multiple packets. -// This function must be non-blocking, and any blocking operations should be done in the wrapped packetSender. -func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSender) C.PacketSender { - metadata := packet.Metadata() - if sd.shouldOverride(metadata) { - for current, config := range sd.sniffers { - if current.SupportNetwork() == C.UDP || current.SupportNetwork() == C.ALLNet { - inWhitelist := current.SupportPort(metadata.DstPort) - overrideDest := config.OverrideDest - - if inWhitelist { - if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok { - return wrapable.WrapperSender(packetSender, overrideDest) - } - - host, err := current.SniffData(packet.Data()) - if err != nil { - continue - } - - replaceDomain(metadata, host, overrideDest) - return packetSender - } - } - } - } - - return packetSender -} - -// TCPSniff returns true if the connection is sniffed to have a domain -func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool { - if sd.shouldOverride(metadata) { - inWhitelist := false - overrideDest := false - for sniffer, config := range sd.sniffers { - if sniffer.SupportNetwork() == C.TCP || sniffer.SupportNetwork() == C.ALLNet { - inWhitelist = sniffer.SupportPort(metadata.DstPort) - if inWhitelist { - overrideDest = config.OverrideDest - break - } - } - } - - if !inWhitelist { - return false - } - forceSniffer := sd.forceSniff(metadata) - - dst := metadata.AddrPort() - if !forceSniffer { - if count, ok := sd.skipList.Get(dst); ok && count > 5 { - log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) - return false - } - } - - host, err := sd.sniffDomain(conn, metadata) - if err != nil { - if !forceSniffer { - sd.cacheSniffFailed(metadata) - } - log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) - return false - } - - for _, matcher := range sd.skipDomain { - if matcher.MatchDomain(host) { - log.Debugln("[Sniffer] Skip sni[%s]", host) - return false - } - } - - sd.skipList.Delete(dst) - - replaceDomain(metadata, host, overrideDest) - return true - } - return false -} - -func replaceDomain(metadata *C.Metadata, host string, overrideDest bool) { - metadata.SniffHost = host - if overrideDest { - log.Debugln("[Sniffer] Sniff %s [%s]-->[%s] success, replace domain [%s]-->[%s]", - metadata.NetWork, - metadata.SourceDetail(), - metadata.RemoteAddress(), - metadata.Host, host) - metadata.Host = host - } - metadata.DNSMode = C.DNSNormal -} - -func (sd *Dispatcher) Enable() bool { - return sd != nil && sd.enable -} - -func (sd *Dispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metadata) (string, error) { - //defer func(start time.Time) { - // log.Debugln("[Sniffer] [%s] Sniffing took %s", metadata.DstIP, time.Since(start)) - //}(time.Now()) - - for s := range sd.sniffers { - if s.SupportNetwork() == C.TCP && s.SupportPort(metadata.DstPort) { - _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) - _, err := conn.Peek(1) - _ = conn.SetReadDeadline(time.Time{}) - if err != nil { - _, ok := err.(*net.OpError) - if ok { - sd.cacheSniffFailed(metadata) - log.Errorln("[Sniffer] [%s] [%s] may not have any sent data, Consider adding skip", metadata.DstIP, s.Protocol()) - _ = conn.Close() - } - - return "", err - } - - bufferedLen := conn.Buffered() - bytes, err := conn.Peek(bufferedLen) - if err != nil { - log.Debugln("[Sniffer] [%s] [%s] the data length not enough, error: %v", metadata.DstIP, s.Protocol(), err) - continue - } - - host, err := s.SniffData(bytes) - var e *errNeedAtLeastData - if errors.As(err, &e) { - //log.Debugln("[Sniffer] [%s] [%s] %v, got length: %d", metadata.DstIP, s.Protocol(), e, len(bytes)) - _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) - bytes, err = conn.Peek(e.length) - _ = conn.SetReadDeadline(time.Time{}) - //log.Debugln("[Sniffer] [%s] [%s] try again, got length: %d", metadata.DstIP, s.Protocol(), len(bytes)) - if err != nil { - log.Debugln("[Sniffer] [%s] [%s] the data length not enough, error: %v", metadata.DstIP, s.Protocol(), err) - continue - } - host, err = s.SniffData(bytes) - } - if err != nil { - //log.Debugln("[Sniffer] [%s] [%s] Sniff data failed, error: %v", metadata.DstIP, s.Protocol(), err) - continue - } - - _, err = netip.ParseAddr(host) - if err == nil { - //log.Debugln("[Sniffer] [%s] [%s] Sniff data failed, got host [%s]", metadata.DstIP, s.Protocol(), host) - continue - } - - //log.Debugln("[Sniffer] [%s] [%s] Sniffed [%s]", metadata.DstIP, s.Protocol(), host) - return host, nil - } - } - - return "", ErrorSniffFailed -} - -func (sd *Dispatcher) cacheSniffFailed(metadata *C.Metadata) { - dst := metadata.AddrPort() - sd.skipList.Compute(dst, func(oldValue uint8, loaded bool) (newValue uint8, delete bool) { - if oldValue <= 5 { - oldValue++ - } - return oldValue, false - }) -} - -type Config struct { - Enable bool - Sniffers map[sniffer.Type]SnifferConfig - ForceDomain []C.DomainMatcher - SkipSrcAddress []C.IpMatcher - SkipDstAddress []C.IpMatcher - SkipDomain []C.DomainMatcher - ForceDnsMapping bool - ParsePureIp bool -} - -func NewDispatcher(snifferConfig *Config) (*Dispatcher, error) { - dispatcher := Dispatcher{ - enable: snifferConfig.Enable, - forceDomain: snifferConfig.ForceDomain, - skipSrcAddress: snifferConfig.SkipSrcAddress, - skipDstAddress: snifferConfig.SkipDstAddress, - skipDomain: snifferConfig.SkipDomain, - skipList: lru.New(lru.WithSize[netip.AddrPort, uint8](128), lru.WithAge[netip.AddrPort, uint8](600)), - forceDnsMapping: snifferConfig.ForceDnsMapping, - parsePureIp: snifferConfig.ParsePureIp, - sniffers: make(map[sniffer.Sniffer]SnifferConfig, len(snifferConfig.Sniffers)), - } - - for snifferName, config := range snifferConfig.Sniffers { - s, err := NewSniffer(snifferName, config) - if err != nil { - log.Errorln("Sniffer name[%s] is error", snifferName) - return &Dispatcher{enable: false}, err - } - dispatcher.sniffers[s] = config - } - - return &dispatcher, nil -} - -func NewSniffer(name sniffer.Type, snifferConfig SnifferConfig) (sniffer.Sniffer, error) { - switch name { - case sniffer.TLS: - return NewTLSSniffer(snifferConfig) - case sniffer.HTTP: - return NewHTTPSniffer(snifferConfig) - case sniffer.QUIC: - return NewQuicSniffer(snifferConfig) - default: - return nil, ErrorUnsupportedSniffer - } -} diff --git a/component/sniffer/http_sniffer.go b/component/sniffer/http_sniffer.go deleted file mode 100644 index 76bf15591f..0000000000 --- a/component/sniffer/http_sniffer.go +++ /dev/null @@ -1,136 +0,0 @@ -package sniffer - -import ( - "bytes" - "errors" - "fmt" - "net" - "strings" - - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/sniffer" -) - -var ( - // refer to https://pkg.go.dev/net/http@master#pkg-constants - methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect", "patch", "trace"} - errNotHTTPMethod = errors.New("not an HTTP method") -) - -type version byte - -const ( - HTTP1 version = iota - HTTP2 -) - -type HTTPSniffer struct { - *BaseSniffer - version version - host string -} - -var _ sniffer.Sniffer = (*HTTPSniffer)(nil) - -func NewHTTPSniffer(snifferConfig SnifferConfig) (*HTTPSniffer, error) { - ports := snifferConfig.Ports - if len(ports) == 0 { - ports = utils.IntRanges[uint16]{utils.NewRange[uint16](80, 80)} - } - return &HTTPSniffer{ - BaseSniffer: NewBaseSniffer(ports, C.TCP), - }, nil -} - -func (http *HTTPSniffer) Protocol() string { - switch http.version { - case HTTP1: - return "http1" - case HTTP2: - return "http2" - default: - return "unknown" - } -} - -func (http *HTTPSniffer) SupportNetwork() C.NetWork { - return C.TCP -} - -func (http *HTTPSniffer) SniffData(bytes []byte) (string, error) { - domain, err := SniffHTTP(bytes) - if err == nil { - return *domain, nil - } else { - return "", err - } -} - -func beginWithHTTPMethod(b []byte) error { - for _, m := range &methods { - if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) { - return nil - } - - if len(b) < len(m) { - return ErrNoClue - } - } - return errNotHTTPMethod -} - -func SniffHTTP(b []byte) (*string, error) { - if err := beginWithHTTPMethod(b); err != nil { - return nil, err - } - - _ = &HTTPSniffer{ - version: HTTP1, - } - - headers := bytes.Split(b, []byte{'\n'}) - for i := 1; i < len(headers); i++ { - header := headers[i] - if len(header) == 0 { - break - } - parts := bytes.SplitN(header, []byte{':'}, 2) - if len(parts) != 2 { - continue - } - key := strings.ToLower(string(parts[0])) - if key == "host" { - rawHost := strings.ToLower(string(bytes.TrimSpace(parts[1]))) - host, _, err := net.SplitHostPort(rawHost) - if err != nil { - if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") { - return parseHost(rawHost) - } else { - return nil, err - } - } - - if net.ParseIP(host) != nil { - return nil, fmt.Errorf("host is ip") - } - - return &host, nil - } - } - return nil, ErrNoClue -} - -func parseHost(host string) (*string, error) { - if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { - if net.ParseIP(host[1:len(host)-1]) != nil { - return nil, fmt.Errorf("host is ip") - } - } - - if net.ParseIP(host) != nil { - return nil, fmt.Errorf("host is ip") - } - - return &host, nil -} diff --git a/component/sniffer/quic_sniffer.go b/component/sniffer/quic_sniffer.go deleted file mode 100644 index a1b39f9247..0000000000 --- a/component/sniffer/quic_sniffer.go +++ /dev/null @@ -1,459 +0,0 @@ -package sniffer - -import ( - "crypto" - "crypto/aes" - "crypto/cipher" - "encoding/binary" - "errors" - "io" - "sync" - "time" - - "github.com/metacubex/mihomo/common/buf" - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/constant" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/sniffer" - - "github.com/metacubex/quic-go/quicvarint" - "golang.org/x/crypto/hkdf" -) - -// Modified from https://github.com/v2fly/v2ray-core/blob/master/common/protocol/quic/sniff.go - -const ( - versionDraft29 uint32 = 0xff00001d - version1 uint32 = 0x1 - - quicPacketTypeInitial = 0x00 - quicPacketType0RTT = 0x01 - - // Timeout before quic sniffer all packets - quicWaitConn = time.Second * 3 - - // maxCryptoStreamOffset is the maximum offset allowed on any of the crypto streams. - // This limits the size of the ClientHello and Certificates that can be received. - maxCryptoStreamOffset = 16 * (1 << 10) -) - -var ( - quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} - quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} - errNotQuic = errors.New("not QUIC") - errNotQuicInitial = errors.New("not QUIC initial packet") -) - -var _ sniffer.Sniffer = (*QuicSniffer)(nil) -var _ sniffer.MultiPacketSniffer = (*QuicSniffer)(nil) - -type QuicSniffer struct { - *BaseSniffer -} - -func NewQuicSniffer(snifferConfig SnifferConfig) (*QuicSniffer, error) { - ports := snifferConfig.Ports - if len(ports) == 0 { - ports = utils.IntRanges[uint16]{utils.NewRange[uint16](443, 443)} - } - return &QuicSniffer{ - BaseSniffer: NewBaseSniffer(ports, C.UDP), - }, nil -} - -func (sniffer *QuicSniffer) Protocol() string { - return "quic" -} - -func (sniffer *QuicSniffer) SupportNetwork() C.NetWork { - return C.UDP -} - -func (sniffer *QuicSniffer) SniffData(b []byte) (string, error) { - return "", ErrorUnsupportedSniffer -} - -func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender { - return &quicPacketSender{ - sender: packetSender, - chClose: make(chan struct{}), - override: override, - } -} - -var _ constant.PacketSender = (*quicPacketSender)(nil) - -type quicPacketSender struct { - lock sync.RWMutex - ranges utils.IntRanges[uint64] - buffer []byte - result string - override bool - - sender constant.PacketSender - - chClose chan struct{} - closed bool -} - -// Send will send PacketAdapter nonblocking -// the implement must call UDPPacket.Drop() inside Send -func (q *quicPacketSender) Send(current constant.PacketAdapter) { - defer q.sender.Send(current) - - q.lock.RLock() - if q.closed { - q.lock.RUnlock() - return - } - q.lock.RUnlock() - - err := q.readQuicData(current.Data()) - if err != nil { - q.close() - return - } -} - -// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy -func (q *quicPacketSender) Process(conn constant.PacketConn, proxy constant.WriteBackProxy) { - q.sender.Process(conn, proxy) -} - -// ResolveUDP wait sniffer recv all fragments and update the domain -func (q *quicPacketSender) ResolveUDP(data *constant.Metadata) error { - select { - case <-q.chClose: - q.lock.RLock() - replaceDomain(data, q.result, q.override) - q.lock.RUnlock() - break - case <-time.After(quicWaitConn): - q.close() - } - - return q.sender.ResolveUDP(data) -} - -// Close stop the Process loop -func (q *quicPacketSender) Close() { - q.sender.Close() - q.close() -} - -func (q *quicPacketSender) close() { - q.lock.Lock() - q.closeLocked() - q.lock.Unlock() -} - -func (q *quicPacketSender) closeLocked() { - if !q.closed { - close(q.chClose) - q.closed = true - if q.buffer != nil { - _ = pool.Put(q.buffer) - q.buffer = nil - } - q.ranges = nil - } -} - -func (q *quicPacketSender) readQuicData(b []byte) error { - buffer := buf.As(b) - typeByte, err := buffer.ReadByte() - if err != nil { - return errNotQuic - } - isLongHeader := typeByte&0x80 > 0 - if !isLongHeader || typeByte&0x40 == 0 { - return errNotQuicInitial - } - - vb, err := buffer.ReadBytes(4) - if err != nil { - return errNotQuic - } - - versionNumber := binary.BigEndian.Uint32(vb) - - if versionNumber != 0 && typeByte&0x40 == 0 { - return errNotQuic - } else if versionNumber != versionDraft29 && versionNumber != version1 { - return errNotQuic - } - - connIdLen, err := buffer.ReadByte() - if err != nil || connIdLen == 0 { - return errNotQuic - } - destConnID := make([]byte, int(connIdLen)) - if _, err := io.ReadFull(buffer, destConnID); err != nil { - return errNotQuic - } - - packetType := (typeByte & 0x30) >> 4 - if packetType != quicPacketTypeInitial { - return nil - } - - if l, err := buffer.ReadByte(); err != nil { - return errNotQuic - } else if _, err := buffer.ReadBytes(int(l)); err != nil { - return errNotQuic - } - - tokenLen, err := quicvarint.Read(buffer) - if err != nil || tokenLen > uint64(len(b)) { - return errNotQuic - } - - if _, err = buffer.ReadBytes(int(tokenLen)); err != nil { - return errNotQuic - } - - packetLen, err := quicvarint.Read(buffer) - if err != nil { - return errNotQuic - } - - hdrLen := len(b) - buffer.Len() - - var salt []byte - if versionNumber == version1 { - salt = quicSalt - } else { - salt = quicSaltOld - } - initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt) - secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) - hpKey := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic hp", 16) - block, err := aes.NewCipher(hpKey) - if err != nil { - return err - } - - cache := buf.NewPacket() - defer cache.Release() - - mask := cache.Extend(block.BlockSize()) - block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16]) - firstByte := b[0] - // Encrypt/decrypt first byte. - - if isLongHeader { - // Long header: 4 bits masked - // High 4 bits are not protected. - firstByte ^= mask[0] & 0x0f - } else { - // Short header: 5 bits masked - // High 3 bits are not protected. - firstByte ^= mask[0] & 0x1f - } - packetNumberLength := int(firstByte&0x3 + 1) // max = 4 (64-bit sequence number) - extHdrLen := hdrLen + packetNumberLength - - // copy to avoid modify origin data - extHdr := cache.Extend(extHdrLen) - copy(extHdr, b) - extHdr[0] = firstByte - - packetNumber := extHdr[hdrLen:extHdrLen] - // Encrypt/decrypt packet number. - for i := range packetNumber { - packetNumber[i] ^= mask[1+i] - } - - if int(packetLen)+hdrLen > len(b) || extHdrLen > len(b) { - return errNotQuic - } - - data := b[extHdrLen : int(packetLen)+hdrLen] - - key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16) - iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) - aesCipher, err := aes.NewCipher(key) - if err != nil { - return err - } - aead, err := cipher.NewGCM(aesCipher) - if err != nil { - return err - } - - // We only decrypt once, so we do not need to XOR it back. - // https://github.com/quic-go/qtls-go1-20/blob/e132a0e6cb45e20ac0b705454849a11d09ba5a54/cipher_suites.go#L496 - for i, b := range packetNumber { - iv[len(iv)-len(packetNumber)+i] ^= b - } - dst := cache.Extend(len(data)) - decrypted, err := aead.Open(dst[:0], iv, data, extHdr) - if err != nil { - return err - } - - buffer = buf.As(decrypted) - - for i := 0; !buffer.IsEmpty(); i++ { - q.lock.RLock() - if q.closed { - q.lock.RUnlock() - // close() was called, just return - return nil - } - q.lock.RUnlock() - - frameType := byte(0x0) // Default to PADDING frame - for frameType == 0x0 && !buffer.IsEmpty() { - frameType, _ = buffer.ReadByte() - } - switch frameType { - case 0x00: // PADDING frame - case 0x01: // PING frame - case 0x02, 0x03: // ACK frame - if _, err = quicvarint.Read(buffer); err != nil { // Field: Largest Acknowledged - return io.ErrUnexpectedEOF - } - if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Delay - return io.ErrUnexpectedEOF - } - ackRangeCount, err := quicvarint.Read(buffer) // Field: ACK Range Count - if err != nil { - return io.ErrUnexpectedEOF - } - if _, err = quicvarint.Read(buffer); err != nil { // Field: First ACK Range - return io.ErrUnexpectedEOF - } - for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range - if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> Gap - return io.ErrUnexpectedEOF - } - if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> ACK Range Length - return io.ErrUnexpectedEOF - } - } - if frameType == 0x03 { - if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT0 Count - return io.ErrUnexpectedEOF - } - if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT1 Count - return io.ErrUnexpectedEOF - } - if _, err = quicvarint.Read(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count - return io.ErrUnexpectedEOF - } - } - case 0x06: // CRYPTO frame, we will use this frame - offset, err := quicvarint.Read(buffer) // Field: Offset - if err != nil { - return io.ErrUnexpectedEOF - } - length, err := quicvarint.Read(buffer) // Field: Length - if err != nil || length > uint64(buffer.Len()) { - return io.ErrUnexpectedEOF - } - - end := offset + length - if end > maxCryptoStreamOffset { - return io.ErrShortBuffer - } - - q.lock.Lock() - if q.closed { - q.lock.Unlock() - // close() was called, just return - return nil - } - if q.buffer == nil { - q.buffer = pool.Get(maxCryptoStreamOffset)[:end] - } else if end > uint64(len(q.buffer)) { - q.buffer = q.buffer[:end] - } - target := q.buffer[offset:end] - if _, err := buffer.Read(target); err != nil { // Field: Crypto Data - q.lock.Unlock() - return io.ErrUnexpectedEOF - } - q.ranges = append(q.ranges, utils.NewRange(offset, end)) - q.ranges = q.ranges.Merge() - q.lock.Unlock() - case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet - if _, err = quicvarint.Read(buffer); err != nil { // Field: Error Code - return io.ErrUnexpectedEOF - } - if _, err = quicvarint.Read(buffer); err != nil { // Field: Frame Type - return io.ErrUnexpectedEOF - } - length, err := quicvarint.Read(buffer) // Field: Reason Phrase Length - if err != nil { - return io.ErrUnexpectedEOF - } - if _, err := buffer.ReadBytes(int(length)); err != nil { // Field: Reason Phrase - return io.ErrUnexpectedEOF - } - default: - // Only above frame types are permitted in initial packet. - // See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8 - return errNotQuicInitial - } - } - - return q.tryAssemble() -} - -func (q *quicPacketSender) tryAssemble() error { - q.lock.RLock() - - if q.closed { - q.lock.RUnlock() - // close() was called, just return - return nil - } - - if len(q.ranges) != 1 || q.ranges[0].Start() != 0 || q.ranges[0].End() != uint64(len(q.buffer)) { - q.lock.RUnlock() - // incomplete fragment, just return - return nil - } - - if len(q.buffer) <= 4 || - // Handshake Type (1) + uint24 Length (3) + ClientHello body - // maxCryptoStreamOffset is in the valid range of uint16 so just ignore the q.buffer[1] - int(binary.BigEndian.Uint16([]byte{q.buffer[2], q.buffer[3]})+4) != len(q.buffer) { - q.lock.RUnlock() - // end of segment not reached, just return - return nil - } - - domain, err := ReadClientHello(q.buffer) - q.lock.RUnlock() - if err != nil { - return err - } - - q.lock.Lock() - q.result = *domain - q.closeLocked() - q.lock.Unlock() - - return nil -} - -func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { - b := make([]byte, 3, 3+6+len(label)+1+len(context)) - binary.BigEndian.PutUint16(b, uint16(length)) - b[2] = uint8(6 + len(label)) - b = append(b, []byte("tls13 ")...) - b = append(b, []byte(label)...) - b = b[:3+6+len(label)+1] - b[3+6+len(label)] = uint8(len(context)) - b = append(b, context...) - - out := make([]byte, length) - n, err := hkdf.Expand(hash.New, secret, b).Read(out) - if err != nil || n != length { - panic("quic: HKDF-Expand-Label invocation failed unexpectedly") - } - return out -} diff --git a/component/sniffer/sniff_test.go b/component/sniffer/sniff_test.go deleted file mode 100644 index bc250d59b4..0000000000 --- a/component/sniffer/sniff_test.go +++ /dev/null @@ -1,341 +0,0 @@ -package sniffer - -import ( - "bytes" - "encoding/hex" - "net" - "net/netip" - "testing" - - "github.com/metacubex/mihomo/constant" - "github.com/stretchr/testify/assert" -) - -type fakeSender struct { - resultCh chan *constant.Metadata -} - -var _ constant.PacketSender = (*fakeSender)(nil) - -func (e *fakeSender) Send(packet constant.PacketAdapter) { - // Ensure that the wrapper's Send can correctly handle the situation where the packet is directly discarded. - packet.Drop() -} - -func (e *fakeSender) Process(constant.PacketConn, constant.WriteBackProxy) { - panic("not implemented") -} - -func (e *fakeSender) ResolveUDP(metadata *constant.Metadata) error { - e.resultCh <- metadata - return nil -} - -func (e *fakeSender) Close() { - panic("not implemented") -} - -type fakeUDPPacket struct { - data []byte - data2 []byte // backup -} - -func (s *fakeUDPPacket) InAddr() net.Addr { - return net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) -} - -func (s *fakeUDPPacket) LocalAddr() net.Addr { - return net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) -} - -func (s *fakeUDPPacket) Data() []byte { - return s.data -} - -func (s *fakeUDPPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) { - return 0, net.ErrClosed -} - -func (s *fakeUDPPacket) Drop() { - for i := range s.data { - if s.data[i] != s.data2[i] { // ensure input data not changed - panic("data has been changed!") - } - s.data[i] = 0 // forcing data to become illegal - } - s.data = nil -} - -var _ constant.UDPPacket = (*fakeUDPPacket)(nil) - -func asPacket(data string) constant.PacketAdapter { - pktData, _ := hex.DecodeString(data) - - meta := &constant.Metadata{} - pkt := &fakeUDPPacket{data: pktData, data2: bytes.Clone(pktData)} - pktAdp := constant.NewPacketAdapter(pkt, meta) - - return pktAdp -} - -func testQuicSniffer(data []string, async bool) (string, error) { - q, err := NewQuicSniffer(SnifferConfig{}) - if err != nil { - return "", err - } - - resultCh := make(chan *constant.Metadata, 1) - emptySender := &fakeSender{resultCh: resultCh} - - sender := q.WrapperSender(emptySender, true) - - go func() { - meta := constant.Metadata{} - err = sender.ResolveUDP(&meta) - if err != nil { - panic(err) - } - }() - - for _, d := range data { - if async { - go sender.Send(asPacket(d)) - } else { - sender.Send(asPacket(d)) - } - } - - meta := <-resultCh - return meta.SniffHost, nil -} - -func TestQuicHeaders(t *testing.T) { - - cases := []struct { - input []string - domain string - }{ - //Normal domain quic sniff - { - input: []string{"cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b"}, - domain: "www.google.com", - }, - { - input: []string{"c3000000011266f50524e8d0fe88cbf51e3ad71a13198235000044c82dc5d943fb34cc6d5c5e433610dc7a44f5951935c2c1d14ac641b02472340a892c4492dbfe3f8262109108fc36d96bdc1e9e46b5f1f6ef6104add2aafbfd8e79246eb3b4637541aaed7d195571724e642ab4d31c909f1db86e7d8516117ce8716bd1e3acb664c499086b0f3bc7258595420e7bb969f934457d195e832ffff4ffddf11123eeadacc48190e356c8f0f6abc381deb7e285e3b0613a795b19bddb9f002ffdf6fd70f0ff2072302b33d2421aac6540bb9f0e85c7237af0dd56225b2264d769160febab952e64bd5155f23e58c6113891143f946591032b41816aed3ac54f521f60605f86791de24c5765b664c1348cc53d5d631b4bbefe1915f2b21fefafb47badeb72d8ba1fd5c3cfeb0ba9d0112396f170e94cd33952c4fa87997b870931bf1a300e8e127f530815ff087815b4f9d004cbcd17013ac143847572a1655a5b36e054e8b9951d747c2c6ff25d7b2edb13a2a6b8074062332f2191f6830cf435a4ed9db5d9c4eb43a143bf3edf0c48f6f9435dafad4afb743a5a33990379df953ecd388e848aff0ebba9ccc052b8303c0bd1fee7e7553af1894e81b7772818bb69249540ccb8cfb47b1517abaf71c81c3bd271f1a5f1b66465f850f377c9db682b8e543c3d0c10fcd2dee263630889b7d1d521d1d27e866ea4ab5f43790d6a7f76ceefd5783678ca92cc131fa42fc4a01e2a81cad734ddf17a53e1bda8e0a21afc9e8c1118c9459b13519f5b3c3d9692c92234f01129d47ae8ec70625170847472801190b46d36f73b868f55f5a18a3cb05af6d38610e0829e4fbf13ddcc202341702e43dcf33be76ff4afe327e5783287c137aad075752940b41e7d9f5146e36d908897c6d7a9fdc343fde2d9c9d6e6a6b237669bd3e6abe0a732861a679eadfa29a876c6a646953c9361830811b012b26b31c9e7158f8de9c9a108346ddee3dd3886da6258364c1281bff8e055f6384e3a23e198b5e6b726fa7f811b3338072019d4b5fd05891770d11e3ed6ab5f7ed33db1c6220c5aa8fa1909949ac55d5435b75982e17aa80940fa574f0aba4dc340129cad491fdf1f5e05c4e83e36ad29ff38f15e1c9436c792024442f57f07583d671dd05446c84ea20b471303f6ae4e5e13f244d671e0ebe94d3d5c17d3f3f378cdd51fa8a6d2c977c78a2397dd1e251cd979803d617d45f575e5d9db0a28b3c4c25fe2af24af5bddac09786b6d6d8aa19cfbd5409bdbfed7d518ef5c863f3ee757bd9d37cddc546cc57d2e52b6ae58789f297a300f1d76c3842603eae4b1224de31a939a68875c86e697aeebf7ebc65568f43fc681bacab830ac4a2164d324e90067125bad702192d01cb3cb3d2689ae681967e86fd7ac93a25cf2e905c88ca5ad7d11962f021754cf3f61224517bd3411d5b5a83955bcea79d702466d073a6eaadc1202b3693e555b051a5b19457023a01e7f943742bb7f5f8aeba8d4e363973aebdccfb12479619cfb93e833be702a307e796dc7431a48abd9b755b392c510b98cd20ef778e2ac88d6a04f23ba8a253d7eb7c13e0c88c3a21f7e23857c58704d139703a47e0965bf2dc8810dc36894ac1f3da73c155e271c106a718b2d184e4e5637c820fe909984642960edfc9e62ac50af5dd3feee6bc560ced7bda676d4e290c9c5916fad52180bbc83d3483e95c79bac15c209936f21042dc2b6253eefdac06e7f4745044eaa0acedabf1d1c8cd9402738"}, - domain: "cloudflare-dns.com", - }, - - // Fragmented quic sniff - { - input: []string{ - "c70000000108afb466a232f7f9f2000044d0168a15a021477ecb9731ed77784d42301462e2d59b0395adc1fa6b569d428583f100860d6b6ae29b6c1b8c0f9c0d9081475ff801f34a9e0677adf685f02b1169fe86c683fb51934915ff43921a73b98fb0b734406f8dd90ce6060d75e923b0d3c738291b421bf16de27ed4785d727ce589f5d0957c413c81d6ee75052e3ab50fe53f1abbb24a138a52e1412683992ad769e65ed301a736914843543e2a3e11eb395726d4fcc9283f8607b38685069f63d05ab8bf38aa24d4073a1e68fa1b6087cec44d7fa628342e9d88a0d20b381014cdd1a07b9d913a3bbcad0cfbddd0560617cf26054138075eb86e06db1e68781541587302e6dda86cae779f9848fcefcc33626f8953bfe4dc293d23e74c87020e79e9ffd58ee345382bd4d1d6e5a3389b0a977124708d05e3c305545857041734dc7092901ab54604b3750b3139dd3b8f2bd94cda89d85be3756fda6f0cfb6f66af3d2e36a7808ff7bce271a0272f8dbc88193ede31613433985cd35c7bd9b627d434e7b2e94b38402b8f1b5619a903572dcf4c2b864c6ee66657c9ec81e03fbe765037f83b2229171888ba08651fc78a1b50c7cc52f6dfe8273723e08932b1a16a6b717a80b5520cf3f40e46f9d9c350eaa914bf99dd4ab700cfdae21437daf695916d4f3121235e4913e0657d8cdcf4afd8f2c7ef977a2dfe49f46fef46c8fa6932e745311d4a6eb3124d5e0a204b9e3227e86a55e662f7002d4f4a72cba8c77c3adc3eff076dfb9195cf68455cecbbfc9b5444d9c4a4775bba68d57ff52edac6ce6ff4efbf6466579bf68308f2ba9a59b2c09506064091a86af621e9dae52366a90599db0d64a23944bc48966b6d3ab8e20f4afb5b0e94370d26a89a9c4207b454554e58ac74f62ffb3eb2686eaa596b9610322a5ce8eeb42f2ead1c71b11b51bc4f1800eb549a2bb529ca4a0d165ae461e45b556b2365e9459d531489d59d0dfa544a76c5c00b0a01270741d4061a331c32fd6cd0e68bbce49137b852e215c9db52f3e430416d8979520e5270be324f3d93132358c0eac35a4618ea7aad997dbbd8e99d4ea577271b935e3fe928f90abd94593806d272a565a414686b8e56c28e34b77671de6a696b09414380bc658c69a309d3225ba8493e9076dac776c845ce11a7ccd6cae58fba5434014250f3e211058b2efe3424b991d679a02ba949b086ba12144c7df3e049b5d026f386e4ae712c9b0b4b02730dd6862ed4e72730224cb6ec9101c5cbb7ee4fc30d497bb1dbf74ffdd49d8cae6c7c9a364ede453d9ae25edf27a2153ab285f3e3be66b2968d67a56480f1f74c4fe61dc69db3451f5b113d7ca02e5afa8627f579c07a9b1814853fb8fdaf0c0f220f89725c757f5617ba4e43cb4f3ad9ce18f48f23d10f9e8950b0fd737070655730532896d93df8768860ebf941365d0634db399feab1f8a88bad28d25e689c5a57321debb8d1435130e90a699e17fa5255f2063f09659a432e9ab5f89eeabe12756bfc5e02fcae2b78a9d0f570934b8d4af8f4afbd57549176f465a0cea485dd89c95a8ae915b4b99548a4c939710c16908f968368baf5f547cfee07f3cbb6142041d6e6084aac253a0d3aeac628cfe76f87b94c3806cb14a912ce8e4981e316511d5ede36f526805d6c3fab5b72d9d91f4eacd26e28cb181ec66611818f5c206ddd52488707a940dc12144ae825d25929bc32b718f46e471fdb30762d299b45c84f6310a72b60", - "c20000000108afb466a232f7f9f2000044d00582e8683e329a63e5bf4dc93e93e325ff661e74b9cabefdfbf6065c7ab203c8a629534e87e5f2d4c0f463352904642358b8f137e99802c3a26cf22235782a777769ecd134c6b4d0dce6aa10b485c45ccdcf6deb805342e99ef97e2777aee0b2a44073843fccc2f8eb837031f76a8e968cb01c13c1268af095f54f860958e4062a84e2527bcc9b25a7791650a844de1b0c4b2476282a0e00c9de9d39a41914d1e797a88a8997b96b25a4c194762912b2ddee0e01a365f1afa1e82ea266c14ae94e47c90b5679e2cd00e63ee5a834505ca33463751bac22f3b87afc80099335dc7bfd12b7df224a23ced3d2e25b58a04c4b5cb089ca187abc54d782973c7bb157cc515c7508431ff5bdc227871da58b9ca8a9a576960f38edb384112b08e4c70672a6f23d17d9d901342e56c12370deaaafcb22810eb352f1a6d9377e96bdc1ad4dd397dbc6a227b70f204c1a4e9a4db2705763b82ec4df1fab11420aae547155c6b49abceeed997ff01b7d24e369c65f7edf18665d067c7d2bda5ec8623281fce8c77d893cb8a42053756713e910894a58ef5bf3d9f3a41071026660dd7cd05e1640767ec68f78e22c1716700ca9c0f076f90a65cffc394c10a32071c6532d07b59414181070d08c9c84e3d13842718d51bf90dd36ab1b3f708df7eeb3939dc8553787308983c3e9ba971e7d447788477a7140196c2f717b9ba4f5da92d73316dd11c1d1830b4200f26f733a6c65ec1cc21549b485e3a43dc7a2b68e95466a53544082a20d9a43387a7ccbfd353f7e590b7047f13bfc0d91923c2d75dad4f8091ea96502f98e83e5c30e52e4cd5c670f6c2248ce37cd6ee8b3970531fbf0c53c5fa9a0d73200442b755c91fa4f70524ffe8a36063b6709d3aa9f6b53eb0aaecc57a8c8c9a7ac5e57e03e9cfb290b67dd8222a245ff5439914147e2799fd1cd2ca2cb22fda299443b81e8024adc59d098058432fa4bde376b8e59075f6b86427b4ef6cd7c83b5c08add0c3d3543aee8d672c41cb287c1f0a17f1bc30f62a57490afb2d9f401bf302fd473ddbaf63f6883221579743d6aa1f386b8b2f5db06d7d6c36be81f29fafd14b82e863d744f116ce2be4921631f1fb2797289fffa9ee16a3e537ddfa52350546bc544459c0c9d66fdcbd41612cfc0e2744f50927983a3224291c1ae51608fbc00f40c60ec72573a7e128c3415b0d9a7db52de8ff763dd66e2eeb03ef2e67838c9e68cfddae4b86a3f34a69e0a473b5a73ab627282648df7912c11a4bf033ade185a8f438036b99b960aa6213c800abbbd751248a7ae600357ab888433125d49c5643705ecb8c86f2980050edd7e3c579ad6fcae9bbe2c8d8b38004426f35eadb543a3bef42355acb1b94c21d7eae7b6ed422ca0d58fa03b227b035628871465ed6509254c8a3bf43dfadbb247ecbc52d80d65e9c03c4bc7bc35a829502bde3868af9c33737cd88d70f7427790313eed4ed1938955c5dd360212ef700f274efcc8c26ea94c4e2e0937d475c5c4909edfb66714d15d12e153e5586725ce0c47e8a1506bb197366754ca8960508f22fe7b83a5eaa40f05f3cb87464dc6b848080c0e0cecf2dae82bfa42cc6f52694478dc3d00ab0e1ed696b98e26c7fd34d2efd969f83e284c28ce3f27b178f4691c772011f61722266153142dd0d526393e6c6848d201115b256e65f12b911a983bc2f96a5b4b99f63f0b58485a521553a3e1d4498ac5d4ee70c3f9", - }, - domain: "quic.nginx.org", - }, - { - input: []string{ - "c00000000108e63b9140d034563d000044d066e1913892ec1d84c179dfa9596e0ce930171a134a09446a888d9e579a6f7bd77df6deda715b028d64f7866603c6deb468d60ecc6488b5e5ee2e2daa1840b76ead998023593c9ebc4178ec89cb198d3c79a867e27177a74ee5f3db74ea194e36e328047ffc3890192665a6feba09ba1e224967fa9575dc7b094e1c29c7f3be9961ba62e3e063f674a09786b7611138e1edaee32cd1d47839e840a74f25ed786463fc48bf3d38a4c793178ab7cbf5a3eb974415b9f9ef7861dfc73460594332f5545c7b7037043afdfc1aa62ac3dfb76ec2c6ae8ebd351f7483992c762d6483b3e2c1454c8ed939ce43f858ccca22d9149cc9da16af86a010be7f3248cf19fa442e94d625ec7f7144b01ac9afb8fb8c595d4cd12fcd2b2d9986371ae65f6f216bed152b79d2782d60f1f01e06b359f88900c4bb3f987f3ce336854a5beaaa616813af4e5f9bd82dca0af6886b544fff0261807bbd8cf90213299f5802b98edc27a6606be8e2bbc18fa7519eac260dcda139f164796a082908459c31aa964a5d3f6fed8944ad61bda126991468f3b7627f2470179619864f234a395ea3bd4f7ba4c0cdf9f5f0dd95d7d59476f2d2a36521c13886265a2fbbd4345e8d1d1e7b5d01a58fb11de23730b087e2b702200155a1ebd50db5751d279438822ac158173533140998a3056893bf470ac84720cb37a4a3205fa88267abc56520bcddacee06011d929c3a114314822d8ccf7cfef89f2fcf0a4fef800afbfca4a62ee848f22066f68c7d3c5c9a24402d422fc2fd5da6d3b470b0ea253f12a883705f7f78bd67006ade4f1c8a3e8fa052656b5b40dacd8062228871cc3bfb1a9c38472b0a720c3c750430edbcbdcecd46b144dfcaa009fee06770238d0270e80671e8ee5f5df18b86dfe8df2f121245c0710ccaefecbeda0ba3db945c768624dc38f21a4ac53741f4e58a5052f3d667fc466b69905f05d0843cfcb830163fae18dd1eb0ce62a59420db9c44958a0eca9ba4258c8060a9956343f155da6c55b2060427d07d9e311729d2971439c7541ae2babfce25a3f5f361fde86c39ef6c04e4e3cf7dd70c9cc0758ec5db3f0cb368e2447080af51c8a5fa6b84ec3175d2d3e6d877b6953e433b4e94b52e1a5f2a1ca37124c27e47f9de5d4c74644181cd37f3f3863ca529c0847bba91c246dadba94b4566b08eaa06a0db4d58b8cb0c8d3070533306a3089891b24a7c4e11b3aa50d5628fc1d136388e8bfbc420a6f12701333ccdc95dec25d09ce25fa4b654260965b91f05b1542c2ee02008d01de4419f14d6749c4bcfcc45a332ba0772def720ea3c8d207802418137b733e779eb406dace0b4b5f5e5e14c787f3e044e6d8160f90fc3c65bcc7f3449205b63294fbc11e9bb92c007d1cb59183eafbf76be9680224cb442806500d71870777d087bf864890848f4a79424c02304f2a6ee2b07f9257f4a2f185ee21239625e246cf680e74b85d292cca44261c6cee6da39bfac3882d28fe547a500f79519ffcd3f54ff5a905c99f22a5e8142c903c41adbe1eb9770b6cf554688529091b126ed2168a23bb191c2b89728e31773623bc58bcb9baebc2c664c79d6ffee7e4404e039723eb05e7f7835c87212431a0131603fcc3fe090cc2fda8239b8f42188b35f98d7fff949b3044544b3bb962ae236a664d76d0c751d9c9ed1271715d240f111febdf7045502f2afd7de8aaaac650511e7bc7716a5b6622ae925abb7", - "c90000000108e63b9140d034563d000044d00b2498988864d8b7f59a00d26165f5ae638fc9b1c12d546ffd86212ccd85f654259cb8b8c9d753c696ddad7ee4847bf3b3c10063606cf3972f75e17ae23e73b6a3029f23541f674256d19677665cdd0b8ac15c3f60984bc14ff5dc7a9ae37395516204f2020965713fccaf35cb0a5823085cd6211d681dc6b39be9db46cbfef154a2b9049ed202e9088961b0b710e94bd73259b0967e4d6b8cdfd5b72774fee2f2ceb16bcafa010f247c43b0a9ca25578e7d45bfda7edb82e91f8e1c0a2cfa990223bf97ece42862d3f329521fe2d12493b717f174f966d173102e5cca10943d5b612101d65d0dd48b44416f9ac1eac4575558ecaaa39c47ade2dee6e25fd219d799b499143b47a5bf449701b939c1dde111349cd0d63efd2ff74fbd3573ed40abfdb2310e2740da40fc50c7a137a3f32c3a26b3d407f80e669fe7f9a3542fdd412a9cb53f845d9c1af0814377bf92e30f05ee387fb8675807a6de083c85d3d7860601c8170923c53e5773ee388b68e510a28cd7009c485bd4cb861eddfdd265de042e5a018d20cb810614e2bb17b0f52d6bf620a6f173e0b41951e1b83ffb29e3b3b3c5d9fff13acd3b409021195201d003e281d8cda7b0f02c273e17b1f9b9e8cec4296d65a1c4923b78a2e4273cb42e4e159980472e440078e542eeddcc5a9bfefa5a72871fbcd9ebb74fef20a50215bf75cfd8572d5ab9ac5945e8d6ca35884caf0af0446ee9aab0a1cc3a452ec79c9de786119e63bb3a75fce0ae29c15a0c320fff87e87cc23a05e75b4f4b30b75c6aa036c4b6657f8200ea014185b31ee7fcd00d1eaf40973f347fae227f89d41794fa57ac1ed1efda3ba840ef27852cf33a9dc9e2d77b56af9ced9e75707837aa8c5395cdc15134ba132de87152ce53d506c53284dab912bbc276542504cc94afaca71a5173ff13ea6cb45b47dde9965428ba5d8eb968cc2a5729c2f9b8f1c1de208943a2cd565196e040dcc415d769ceb6300c7909d7e32bbbe83c4cbf4d49f6e34fe56b651838628f3a0001e99f39cafe45c98e455aff8d98f89942a862f7505b9f7fe3f64dacf8c574affacf91c2c05f094127acaa5187f9dfa188f67db421243a02e583942138c2edf45fec4c6b6a8a791da9055be247e9b252e9f7c1330e76f9cb3aa5feebb21f871315b5fb90a1df0b8056513b74daeb6ac995f85c64150ad115a14830d145e5f4e6638c26987b676a1dd19a9775df29ab442ce6143b0fbf8f8d4618084896e34812ed59d63041e2b4ccf6c959a6c849813dd926082bb7b1adedf69246547f335552bcdbae7e466ac31e07e442530ad114abebc6f58015b786e7f35644307fa7ad3d9248c56c8ff472735c6911da1843fe53821b8f5180f8844db4a9f7a826a919fd93c4db4d25861054929260dcdc46d085827c46d60f1097424a6ef250f5aaf3235c80230eda4eb580ce93e1ac8aac422a7aa1241562af601981b84b74949f1c476705c8030eb5d447b2414f9716ff3fd606cd750030b94345c016078bdcb97b7ebc24f661fbd08802f32df18d6a2aa85bfe2e9b8dc76b121c44ae9f29e4413051b527e99fde29720724337476c0eff325cb6220a290a9eb852151c84836729d6a223032e2c638857d9e7f469b84d7d650c45e56e763aee73f902e82b055425c4568725e2d4efd7fde8b02906bda48af86bf47ea27ff00f4528494b74be9bbff001cc841449a184a4e00d64e51a72660a2c21f704f", - }, - domain: "chat.openai.com", - }, - // Fragmented quic and 0-rtt packet sniff - { - input: []string{ - "de0000000108c2751a596bd51c6e004041948ab7d9d493e9e1e9902a7734534fb9eaddc70ca7f821d1b58a406b23ba9db1d03266ae74765b03fac21c284fd50cb0a3d1ca71d8c3cabef5553dd1cb748ac662", - "c50000000108c2751a596bd51c6e000044d0538af4ba75e226a6fc7f43e7f1f59610973b8a6670bb8338ca7ef7d90f81aa59f179dae5f8f6dbd24ec6fe576b28f6ce6cd46f26de143b8c99cdadaecf2041948a61bd5a8591486e10022fd100aa20e6423b4f4ca5773edb1aba79b73d6150ee185e66da60e658b2a698098462122b6b80c7fbc5542b0b8e9532898c1f31aa2ef55cbdf036d74c3069abbb261660f048d950b00b7db279ec2bc39912102679ddbffb53f1b1921f137fce43e164af86c72908532f4cdc48eb462a9d9e9cdd6d3c3faaf8aa8aea312dcac5d6aa75b1ade4af6901576649da7e3efd4199b92107d7acee8bbf06734b2484957c3d8cbb1f3fc0ccd56c55223628ed8ea514ffd101bac370c97b28c7da81175ab0508c0002d458cf41f7159dfce22b447c1ec502c186b782c1854718b7fc0fc39e5c09aee31113fc4c5003803fc27ca48850c08a54dbbfdef6ea9a6a138cac0ecd045cfd5607cb6c99c39c0cb21778857f97416b78fa7c6ac8ae3fa2ef2adb3b85fe3fdba70ef9265bb3d54e56ec68b8887d54d02d4a571a6b793ae4df8ff171c881a554b5c5a7848351d446ab94c90ee9c600f03b785fee6300450a4ffc2a55d417952e15449a491296d463ac6942bce4ca93c99440396bc8984073ec028b11ad412e97e26f9248031dc4b1a6ae385803bb578fa1a3b3a58a8ef19c6c511f17b28a275e8c40e51fca8f410a4a1879b5d8749a44a6a9f97c0c9df25318cc28fd0cc61eea78ddc603a17e74eb542c8c08cdbaafa3b44566db4d67e8d1429332375cd30cdaead9594c46d8ce91bce9813c3ca23f55ec2f4dd3ff141471bc3df590367bc65e4830018ff7d845ec4987d11e471d114c48acd1ae9b7670341a34077ae59ea6c3bfc4675cf419d37db48a98a5573b69867039731f537098b46415a193f50b2c85bf9e5da45d6757c5c366e21f04ea62d64b81c28be5148d89e53535414067cf609e59686b7fd135f5cb473e57f6c82dbb291308a1065e0f755935d77517adecee55e72cf37ecaab1b5c0c6e0c7463a014e7e439757913f6e43abb6af775d21ab6e43cbdbcd1935a000cf8025ebc11378d86d6f72d51bf2dfe4be1db5d3b0fcacd13e1b9fbaac6e9153c3d1f4e876f2fa9c3cfc84fd0910b778105b66be70827b1830b7b3c9633af5d83ad527efd81498cbbdd112873cc5ced573e6579acfe817b62280c2122b582b591d52b96cac047bff91192a5cfc001d15c811e055dcb1c9710dc892258ed1ab5152af2cfc57a0b93205dd41fd82b86090b4281b1493a8828ebc96bbd603b888cbca4a15799a5f3eaef93655d5609948080ca57c696d0ffc9a07665bdb063b547bb5a862c3b058c9efb2e7b79cf405fd83efacaa4b8e3a1fd126270587119756562c03d69a9cb67550369030a0204e531cb8df91ab2dfa2e4106c590c59b1b13c447843937929a574d3ea1785db0d52b4b2eeefd1a07c69729bec7c2813c9eb1249f706b3cc14a3d489d6b42a641dfd9e91aa70c7d3222e154af2d7fc1a8f48e5ba11739ae128d1f32ff929aaf4b249df5ea23f7847301e36ffda02342cdf1bd9dfd1979cbd8de32eb8b1eb8c415ddd267efe53f54678d9fc32435b34b00ee2256d8b6190e30a280df5bc48cf9fd669a52469954deccb0f1da37371d513ea57f31ead22a34f9379c7931fd18286d9fde6ecfaac8ca2a9be79d688c5401c65407543c066532f6621f256551c4a98a86b543c576ed0f3254daa4915", - "cc0000000108c2751a596bd51c6e000044d07b624bf3d95fb3b7299b67dd836fbbbeb05a51650f9b2da3b2695070a0d19ab0d5334cc04de7ea7494fbe6c438f4e84fa56a3f246132468b5b4f1ba0fcc0251cf278338e15fdd715d5bfed18c1f98ca3cd3cc7b6f904aeeea2914a8b998dd3ae7df694c49c1742dccbf4c3472ddfe2e447959655459c11f18bddd9481eb597b887fb3f90a7d0f05224a144f87a5fdad502ea1e46c1c9f4b4154bafa4542c026296040228703bcd020202acceb772b596bf788341cceca864c8907037c39739e511b04e8ba956efa0fb5cb151ac90eb5817444f6488d593325ad4466058ba45214b965c5738f33d5591624584559ba18e89913b868619d498072e3aa1f333f5d6e3d1db88b28adf7d9350c3c383c1eda894f36bf1bb2a58c7a5e5c8b20597b71a099e46bbd3d8894877e43b0183919185b4e9f059472203979d3334c535fc4eaedebebc79bd1e423184765047a50e6dcc76ba2b23ad23511cae2edd2ad8e7f7f302226dbf6c0e4dbc8c08cda26340b9abfef1ef3333cd511295f14c87197d7890576b4076dd9686047854e67733599d96a99194aecf7b927cae2e5fa4568afc71e748dabd3bd71e6c3984f45b06a068a7c9c3a1ca7b5c245a9bb2cc7e2726e833e283430a25b6ccad55bc5b7644b44f99fedeff3c3bbf995a0387cf1e45a5684e5d1c01350d0cd2d615ffb6d1011d80ad16b75925efcbee483e4e2c0e2386e9e1b35b5a107ed97058adb60e323342989559856faeaafe5149bdcd60c113230f9923b2f654c95f986944a014198686f9c2275053c05080e3bf9fab7d46302948b152e2f2fb1ecbe71b412016b3f25ae512ad45cd096d5f284a0c2808b5eab03b4b9b2dff4d81bf234e75e30d480f39a5f9737563e31a19b14d1038296915af33e0ac0dc18e9c871e539e8772d525e5fa19afc582b1c00ae573af39fe293e16d182bbe57af5bee1c0939862ffb62e3d52a60aeb71e4db2a4a1708e75afa5f37d72cd6c0e036abcb4eb8db6515fbbdf98be95d0a6d261a9445797a8f38c3579a2f04c9f5b74dfd1ffaba2c6aa05959704b9b8cb0db30bcc360711c5afef0d1e7c2b076466dcaab104c70f3cc0cda33d7a47462c3fa3d7e34a99b2d8ff3fe5cafd27ede28b9e09b547cf955b97b0d0d4ec126957601c6982d176252be422df3366118895ca25fe27a96c9c234d484fe98634fb9e970e0d2b096f2ced5d56603505990a65363726c828aed2df0f112e0c44f058424ff5c25ae60aa2cb5fdfa289e8ebb63908365aa4e4609eae87e567f1e86d92c43992e6d505f55226fe3533f9fc9c9facff9dae02a3e3c97ca54191bebab93881c0e89b9de5bb4acd5c6fed5b1e7978803f693bfdbc125b4d08fd34fdc6aaf02444c4b06010b0eb2f15d86850a7aa5af05af438f6b7345fad4315f631bc5b017c7482e7af725a09844472f48e4de79b15284932a7e99a46ae72b187ee3faaa0f31a36726056e86eb706bc8eac04b68a3302307a157c91639f30bafc2d180670625673310a9a45a171063011e59c57c8eb67353a8ea344a87853e7b600c2b49a7a1b60a2904c0ea55951af6430667ecdfa6e90a8d2d0ed9857ba5b876cf78af190d5013d16208d2b30d02cf2c23e6ad1466f76c30d11034d5d2eca113e2764b2fb6298fc4940c16d971e28e3e6e5d0e8eea1ccb9b4b89741ed675861fc3680457ee08547f4efcf68bb6247313f8218ae3ec372e51ba8786ecae115dcff241e0", - }, - domain: "fonts.gstatic.com", - }, - // Test sniffer packet out of order - { - input: []string{ - "c50000000108b4b6d5c8b9a19769004047007e07df0d887979774085206f2e7f0146b02a8699715a54fc71ef27ab5a9e8cfbf155497bed9e25934aa74db1b3b270112472b7bf7587423b3ab2aaf99de34cdc591bfe04cc0a448875483ec1f071622121a49c456dc3ce16bae5f61f84ceaef9e8b71db56479845b764507dd9416e8c44b8c93406a230945eb8e484471c1b6207c9afd944fa0fee555a5c966f27ccffb4bfed37fe3936f2c84e9852c0d46c7e2e94b897fcef18c4b0b83d966aef75c0af4240325a24668bc017e0d3f69680ea5b2f59bd0b964062bc40190be86aef3ed0716a18a67057f309faaf3a040222812142a399deb72ebb330d03d59961e2ca10cf78d40886dd094368a881db261068920968f6adf7a7b1266faf8842e71840a29859e877c66e3ebc47d7fe3ee586b6512d9b0e1bea82b302647706473e68dc8209f4e9ca19f1dd25fe386e62c21d9c741e75cb8b11606739ba3de6d6325ee3a9cd1bb2b9613746140ccdaaf936eefdaa1ca7ad73d684e5d82b1ba1dd3356ca0c881f6eee72c02c8b78d02a8217a8fd972e463c77374d0fcbb761459e3ab0bb5492e516d7d4304c19c16a4bed11ea7f4e75616a26a7c81b04ffa580cce04d59825b8ed929578f9219e64bdbc6352ae6e4150a993fc3cc27ce4d66c62893866b9053bb737ac40364094b53d91e8b325b9dab5f537af04f10bf8db644897b0b03b42b1bd6c3aedfe018a6e4f6533183649f4ef6a6300383430f86e802fb4e51976d056a3c40c3b53c847b8308cfbe54dc2d20b8cdc870c73f5fc22c376c35d9a85348ca6a2288ae03dda6b97f0f502f35219e19cff3a810143289cb1f0715f8785028f887bf02c656c9cc372bdc419290f05957ad3dee82b56db352db65aca58e6fa0bd2f753160dd9e7214968c0496be1ab49f978a9252e49266939fedf542760abd653dd38b1659bcf452c753cb89e8235bcf732afcff8f524331be9b6f4a5081c81255e68c358b3444fb1d57bf5659d86b6674544fe2826ca81ee52f93a17b3291826678e488c3074c259223845e4083a413af7fc93d9992823620a8d29d321438a760293e36c4232216207060dd3ee5c4036250ede71ca9cbe335a1e068eb3ff6c10a7f1c8204750d6d0f3145014949a7b4e88a723566ee5446f960a95d9f81cc45155443da561d85a3a311df8172a1c4eb118bf27ec4b3cc4573b1ab421d96d41cc1e5557797ca68f701fe75c474527144d30b9bb00a117637f88896b0b2dcb9bb29ba144ec384b5a085e82e7387e0560a4621423c306b041ad42e84928ce23bc2a7f995ef5c21616de43be8a1657847489b32c8e364846389e7c8cae99530c499f3662a2ae7090e54958ba940b5d3eaf1333ebcecc7f06f29f68ffd97defe65017519c29d355ecb0a4b47ab08dbad8cb0cc5c86de65dfa703110c60a0c55281925018fb4ef49fe5d0132dcc86602c2ab9921a8f3451480d3e931f01c2f9a81873435bb83860128aa78dcc950fb13e416d90ea969aa92763f9caefa0fe3ef4ea82e3af4a3e717fabcc589fe8cb9bfba6810ddf7def8c1445fc0048fb07be043a628e9c920bb72c04d3b9472caafc6c14bffb854a1ba2170dda919322a6d79eab92e3a88888a224093946b87840033fe41941f780f569eaf1fdac55e36b74514d72d09823d71f48f5d5f0ceb7b6d69c5da0e0408c1b13c265d4775db6a0f952ae72bd5c277b22c4be2f2728451ce31e921c856000d20da0489103bad6a6ab4", - "c60000000108b4b6d5c8b9a19769004047007e07df0d887979774085206f2e7f0146b02a8699715a54fc71ef27ab5a9e8cfbf155497bed9e25934aa74db1b3b270112472b7bf7587423b3ab2aaf99de34cdc591bfe04cc0a44884e9e716461869ca408431e1ba92740c598aa74e9cd45706f28942f9cc64dbfd7c292cd33e82b50ae0e2e08dc478c19886718cde33e56c38517f8834d64904bf4fb1d30650caedecb9567ea8ef50157c287a2741e98a00f8e7e19e76bbb0143ac7862a49393f17ec66aa0e2c02123ffb5abcc96ccf92cd542c8f571bd7a4382ff81432d11f83796959696c38f2029db6c6a536a9ea24b74c848b95882562d74739ac95f5a069d48e8756d1a9750c7ebc23d4ee22d617b29b415b7458b3bb8106c22de3a9ace9ec689e6e00471aa33e570f7481d15911d7cf46a429cee1a416558c5e78360795d905ff1e0c81d18fcf4954131fa5b9289ed2291e122cdffd666c66209aa2cab01730739249ce293b3ba3abb31683c108bdfd51f54593f47411077e948f01105bd9bfff1578d235674e96a8b9cfdde119edaa960b84e70fd681312514151de1d5939c79abdfe4953e22be5ad3e6e242d0ce9b3f2e589ce3c768f610d4d3a32e33225d8a5ce2ad74a9b40859cdd9ea99f14fa2a7018e4b6aa6e46a0d73d46d161ec5d3b30bd55078e23987865551a605a33472931428ce222040d20c07d1ebe970e576d9d54ae688a3fe9388adda3da4d011a7cbb604f1f19d2ef1be7ef4713bfa84d4d69ffa606a08b61a1ebb99aacc4e19d0c5034642da1ce2d7d5abecc8adbbc6d7f72ff2da4ce5228ff8626509b38e17b31717c0b7821558b021ba81502d54da7e778d4526367109333383e7c67d5d5bde86bd4001fa13a703ff9259e1c2268ca8f4ed2e6c022a7466e2178bc725f59792803ba28c629e3df7696c416dc294b510920077b2d2b258fdc3506c36c42d37796c8fdb20ba797ee68fdc410325a355f6c1189aa9fc9ee220d42186677e3955cd3c844ce505cd601f04201cc390e923db2ea6407fa2fb4ca7f3f82d0a82d52697ff5ba5d4633bb0d655d7ee3348b89c9cb42870cdfe7c0c162babab4208a9a54700c5785d4134e9e33361480e3512ac8b556e11775536e90ee1270a4cb4d6bf2faa72d7e1f23ceb4fc3aded0e423b6be6a55bc25e5a99163b4f5f72ec4a24fe96f68c739d1848c92c4236a5a637d19871456b8dae671ea6ae5c16ed4fc257612a0821e6dc1cbe2ef4963a1436925dcc4e6ce528fa75e41f7721b379fae8ca09e6fb51d0c3e3ae6c19b98860ab9f74013146c6d375656dd1f530abfa64670a510390e9a54bb9a4ad19977491377c8cd743597bc156ee3f58cfcafa5a547b20852749e66fa8838c100ebde039ea25c8ec32b0c6325b793797546a095e79b9388d8e67dc6b4b3892f93ecd13e64ba4b2ad26fc810fedc374b831921531344c581927da9ba822bd625584d98c7582759ae40f01e14277a0a13d30c2c12536df698330d8aa6a3613a42c493c42692b468b4a2cc6bb6dd45684ee6115848110bf517074efd93bf212c071013f4359f140cfed17bbe10328f2026cb8ada16427122d3fc8a933119a1e3e4cfe2b95cbc73af5044cb099cf34247228972495488ebaa4696280d17665c421be5f1727c5d5b013d8aac0e9943bbbb7fbc2162a4000a306dffe3bc4425cf272f1ebb63c8e4998f867fa6b05d71a8642e29392244d4e2e2351bc149d665efe1b9519cb1b15005393f938d", - }, - domain: "ogads-pa.clients6.google.com", - }, - { - input: []string{ - "cf0000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f9944884c88b10f198e849dddbc1e9f9bac61f98f67f27d5452da6e2bda1f5210a145b1f1416ad2fc15e60aa00444362630650bcd0ee47999b689a40100dcacf40a4c3d74fa6293d4a5cb0487d8c76787c04dd2b47ec7718df5a2dc6942069062617b3d40a95360802957419433436c9065bda5a6156291d909a079b6d3819941368d7e17a2e97e36be829bb421b44545af47e37d7815ee1f200ca28ffd361d955ebe0484fb234a7e8a7c68ad824fd14d517fa7b35f878beebaf3dd22bd9f7a39cb7e0fd8369cdd28c05a06323be7af0b2d69ed2a2f4ea9f25d000de71bf5bd6765a20ddf81d976cff2321f1a4584ad6c4b7e9a42a6d4aa3a02b59f7d994a8e4a3070a4646e51fdf354448420ebfd0aa9118d010d019cc168f2fe5a9ff0c42e6091676be11f28a372ea97d008a1a02efd58149106cfdec7ef86f5416c4b1a408d8efba6c8d4742d781374ff0a1a8ac183bffa1345dc8e3a7cce04f66cc865f434decb912dc9e8e811eb59b80d3e39d5788639ae7c5ede73a935edb47d907725656be0522195bd2c099b0241f36664fad1543e4ae43862252662707fb424a8f5f9486b8e3779ac24bac457671ad664475d1fc9eb1de3c46f624b559742b3477953552e44f20cc1725a11ab915423fdce7cfbc8dafebc0c43d1ac3d3373ca2f0210924433c46e5fcface47a65579efaa1999d52b2632f69c33c3c63537c01be68fb679f9229f8f68c5caaa23dc4c61d3c45dee90affed984dbbfb06b2659447400b4dcbf6e574719e8d49fe0dacea9509182a42f6463138d8693a3b8d797d3bb6b0b02648829d666341373939ac41a57e90fdc2469623b6e2d772199d7c806d5998f439603c0de8413f9d29f79323ec5410b409ab8c95547ab50bb921fe0c407b7aaaf663389bdea5ba56c023dc4622d6dd9cacd8f318a6a0297d041cc6ed455d906be50dc85a25ecb32f4a565432fec9f359833be1c6a6b7b4bd119d3c4b29932eeec8d140dd467ab4d969bd23e9d2a95b92835587f32428f957b6785b8206a4834e00a3013e0b6a5855f16207268bbdf311572c54d2e6ff9c659cd02c258f494c3b168ea170c69138b63e0dde487b72576e87657befa44548b0b4e1e5a837dbbe66a559cd1df8f2151ba513930243fd2b7705bd29b183dff966224d87ffabb74017d634ab2e4b368052504a7f6bc1c62d39a29dc2dcfba683bee2039e376ff391abbd13a0b89512fd8f6a4e66051dfa04e0e1a3cb4bd56a9b17e27651873bf2ed50f65cf1cc608afaf06fe7e6238347adb66f01d1f0b9b51f0078615553cb8ff8d6786b87e19dbc44000025693c4b34cfd695601a680efdc1e7465a981b0f028cbd3dbb938789f240e39223290e34ec303ff5c78a4a637ac04dad60d744f82e96c3c9e8ed6cb0248ac73b5b3a92007edfc1277c3cc6fa1d0045c1c371820f06bedaf046dd999665cc4745ddf8934084ae02e9238acae6dea330b5798e046138f5b15011875eae72d6eb6689e56e0ac5c5d9e25dc4fc1874cf37265e68ce5b8630b84ad8dab7704474f0bfd08ac295b3a508284fb6ff201f0aee6388d0e1d5cdaaf4c20429874792109f5b8e2f3eae6c397e46a510ed829a6746e523481465f64be4e145c83d6fa6951229d3", - "c90000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f994488828e6082edd53e228164d8862067483762ea9523c90d565b9e4b185b7805eaf8220664264e82a95164ab6cab4fb3f5e795e246e7205aca236b3c94dde0ff4fa66ce0924d654829d59c3eb690470b20c5011c739102257e9c2247dee67c0b98190d0015154d31041aadb026b8d3a828c861a15dccdab0cec8cc99b8d6c2acddbb93ab66253e87ac39016507dba42e8fa9f5d22c7f27645a02361842a59ebb2eafefd0f3b92bd9692a96b93875defcfe2796243be8861c59ce5ab03f1d65d308ae456cb9656da1f01026ef0807cc9930021b29d69b36881c3e7d70fc68799ca81922008db93c9ca4a365ee191e214d9829481fd430194ad4583a0ab2e920c25244d7d64662872b3b69ab413ccf0dfb6bf2ea9a9b93e04ed19f8a0e146613ca9d511179f80aeab40d573590d38a7c10840e3f8b9ac1bd23b0826aecabf6d1cdb2aef02deb982c2029dd6d8bc21da6c262c8116b7b383ce8c9eec69da3e16c044dd96ae08a98595d128e89dd55e6dc8eb08b8d51327278027137f60a0e1b42878f98ca898587474f6d509c3a58ff4dd7f8b10905c200cf3170bdec725ee14a1ac8ebd1022509d3e499f5e72168eac43264d7246daa0bfc81a216ca97730b7e043cad8d8a9af5c443a5d15e9a88d82b6750c740eecfd63561712a185c69532b1a18b23513d7cf871f14d164ec544f22b6a8cc77d6fae5fc6e47eb64f08617098d229da78a378d6a0864684a7978f650c7922c907f97b0ebf2be29cc834ffe995c9636b310f4a8c2c5623c3b7b533518193d226923f111da1a0e8055b9053ad7f7504d194fbc3ae2b41cef30aa099624d5e229ffb56d5883a5a09163d22455cac52e37ee0ed5367b7c3bbcd4818a46b9b363b592c53c780eeae2c8b80a1d60d296614c998a9774f76453a58bc55d1c26bb10dc321c159858d7ba2f7855ba01aadf3585632c097e5471591dcc24d87e9b76509c10e2710310e4869de710ce0f484d326be751f8e9f765a685312423f1801aefb28dfe0c8f286432356d06857101a67a432497c5849111db2792fa0ee4ffff49a9124c152bcff82da1951258f989681e4f1338357f2c9f82333f6051b188f640bf200a0a75be1d35d2301e8d3813f7ba1926a28a0df05c21413cc0c4090c1e4ba4877dca8e129876c72ab3a801b4093320f5f685120680541d97889eea5dfcaf07a7ecb00c0ba0ae193969a4cddcbd753609a5304ea88783358ab0ae005c6af27bb58b2c4282186461ea50540845e2e2a2f4efced88c8ab9cd9fb4a226a265714c77ce7b79d1a40bd00b24cbba498dafde6bbad91686cf2e13e75669234bc342218887ba910ac81680122ddd36466e7e8a983d5a0fc18a6e9a386762c32132be08abe5554e334ec7d88734cfad9a378553b71222c55f6aa114392e015dfa2bb6cb4ad241c6bca82fdd0a00eb8d6b4afac61268130dea2807a97e4c0adc0e2be39abccbe64dca5c480e09c4bebb8b598e4f60afb0e92dce710859013b1ffa9c78fbf380160f31b1e72340dea86d353ff0e95884b72e2c2c10f6eff5f36c588ee845b7bc97c3b6bec4aa879dd0eb56b838b7bc2ec6e66a5b5517908197a67566dce7df421a8daedc98848c70d1d2c39b2f3538e6f17800bee3d3", - "df0000000108277148f2b916666000403a52317841946860def0d7829c06fec03ffe8b97f84e10116fafd93d1d2d39bbfda0d148778c21bb1e1667eb789b1ff70c2e3d557ed9c31570d20d", - "d00000000108277148f2b91666600044cd5e860e3fa7ac4769ec75d9b7d20f19e69265939a42afd3c4248a7f5210358a044f42869567e72a05642e96ddffa67bf24ec2d966d860c6accdee01d6917c8c43d4d089d8bb63ff848b617c13fbeefafcbb049ab0822a9ca7716c95af84d019b755b145dfe43c218555d1a7e047deda7d8db352a386b2b6d03f2e7f4510f47ff4ab199348dfa81c86bea5d09d7c7af4ef3f04e99fe4e6c21d53c4335407e27913129152033f17580f97d0345c8487a7ad329dc5c97b298ec7b80fee7813f1d6f94945a44ff662a69453c2dc7ac5e8a1cb90400e63818632d7f9654f140a61280df183b3d9f9b824e53d82f2c14ea3de89befdc79b84a4a3eb659a41db25622add94f2ad4b0d5977f1091aae0a4b83c7b41bca61c6c8d807ef02a8ce6240b76d442559a8b338b39418d27e99aff38840fc79a20995af65b3bbe1e3177074079a47578c51655a4016363364fd2c108d384e602deebd022da3c814549cd57d73c5bfc20e279045e2ad436fbd7e7c9e1985f0ec2f422e310e7aa8cfc48e637f9ac61d06d6482cb40b4376ff3c7abff3c3c26634689ae16d704bab1343d6413fc7b6c076eb0454eda2e0d1e077db40c922ebba6b0b1fa814e3ba76d8d6c4289abbd655f0cf5968eb2aba7131680b44da8910056a76647a6dfea95f27364a7ce694b8fbe19ebcb2a47e7350d33a36f7f5ca67af5e934f449125f4aae870a5b23b4370680ee02b194784d5d188ecdf58ae5454221406bde0ddd3e50d3363a564d6ca9fe0fb57d4df8716cb430cf553be573aa690e5645075ec74edd38cf23215bd50bcda0639dfbbe08dd6c476249e35da819ea6ccef808911b0eef6efaa4947244472795bc071d7154ed87e4a43575b3d61a551fcfccfb7ca3edaee9324f33f54dc9809747e59e24e79f256e8e72f01b8647f71c4b9dc260715fd9d83d3dbd9e124c432c04b3398e74efa3869fe129e368c15b6ca234a243fcac675adcc1db247e3f8485ac4a78f4a1ce2db3b437a1960b02f0c227901d165dcc05abdb3929a80dff2eaf72816185d4af4e28eea05430b736ddd2962e03ec64fa48649dd610e0e221c48f781b45cd9963c176126110e662369874e6a55f28039a23484c5c53714fc2d2030b48f1c895102ca9ad8acae1ec4eb0ae8d8bde31cd74fc515930078d22ad07dc3c7221ddbc4027c746207fa038b31080714091459c9a66ba4f5912d8d3905d3a9a47e4d8829a8110c96c0c9c81291c7985073808814109364df15b04520dc07e8d67cafcda71f0ca59423df5fadae92417a8661b3cdbbf6b1059780fb8b43eb4dcdacf731bb8db26294f978f6be7506b87d17a95367cdb83000565a4986e66dd60d0851f9b593d68790f8097434f62ea7a7396017c3c84754845d3a97f028cf8697d929a2826451653ccf84aba4d2f40fa530b258c13f08c6523c3c02d9669fd46b6a51f20ad323857d767150e3530a66bf88976dbadf99aeea549254c07e11e14085979b60f3b7e1728a4a2d7a35b0377c6501ae7d1d4bba338fb51a17ca8f7e698bd70cd01e8f30edf3e83591a2eb0038811e347bfcfab159b0d1ff6153e0f9ee4c129cfb7687e30b82eed74130c466eee06506dde50805b58c2acccd4cf4b2cc86c52fa2af602a8a7064eb9d90e1c568373b19e43ef4e7c1e4e1c9a58ccedf80a02a46ed64e68e72d4e75c7436e2bc0ba59f95a00456e5680af9e6cf4bf3a6d302ddaf8847cfd5ea606797", - "d30000000108277148f2b91666600043d24b66b2531ed9f9c13b07b2654186b0410a608592fdf728479734933197ec06a1cde860f36b3170fb2a9c85c62a7867ba6520dcb2d0ab2f6a484d9ebf8237d7a6f3c1fb16c1e0458ccf20e6d1b298a7530cea42636166027d92812915e76fbcc436a5e414147672dd7b0d19ff24513800e63cd86984f1c93ef1430bb848d37830eed61675d7c9999b92c6e5796d384554c74dd5a163de341ab309d6b0cb028aa08e56d79c60980d4a49a1c095456ca119fc3f04e496c93a084d017f60c6e031d6e9ad2e4fa699bb4b0c92fdcb44131129db0d30ce9efb740d3db0339127d9bdd1d4f677b1cb532a33647851ba9bb20bd8d6aa593271a85c3a9dc9835065663e61faa8dc6af209a0caf183d0fda3d4839d40edd5659dd053778642db8fba21f1f793e45c5c517e68bbef8543e3a727743c7bf87d047d441d13226b9021fac56904872774cf6768dc91db8ea489a244500e9e527acdc0088437357acf9397b014e66fef2db1248f9c6a578af07d7a02b1356fee02e27b8207e57633fa7bfd87ccd382e368c14b946aea780fcbe696d6e4fa3aa589184e104177db2fc3d91d4af120d9da3bdad021d003796b8261b590d8113f995dc1db4fac1c62cb68370d41cc87c982815017ae2143d5a469b742d019e5556d813877fec9d021cb37f80e5987d9f743c2b39093a34f6654164a8185a5caefbbef8ea17f62f6801a3fd89fae333c878cec9b25d10dfee2abca65d7c909ad2e4f11736d13b1642df4c5a0761f8f29f35f37def9ed327f4a9d8e53269fa6c7cedd0f4fd67d6cde81934e291d9fca695cc9745890cb54503e29e09f4a30f80e2f574bbbeedb7d20481c583d8362d22b2dbec09494095a043cdae283e86f905d8807f7b7c0f06ce968487bbca1e20b87245b68f24537a7c7e768c838f1bf26650afdabec2c0bb9736b345473f279c9b73ecf0d2c4aea49330ecfef0949ef7cb81861b05950ec0772db856365b136ba75d5509c01d7a970c84ebc77d8d5c3ceae1ef5f3079afc7d78965ffa3bc4c64ef1b4718ffb488a571528c83b615c43022616bb4c494c838b556df5ede711a688b0315c1ce6e2892247df582b7c3f2b06cac0bd8d670e2b581f074750596ba162189060b8af3dfc650ba3b45932edc4f94f08741d3072bfd1ef8159b27a7f3673a4fc504304c12116e3c2d7636c663c9fa1b2f5571be88769f33ccb94a09abd9c5a7dc8a8c2031bb2bc256b84aeb68a9abf7673151cec41b48bdd74f395a46acf30dae43e060e596bb2e739274210701ee9bb6cc3ff81ace751e375a01f17b3c5cc5f1234c488d69611bb27f6e3ee17e3c3843ebe4a280d6aa8ef017058a872810a437f85331adb3cb8d382650897b1b1589ee6", - "dd0000000108277148f2b9166660004053972df1beb451f73eb070e33ed63f681eb9b7e1e03f20baff3f54157598c7dd90a0de49850a3ccd6eb1b1cfc9dc6d3ba9ed1c0a19c69bf433da300d3cecc4ef151c44a721d680e3e3aaaf3eefec23091c5fde22", - "c90000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f99448825ee873013b006b2a6c87c7581c7117bfeb4ec3d68405a68d9488f6d58474dd16539677e869812ead055e70d655a660062e17083995c0dbecd565c79800b11c8ca0c351ecffa61e707d62d443b3810bc60d4ef87aa99b979ff55ee1ea46b65436c15534e5315113138aed6daa9f04d3050d77a7e379c83b948d3797177c1793e59b2555423bd52595d93e293ea8ffa3c428c6dbba4e202d76933caf6a5609b0a4aa6cf4fd2aadb6505382381ef2d5b33efc43eba24c84b7805baf2ddab44a50180e5e6f2a31f9ea8089aef562d3b578a799d61befec99c016fadec3363f68a1be4ca1e13e8bdd2809a1dacc41134663e22f21978167c5ee8ef49652ae152fc6c1bcf52109cd3076cdd599cb43261941de7aed148d7d3e956cd615549a9647496f43f998daad4c841cc40ce1501fbfc152b957c94be558f6743061e312d746137db2ae6a44e181587dbf6b0d9508cef4aefd99ea5d3369898bd4c3df5e95ac89eaaba54019ffe0402b8f567c91b9371e80c621c67d3c831331acc063892bbd8a81cfc0498e78474b11e8c05dd8f540c449505342ae95f6281940aae973db35b8e31ff801f6bc8975f592538881ae9cc4cedcbdb39a784a9fe962a1f12be51c11b91d4dedf649bb5672dec8e03db97b0d69fce36edfbecb6836644bad1ab8e6d4e13644d9c3476db0e8a8eb4b5a5c32f7a5604c8e19700c53602839478531579cb4c4bb5cc969cf482f325dd837629318baf128920d9978e23296d7016e6c05c954f95881b4f9f7e43bcea393951e91af0e4a671400dc435bd2a1616c60618df2476d0ece060dbbda11e751e256956a0dbcd7e4a8d6d85a3319f22a2c5f26dad50e82f70f3dd91feff19c775aa60499a3b7daa57e344c07c3787e99d53303488801d2b17cdbfdee61ea3fc473f6c146f06eb60d70594a59e0ed79cee6ca4a5f78b037637ddab69fb8522c0f7bf37aa7f59cc7fa659e759db69966455944975cd22a1a1355f35a589a4978c8f3272e1c4f6793288a00ab879299aa6ad02d966e3dc67cee0c808b1a046458cff9bdac25a4071eb10038a6389a0ef7233003641bd4ee1efad0e9b2f693396a89ca0db3c05b6abfed3b246eb1b23a6b77e8b486f26d9c3dde9dd6f3637a8115940ed2ca762ca6320609f61c37ffc9c3f2f7a0f27edc9891c2eeac49ba258a0d09c35c4fe1dc52d4d9319aa9b1a271a5d8d2d3a75fef4d59fb04679ba526aecbd19d73f72fee537630444326e2543ce564c669bf378499738385dda9ac63521a1b91f580d0737a7326009f0ff0dcb05aa8b86222c934d9ddb4628e30b6e12ae370154ab39c605431b4c40683592afcfd6fccf35df9fe5850442595d24be3d9f4298bf3d541f09e7e71f552c88eed9642df46953622d5aea05b5060325304ec81c0447ac95b90f9da4359e3286938f06aea3d45030cb836be15b1c65e3edf44cbcfe2f01ef8d7209c69d7c81334c866ebee50e418a28336cea1982069b4df090eab81303761d1af337e083f1e0ad1440a02ef1eefb03506c39d2377807e335ee64bdb76527f786223cee5233299eda9fcb1d38f19c34480f790a328b0735f80908e3aa70086df828d56b6c79516f71a24c9d94f60335f86e9d29c0c5d3872b", - "dd0000000108277148f2b916666000406672db10ab41db38c01f7021709bac4d1659d872623eb5852b12b494535d13779a88d37e9685da572f6b2de35793a519a457493456ac4ee242933cf92d783f783656899c31832274bf1c26d24720d9d8ecfec598e19c58a478d2991dfc1cda3000f7bd7bd17e80", - "d60000000108277148f2b916666000404ed98b1b4ac35c0c0ef18c88adf08a6701ccb0876ea75aac8c128349936fa3cb6728e4e58de8673dd7dc8457b092957f26bc8194233bb81c7e78127844f9b833f196dc46c5cb4064c773f3c6e0bc73", - }, - domain: "www.google.com", - }, - } - - for _, test := range cases { - data, err := testQuicSniffer(test.input, true) - assert.NoError(t, err) - assert.Equal(t, test.domain, data) - - data, err = testQuicSniffer(test.input, false) - assert.NoError(t, err) - assert.Equal(t, test.domain, data) - } -} - -func TestTLSHeaders(t *testing.T) { - cases := []struct { - input []byte - domain string - err bool - }{ - { - input: []byte{ - 0x16, 0x03, 0x01, 0x00, 0xc8, 0x01, 0x00, 0x00, - 0xc4, 0x03, 0x03, 0x1a, 0xac, 0xb2, 0xa8, 0xfe, - 0xb4, 0x96, 0x04, 0x5b, 0xca, 0xf7, 0xc1, 0xf4, - 0x2e, 0x53, 0x24, 0x6e, 0x34, 0x0c, 0x58, 0x36, - 0x71, 0x97, 0x59, 0xe9, 0x41, 0x66, 0xe2, 0x43, - 0xa0, 0x13, 0xb6, 0x00, 0x00, 0x20, 0x1a, 0x1a, - 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, - 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, - 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, - 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, - 0x00, 0x7b, 0xba, 0xba, 0x00, 0x00, 0xff, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, - 0x14, 0x00, 0x00, 0x11, 0x63, 0x2e, 0x73, 0x2d, - 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, - 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, - 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00, - 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04, - 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, - 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00, - 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, - 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, - 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, - 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x0b, 0x00, 0x02, - 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, - 0xaa, 0xaa, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, - 0xaa, 0xaa, 0x00, 0x01, 0x00, - }, - domain: "c.s-microsoft.com", - err: false, - }, - { - input: []byte{ - 0x16, 0x03, 0x01, 0x00, 0xee, 0x01, 0x00, 0x00, - 0xea, 0x03, 0x03, 0xe7, 0x91, 0x9e, 0x93, 0xca, - 0x78, 0x1b, 0x3c, 0xe0, 0x65, 0x25, 0x58, 0xb5, - 0x93, 0xe1, 0x0f, 0x85, 0xec, 0x9a, 0x66, 0x8e, - 0x61, 0x82, 0x88, 0xc8, 0xfc, 0xae, 0x1e, 0xca, - 0xd7, 0xa5, 0x63, 0x20, 0xbd, 0x1c, 0x00, 0x00, - 0x8b, 0xee, 0x09, 0xe3, 0x47, 0x6a, 0x0e, 0x74, - 0xb0, 0xbc, 0xa3, 0x02, 0xa7, 0x35, 0xe8, 0x85, - 0x70, 0x7c, 0x7a, 0xf0, 0x00, 0xdf, 0x4a, 0xea, - 0x87, 0x01, 0x14, 0x91, 0x00, 0x20, 0xea, 0xea, - 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, - 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, - 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, - 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, - 0x00, 0x81, 0x9a, 0x9a, 0x00, 0x00, 0xff, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, - 0x16, 0x00, 0x00, 0x13, 0x77, 0x77, 0x77, 0x30, - 0x37, 0x2e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x74, - 0x61, 0x6c, 0x65, 0x2e, 0x6e, 0x65, 0x74, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, - 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, - 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, - 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, - 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, - 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x75, 0x50, - 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, - 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x9a, 0x9a, - 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x8a, 0x8a, - 0x00, 0x01, 0x00, - }, - domain: "www07.clicktale.net", - err: false, - }, - { - input: []byte{ - 0x16, 0x03, 0x01, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe2, 0x03, 0x03, 0x81, 0x47, 0xc1, - 0x66, 0xd5, 0x1b, 0xfa, 0x4b, 0xb5, 0xe0, 0x2a, 0xe1, 0xa7, 0x87, 0x13, 0x1d, 0x11, 0xaa, 0xc6, - 0xce, 0xfc, 0x7f, 0xab, 0x94, 0xc8, 0x62, 0xad, 0xc8, 0xab, 0x0c, 0xdd, 0xcb, 0x20, 0x6f, 0x9d, - 0x07, 0xf1, 0x95, 0x3e, 0x99, 0xd8, 0xf3, 0x6d, 0x97, 0xee, 0x19, 0x0b, 0x06, 0x1b, 0xf4, 0x84, - 0x0b, 0xb6, 0x8f, 0xcc, 0xde, 0xe2, 0xd0, 0x2d, 0x6b, 0x0c, 0x1f, 0x52, 0x53, 0x13, 0x00, 0x08, - 0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c, - 0x00, 0x0a, 0x00, 0x00, 0x07, 0x64, 0x6f, 0x67, 0x66, 0x69, 0x73, 0x68, 0x00, 0x0b, 0x00, 0x04, - 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e, - 0x00, 0x19, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, - 0x00, 0x0d, 0x00, 0x1e, 0x00, 0x1c, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08, - 0x08, 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, - 0x06, 0x01, 0x00, 0x2b, 0x00, 0x07, 0x06, 0x7f, 0x1c, 0x7f, 0x1b, 0x7f, 0x1a, 0x00, 0x2d, 0x00, - 0x02, 0x01, 0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2f, 0x35, 0x0c, - 0xb6, 0x90, 0x0a, 0xb7, 0xd5, 0xc4, 0x1b, 0x2f, 0x60, 0xaa, 0x56, 0x7b, 0x3f, 0x71, 0xc8, 0x01, - 0x7e, 0x86, 0xd3, 0xb7, 0x0c, 0x29, 0x1a, 0x9e, 0x5b, 0x38, 0x3f, 0x01, 0x72, - }, - domain: "dogfish", - err: false, - }, - { - input: []byte{ - 0x16, 0x03, 0x01, 0x01, 0x03, 0x01, 0x00, 0x00, - 0xff, 0x03, 0x03, 0x3d, 0x89, 0x52, 0x9e, 0xee, - 0xbe, 0x17, 0x63, 0x75, 0xef, 0x29, 0xbd, 0x14, - 0x6a, 0x49, 0xe0, 0x2c, 0x37, 0x57, 0x71, 0x62, - 0x82, 0x44, 0x94, 0x8f, 0x6e, 0x94, 0x08, 0x45, - 0x7f, 0xdb, 0xc1, 0x00, 0x00, 0x3e, 0xc0, 0x2c, - 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, - 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, 0x00, 0x9e, - 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, - 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14, - 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, - 0x00, 0x9d, 0x00, 0x9c, 0x13, 0x02, 0x13, 0x03, - 0x13, 0x01, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, - 0x00, 0x2f, 0x00, 0xff, 0x01, 0x00, 0x00, 0x98, - 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00, - 0x0b, 0x31, 0x30, 0x2e, 0x34, 0x32, 0x2e, 0x30, - 0x2e, 0x32, 0x34, 0x33, 0x00, 0x0b, 0x00, 0x04, - 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0a, - 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, - 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, - 0x00, 0x20, 0x00, 0x1e, 0x04, 0x03, 0x05, 0x03, - 0x06, 0x03, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, - 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, - 0x02, 0x01, 0x02, 0x02, 0x04, 0x02, 0x05, 0x02, - 0x06, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, - 0x00, 0x00, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x7f, - 0x14, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, - 0x2d, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x28, - 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, - 0x13, 0x7c, 0x6e, 0x97, 0xc4, 0xfd, 0x09, 0x2e, - 0x70, 0x2f, 0x73, 0x5a, 0x9b, 0x57, 0x4d, 0x5f, - 0x2b, 0x73, 0x2c, 0xa5, 0x4a, 0x98, 0x40, 0x3d, - 0x75, 0x6e, 0xb4, 0x76, 0xf9, 0x48, 0x8f, 0x36, - }, - domain: "10.42.0.243", - err: false, - }, - } - - for _, test := range cases { - input := bytes.Clone(test.input) - domain, err := SniffTLS(test.input) - if test.err { - if err == nil { - t.Errorf("Exepct error but nil in test %v", test) - } - } else { - if err != nil { - t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) - } - if *domain != test.domain { - t.Error("expect domain ", test.domain, " but got ", domain) - } - } - assert.Equal(t, input, test.input) - } -} diff --git a/component/sniffer/tls_sniffer.go b/component/sniffer/tls_sniffer.go deleted file mode 100644 index b57f36ec88..0000000000 --- a/component/sniffer/tls_sniffer.go +++ /dev/null @@ -1,188 +0,0 @@ -package sniffer - -import ( - "encoding/binary" - "errors" - "fmt" - "strings" - - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/sniffer" -) - -var ( - errNotTLS = errors.New("not TLS header") - errNotClientHello = errors.New("not client hello") -) - -type errNeedAtLeastData struct { - length int - err error -} - -func (e *errNeedAtLeastData) Error() string { - return fmt.Sprintf("%v, need at least length: %d", e.err, e.length) -} - -func (e *errNeedAtLeastData) Unwrap() error { - return e.err -} - -var _ sniffer.Sniffer = (*TLSSniffer)(nil) - -type TLSSniffer struct { - *BaseSniffer -} - -func NewTLSSniffer(snifferConfig SnifferConfig) (*TLSSniffer, error) { - ports := snifferConfig.Ports - if len(ports) == 0 { - ports = utils.IntRanges[uint16]{utils.NewRange[uint16](443, 443)} - } - return &TLSSniffer{ - BaseSniffer: NewBaseSniffer(ports, C.TCP), - }, nil -} - -func (tls *TLSSniffer) Protocol() string { - return "tls" -} - -func (tls *TLSSniffer) SupportNetwork() C.NetWork { - return C.TCP -} - -func (tls *TLSSniffer) SniffData(bytes []byte) (string, error) { - domain, err := SniffTLS(bytes) - if err == nil { - return *domain, nil - } else { - return "", err - } -} - -func IsValidTLSVersion(major, minor byte) bool { - return major == 3 -} - -// ReadClientHello returns server name (if any) from TLS client hello message. -// https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go#L300 -func ReadClientHello(data []byte) (*string, error) { - if len(data) < 42 { - return nil, ErrNoClue - } - sessionIDLen := int(data[38]) - if sessionIDLen > 32 || len(data) < 39+sessionIDLen { - return nil, ErrNoClue - } - data = data[39+sessionIDLen:] - if len(data) < 2 { - return nil, ErrNoClue - } - // cipherSuiteLen is the number of bytes of cipher suite numbers. Since - // they are uint16s, the number must be even. - cipherSuiteLen := int(data[0])<<8 | int(data[1]) - if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen { - return nil, errNotClientHello - } - data = data[2+cipherSuiteLen:] - if len(data) < 1 { - return nil, ErrNoClue - } - compressionMethodsLen := int(data[0]) - if len(data) < 1+compressionMethodsLen { - return nil, ErrNoClue - } - data = data[1+compressionMethodsLen:] - - if len(data) == 0 { - return nil, errNotClientHello - } - if len(data) < 2 { - return nil, errNotClientHello - } - - extensionsLength := int(data[0])<<8 | int(data[1]) - data = data[2:] - if extensionsLength != len(data) { - return nil, errNotClientHello - } - - for len(data) != 0 { - if len(data) < 4 { - return nil, errNotClientHello - } - extension := uint16(data[0])<<8 | uint16(data[1]) - length := int(data[2])<<8 | int(data[3]) - data = data[4:] - if len(data) < length { - return nil, errNotClientHello - } - - if extension == 0x00 { /* extensionServerName */ - d := data[:length] - if len(d) < 2 { - return nil, errNotClientHello - } - namesLen := int(d[0])<<8 | int(d[1]) - d = d[2:] - if len(d) != namesLen { - return nil, errNotClientHello - } - for len(d) > 0 { - if len(d) < 3 { - return nil, errNotClientHello - } - nameType := d[0] - nameLen := int(d[1])<<8 | int(d[2]) - d = d[3:] - if len(d) < nameLen { - return nil, errNotClientHello - } - if nameType == 0 { - serverName := string(d[:nameLen]) - // An SNI value may not include a - // trailing dot. See - // https://tools.ietf.org/html/rfc6066#section-3. - if strings.HasSuffix(serverName, ".") { - return nil, errNotClientHello - } - - return &serverName, nil - } - - d = d[nameLen:] - } - } - data = data[length:] - } - - return nil, errNotTLS -} - -func SniffTLS(b []byte) (*string, error) { - if len(b) < 5 { - return nil, ErrNoClue - } - - if b[0] != 0x16 /* TLS Handshake */ { - return nil, errNotTLS - } - if !IsValidTLSVersion(b[1], b[2]) { - return nil, errNotTLS - } - headerLen := int(binary.BigEndian.Uint16(b[3:5])) - if 5+headerLen > len(b) { - return nil, &errNeedAtLeastData{ - length: 5 + headerLen, - err: ErrNoClue, - } - } - - domain, err := ReadClientHello(b[5 : 5+headerLen]) - if err == nil { - return domain, nil - } - return nil, err -} diff --git a/component/tls/reality.go b/component/tls/reality.go deleted file mode 100644 index 6a5cdc5fe1..0000000000 --- a/component/tls/reality.go +++ /dev/null @@ -1,212 +0,0 @@ -package tls - -import ( - "bytes" - "context" - "crypto/aes" - "crypto/cipher" - "crypto/ecdh" - "crypto/ed25519" - "crypto/hmac" - "crypto/sha256" - "crypto/sha512" - "crypto/tls" - "crypto/x509" - "encoding/binary" - "errors" - "net" - "net/http" - "strings" - "time" - - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/ntp" - - "github.com/metacubex/randv2" - utls "github.com/metacubex/utls" - "golang.org/x/crypto/chacha20poly1305" - "golang.org/x/crypto/hkdf" - "golang.org/x/exp/slices" - "golang.org/x/net/http2" -) - -const RealityMaxShortIDLen = 8 - -type RealityConfig struct { - PublicKey *ecdh.PublicKey - ShortID [RealityMaxShortIDLen]byte -} - -func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) { - for retry := 0; ; retry++ { - verifier := &realityVerifier{ - serverName: tlsConfig.ServerName, - } - uConfig := &utls.Config{ - ServerName: tlsConfig.ServerName, - InsecureSkipVerify: true, - SessionTicketsDisabled: true, - VerifyPeerCertificate: verifier.VerifyPeerCertificate, - } - clientID := utls.ClientHelloID{ - Client: fingerprint.Client, - Version: fingerprint.Version, - Seed: fingerprint.Seed, - } - uConn := utls.UClient(conn, uConfig, clientID) - verifier.UConn = uConn - err := uConn.BuildHandshakeState() - if err != nil { - return nil, err - } - - // ------for X25519MLKEM768 does not work properly with reality------- - // Iterate over extensions and check - for _, extension := range uConn.Extensions { - if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { - ce.Curves = slices.DeleteFunc(ce.Curves, func(curveID utls.CurveID) bool { - return curveID == utls.X25519MLKEM768 - }) - } - if ks, ok := extension.(*utls.KeyShareExtension); ok { - ks.KeyShares = slices.DeleteFunc(ks.KeyShares, func(share utls.KeyShare) bool { - return share.Group == utls.X25519MLKEM768 - }) - } - } - // Rebuild the client hello - err = uConn.BuildHandshakeState() - if err != nil { - return nil, err - } - // -------------------------------------------------------------------- - - hello := uConn.HandshakeState.Hello - rawSessionID := hello.Raw[39 : 39+32] // the location of session ID - for i := range rawSessionID { // https://github.com/golang/go/issues/5373 - rawSessionID[i] = 0 - } - - binary.BigEndian.PutUint64(hello.SessionId, uint64(ntp.Now().Unix())) - - copy(hello.SessionId[8:], realityConfig.ShortID[:]) - hello.SessionId[0] = 1 - hello.SessionId[1] = 8 - hello.SessionId[2] = 2 - - //log.Debugln("REALITY hello.sessionId[:16]: %v", hello.SessionId[:16]) - - keyShareKeys := uConn.HandshakeState.State13.KeyShareKeys - if keyShareKeys == nil { - // WTF??? - if retry > 2 { - return nil, errors.New("nil keyShareKeys") - } - continue // retry - } - ecdheKey := keyShareKeys.Ecdhe - if ecdheKey == nil { - // WTF??? - if retry > 2 { - return nil, errors.New("nil ecdheKey") - } - continue // retry - } - authKey, err := ecdheKey.ECDH(realityConfig.PublicKey) - if err != nil { - return nil, err - } - if authKey == nil { - return nil, errors.New("nil auth_key") - } - verifier.authKey = authKey - _, err = hkdf.New(sha256.New, authKey, hello.Random[:20], []byte("REALITY")).Read(authKey) - if err != nil { - return nil, err - } - var aeadCipher cipher.AEAD - if utls.AesgcmPreferred(hello.CipherSuites) { - aesBlock, _ := aes.NewCipher(authKey) - aeadCipher, _ = cipher.NewGCM(aesBlock) - } else { - aeadCipher, _ = chacha20poly1305.New(authKey) - } - aeadCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) - copy(hello.Raw[39:], hello.SessionId) - //log.Debugln("REALITY hello.sessionId: %v", hello.SessionId) - //log.Debugln("REALITY uConn.AuthKey: %v", authKey) - - err = uConn.HandshakeContext(ctx) - if err != nil { - return nil, err - } - - log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher) - - if !verifier.verified { - go realityClientFallback(uConn, uConfig.ServerName, clientID) - return nil, errors.New("REALITY authentication failed") - } - - return uConn, nil - } -} - -func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) { - defer uConn.Close() - client := http.Client{ - Transport: &http2.Transport{ - DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) { - return uConn, nil - }, - }, - } - request, err := http.NewRequest("GET", "https://"+serverName, nil) - if err != nil { - return - } - request.Header.Set("User-Agent", fingerprint.Client) - request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", randv2.IntN(32)+30)}) - response, err := client.Do(request) - if err != nil { - return - } - //_, _ = io.Copy(io.Discard, response.Body) - time.Sleep(time.Duration(5+randv2.IntN(10)) * time.Second) - response.Body.Close() - client.CloseIdleConnections() -} - -type realityVerifier struct { - *utls.UConn - serverName string - authKey []byte - verified bool -} - -//var pOffset = utils.MustOK(reflect.TypeOf((*utls.Conn)(nil)).Elem().FieldByName("peerCertificates")).Offset - -func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - //p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") - //certs := *(*[]*x509.Certificate)(unsafe.Add(unsafe.Pointer(c.Conn), pOffset)) - certs := c.Conn.PeerCertificates() - if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok { - h := hmac.New(sha512.New, c.authKey) - h.Write(pub) - if bytes.Equal(h.Sum(nil), certs[0].Signature) { - c.verified = true - return nil - } - } - opts := x509.VerifyOptions{ - DNSName: c.serverName, - Intermediates: x509.NewCertPool(), - } - for _, cert := range certs[1:] { - opts.Intermediates.AddCert(cert) - } - if _, err := certs[0].Verify(opts); err != nil { - return err - } - return nil -} diff --git a/component/tls/utls.go b/component/tls/utls.go deleted file mode 100644 index 80b37f38a8..0000000000 --- a/component/tls/utls.go +++ /dev/null @@ -1,167 +0,0 @@ -package tls - -import ( - "crypto/tls" - "net" - - "github.com/metacubex/mihomo/common/once" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/log" - - utls "github.com/metacubex/utls" - "github.com/mroth/weightedrand/v2" -) - -type Conn = utls.Conn -type UConn = utls.UConn -type UClientHelloID = utls.ClientHelloID - -const VersionTLS13 = utls.VersionTLS13 - -func Client(c net.Conn, config *utls.Config) *Conn { - return utls.Client(c, config) -} - -func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn { - return utls.UClient(c, config, fingerprint) -} - -func GetFingerprint(clientFingerprint string) (UClientHelloID, bool) { - if len(clientFingerprint) == 0 { - clientFingerprint = globalFingerprint - } - if len(clientFingerprint) == 0 || clientFingerprint == "none" { - return UClientHelloID{}, false - } - - if clientFingerprint == "random" { - fingerprint := randomFingerprint() - log.Debugln("use initial random HelloID:%s", fingerprint.Client) - return fingerprint, true - } - - if fingerprint, ok := fingerprints[clientFingerprint]; ok { - log.Debugln("use specified fingerprint:%s", fingerprint.Client) - return fingerprint, true - } else { - log.Warnln("wrong clientFingerprint:%s", clientFingerprint) - return UClientHelloID{}, false - } -} - -var randomFingerprint = once.OnceValue(func() UClientHelloID { - chooser, _ := weightedrand.NewChooser( - weightedrand.NewChoice("chrome", 6), - weightedrand.NewChoice("safari", 3), - weightedrand.NewChoice("ios", 2), - weightedrand.NewChoice("firefox", 1), - ) - initClient := chooser.Pick() - log.Debugln("initial random HelloID:%s", initClient) - fingerprint, ok := fingerprints[initClient] - if !ok { - log.Warnln("error in initial random HelloID:%s", initClient) - } - return fingerprint -}) - -var fingerprints = map[string]UClientHelloID{ - "chrome": utls.HelloChrome_Auto, - "chrome_psk": utls.HelloChrome_100_PSK, - "chrome_psk_shuffle": utls.HelloChrome_106_Shuffle, - "chrome_padding_psk_shuffle": utls.HelloChrome_114_Padding_PSK_Shuf, - "chrome_pq": utls.HelloChrome_115_PQ, - "chrome_pq_psk": utls.HelloChrome_115_PQ_PSK, - "firefox": utls.HelloFirefox_Auto, - "safari": utls.HelloSafari_Auto, - "ios": utls.HelloIOS_Auto, - "android": utls.HelloAndroid_11_OkHttp, - "edge": utls.HelloEdge_Auto, - "360": utls.Hello360_Auto, - "qq": utls.HelloQQ_Auto, - "random": {}, - "randomized": utls.HelloRandomized, -} - -func init() { - weights := utls.DefaultWeights - weights.TLSVersMax_Set_VersionTLS13 = 1 - weights.FirstKeyShare_Set_CurveP256 = 0 - randomized := utls.HelloRandomized - randomized.Seed, _ = utls.NewPRNGSeed() - randomized.Weights = &weights - fingerprints["randomized"] = randomized -} - -func UCertificates(it tls.Certificate) utls.Certificate { - return utls.Certificate{ - Certificate: it.Certificate, - PrivateKey: it.PrivateKey, - SupportedSignatureAlgorithms: utils.Map(it.SupportedSignatureAlgorithms, func(it tls.SignatureScheme) utls.SignatureScheme { - return utls.SignatureScheme(it) - }), - OCSPStaple: it.OCSPStaple, - SignedCertificateTimestamps: it.SignedCertificateTimestamps, - Leaf: it.Leaf, - } -} - -type Config = utls.Config - -func UConfig(config *tls.Config) *utls.Config { - return &utls.Config{ - Rand: config.Rand, - Time: config.Time, - Certificates: utils.Map(config.Certificates, UCertificates), - VerifyPeerCertificate: config.VerifyPeerCertificate, - RootCAs: config.RootCAs, - NextProtos: config.NextProtos, - ServerName: config.ServerName, - InsecureSkipVerify: config.InsecureSkipVerify, - CipherSuites: config.CipherSuites, - MinVersion: config.MinVersion, - MaxVersion: config.MaxVersion, - CurvePreferences: utils.Map(config.CurvePreferences, func(it tls.CurveID) utls.CurveID { - return utls.CurveID(it) - }), - SessionTicketsDisabled: config.SessionTicketsDisabled, - Renegotiation: utls.RenegotiationSupport(config.Renegotiation), - } -} - -// BuildWebsocketHandshakeState it will only send http/1.1 in its ALPN. -// Copy from https://github.com/XTLS/Xray-core/blob/main/transport/internet/tls/tls.go -func BuildWebsocketHandshakeState(c *UConn) error { - // Build the handshake state. This will apply every variable of the TLS of the - // fingerprint in the UConn - if err := c.BuildHandshakeState(); err != nil { - return err - } - // Iterate over extensions and check for utls.ALPNExtension - hasALPNExtension := false - for _, extension := range c.Extensions { - if alpn, ok := extension.(*utls.ALPNExtension); ok { - hasALPNExtension = true - alpn.AlpnProtocols = []string{"http/1.1"} - break - } - } - if !hasALPNExtension { // Append extension if doesn't exists - c.Extensions = append(c.Extensions, &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}) - } - // Rebuild the client hello - if err := c.BuildHandshakeState(); err != nil { - return err - } - return nil -} - -var globalFingerprint string - -func SetGlobalFingerprint(fingerprint string) { - globalFingerprint = fingerprint -} - -func GetGlobalFingerprint() string { - return globalFingerprint -} diff --git a/component/trie/domain.go b/component/trie/domain.go deleted file mode 100644 index 574a59caa7..0000000000 --- a/component/trie/domain.go +++ /dev/null @@ -1,180 +0,0 @@ -package trie - -import ( - "errors" - "strings" - "unicode" - "unicode/utf8" -) - -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[T any] struct { - root *Node[T] -} - -func ValidAndSplitDomain(domain string) ([]string, bool) { - if domain != "" && domain[len(domain)-1] == '.' { - return nil, false - } - if domain != "" { - if r, _ := utf8.DecodeRuneInString(domain); unicode.IsSpace(r) { - return nil, false - } - if r, _ := utf8.DecodeLastRuneInString(domain); unicode.IsSpace(r) { - return nil, false - } - } - domain = strings.ToLower(domain) - 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[T]) Insert(domain string, data T) 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[T]) insert(parts []string, data T) { - node := t.root - // reverse storage domain part to save space - for i := len(parts) - 1; i >= 0; i-- { - part := parts[i] - node = node.getOrNewChild(part) - } - - node.setData(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[T]) Search(domain string) *Node[T] { - parts, valid := ValidAndSplitDomain(domain) - if !valid || parts[0] == "" { - return nil - } - - n := t.search(t.root, parts) - - if n.isEmpty() { - return nil - } - - return n -} - -func (t *DomainTrie[T]) search(node *Node[T], parts []string) *Node[T] { - if len(parts) == 0 { - return node - } - - if c := node.getChild(parts[len(parts)-1]); c != nil { - if n := t.search(c, parts[:len(parts)-1]); !n.isEmpty() { - return n - } - } - - if c := node.getChild(wildcard); c != nil { - if n := t.search(c, parts[:len(parts)-1]); !n.isEmpty() { - return n - } - } - - return node.getChild(dotWildcard) -} - -func (t *DomainTrie[T]) Optimize() { - t.root.optimize() -} - -func (t *DomainTrie[T]) Foreach(fn func(domain string, data T) bool) { - for key, data := range t.root.getChildren() { - recursion([]string{key}, data, fn) - if !data.isEmpty() { - if !fn(joinDomain([]string{key}), data.data) { - return - } - } - } -} - -func (t *DomainTrie[T]) IsEmpty() bool { - if t == nil || t.root == nil { - return true - } - return len(t.root.getChildren()) == 0 -} - -func recursion[T any](items []string, node *Node[T], fn func(domain string, data T) bool) bool { - for key, data := range node.getChildren() { - newItems := append([]string{key}, items...) - if !data.isEmpty() { - domain := joinDomain(newItems) - if domain[0] == domainStepByte { - domain = complexWildcard + domain - } - if !fn(domain, data.Data()) { - return false - } - } - if !recursion(newItems, data, fn) { - return false - } - } - return true -} - -func joinDomain(items []string) string { - return strings.Join(items, domainStep) -} - -// New returns a new, empty Trie. -func New[T any]() *DomainTrie[T] { - return &DomainTrie[T]{root: newNode[T]()} -} diff --git a/component/trie/domain_set.go b/component/trie/domain_set.go deleted file mode 100644 index 3fd8041fb8..0000000000 --- a/component/trie/domain_set.go +++ /dev/null @@ -1,216 +0,0 @@ -package trie - -// Package succinct provides several succinct data types. -// Modify from https://github.com/openacid/succinct/blob/d4684c35d123f7528b14e03c24327231723db704/sskv.go - -import ( - "sort" - "strings" - - "github.com/metacubex/mihomo/common/utils" - "github.com/openacid/low/bitmap" -) - -const ( - complexWildcardByte = byte('+') - wildcardByte = byte('*') - domainStepByte = byte('.') -) - -type DomainSet struct { - leaves, labelBitmap []uint64 - labels []byte - ranks, selects []int32 -} - -type qElt struct{ s, e, col int } - -// NewDomainSet creates a new *DomainSet struct, from a DomainTrie. -func (t *DomainTrie[T]) NewDomainSet() *DomainSet { - reserveDomains := make([]string, 0) - t.Foreach(func(domain string, data T) bool { - reserveDomains = append(reserveDomains, utils.Reverse(domain)) - return true - }) - // ensure that the same prefix is continuous - // and according to the ascending sequence of length - sort.Strings(reserveDomains) - keys := reserveDomains - if len(keys) == 0 { - return nil - } - ss := &DomainSet{} - lIdx := 0 - - queue := []qElt{{0, len(keys), 0}} - for i := 0; i < len(queue); i++ { - elt := queue[i] - if elt.col == len(keys[elt.s]) { - elt.s++ - // a leaf node - setBit(&ss.leaves, i, 1) - } - - for j := elt.s; j < elt.e; { - - frm := j - - for ; j < elt.e && keys[j][elt.col] == keys[frm][elt.col]; j++ { - } - queue = append(queue, qElt{frm, j, elt.col + 1}) - ss.labels = append(ss.labels, keys[frm][elt.col]) - setBit(&ss.labelBitmap, lIdx, 0) - lIdx++ - } - setBit(&ss.labelBitmap, lIdx, 1) - lIdx++ - } - - ss.init() - return ss -} - -// Has query for a key and return whether it presents in the DomainSet. -func (ss *DomainSet) Has(key string) bool { - if ss == nil { - return false - } - key = utils.Reverse(key) - key = strings.ToLower(key) - // no more labels in this node - // skip character matching - // go to next level - nodeId, bmIdx := 0, 0 - type wildcardCursor struct { - bmIdx, index int - } - stack := make([]wildcardCursor, 0) - for i := 0; i < len(key); i++ { - RESTART: - c := key[i] - for ; ; bmIdx++ { - if getBit(ss.labelBitmap, bmIdx) != 0 { - if len(stack) > 0 { - cursor := stack[len(stack)-1] - stack = stack[0 : len(stack)-1] - // back wildcard and find next node - nextNodeId := countZeros(ss.labelBitmap, ss.ranks, cursor.bmIdx+1) - nextBmIdx := selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nextNodeId-1) + 1 - j := cursor.index - for ; j < len(key) && key[j] != domainStepByte; j++ { - } - if j == len(key) { - if getBit(ss.leaves, nextNodeId) != 0 { - return true - } else { - goto RESTART - } - } - for ; nextBmIdx-nextNodeId < len(ss.labels); nextBmIdx++ { - if ss.labels[nextBmIdx-nextNodeId] == domainStepByte { - bmIdx = nextBmIdx - nodeId = nextNodeId - i = j - goto RESTART - } - } - } - return false - } - // handle wildcard for domain - if ss.labels[bmIdx-nodeId] == complexWildcardByte { - return true - } else if ss.labels[bmIdx-nodeId] == wildcardByte { - cursor := wildcardCursor{} - cursor.bmIdx = bmIdx - cursor.index = i - stack = append(stack, cursor) - } else if ss.labels[bmIdx-nodeId] == c { - break - } - } - nodeId = countZeros(ss.labelBitmap, ss.ranks, bmIdx+1) - bmIdx = selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nodeId-1) + 1 - } - - return getBit(ss.leaves, nodeId) != 0 - -} - -func (ss *DomainSet) keys(f func(key string) bool) { - var currentKey []byte - var traverse func(int, int) bool - traverse = func(nodeId, bmIdx int) bool { - if getBit(ss.leaves, nodeId) != 0 { - if !f(string(currentKey)) { - return false - } - } - - for ; ; bmIdx++ { - if getBit(ss.labelBitmap, bmIdx) != 0 { - return true - } - nextLabel := ss.labels[bmIdx-nodeId] - currentKey = append(currentKey, nextLabel) - nextNodeId := countZeros(ss.labelBitmap, ss.ranks, bmIdx+1) - nextBmIdx := selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nextNodeId-1) + 1 - if !traverse(nextNodeId, nextBmIdx) { - return false - } - currentKey = currentKey[:len(currentKey)-1] - } - } - - traverse(0, 0) - return -} - -func (ss *DomainSet) Foreach(f func(key string) bool) { - ss.keys(func(key string) bool { - return f(utils.Reverse(key)) - }) -} - -// MatchDomain implements C.DomainMatcher -func (ss *DomainSet) MatchDomain(domain string) bool { - return ss.Has(domain) -} - -func setBit(bm *[]uint64, i int, v int) { - for i>>6 >= len(*bm) { - *bm = append(*bm, 0) - } - (*bm)[i>>6] |= uint64(v) << uint(i&63) -} - -func getBit(bm []uint64, i int) uint64 { - return bm[i>>6] & (1 << uint(i&63)) -} - -// init builds pre-calculated cache to speed up rank() and select() -func (ss *DomainSet) init() { - ss.selects, ss.ranks = bitmap.IndexSelect32R64(ss.labelBitmap) -} - -// countZeros counts the number of "0" in a bitmap before the i-th bit(excluding -// the i-th bit) on behalf of rank index. -// E.g.: -// -// countZeros("010010", 4) == 3 -// // 012345 -func countZeros(bm []uint64, ranks []int32, i int) int { - a, _ := bitmap.Rank64(bm, ranks, int32(i)) - return i - int(a) -} - -// selectIthOne returns the index of the i-th "1" in a bitmap, on behalf of rank -// and select indexes. -// E.g.: -// -// selectIthOne("010010", 1) == 4 -// // 012345 -func selectIthOne(bm []uint64, ranks, selects []int32, i int) int { - a, _ := bitmap.Select32R64(bm, selects, ranks, int32(i)) - return int(a) -} diff --git a/component/trie/domain_set_bin.go b/component/trie/domain_set_bin.go deleted file mode 100644 index 27d15802e0..0000000000 --- a/component/trie/domain_set_bin.go +++ /dev/null @@ -1,115 +0,0 @@ -package trie - -import ( - "encoding/binary" - "errors" - "io" -) - -func (ss *DomainSet) WriteBin(w io.Writer) (err error) { - // version - _, err = w.Write([]byte{1}) - if err != nil { - return err - } - - // leaves - err = binary.Write(w, binary.BigEndian, int64(len(ss.leaves))) - if err != nil { - return err - } - for _, d := range ss.leaves { - err = binary.Write(w, binary.BigEndian, d) - if err != nil { - return err - } - } - - // labelBitmap - err = binary.Write(w, binary.BigEndian, int64(len(ss.labelBitmap))) - if err != nil { - return err - } - for _, d := range ss.labelBitmap { - err = binary.Write(w, binary.BigEndian, d) - if err != nil { - return err - } - } - - // labels - err = binary.Write(w, binary.BigEndian, int64(len(ss.labels))) - if err != nil { - return err - } - _, err = w.Write(ss.labels) - if err != nil { - return err - } - - return nil -} - -func ReadDomainSetBin(r io.Reader) (ds *DomainSet, err error) { - // version - version := make([]byte, 1) - _, err = io.ReadFull(r, version) - if err != nil { - return nil, err - } - if version[0] != 1 { - return nil, errors.New("version is invalid") - } - - ds = &DomainSet{} - var length int64 - - // leaves - err = binary.Read(r, binary.BigEndian, &length) - if err != nil { - return nil, err - } - if length < 1 { - return nil, errors.New("length is invalid") - } - ds.leaves = make([]uint64, length) - for i := int64(0); i < length; i++ { - err = binary.Read(r, binary.BigEndian, &ds.leaves[i]) - if err != nil { - return nil, err - } - } - - // labelBitmap - err = binary.Read(r, binary.BigEndian, &length) - if err != nil { - return nil, err - } - if length < 1 { - return nil, errors.New("length is invalid") - } - ds.labelBitmap = make([]uint64, length) - for i := int64(0); i < length; i++ { - err = binary.Read(r, binary.BigEndian, &ds.labelBitmap[i]) - if err != nil { - return nil, err - } - } - - // labels - err = binary.Read(r, binary.BigEndian, &length) - if err != nil { - return nil, err - } - if length < 1 { - return nil, errors.New("length is invalid") - } - ds.labels = make([]byte, length) - _, err = io.ReadFull(r, ds.labels) - if err != nil { - return nil, err - } - - ds.init() - return ds, nil -} diff --git a/component/trie/domain_set_test.go b/component/trie/domain_set_test.go deleted file mode 100644 index 38ba1622a3..0000000000 --- a/component/trie/domain_set_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package trie_test - -import ( - "golang.org/x/exp/slices" - "testing" - - "github.com/metacubex/mihomo/component/trie" - "github.com/stretchr/testify/assert" -) - -func testDump(t *testing.T, tree *trie.DomainTrie[struct{}], set *trie.DomainSet) { - var dataSrc []string - tree.Foreach(func(domain string, data struct{}) bool { - dataSrc = append(dataSrc, domain) - return true - }) - slices.Sort(dataSrc) - var dataSet []string - set.Foreach(func(key string) bool { - dataSet = append(dataSet, key) - return true - }) - slices.Sort(dataSet) - assert.Equal(t, dataSrc, dataSet) -} - -func TestDomainSet(t *testing.T) { - tree := trie.New[struct{}]() - domainSet := []string{ - "baidu.com", - "google.com", - "www.google.com", - "test.a.net", - "test.a.oc", - "Mijia Cloud", - ".qq.com", - "+.cn", - } - - for _, domain := range domainSet { - assert.NoError(t, tree.Insert(domain, struct{}{})) - } - assert.False(t, tree.IsEmpty()) - set := tree.NewDomainSet() - assert.NotNil(t, set) - assert.True(t, set.Has("test.cn")) - assert.True(t, set.Has("cn")) - assert.True(t, set.Has("Mijia Cloud")) - assert.True(t, set.Has("test.a.net")) - assert.True(t, set.Has("www.qq.com")) - assert.True(t, set.Has("google.com")) - assert.False(t, set.Has("qq.com")) - assert.False(t, set.Has("www.baidu.com")) - testDump(t, tree, set) -} - -func TestDomainSetComplexWildcard(t *testing.T) { - tree := trie.New[struct{}]() - domainSet := []string{ - "+.baidu.com", - "+.a.baidu.com", - "www.baidu.com", - "+.bb.baidu.com", - "test.a.net", - "test.a.oc", - "www.qq.com", - } - - for _, domain := range domainSet { - assert.NoError(t, tree.Insert(domain, struct{}{})) - } - assert.False(t, tree.IsEmpty()) - set := tree.NewDomainSet() - assert.NotNil(t, set) - assert.False(t, set.Has("google.com")) - assert.True(t, set.Has("www.baidu.com")) - assert.True(t, set.Has("test.test.baidu.com")) - testDump(t, tree, set) -} - -func TestDomainSetWildcard(t *testing.T) { - tree := trie.New[struct{}]() - domainSet := []string{ - "*.*.*.baidu.com", - "www.baidu.*", - "stun.*.*", - "*.*.qq.com", - "test.*.baidu.com", - "*.apple.com", - } - - for _, domain := range domainSet { - assert.NoError(t, tree.Insert(domain, struct{}{})) - } - assert.False(t, tree.IsEmpty()) - set := tree.NewDomainSet() - assert.NotNil(t, set) - assert.True(t, set.Has("www.baidu.com")) - assert.True(t, set.Has("test.test.baidu.com")) - assert.True(t, set.Has("test.test.qq.com")) - assert.True(t, set.Has("stun.ab.cd")) - assert.False(t, set.Has("test.baidu.com")) - assert.False(t, set.Has("www.google.com")) - assert.False(t, set.Has("a.www.google.com")) - assert.False(t, set.Has("test.qq.com")) - assert.False(t, set.Has("test.test.test.qq.com")) - testDump(t, tree, set) -} diff --git a/component/trie/domain_test.go b/component/trie/domain_test.go deleted file mode 100644 index 6aab72d3a7..0000000000 --- a/component/trie/domain_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package trie_test - -import ( - "net/netip" - "testing" - - "github.com/metacubex/mihomo/component/trie" - "github.com/stretchr/testify/assert" -) - -var localIP = netip.AddrFrom4([4]byte{127, 0, 0, 1}) - -func TestTrie_Basic(t *testing.T) { - tree := trie.New[netip.Addr]() - domains := []string{ - "example.com", - "google.com", - "localhost", - } - - for _, domain := range domains { - assert.NoError(t, tree.Insert(domain, localIP)) - } - - node := tree.Search("example.com") - assert.NotNil(t, node) - assert.True(t, node.Data() == 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 := trie.New[netip.Addr]() - 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 { - assert.NoError(t, 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 := trie.New[int]() - 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 { - assert.NoError(t, tree.Insert(domain, idx+1)) - } - - assertFn("test.dev", 1) - assertFn("foo.bar.dev", 1) - assertFn("example.dev", 2) - assertFn("foo.example.dev", 3) - assertFn("test.example.dev", 4) -} - -func TestTrie_Boundary(t *testing.T) { - tree := trie.New[netip.Addr]() - assert.NoError(t, 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 := trie.New[netip.Addr]() - assert.NoError(t, tree.Insert("+.*", localIP)) - assert.NoError(t, tree.Insert("stun.*.*.*", localIP)) - - assert.NotNil(t, tree.Search("example.com")) -} - -func TestTrie_Foreach(t *testing.T) { - tree := trie.New[netip.Addr]() - domainList := []string{ - "google.com", - "stun.*.*.*", - "test.*.google.com", - "+.baidu.com", - "*.baidu.com", - "*.*.baidu.com", - } - for _, domain := range domainList { - assert.NoError(t, tree.Insert(domain, localIP)) - } - count := 0 - tree.Foreach(func(domain string, data netip.Addr) bool { - count++ - return true - }) - assert.Equal(t, 7, count) -} - -func TestTrie_Space(t *testing.T) { - validDomain := func(domain string) bool { - _, ok := trie.ValidAndSplitDomain(domain) - return ok - } - assert.True(t, validDomain("google.com")) - assert.False(t, validDomain(" google.com")) - assert.False(t, validDomain(" google.com ")) - assert.True(t, validDomain("Mijia Cloud")) -} diff --git a/component/trie/ipcidr_node.go b/component/trie/ipcidr_node.go deleted file mode 100644 index acaf9a8f64..0000000000 --- a/component/trie/ipcidr_node.go +++ /dev/null @@ -1,44 +0,0 @@ -package trie - -import "errors" - -var ( - ErrorOverMaxValue = errors.New("the value don't over max value") -) - -type IpCidrNode struct { - Mark bool - child map[uint32]*IpCidrNode - maxValue uint32 -} - -func NewIpCidrNode(mark bool, maxValue uint32) *IpCidrNode { - ipCidrNode := &IpCidrNode{ - Mark: mark, - child: map[uint32]*IpCidrNode{}, - maxValue: maxValue, - } - - return ipCidrNode -} - -func (n *IpCidrNode) addChild(value uint32) error { - if value > n.maxValue { - return ErrorOverMaxValue - } - - n.child[value] = NewIpCidrNode(false, n.maxValue) - return nil -} - -func (n *IpCidrNode) hasChild(value uint32) bool { - return n.getChild(value) != nil -} - -func (n *IpCidrNode) getChild(value uint32) *IpCidrNode { - if value <= n.maxValue { - return n.child[value] - } - - return nil -} diff --git a/component/trie/ipcidr_trie.go b/component/trie/ipcidr_trie.go deleted file mode 100644 index ee8659beff..0000000000 --- a/component/trie/ipcidr_trie.go +++ /dev/null @@ -1,249 +0,0 @@ -package trie - -import ( - "net" - - "github.com/metacubex/mihomo/log" -) - -type IPV6 bool - -const ( - ipv4GroupMaxValue = 0xFF - ipv6GroupMaxValue = 0xFFFF -) - -type IpCidrTrie struct { - ipv4Trie *IpCidrNode - ipv6Trie *IpCidrNode -} - -func NewIpCidrTrie() *IpCidrTrie { - return &IpCidrTrie{ - ipv4Trie: NewIpCidrNode(false, ipv4GroupMaxValue), - ipv6Trie: NewIpCidrNode(false, ipv6GroupMaxValue), - } -} - -func (trie *IpCidrTrie) AddIpCidr(ipCidr *net.IPNet) error { - subIpCidr, subCidr, isIpv4, err := ipCidrToSubIpCidr(ipCidr) - if err != nil { - return err - } - - for _, sub := range subIpCidr { - addIpCidr(trie, isIpv4, sub, subCidr/8) - } - - return nil -} - -func (trie *IpCidrTrie) AddIpCidrForString(ipCidr string) error { - _, ipNet, err := net.ParseCIDR(ipCidr) - if err != nil { - return err - } - - return trie.AddIpCidr(ipNet) -} - -func (trie *IpCidrTrie) IsContain(ip net.IP) bool { - if ip == nil { - return false - } - isIpv4 := len(ip) == net.IPv4len - var groupValues []uint32 - var ipCidrNode *IpCidrNode - - if isIpv4 { - ipCidrNode = trie.ipv4Trie - for _, group := range ip { - groupValues = append(groupValues, uint32(group)) - } - } else { - ipCidrNode = trie.ipv6Trie - for i := 0; i < len(ip); i += 2 { - groupValues = append(groupValues, getIpv6GroupValue(ip[i], ip[i+1])) - } - } - - return search(ipCidrNode, groupValues) != nil -} - -func (trie *IpCidrTrie) IsContainForString(ipString string) bool { - ip := net.ParseIP(ipString) - // deal with 4in6 - actualIp := ip.To4() - if actualIp == nil { - actualIp = ip - } - return trie.IsContain(actualIp) -} - -func ipCidrToSubIpCidr(ipNet *net.IPNet) ([]net.IP, int, bool, error) { - maskSize, _ := ipNet.Mask.Size() - var ( - ipList []net.IP - newMaskSize int - isIpv4 bool - err error - ) - isIpv4 = len(ipNet.IP) == net.IPv4len - ipList, newMaskSize, err = subIpCidr(ipNet.IP, maskSize, isIpv4) - - return ipList, newMaskSize, isIpv4, err -} - -func subIpCidr(ip net.IP, maskSize int, isIpv4 bool) ([]net.IP, int, error) { - var subIpCidrList []net.IP - groupSize := 8 - if !isIpv4 { - groupSize = 16 - } - - if maskSize%groupSize == 0 { - return append(subIpCidrList, ip), maskSize, nil - } - - lastByteMaskSize := maskSize % 8 - lastByteMaskIndex := maskSize / 8 - subIpCidrNum := 0xFF >> lastByteMaskSize - for i := 0; i <= subIpCidrNum; i++ { - subIpCidr := make([]byte, len(ip)) - copy(subIpCidr, ip) - subIpCidr[lastByteMaskIndex] += byte(i) - subIpCidrList = append(subIpCidrList, subIpCidr) - } - - newMaskSize := (lastByteMaskIndex + 1) * 8 - if !isIpv4 { - newMaskSize = (lastByteMaskIndex/2 + 1) * 16 - } - - return subIpCidrList, newMaskSize, nil -} - -func addIpCidr(trie *IpCidrTrie, isIpv4 bool, ip net.IP, groupSize int) { - if isIpv4 { - addIpv4Cidr(trie, ip, groupSize) - } else { - addIpv6Cidr(trie, ip, groupSize) - } -} - -func addIpv4Cidr(trie *IpCidrTrie, ip net.IP, groupSize int) { - preNode := trie.ipv4Trie - node := preNode.getChild(uint32(ip[0])) - if node == nil { - err := preNode.addChild(uint32(ip[0])) - if err != nil { - return - } - - node = preNode.getChild(uint32(ip[0])) - } - - for i := 1; i < groupSize; i++ { - if node.Mark { - return - } - - groupValue := uint32(ip[i]) - if !node.hasChild(groupValue) { - err := node.addChild(groupValue) - if err != nil { - log.Errorln(err.Error()) - } - } - - preNode = node - node = node.getChild(groupValue) - if node == nil { - err := preNode.addChild(uint32(ip[i-1])) - if err != nil { - return - } - - node = preNode.getChild(uint32(ip[i-1])) - } - } - - node.Mark = true - cleanChild(node) -} - -func addIpv6Cidr(trie *IpCidrTrie, ip net.IP, groupSize int) { - preNode := trie.ipv6Trie - node := preNode.getChild(getIpv6GroupValue(ip[0], ip[1])) - if node == nil { - err := preNode.addChild(getIpv6GroupValue(ip[0], ip[1])) - if err != nil { - return - } - - node = preNode.getChild(getIpv6GroupValue(ip[0], ip[1])) - } - - for i := 2; i < groupSize; i += 2 { - if ip[i] == 0 && ip[i+1] == 0 { - node.Mark = true - } - - if node.Mark { - return - } - - groupValue := getIpv6GroupValue(ip[i], ip[i+1]) - if !node.hasChild(groupValue) { - err := node.addChild(groupValue) - if err != nil { - log.Errorln(err.Error()) - } - } - - preNode = node - node = node.getChild(groupValue) - if node == nil { - err := preNode.addChild(getIpv6GroupValue(ip[i-2], ip[i-1])) - if err != nil { - return - } - - node = preNode.getChild(getIpv6GroupValue(ip[i-2], ip[i-1])) - } - } - - node.Mark = true - cleanChild(node) -} - -func getIpv6GroupValue(high, low byte) uint32 { - return (uint32(high) << 8) | uint32(low) -} - -func cleanChild(node *IpCidrNode) { - for i := uint32(0); i < uint32(len(node.child)); i++ { - delete(node.child, i) - } -} - -func search(root *IpCidrNode, groupValues []uint32) *IpCidrNode { - node := root.getChild(groupValues[0]) - if node == nil || node.Mark { - return node - } - - for _, value := range groupValues[1:] { - if !node.hasChild(value) { - return nil - } - - node = node.getChild(value) - - if node == nil || node.Mark { - return node - } - } - - return nil -} diff --git a/component/trie/node.go b/component/trie/node.go deleted file mode 100644 index 3aa2bc7d6c..0000000000 --- a/component/trie/node.go +++ /dev/null @@ -1,137 +0,0 @@ -package trie - -import "strings" - -// Node is the trie's node -type Node[T any] struct { - childMap map[string]*Node[T] - childNode *Node[T] // optimize for only one child - childStr string - inited bool - data T -} - -func (n *Node[T]) getChild(s string) *Node[T] { - if n.childMap == nil { - if n.childNode != nil && n.childStr == s { - return n.childNode - } - return nil - } - return n.childMap[s] -} - -func (n *Node[T]) hasChild(s string) bool { - return n.getChild(s) != nil -} - -func (n *Node[T]) addChild(s string, child *Node[T]) { - if n.childMap == nil { - if n.childNode == nil { - n.childStr = s - n.childNode = child - return - } - n.childMap = map[string]*Node[T]{} - if n.childNode != nil { - n.childMap[n.childStr] = n.childNode - } - n.childStr = "" - n.childNode = nil - } - - n.childMap[s] = child -} - -func (n *Node[T]) getOrNewChild(s string) *Node[T] { - node := n.getChild(s) - if node == nil { - node = newNode[T]() - n.addChild(s, node) - } - return node -} - -func (n *Node[T]) optimize() { - if len(n.childStr) > 0 { - n.childStr = strClone(n.childStr) - } - if n.childNode != nil { - n.childNode.optimize() - } - if n.childMap == nil { - return - } - switch len(n.childMap) { - case 0: - n.childMap = nil - return - case 1: - for key := range n.childMap { - n.childStr = key - n.childNode = n.childMap[key] - } - n.childMap = nil - n.optimize() - return - } - children := make(map[string]*Node[T], len(n.childMap)) // avoid map reallocate memory - for key := range n.childMap { - child := n.childMap[key] - if child == nil { - continue - } - key = strClone(key) - children[key] = child - child.optimize() - } - n.childMap = children -} - -func strClone(key string) string { - switch key { // try to save string's memory - case wildcard: - key = wildcard - case dotWildcard: - key = dotWildcard - case complexWildcard: - key = complexWildcard - case domainStep: - key = domainStep - default: - key = strings.Clone(key) - } - return key -} - -func (n *Node[T]) isEmpty() bool { - if n == nil || n.inited == false { - return true - } - return false -} - -func (n *Node[T]) setData(data T) { - n.data = data - n.inited = true -} - -func (n *Node[T]) getChildren() map[string]*Node[T] { - if n.childMap == nil { - if n.childNode != nil { - m := make(map[string]*Node[T]) - m[n.childStr] = n.childNode - return m - } - } else { - return n.childMap - } - return nil -} -func (n *Node[T]) Data() T { - return n.data -} - -func newNode[T any]() *Node[T] { - return &Node[T]{} -} diff --git a/component/trie/trie_test.go b/component/trie/trie_test.go deleted file mode 100644 index a589b7ca86..0000000000 --- a/component/trie/trie_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package trie - -import ( - "net" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestIpv4AddSuccess(t *testing.T) { - trie := NewIpCidrTrie() - err := trie.AddIpCidrForString("10.0.0.2/16") - assert.Equal(t, nil, err) -} - -func TestIpv4AddFail(t *testing.T) { - trie := NewIpCidrTrie() - err := trie.AddIpCidrForString("333.00.23.2/23") - assert.IsType(t, new(net.ParseError), err) - - err = trie.AddIpCidrForString("22.3.34.2/222") - assert.IsType(t, new(net.ParseError), err) - - err = trie.AddIpCidrForString("2.2.2.2") - assert.IsType(t, new(net.ParseError), err) -} - -func TestIpv4Search(t *testing.T) { - trie := NewIpCidrTrie() - // Boundary testing - assert.NoError(t, trie.AddIpCidrForString("149.154.160.0/20")) - assert.Equal(t, true, trie.IsContainForString("149.154.160.0")) - assert.Equal(t, true, trie.IsContainForString("149.154.175.255")) - assert.Equal(t, false, trie.IsContainForString("149.154.176.0")) - assert.Equal(t, false, trie.IsContainForString("149.154.159.255")) - - assert.NoError(t, trie.AddIpCidrForString("129.2.36.0/16")) - assert.NoError(t, trie.AddIpCidrForString("10.2.36.0/18")) - assert.NoError(t, trie.AddIpCidrForString("16.2.23.0/24")) - assert.NoError(t, trie.AddIpCidrForString("11.2.13.2/26")) - assert.NoError(t, trie.AddIpCidrForString("55.5.6.3/8")) - assert.NoError(t, trie.AddIpCidrForString("66.23.25.4/6")) - - assert.Equal(t, true, trie.IsContainForString("129.2.3.65")) - assert.Equal(t, false, trie.IsContainForString("15.2.3.1")) - assert.Equal(t, true, trie.IsContainForString("11.2.13.1")) - assert.Equal(t, true, trie.IsContainForString("55.0.0.0")) - assert.Equal(t, true, trie.IsContainForString("64.0.0.0")) - assert.Equal(t, false, trie.IsContainForString("128.0.0.0")) - - assert.Equal(t, false, trie.IsContain(net.ParseIP("22"))) - assert.Equal(t, false, trie.IsContain(net.ParseIP(""))) - -} - -func TestIpv6AddSuccess(t *testing.T) { - trie := NewIpCidrTrie() - err := trie.AddIpCidrForString("2001:0db8:02de:0000:0000:0000:0000:0e13/32") - assert.Equal(t, nil, err) - - err = trie.AddIpCidrForString("2001:1db8:f2de::0e13/18") - assert.Equal(t, nil, err) -} - -func TestIpv6AddFail(t *testing.T) { - trie := NewIpCidrTrie() - err := trie.AddIpCidrForString("2001::25de::cade/23") - assert.IsType(t, new(net.ParseError), err) - - err = trie.AddIpCidrForString("2001:0fa3:25de::cade/222") - assert.IsType(t, new(net.ParseError), err) - - err = trie.AddIpCidrForString("2001:0fa3:25de::cade") - assert.IsType(t, new(net.ParseError), err) -} - -func TestIpv6SearchSub(t *testing.T) { - trie := NewIpCidrTrie() - assert.NoError(t, trie.AddIpCidrForString("240e::/18")) - - assert.Equal(t, true, trie.IsContainForString("240e:964:ea02:100:1800::71")) - -} - -func TestIpv6Search(t *testing.T) { - trie := NewIpCidrTrie() - - // Boundary testing - assert.NoError(t, trie.AddIpCidrForString("2a0a:f280::/32")) - assert.Equal(t, true, trie.IsContainForString("2a0a:f280:0000:0000:0000:0000:0000:0000")) - assert.Equal(t, true, trie.IsContainForString("2a0a:f280:ffff:ffff:ffff:ffff:ffff:ffff")) - assert.Equal(t, false, trie.IsContainForString("2a0a:f279:ffff:ffff:ffff:ffff:ffff:ffff")) - assert.Equal(t, false, trie.IsContainForString("2a0a:f281:0000:0000:0000:0000:0000:0000")) - - assert.NoError(t, trie.AddIpCidrForString("2001:b28:f23d:f001::e/128")) - assert.NoError(t, trie.AddIpCidrForString("2001:67c:4e8:f002::e/12")) - assert.NoError(t, trie.AddIpCidrForString("2001:b28:f23d:f003::e/96")) - assert.NoError(t, trie.AddIpCidrForString("2001:67c:4e8:f002::a/32")) - assert.NoError(t, trie.AddIpCidrForString("2001:67c:4e8:f004::a/60")) - assert.NoError(t, trie.AddIpCidrForString("2001:b28:f23f:f005::a/64")) - assert.Equal(t, true, trie.IsContainForString("2001:b28:f23d:f001::e")) - assert.Equal(t, false, trie.IsContainForString("2222::fff2")) - assert.Equal(t, true, trie.IsContainForString("2000::ffa0")) - assert.Equal(t, true, trie.IsContainForString("2001:b28:f23f:f005:5662::")) - assert.Equal(t, true, trie.IsContainForString("2001:67c:4e8:9666::1213")) - - assert.Equal(t, false, trie.IsContain(net.ParseIP("22233:22"))) -} - -func TestIpv4InIpv6(t *testing.T) { - trie := NewIpCidrTrie() - - // Boundary testing - assert.NoError(t, trie.AddIpCidrForString("::ffff:198.18.5.138/128")) -} diff --git a/component/updater/update_core.go b/component/updater/update_core.go deleted file mode 100644 index 2aab7833ea..0000000000 --- a/component/updater/update_core.go +++ /dev/null @@ -1,499 +0,0 @@ -package updater - -import ( - "archive/zip" - "compress/gzip" - "context" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "sync" - "time" - - mihomoHttp "github.com/metacubex/mihomo/component/http" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - "github.com/klauspost/cpuid/v2" -) - -// modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/updater/updater.go -// Updater is the mihomo updater. -var ( - goarm string - gomips string - amd64Compatible string - - workDir string - - // mu protects all fields below. - mu sync.Mutex - - currentExeName string // 当前可执行文件 - updateDir string // 更新目录 - packageName string // 更新压缩文件 - backupDir string // 备份目录 - backupExeName string // 备份文件名 - updateExeName string // 更新后的可执行文件 - - baseURL string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/mihomo" - versionURL string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt" - packageURL string - latestVersion string -) - -func init() { - if runtime.GOARCH == "amd64" && cpuid.CPU.X64Level() < 3 { - amd64Compatible = "-compatible" - } - if !strings.HasPrefix(C.Version, "alpha") { - baseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/mihomo" - versionURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt" - } -} - -type updateError struct { - Message string -} - -func (e *updateError) Error() string { - return fmt.Sprintf("update error: %s", e.Message) -} - -// Update performs the auto-updater. It returns an error if the updater failed. -// If firstRun is true, it assumes the configuration file doesn't exist. -func UpdateCore(execPath string) (err error) { - mu.Lock() - defer mu.Unlock() - - latestVersion, err = getLatestVersion() - if err != nil { - return err - } - - log.Infoln("current version %s, latest version %s", C.Version, latestVersion) - - if latestVersion == C.Version { - err := &updateError{Message: "already using latest version"} - return err - } - - updateDownloadURL() - - defer func() { - if err != nil { - log.Errorln("updater: failed: %v", err) - } else { - log.Infoln("updater: finished") - } - }() - - workDir = filepath.Dir(execPath) - - err = prepare(execPath) - if err != nil { - return fmt.Errorf("preparing: %w", err) - } - - defer clean() - - err = downloadPackageFile() - if err != nil { - return fmt.Errorf("downloading package file: %w", err) - } - - err = unpack() - if err != nil { - return fmt.Errorf("unpacking: %w", err) - } - - err = backup() - if err != nil { - return fmt.Errorf("backuping: %w", err) - } - - err = replace() - if err != nil { - return fmt.Errorf("replacing: %w", err) - } - - return nil -} - -// prepare fills all necessary fields in Updater object. -func prepare(exePath string) (err error) { - updateDir = filepath.Join(workDir, "meta-update") - currentExeName = exePath - _, pkgNameOnly := filepath.Split(packageURL) - if pkgNameOnly == "" { - return fmt.Errorf("invalid PackageURL: %q", packageURL) - } - - packageName = filepath.Join(updateDir, pkgNameOnly) - //log.Infoln(packageName) - backupDir = filepath.Join(workDir, "meta-backup") - - if runtime.GOOS == "windows" { - updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible + ".exe" - } else if runtime.GOOS == "android" && runtime.GOARCH == "arm64" { - updateExeName = "mihomo-android-arm64-v8" - } else { - updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible - } - - log.Infoln("updateExeName: %s ", updateExeName) - - backupExeName = filepath.Join(backupDir, filepath.Base(exePath)) - updateExeName = filepath.Join(updateDir, updateExeName) - - log.Infoln( - "updater: updating using url: %s", - packageURL, - ) - - currentExeName = exePath - _, err = os.Stat(currentExeName) - if err != nil { - return fmt.Errorf("checking %q: %w", currentExeName, err) - } - - return nil -} - -// unpack extracts the files from the downloaded archive. -func unpack() error { - var err error - _, pkgNameOnly := filepath.Split(packageURL) - - log.Infoln("updater: unpacking package") - if strings.HasSuffix(pkgNameOnly, ".zip") { - _, err = zipFileUnpack(packageName, updateDir) - if err != nil { - return fmt.Errorf(".zip unpack failed: %w", err) - } - - } else if strings.HasSuffix(pkgNameOnly, ".gz") { - _, err = gzFileUnpack(packageName, updateDir) - if err != nil { - return fmt.Errorf(".gz unpack failed: %w", err) - } - - } else { - return fmt.Errorf("unknown package extension") - } - - return nil -} - -// backup makes a backup of the current executable file -func backup() (err error) { - log.Infoln("updater: backing up current ExecFile:%s to %s", currentExeName, backupExeName) - _ = os.Mkdir(backupDir, 0o755) - - err = os.Rename(currentExeName, backupExeName) - if err != nil { - return err - } - - return nil -} - -// replace moves the current executable with the updated one -func replace() error { - var err error - - log.Infoln("replacing: %s to %s", updateExeName, currentExeName) - if runtime.GOOS == "windows" { - // rename fails with "File in use" error - err = copyFile(updateExeName, currentExeName) - } else { - err = os.Rename(updateExeName, currentExeName) - } - if err != nil { - return err - } - - log.Infoln("updater: renamed: %s to %s", updateExeName, currentExeName) - - return nil -} - -// clean removes the temporary directory itself and all it's contents. -func clean() { - _ = os.RemoveAll(updateDir) -} - -// MaxPackageFileSize is a maximum package file length in bytes. The largest -// package whose size is limited by this constant currently has the size of -// approximately 32 MiB. -const MaxPackageFileSize = 32 * 1024 * 1024 - -// Download package file and save it to disk -func downloadPackageFile() (err error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil) - if err != nil { - return fmt.Errorf("http request failed: %w", err) - } - - defer func() { - closeErr := resp.Body.Close() - if closeErr != nil && err == nil { - err = closeErr - } - }() - - var r io.Reader - r, err = LimitReader(resp.Body, MaxPackageFileSize) - if err != nil { - return fmt.Errorf("http request failed: %w", err) - } - - log.Debugln("updater: reading http body") - // This use of ReadAll is now safe, because we limited body's Reader. - body, err := io.ReadAll(r) - if err != nil { - return fmt.Errorf("io.ReadAll() failed: %w", err) - } - - log.Debugln("updateDir %s", updateDir) - err = os.Mkdir(updateDir, 0o755) - if err != nil { - return fmt.Errorf("mkdir error: %w", err) - } - - log.Debugln("updater: saving package to file %s", packageName) - err = os.WriteFile(packageName, body, 0o644) - if err != nil { - return fmt.Errorf("os.WriteFile() failed: %w", err) - } - return nil -} - -// Unpack a single .gz file to the specified directory -// Existing files are overwritten -// All files are created inside outDir, subdirectories are not created -// Return the output file name -func gzFileUnpack(gzfile, outDir string) (string, error) { - f, err := os.Open(gzfile) - if err != nil { - return "", fmt.Errorf("os.Open(): %w", err) - } - - defer func() { - closeErr := f.Close() - if closeErr != nil && err == nil { - err = closeErr - } - }() - - gzReader, err := gzip.NewReader(f) - if err != nil { - return "", fmt.Errorf("gzip.NewReader(): %w", err) - } - - defer func() { - closeErr := gzReader.Close() - if closeErr != nil && err == nil { - err = closeErr - } - }() - // Get the original file name from the .gz file header - originalName := gzReader.Header.Name - if originalName == "" { - // Fallback: remove the .gz extension from the input file name if the header doesn't provide the original name - originalName = filepath.Base(gzfile) - originalName = strings.TrimSuffix(originalName, ".gz") - } - - outputName := filepath.Join(outDir, originalName) - - // Create the output file - wc, err := os.OpenFile( - outputName, - os.O_WRONLY|os.O_CREATE|os.O_TRUNC, - 0o755, - ) - if err != nil { - return "", fmt.Errorf("os.OpenFile(%s): %w", outputName, err) - } - - defer func() { - closeErr := wc.Close() - if closeErr != nil && err == nil { - err = closeErr - } - }() - - // Copy the contents of the gzReader to the output file - _, err = io.Copy(wc, gzReader) - if err != nil { - return "", fmt.Errorf("io.Copy(): %w", err) - } - - return outputName, nil -} - -// Unpack a single file from .zip file to the specified directory -// Existing files are overwritten -// All files are created inside 'outDir', subdirectories are not created -// Return the output file name -func zipFileUnpack(zipfile, outDir string) (string, error) { - zrc, err := zip.OpenReader(zipfile) - if err != nil { - return "", fmt.Errorf("zip.OpenReader(): %w", err) - } - - defer func() { - closeErr := zrc.Close() - if closeErr != nil && err == nil { - err = closeErr - } - }() - if len(zrc.File) == 0 { - return "", fmt.Errorf("no files in the zip archive") - } - - // Assuming the first file in the zip archive is the target file - zf := zrc.File[0] - var rc io.ReadCloser - rc, err = zf.Open() - if err != nil { - return "", fmt.Errorf("zip file Open(): %w", err) - } - - defer func() { - closeErr := rc.Close() - if closeErr != nil && err == nil { - err = closeErr - } - }() - fi := zf.FileInfo() - name := fi.Name() - outputName := filepath.Join(outDir, name) - - if fi.IsDir() { - return "", fmt.Errorf("the target file is a directory") - } - - var wc io.WriteCloser - wc, err = os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) - if err != nil { - return "", fmt.Errorf("os.OpenFile(): %w", err) - } - - defer func() { - closeErr := wc.Close() - if closeErr != nil && err == nil { - err = closeErr - } - }() - _, err = io.Copy(wc, rc) - if err != nil { - return "", fmt.Errorf("io.Copy(): %w", err) - } - - return outputName, nil -} - -// Copy file on disk -func copyFile(src, dst string) error { - d, e := os.ReadFile(src) - if e != nil { - return e - } - e = os.WriteFile(dst, d, 0o644) - if e != nil { - return e - } - return nil -} - -func getLatestVersion() (version string, err error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil) - if err != nil { - return "", fmt.Errorf("get Latest Version fail: %w", err) - } - defer func() { - closeErr := resp.Body.Close() - if closeErr != nil && err == nil { - err = closeErr - } - }() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("get Latest Version fail: %w", err) - } - content := strings.TrimRight(string(body), "\n") - return content, nil -} - -func updateDownloadURL() { - var middle string - - if runtime.GOARCH == "arm" && probeGoARM() { - //-linux-armv7-alpha-e552b54.gz - middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, goarm, latestVersion) - } else if runtime.GOARCH == "arm64" { - //-linux-arm64-alpha-e552b54.gz - if runtime.GOOS == "android" { - middle = fmt.Sprintf("-%s-%s-v8-%s", runtime.GOOS, runtime.GOARCH, latestVersion) - } else { - middle = fmt.Sprintf("-%s-%s-%s", runtime.GOOS, runtime.GOARCH, latestVersion) - } - } else if isMIPS(runtime.GOARCH) && gomips != "" { - middle = fmt.Sprintf("-%s-%s-%s-%s", runtime.GOOS, runtime.GOARCH, gomips, latestVersion) - } else { - middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, amd64Compatible, latestVersion) - } - - if runtime.GOOS == "windows" { - middle += ".zip" - } else { - middle += ".gz" - } - packageURL = baseURL + middle - //log.Infoln(packageURL) -} - -// isMIPS returns true if arch is any MIPS architecture. -func isMIPS(arch string) (ok bool) { - switch arch { - case - "mips", - "mips64", - "mips64le", - "mipsle": - return true - default: - return false - } -} - -// linux only -func probeGoARM() (ok bool) { - cmd := exec.Command("cat", "/proc/cpuinfo") - output, err := cmd.Output() - if err != nil { - log.Errorln("probe goarm error:%s", err) - return false - } - cpuInfo := string(output) - if strings.Contains(cpuInfo, "vfpv3") || strings.Contains(cpuInfo, "vfpv4") { - goarm = "v7" - } else if strings.Contains(cpuInfo, "vfp") { - goarm = "v6" - } else { - goarm = "v5" - } - return true -} diff --git a/component/updater/update_geo.go b/component/updater/update_geo.go deleted file mode 100644 index 0778087af0..0000000000 --- a/component/updater/update_geo.go +++ /dev/null @@ -1,266 +0,0 @@ -package updater - -import ( - "context" - "errors" - "fmt" - "os" - "runtime" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/geodata" - _ "github.com/metacubex/mihomo/component/geodata/standard" - "github.com/metacubex/mihomo/component/mmdb" - "github.com/metacubex/mihomo/component/resource" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - "github.com/oschwald/maxminddb-golang" - "golang.org/x/sync/errgroup" -) - -var ( - autoUpdate bool - updateInterval int - - updatingGeo atomic.Bool -) - -func GeoAutoUpdate() bool { - return autoUpdate -} - -func GeoUpdateInterval() int { - return updateInterval -} - -func SetGeoAutoUpdate(newAutoUpdate bool) { - autoUpdate = newAutoUpdate -} - -func SetGeoUpdateInterval(newGeoUpdateInterval int) { - updateInterval = newGeoUpdateInterval -} - -func UpdateMMDB() (err error) { - vehicle := resource.NewHTTPVehicle(geodata.MmdbUrl(), C.Path.MMDB(), "", nil, defaultHttpTimeout, 0) - var oldHash utils.HashType - if buf, err := os.ReadFile(vehicle.Path()); err == nil { - oldHash = utils.MakeHash(buf) - } - data, hash, err := vehicle.Read(context.Background(), oldHash) - if err != nil { - return fmt.Errorf("can't download MMDB database file: %w", err) - } - if oldHash.Equal(hash) { // same hash, ignored - return nil - } - if len(data) == 0 { - return fmt.Errorf("can't download MMDB database file: no data") - } - - instance, err := maxminddb.FromBytes(data) - if err != nil { - return fmt.Errorf("invalid MMDB database file: %s", err) - } - _ = instance.Close() - - defer mmdb.ReloadIP() - mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file - if err = vehicle.Write(data); err != nil { - return fmt.Errorf("can't save MMDB database file: %w", err) - } - return nil -} - -func UpdateASN() (err error) { - vehicle := resource.NewHTTPVehicle(geodata.ASNUrl(), C.Path.ASN(), "", nil, defaultHttpTimeout, 0) - var oldHash utils.HashType - if buf, err := os.ReadFile(vehicle.Path()); err == nil { - oldHash = utils.MakeHash(buf) - } - data, hash, err := vehicle.Read(context.Background(), oldHash) - if err != nil { - return fmt.Errorf("can't download ASN database file: %w", err) - } - if oldHash.Equal(hash) { // same hash, ignored - return nil - } - if len(data) == 0 { - return fmt.Errorf("can't download ASN database file: no data") - } - - instance, err := maxminddb.FromBytes(data) - if err != nil { - return fmt.Errorf("invalid ASN database file: %s", err) - } - _ = instance.Close() - - defer mmdb.ReloadASN() - mmdb.ASNInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file - if err = vehicle.Write(data); err != nil { - return fmt.Errorf("can't save ASN database file: %w", err) - } - return nil -} - -func UpdateGeoIp() (err error) { - geoLoader, err := geodata.GetGeoDataLoader("standard") - - vehicle := resource.NewHTTPVehicle(geodata.GeoIpUrl(), C.Path.GeoIP(), "", nil, defaultHttpTimeout, 0) - var oldHash utils.HashType - if buf, err := os.ReadFile(vehicle.Path()); err == nil { - oldHash = utils.MakeHash(buf) - } - data, hash, err := vehicle.Read(context.Background(), oldHash) - if err != nil { - return fmt.Errorf("can't download GeoIP database file: %w", err) - } - if oldHash.Equal(hash) { // same hash, ignored - return nil - } - if len(data) == 0 { - return fmt.Errorf("can't download GeoIP database file: no data") - } - - if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil { - return fmt.Errorf("invalid GeoIP database file: %s", err) - } - - defer geodata.ClearGeoIPCache() - if err = vehicle.Write(data); err != nil { - return fmt.Errorf("can't save GeoIP database file: %w", err) - } - return nil -} - -func UpdateGeoSite() (err error) { - geoLoader, err := geodata.GetGeoDataLoader("standard") - - vehicle := resource.NewHTTPVehicle(geodata.GeoSiteUrl(), C.Path.GeoSite(), "", nil, defaultHttpTimeout, 0) - var oldHash utils.HashType - if buf, err := os.ReadFile(vehicle.Path()); err == nil { - oldHash = utils.MakeHash(buf) - } - data, hash, err := vehicle.Read(context.Background(), oldHash) - if err != nil { - return fmt.Errorf("can't download GeoSite database file: %w", err) - } - if oldHash.Equal(hash) { // same hash, ignored - return nil - } - if len(data) == 0 { - return fmt.Errorf("can't download GeoSite database file: no data") - } - - if _, err = geoLoader.LoadSiteByBytes(data, "cn"); err != nil { - return fmt.Errorf("invalid GeoSite database file: %s", err) - } - - defer geodata.ClearGeoSiteCache() - if err = vehicle.Write(data); err != nil { - return fmt.Errorf("can't save GeoSite database file: %w", err) - } - return nil -} - -func updateGeoDatabases() error { - defer runtime.GC() - - b := errgroup.Group{} - - if geodata.GeoIpEnable() { - if geodata.GeodataMode() { - b.Go(UpdateGeoIp) - } else { - b.Go(UpdateMMDB) - } - } - - if geodata.ASNEnable() { - b.Go(UpdateASN) - } - - if geodata.GeoSiteEnable() { - b.Go(UpdateGeoSite) - } - - return b.Wait() -} - -var ErrGetDatabaseUpdateSkip = errors.New("GEO database is updating, skip") - -func UpdateGeoDatabases() error { - log.Infoln("[GEO] Start updating GEO database") - - if updatingGeo.Load() { - return ErrGetDatabaseUpdateSkip - } - - updatingGeo.Store(true) - defer updatingGeo.Store(false) - - log.Infoln("[GEO] Updating GEO database") - - if err := updateGeoDatabases(); err != nil { - log.Errorln("[GEO] update GEO database error: %s", err.Error()) - return err - } - - return nil -} - -func getUpdateTime() (err error, time time.Time) { - filesToCheck := []string{ - C.Path.GeoIP(), - C.Path.MMDB(), - C.Path.ASN(), - C.Path.GeoSite(), - } - - for _, file := range filesToCheck { - var fileInfo os.FileInfo - fileInfo, err = os.Stat(file) - if err == nil { - return nil, fileInfo.ModTime() - } - } - - return -} - -func RegisterGeoUpdater() { - if updateInterval <= 0 { - log.Errorln("[GEO] Invalid update interval: %d", updateInterval) - return - } - - go func() { - ticker := time.NewTicker(time.Duration(updateInterval) * time.Hour) - defer ticker.Stop() - - err, lastUpdate := getUpdateTime() - if err != nil { - log.Errorln("[GEO] Get GEO database update time error: %s", err.Error()) - return - } - - log.Infoln("[GEO] last update time %s", lastUpdate) - if lastUpdate.Add(time.Duration(updateInterval) * time.Hour).Before(time.Now()) { - log.Infoln("[GEO] Database has not been updated for %v, update now", time.Duration(updateInterval)*time.Hour) - if err := UpdateGeoDatabases(); err != nil { - log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) - return - } - } - - for range ticker.C { - log.Infoln("[GEO] updating database every %d hours", updateInterval) - if err := UpdateGeoDatabases(); err != nil { - log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) - } - } - }() -} diff --git a/component/updater/update_ui.go b/component/updater/update_ui.go deleted file mode 100644 index 94bc27de58..0000000000 --- a/component/updater/update_ui.go +++ /dev/null @@ -1,419 +0,0 @@ -package updater - -import ( - "archive/tar" - "archive/zip" - "compress/gzip" - "fmt" - "io" - "os" - "path" - "path/filepath" - "strings" - "sync" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type UIUpdater struct { - externalUIURL string - externalUIPath string - autoDownloadUI bool - - mutex sync.Mutex -} - -type compressionType int - -const ( - typeUnknown compressionType = iota - typeZip - typeTarGzip -) - -var DefaultUiUpdater = &UIUpdater{} - -func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater { - updater := &UIUpdater{} - // checkout externalUI exist - if externalUI != "" { - updater.autoDownloadUI = true - updater.externalUIPath = C.Path.Resolve(externalUI) - } else { - // default externalUI path - updater.externalUIPath = path.Join(C.Path.HomeDir(), "ui") - } - - // checkout UIpath/name exist - if externalUIName != "" { - updater.autoDownloadUI = true - updater.externalUIPath = path.Join(updater.externalUIPath, externalUIName) - } - - if externalUIURL != "" { - updater.externalUIURL = externalUIURL - } - return updater -} - -func (u *UIUpdater) AutoDownloadUI() { - u.mutex.Lock() - defer u.mutex.Unlock() - if u.autoDownloadUI { - dirEntries, _ := os.ReadDir(u.externalUIPath) - if len(dirEntries) > 0 { - log.Infoln("UI already exists, skip downloading") - } else { - log.Infoln("External UI downloading ...") - err := u.downloadUI() - if err != nil { - log.Errorln("Error downloading UI: %s", err) - } - } - } -} - -func (u *UIUpdater) DownloadUI() error { - u.mutex.Lock() - defer u.mutex.Unlock() - return u.downloadUI() -} - -func detectFileType(data []byte) compressionType { - if len(data) < 4 { - return typeUnknown - } - - // Zip: 0x50 0x4B 0x03 0x04 - if data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04 { - return typeZip - } - - // GZip: 0x1F 0x8B - if data[0] == 0x1F && data[1] == 0x8B { - return typeTarGzip - } - - return typeUnknown -} - -func (u *UIUpdater) downloadUI() error { - err := u.prepareUIPath() - if err != nil { - return fmt.Errorf("prepare UI path failed: %w", err) - } - - data, err := downloadForBytes(u.externalUIURL) - if err != nil { - return fmt.Errorf("can't download file: %w", err) - } - - fileType := detectFileType(data) - if fileType == typeUnknown { - return fmt.Errorf("unknown or unsupported file type") - } - - ext := ".zip" - if fileType == typeTarGzip { - ext = ".tgz" - } - - saved := path.Join(C.Path.HomeDir(), "download"+ext) - log.Debugln("compression Type: %s", ext) - if err = saveFile(data, saved); err != nil { - return fmt.Errorf("can't save compressed file: %w", err) - } - defer os.Remove(saved) - - err = cleanup(u.externalUIPath) - if err != nil { - if !os.IsNotExist(err) { - return fmt.Errorf("cleanup exist file error: %w", err) - } - } - - extractedFolder, err := extract(saved, C.Path.HomeDir()) - if err != nil { - return fmt.Errorf("can't extract compressed file: %w", err) - } - - err = os.Rename(extractedFolder, u.externalUIPath) - if err != nil { - return fmt.Errorf("rename UI folder failed: %w", err) - } - return nil -} - -func (u *UIUpdater) prepareUIPath() error { - if _, err := os.Stat(u.externalUIPath); os.IsNotExist(err) { - log.Infoln("dir %s does not exist, creating", u.externalUIPath) - if err := os.MkdirAll(u.externalUIPath, os.ModePerm); err != nil { - log.Warnln("create dir %s error: %s", u.externalUIPath, err) - } - } - return nil -} - -func unzip(src, dest string) (string, error) { - r, err := zip.OpenReader(src) - if err != nil { - return "", err - } - defer r.Close() - - // check whether or not only exists singleRoot dir - rootDir := "" - isSingleRoot := true - rootItemCount := 0 - for _, f := range r.File { - parts := strings.Split(strings.Trim(f.Name, "/"), "/") - if len(parts) == 0 { - continue - } - - if len(parts) == 1 { - isDir := strings.HasSuffix(f.Name, "/") - if !isDir { - isSingleRoot = false - break - } - - if rootDir == "" { - rootDir = parts[0] - } - rootItemCount++ - } - } - - if rootItemCount != 1 { - isSingleRoot = false - } - - // build the dir of extraction - var extractedFolder string - if isSingleRoot && rootDir != "" { - // if the singleRoot, use it directly - log.Debugln("Match the singleRoot") - extractedFolder = filepath.Join(dest, rootDir) - log.Debugln("extractedFolder: %s", extractedFolder) - } else { - log.Debugln("Match the multiRoot") - // or put the files/dirs into new dir - baseName := filepath.Base(src) - baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName)) - extractedFolder = filepath.Join(dest, baseName) - - for i := 1; ; i++ { - if _, err := os.Stat(extractedFolder); os.IsNotExist(err) { - break - } - extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i)) - } - log.Debugln("extractedFolder: %s", extractedFolder) - } - - for _, f := range r.File { - var fpath string - if isSingleRoot && rootDir != "" { - fpath = filepath.Join(dest, f.Name) - } else { - fpath = filepath.Join(extractedFolder, f.Name) - } - - if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { - return "", fmt.Errorf("invalid file path: %s", fpath) - } - if f.FileInfo().IsDir() { - os.MkdirAll(fpath, os.ModePerm) - continue - } - if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { - return "", err - } - outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return "", err - } - rc, err := f.Open() - if err != nil { - return "", err - } - _, err = io.Copy(outFile, rc) - outFile.Close() - rc.Close() - if err != nil { - return "", err - } - } - return extractedFolder, nil -} - -func untgz(src, dest string) (string, error) { - file, err := os.Open(src) - if err != nil { - return "", err - } - defer file.Close() - - gzr, err := gzip.NewReader(file) - if err != nil { - return "", err - } - defer gzr.Close() - - tr := tar.NewReader(gzr) - - rootDir := "" - isSingleRoot := true - rootItemCount := 0 - for { - header, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return "", err - } - - parts := strings.Split(cleanTarPath(header.Name), string(os.PathSeparator)) - if len(parts) == 0 { - continue - } - - if len(parts) == 1 { - isDir := header.Typeflag == tar.TypeDir - if !isDir { - isSingleRoot = false - break - } - - if rootDir == "" { - rootDir = parts[0] - } - rootItemCount++ - } - } - - if rootItemCount != 1 { - isSingleRoot = false - } - - file.Seek(0, 0) - gzr, _ = gzip.NewReader(file) - tr = tar.NewReader(gzr) - - var extractedFolder string - if isSingleRoot && rootDir != "" { - log.Debugln("Match the singleRoot") - extractedFolder = filepath.Join(dest, rootDir) - log.Debugln("extractedFolder: %s", extractedFolder) - } else { - log.Debugln("Match the multiRoot") - baseName := filepath.Base(src) - baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName)) - baseName = strings.TrimSuffix(baseName, ".tar") - extractedFolder = filepath.Join(dest, baseName) - - for i := 1; ; i++ { - if _, err := os.Stat(extractedFolder); os.IsNotExist(err) { - break - } - extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i)) - } - log.Debugln("extractedFolder: %s", extractedFolder) - } - - for { - header, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return "", err - } - - var fpath string - if isSingleRoot && rootDir != "" { - fpath = filepath.Join(dest, cleanTarPath(header.Name)) - } else { - fpath = filepath.Join(extractedFolder, cleanTarPath(header.Name)) - } - - if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { - return "", fmt.Errorf("invalid file path: %s", fpath) - } - - switch header.Typeflag { - case tar.TypeDir: - if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil { - return "", err - } - case tar.TypeReg: - if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { - return "", err - } - outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode)) - if err != nil { - return "", err - } - if _, err := io.Copy(outFile, tr); err != nil { - outFile.Close() - return "", err - } - outFile.Close() - } - } - return extractedFolder, nil -} - -func extract(src, dest string) (string, error) { - srcLower := strings.ToLower(src) - switch { - case strings.HasSuffix(srcLower, ".tar.gz") || - strings.HasSuffix(srcLower, ".tgz"): - return untgz(src, dest) - case strings.HasSuffix(srcLower, ".zip"): - return unzip(src, dest) - default: - return "", fmt.Errorf("unsupported file format: %s", src) - } -} - -func cleanTarPath(path string) string { - // remove prefix ./ or ../ - path = strings.TrimPrefix(path, "./") - path = strings.TrimPrefix(path, "../") - - // normalize path - path = filepath.Clean(path) - - // transfer delimiters to system std - path = filepath.FromSlash(path) - - // remove prefix path delimiters - path = strings.TrimPrefix(path, string(os.PathSeparator)) - - return path -} - -func cleanup(root string) error { - if _, err := os.Stat(root); os.IsNotExist(err) { - return nil - } - return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - if err := os.RemoveAll(path); err != nil { - return err - } - } else { - if err := os.Remove(path); err != nil { - return err - } - } - return nil - }) -} diff --git a/component/updater/utils.go b/component/updater/utils.go deleted file mode 100644 index b5c694ff44..0000000000 --- a/component/updater/utils.go +++ /dev/null @@ -1,91 +0,0 @@ -package updater - -import ( - "context" - "fmt" - "io" - "net/http" - "os" - "time" - - mihomoHttp "github.com/metacubex/mihomo/component/http" - - "golang.org/x/exp/constraints" -) - -const defaultHttpTimeout = time.Second * 90 - -func downloadForBytes(url string) ([]byte, error) { - ctx, cancel := context.WithTimeout(context.Background(), defaultHttpTimeout) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - return io.ReadAll(resp.Body) -} - -func saveFile(bytes []byte, path string) error { - return os.WriteFile(path, bytes, 0o644) -} - -// LimitReachedError records the limit and the operation that caused it. -type LimitReachedError struct { - Limit int64 -} - -// Error implements the [error] interface for *LimitReachedError. -// -// TODO(a.garipov): Think about error string format. -func (lre *LimitReachedError) Error() string { - return fmt.Sprintf("attempted to read more than %d bytes", lre.Limit) -} - -// limitedReader is a wrapper for [io.Reader] limiting the input and dealing -// with errors package. -type limitedReader struct { - r io.Reader - limit int64 - n int64 -} - -// Read implements the [io.Reader] interface. -func (lr *limitedReader) Read(p []byte) (n int, err error) { - if lr.n == 0 { - return 0, &LimitReachedError{ - Limit: lr.limit, - } - } - - p = p[:Min(lr.n, int64(len(p)))] - - n, err = lr.r.Read(p) - lr.n -= int64(n) - - return n, err -} - -// LimitReader wraps Reader to make it's Reader stop with ErrLimitReached after -// n bytes read. -func LimitReader(r io.Reader, n int64) (limited io.Reader, err error) { - if n < 0 { - return nil, &updateError{Message: "limit must be non-negative"} - } - - return &limitedReader{ - r: r, - limit: n, - n: n, - }, nil -} - -// Min returns the smaller of x or y. -func Min[T constraints.Integer | ~string](x, y T) (res T) { - if x < y { - return x - } - - return y -} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 56b2aa7bce..0000000000 --- a/config/config.go +++ /dev/null @@ -1,1816 +0,0 @@ -package config - -import ( - "container/list" - "errors" - "fmt" - "net" - "net/netip" - "net/url" - "strings" - "time" - _ "unsafe" - - "github.com/metacubex/mihomo/adapter" - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/adapter/outboundgroup" - "github.com/metacubex/mihomo/adapter/provider" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/auth" - "github.com/metacubex/mihomo/component/cidr" - "github.com/metacubex/mihomo/component/fakeip" - "github.com/metacubex/mihomo/component/geodata" - P "github.com/metacubex/mihomo/component/process" - "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/component/sniffer" - "github.com/metacubex/mihomo/component/trie" - C "github.com/metacubex/mihomo/constant" - providerTypes "github.com/metacubex/mihomo/constant/provider" - snifferTypes "github.com/metacubex/mihomo/constant/sniffer" - "github.com/metacubex/mihomo/dns" - L "github.com/metacubex/mihomo/listener" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/log" - R "github.com/metacubex/mihomo/rules" - RC "github.com/metacubex/mihomo/rules/common" - RP "github.com/metacubex/mihomo/rules/provider" - T "github.com/metacubex/mihomo/tunnel" - - orderedmap "github.com/wk8/go-ordered-map/v2" - "golang.org/x/exp/slices" - "gopkg.in/yaml.v3" -) - -// General config -type General struct { - Inbound - Mode T.TunnelMode `json:"mode"` - UnifiedDelay bool `json:"unified-delay"` - LogLevel log.LogLevel `json:"log-level"` - IPv6 bool `json:"ipv6"` - Interface string `json:"interface-name"` - RoutingMark int `json:"routing-mark"` - GeoXUrl GeoXUrl `json:"geox-url"` - GeoAutoUpdate bool `json:"geo-auto-update"` - GeoUpdateInterval int `json:"geo-update-interval"` - GeodataMode bool `json:"geodata-mode"` - GeodataLoader string `json:"geodata-loader"` - GeositeMatcher string `json:"geosite-matcher"` - TCPConcurrent bool `json:"tcp-concurrent"` - FindProcessMode P.FindProcessMode `json:"find-process-mode"` - Sniffing bool `json:"sniffing"` - GlobalClientFingerprint string `json:"global-client-fingerprint"` - GlobalUA string `json:"global-ua"` - ETagSupport bool `json:"etag-support"` - KeepAliveIdle int `json:"keep-alive-idle"` - KeepAliveInterval int `json:"keep-alive-interval"` - DisableKeepAlive bool `json:"disable-keep-alive"` -} - -// Inbound config -type Inbound 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"` - Tun LC.Tun `json:"tun"` - TuicServer LC.TuicServer `json:"tuic-server"` - ShadowSocksConfig string `json:"ss-config"` - VmessConfig string `json:"vmess-config"` - Authentication []string `json:"authentication"` - SkipAuthPrefixes []netip.Prefix `json:"skip-auth-prefixes"` - LanAllowedIPs []netip.Prefix `json:"lan-allowed-ips"` - LanDisAllowedIPs []netip.Prefix `json:"lan-disallowed-ips"` - AllowLan bool `json:"allow-lan"` - BindAddress string `json:"bind-address"` - InboundTfo bool `json:"inbound-tfo"` - InboundMPTCP bool `json:"inbound-mptcp"` -} - -// GeoXUrl config -type GeoXUrl struct { - GeoIp string `json:"geo-ip"` - Mmdb string `json:"mmdb"` - ASN string `json:"asn"` - GeoSite string `json:"geo-site"` -} - -// Controller config -type Controller struct { - ExternalController string - ExternalControllerTLS string - ExternalControllerUnix string - ExternalControllerPipe string - ExternalUI string - ExternalUIURL string - ExternalUIName string - ExternalDohServer string - Secret string - Cors Cors -} - -type Cors struct { - AllowOrigins []string - AllowPrivateNetwork bool -} - -// Experimental config -type Experimental struct { - Fingerprints []string - QUICGoDisableGSO bool - QUICGoDisableECN bool - IP4PEnable bool -} - -// IPTables config -type IPTables struct { - Enable bool - InboundInterface string - Bypass []string - DnsRedirect bool -} - -// NTP config -type NTP struct { - Enable bool - Server string - Port int - Interval int - DialerProxy string - WriteToSystem bool -} - -// DNS config -type DNS struct { - Enable bool - PreferH3 bool - IPv6 bool - IPv6Timeout uint - UseSystemHosts bool - NameServer []dns.NameServer - Fallback []dns.NameServer - FallbackIPFilter []C.IpMatcher - FallbackDomainFilter []C.DomainMatcher - Listen string - EnhancedMode C.DNSMode - DefaultNameserver []dns.NameServer - CacheAlgorithm string - FakeIPRange *fakeip.Pool - Hosts *trie.DomainTrie[resolver.HostValue] - NameServerPolicy []dns.Policy - ProxyServerNameserver []dns.NameServer - DirectNameServer []dns.NameServer - DirectFollowPolicy bool -} - -// Profile config -type Profile struct { - StoreSelected bool - StoreFakeIP bool -} - -// TLS config -type TLS struct { - Certificate string - PrivateKey string - CustomTrustCert []string -} - -// Config is mihomo config manager -type Config struct { - General *General - Controller *Controller - Experimental *Experimental - IPTables *IPTables - NTP *NTP - DNS *DNS - Hosts *trie.DomainTrie[resolver.HostValue] - Profile *Profile - Rules []C.Rule - SubRules map[string][]C.Rule - Users []auth.AuthUser - Proxies map[string]C.Proxy - Listeners map[string]C.InboundListener - Providers map[string]providerTypes.ProxyProvider - RuleProviders map[string]providerTypes.RuleProvider - Tunnels []LC.Tunnel - Sniffer *sniffer.Config - TLS *TLS -} - -type RawCors struct { - AllowOrigins []string `yaml:"allow-origins" json:"allow-origins"` - AllowPrivateNetwork bool `yaml:"allow-private-network" json:"allow-private-network"` -} - -type RawDNS struct { - Enable bool `yaml:"enable" json:"enable"` - PreferH3 bool `yaml:"prefer-h3" json:"prefer-h3"` - IPv6 bool `yaml:"ipv6" json:"ipv6"` - IPv6Timeout uint `yaml:"ipv6-timeout" json:"ipv6-timeout"` - UseHosts bool `yaml:"use-hosts" json:"use-hosts"` - UseSystemHosts bool `yaml:"use-system-hosts" json:"use-system-hosts"` - RespectRules bool `yaml:"respect-rules" json:"respect-rules"` - NameServer []string `yaml:"nameserver" json:"nameserver"` - Fallback []string `yaml:"fallback" json:"fallback"` - FallbackFilter RawFallbackFilter `yaml:"fallback-filter" json:"fallback-filter"` - Listen string `yaml:"listen" json:"listen"` - EnhancedMode C.DNSMode `yaml:"enhanced-mode" json:"enhanced-mode"` - FakeIPRange string `yaml:"fake-ip-range" json:"fake-ip-range"` - FakeIPFilter []string `yaml:"fake-ip-filter" json:"fake-ip-filter"` - FakeIPFilterMode C.FilterMode `yaml:"fake-ip-filter-mode" json:"fake-ip-filter-mode"` - DefaultNameserver []string `yaml:"default-nameserver" json:"default-nameserver"` - CacheAlgorithm string `yaml:"cache-algorithm" json:"cache-algorithm"` - NameServerPolicy *orderedmap.OrderedMap[string, any] `yaml:"nameserver-policy" json:"nameserver-policy"` - ProxyServerNameserver []string `yaml:"proxy-server-nameserver" json:"proxy-server-nameserver"` - DirectNameServer []string `yaml:"direct-nameserver" json:"direct-nameserver"` - DirectNameServerFollowPolicy bool `yaml:"direct-nameserver-follow-policy" json:"direct-nameserver-follow-policy"` -} - -type RawFallbackFilter struct { - GeoIP bool `yaml:"geoip" json:"geoip"` - GeoIPCode string `yaml:"geoip-code" json:"geoip-code"` - IPCIDR []string `yaml:"ipcidr" json:"ipcidr"` - Domain []string `yaml:"domain" json:"domain"` - GeoSite []string `yaml:"geosite" json:"geosite"` -} - -type RawClashForAndroid struct { - AppendSystemDNS bool `yaml:"append-system-dns" json:"append-system-dns"` - UiSubtitlePattern string `yaml:"ui-subtitle-pattern" json:"ui-subtitle-pattern"` -} - -type RawNTP struct { - Enable bool `yaml:"enable" json:"enable"` - Server string `yaml:"server" json:"server"` - Port int `yaml:"port" json:"port"` - Interval int `yaml:"interval" json:"interval"` - DialerProxy string `yaml:"dialer-proxy" json:"dialer-proxy"` - WriteToSystem bool `yaml:"write-to-system" json:"write-to-system"` -} - -type RawTun struct { - Enable bool `yaml:"enable" json:"enable"` - Device string `yaml:"device" json:"device"` - Stack C.TUNStack `yaml:"stack" json:"stack"` - DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"` - AutoRoute bool `yaml:"auto-route" json:"auto-route"` - AutoDetectInterface bool `yaml:"auto-detect-interface"` - - MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` - GSO bool `yaml:"gso" json:"gso,omitempty"` - GSOMaxSize uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` - //Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` - Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` - IPRoute2TableIndex int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` - IPRoute2RuleIndex int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` - AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` - AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` - AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` - StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` - RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` - RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"` - RouteExcludeAddress []netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` - RouteExcludeAddressSet []string `yaml:"route-exclude-address-set" json:"route-exclude-address-set,omitempty"` - IncludeInterface []string `yaml:"include-interface" json:"include-interface,omitempty"` - ExcludeInterface []string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` - IncludeUID []uint32 `yaml:"include-uid" json:"include-uid,omitempty"` - IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` - ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` - ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` - ExcludeSrcPort []uint16 `yaml:"exclude-src-port" json:"exclude-src-port,omitempty"` - ExcludeSrcPortRange []string `yaml:"exclude-src-port-range" json:"exclude-src-port-range,omitempty"` - ExcludeDstPort []uint16 `yaml:"exclude-dst-port" json:"exclude-dst-port,omitempty"` - ExcludeDstPortRange []string `yaml:"exclude-dst-port-range" json:"exclude-dst-port-range,omitempty"` - IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` - IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` - ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` - EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` - UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` - FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` - - Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` - Inet6RouteAddress []netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` - Inet4RouteExcludeAddress []netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"` - Inet6RouteExcludeAddress []netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"` -} - -type RawTuicServer struct { - Enable bool `yaml:"enable" json:"enable"` - Listen string `yaml:"listen" json:"listen"` - Token []string `yaml:"token" json:"token"` - Users map[string]string `yaml:"users" json:"users,omitempty"` - Certificate string `yaml:"certificate" json:"certificate"` - PrivateKey string `yaml:"private-key" json:"private-key"` - CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` - MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` - AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` - ALPN []string `yaml:"alpn" json:"alpn,omitempty"` - MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` - CWND int `yaml:"cwnd" json:"cwnd,omitempty"` -} - -type RawIPTables struct { - Enable bool `yaml:"enable" json:"enable"` - InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"` - Bypass []string `yaml:"bypass" json:"bypass"` - DnsRedirect bool `yaml:"dns-redirect" json:"dns-redirect"` -} - -type RawExperimental struct { - Fingerprints []string `yaml:"fingerprints"` - QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"` - QUICGoDisableECN bool `yaml:"quic-go-disable-ecn"` - IP4PEnable bool `yaml:"dialer-ip4p-convert"` -} - -type RawProfile struct { - StoreSelected bool `yaml:"store-selected" json:"store-selected"` - StoreFakeIP bool `yaml:"store-fake-ip" json:"store-fake-ip"` -} - -type RawGeoXUrl struct { - GeoIp string `yaml:"geoip" json:"geoip"` - Mmdb string `yaml:"mmdb" json:"mmdb"` - ASN string `yaml:"asn" json:"asn"` - GeoSite string `yaml:"geosite" json:"geosite"` -} - -type RawSniffer struct { - Enable bool `yaml:"enable" json:"enable"` - OverrideDest bool `yaml:"override-destination" json:"override-destination"` - Sniffing []string `yaml:"sniffing" json:"sniffing"` - ForceDomain []string `yaml:"force-domain" json:"force-domain"` - SkipSrcAddress []string `yaml:"skip-src-address" json:"skip-src-address"` - SkipDstAddress []string `yaml:"skip-dst-address" json:"skip-dst-address"` - SkipDomain []string `yaml:"skip-domain" json:"skip-domain"` - Ports []string `yaml:"port-whitelist" json:"port-whitelist"` - ForceDnsMapping bool `yaml:"force-dns-mapping" json:"force-dns-mapping"` - ParsePureIp bool `yaml:"parse-pure-ip" json:"parse-pure-ip"` - - Sniff map[string]RawSniffingConfig `yaml:"sniff" json:"sniff"` -} - -type RawSniffingConfig struct { - Ports []string `yaml:"ports" json:"ports"` - OverrideDest *bool `yaml:"override-destination" json:"override-destination"` -} - -type RawTLS struct { - Certificate string `yaml:"certificate" json:"certificate"` - PrivateKey string `yaml:"private-key" json:"private-key"` - CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"` -} - -type RawConfig struct { - Port int `yaml:"port" json:"port"` - SocksPort int `yaml:"socks-port" json:"socks-port"` - RedirPort int `yaml:"redir-port" json:"redir-port"` - TProxyPort int `yaml:"tproxy-port" json:"tproxy-port"` - MixedPort int `yaml:"mixed-port" json:"mixed-port"` - ShadowSocksConfig string `yaml:"ss-config" json:"ss-config"` - VmessConfig string `yaml:"vmess-config" json:"vmess-config"` - InboundTfo bool `yaml:"inbound-tfo" json:"inbound-tfo"` - InboundMPTCP bool `yaml:"inbound-mptcp" json:"inbound-mptcp"` - Authentication []string `yaml:"authentication" json:"authentication"` - SkipAuthPrefixes []netip.Prefix `yaml:"skip-auth-prefixes" json:"skip-auth-prefixes"` - LanAllowedIPs []netip.Prefix `yaml:"lan-allowed-ips" json:"lan-allowed-ips"` - LanDisAllowedIPs []netip.Prefix `yaml:"lan-disallowed-ips" json:"lan-disallowed-ips"` - AllowLan bool `yaml:"allow-lan" json:"allow-lan"` - BindAddress string `yaml:"bind-address" json:"bind-address"` - Mode T.TunnelMode `yaml:"mode" json:"mode"` - UnifiedDelay bool `yaml:"unified-delay" json:"unified-delay"` - LogLevel log.LogLevel `yaml:"log-level" json:"log-level"` - IPv6 bool `yaml:"ipv6" json:"ipv6"` - ExternalController string `yaml:"external-controller" json:"external-controller"` - ExternalControllerPipe string `yaml:"external-controller-pipe" json:"external-controller-pipe"` - ExternalControllerUnix string `yaml:"external-controller-unix" json:"external-controller-unix"` - ExternalControllerTLS string `yaml:"external-controller-tls" json:"external-controller-tls"` - ExternalControllerCors RawCors `yaml:"external-controller-cors" json:"external-controller-cors"` - ExternalUI string `yaml:"external-ui" json:"external-ui"` - ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"` - ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"` - ExternalDohServer string `yaml:"external-doh-server" json:"external-doh-server"` - Secret string `yaml:"secret" json:"secret"` - Interface string `yaml:"interface-name" json:"interface-name"` - RoutingMark int `yaml:"routing-mark" json:"routing-mark"` - Tunnels []LC.Tunnel `yaml:"tunnels" json:"tunnels"` - GeoAutoUpdate bool `yaml:"geo-auto-update" json:"geo-auto-update"` - GeoUpdateInterval int `yaml:"geo-update-interval" json:"geo-update-interval"` - GeodataMode bool `yaml:"geodata-mode" json:"geodata-mode"` - GeodataLoader string `yaml:"geodata-loader" json:"geodata-loader"` - GeositeMatcher string `yaml:"geosite-matcher" json:"geosite-matcher"` - TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` - FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"` - GlobalClientFingerprint string `yaml:"global-client-fingerprint" json:"global-client-fingerprint"` - GlobalUA string `yaml:"global-ua" json:"global-ua"` - ETagSupport bool `yaml:"etag-support" json:"etag-support"` - KeepAliveIdle int `yaml:"keep-alive-idle" json:"keep-alive-idle"` - KeepAliveInterval int `yaml:"keep-alive-interval" json:"keep-alive-interval"` - DisableKeepAlive bool `yaml:"disable-keep-alive" json:"disable-keep-alive"` - - ProxyProvider map[string]map[string]any `yaml:"proxy-providers" json:"proxy-providers"` - RuleProvider map[string]map[string]any `yaml:"rule-providers" json:"rule-providers"` - Proxy []map[string]any `yaml:"proxies" json:"proxies"` - ProxyGroup []map[string]any `yaml:"proxy-groups" json:"proxy-groups"` - Rule []string `yaml:"rules" json:"rule"` - SubRules map[string][]string `yaml:"sub-rules" json:"sub-rules"` - Listeners []map[string]any `yaml:"listeners" json:"listeners"` - Hosts map[string]any `yaml:"hosts" json:"hosts"` - DNS RawDNS `yaml:"dns" json:"dns"` - NTP RawNTP `yaml:"ntp" json:"ntp"` - Tun RawTun `yaml:"tun" json:"tun"` - TuicServer RawTuicServer `yaml:"tuic-server" json:"tuic-server"` - IPTables RawIPTables `yaml:"iptables" json:"iptables"` - Experimental RawExperimental `yaml:"experimental" json:"experimental"` - Profile RawProfile `yaml:"profile" json:"profile"` - GeoXUrl RawGeoXUrl `yaml:"geox-url" json:"geox-url"` - Sniffer RawSniffer `yaml:"sniffer" json:"sniffer"` - TLS RawTLS `yaml:"tls" json:"tls"` - - ClashForAndroid RawClashForAndroid `yaml:"clash-for-android" json:"clash-for-android"` -} - -// Parse config -func Parse(buf []byte) (*Config, error) { - rawCfg, err := UnmarshalRawConfig(buf) - if err != nil { - return nil, err - } - - return ParseRawConfig(rawCfg) -} - -func DefaultRawConfig() *RawConfig { - return &RawConfig{ - AllowLan: false, - BindAddress: "*", - LanAllowedIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")}, - IPv6: true, - Mode: T.Rule, - GeoAutoUpdate: false, - GeoUpdateInterval: 24, - GeodataMode: geodata.GeodataMode(), - GeodataLoader: "memconservative", - UnifiedDelay: false, - Authentication: []string{}, - LogLevel: log.INFO, - Hosts: map[string]any{}, - Rule: []string{}, - Proxy: []map[string]any{}, - ProxyGroup: []map[string]any{}, - TCPConcurrent: false, - FindProcessMode: P.FindProcessStrict, - GlobalUA: "clash.meta/" + C.Version, - ETagSupport: true, - DNS: RawDNS{ - Enable: false, - IPv6: false, - UseHosts: true, - UseSystemHosts: true, - IPv6Timeout: 100, - EnhancedMode: C.DNSMapping, - FakeIPRange: "198.18.0.1/16", - FallbackFilter: RawFallbackFilter{ - GeoIP: true, - GeoIPCode: "CN", - IPCIDR: []string{}, - GeoSite: []string{}, - }, - DefaultNameserver: []string{ - "114.114.114.114", - "223.5.5.5", - "8.8.8.8", - "1.0.0.1", - }, - NameServer: []string{ - "https://doh.pub/dns-query", - "tls://223.5.5.5:853", - }, - FakeIPFilter: []string{ - "dns.msftnsci.com", - "www.msftnsci.com", - "www.msftconnecttest.com", - }, - FakeIPFilterMode: C.FilterBlackList, - }, - NTP: RawNTP{ - Enable: false, - WriteToSystem: false, - Server: "time.apple.com", - Port: 123, - Interval: 30, - }, - Tun: RawTun{ - Enable: false, - Device: "", - Stack: C.TunGvisor, - DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query - AutoRoute: true, - AutoDetectInterface: true, - Inet6Address: []netip.Prefix{netip.MustParsePrefix("fdfe:dcba:9876::1/126")}, - }, - TuicServer: RawTuicServer{ - Enable: false, - Token: nil, - Users: nil, - Certificate: "", - PrivateKey: "", - Listen: "", - CongestionController: "", - MaxIdleTime: 15000, - AuthenticationTimeout: 1000, - ALPN: []string{"h3"}, - MaxUdpRelayPacketSize: 1500, - }, - IPTables: RawIPTables{ - Enable: false, - InboundInterface: "lo", - Bypass: []string{}, - DnsRedirect: true, - }, - Experimental: RawExperimental{ - // https://github.com/quic-go/quic-go/issues/4178 - // Quic-go currently cannot automatically fall back on platforms that do not support ecn, so this feature is turned off by default. - QUICGoDisableECN: true, - }, - Profile: RawProfile{ - StoreSelected: true, - }, - GeoXUrl: RawGeoXUrl{ - Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", - ASN: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb", - GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", - GeoSite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat", - }, - Sniffer: RawSniffer{ - Enable: false, - Sniff: map[string]RawSniffingConfig{}, - ForceDomain: []string{}, - SkipDomain: []string{}, - Ports: []string{}, - ForceDnsMapping: true, - ParsePureIp: true, - OverrideDest: true, - }, - ExternalUIURL: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip", - ExternalControllerCors: RawCors{ - AllowOrigins: []string{"*"}, - AllowPrivateNetwork: true, - }, - } -} - -func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { - // config with default value - rawCfg := DefaultRawConfig() - - if err := yaml.Unmarshal(buf, rawCfg); err != nil { - return nil, err - } - - return rawCfg, nil -} - -func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { - config := &Config{} - log.Infoln("Start initial configuration in progress") //Segment finished in xxm - startTime := time.Now() - - general, err := parseGeneral(rawCfg) - if err != nil { - return nil, err - } - config.General = general - - // We need to temporarily apply some configuration in general and roll back after parsing the complete configuration. - // The loading and downloading of geodata in the parseRules and parseRuleProviders rely on these. - // This implementation is very disgusting, but there is currently no better solution - rollback := temporaryUpdateGeneral(config.General) - defer rollback() - - controller, err := parseController(rawCfg) - if err != nil { - return nil, err - } - config.Controller = controller - - experimental, err := parseExperimental(rawCfg) - if err != nil { - return nil, err - } - config.Experimental = experimental - - iptables, err := parseIPTables(rawCfg) - if err != nil { - return nil, err - } - config.IPTables = iptables - - ntpCfg, err := parseNTP(rawCfg) - if err != nil { - return nil, err - } - config.NTP = ntpCfg - - profile, err := parseProfile(rawCfg) - if err != nil { - return nil, err - } - config.Profile = profile - - tlsCfg, err := parseTLS(rawCfg) - if err != nil { - return nil, err - } - config.TLS = tlsCfg - - proxies, providers, err := parseProxies(rawCfg) - if err != nil { - return nil, err - } - config.Proxies = proxies - config.Providers = providers - - listener, err := parseListeners(rawCfg) - if err != nil { - return nil, err - } - config.Listeners = listener - - log.Infoln("Geodata Loader mode: %s", geodata.LoaderName()) - log.Infoln("Geosite Matcher implementation: %s", geodata.SiteMatcherName()) - ruleProviders, err := parseRuleProviders(rawCfg) - if err != nil { - return nil, err - } - config.RuleProviders = ruleProviders - - subRules, err := parseSubRules(rawCfg, proxies, ruleProviders) - if err != nil { - return nil, err - } - config.SubRules = subRules - - rules, err := parseRules(rawCfg.Rule, proxies, ruleProviders, subRules, "rules") - 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, ruleProviders) - if err != nil { - return nil, err - } - config.DNS = dnsCfg - - err = parseTun(rawCfg.Tun, config.General) - if err != nil { - return nil, err - } - - err = parseTuicServer(rawCfg.TuicServer, config.General) - if err != nil { - return nil, err - } - - config.Users = parseAuthentication(rawCfg.Authentication) - - config.Tunnels = rawCfg.Tunnels - // verify tunnels - for _, t := range config.Tunnels { - if len(t.Proxy) > 0 { - if _, ok := config.Proxies[t.Proxy]; !ok { - return nil, fmt.Errorf("tunnel proxy %s not found", t.Proxy) - } - } - } - - config.Sniffer, err = parseSniffer(rawCfg.Sniffer, ruleProviders) - if err != nil { - return nil, err - } - - elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms - log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm - - return config, nil -} - -//go:linkname temporaryUpdateGeneral -func temporaryUpdateGeneral(general *General) func() - -func parseGeneral(cfg *RawConfig) (*General, error) { - return &General{ - Inbound: Inbound{ - Port: cfg.Port, - SocksPort: cfg.SocksPort, - RedirPort: cfg.RedirPort, - TProxyPort: cfg.TProxyPort, - MixedPort: cfg.MixedPort, - ShadowSocksConfig: cfg.ShadowSocksConfig, - VmessConfig: cfg.VmessConfig, - AllowLan: cfg.AllowLan, - SkipAuthPrefixes: cfg.SkipAuthPrefixes, - LanAllowedIPs: cfg.LanAllowedIPs, - LanDisAllowedIPs: cfg.LanDisAllowedIPs, - BindAddress: cfg.BindAddress, - InboundTfo: cfg.InboundTfo, - InboundMPTCP: cfg.InboundMPTCP, - }, - UnifiedDelay: cfg.UnifiedDelay, - Mode: cfg.Mode, - LogLevel: cfg.LogLevel, - IPv6: cfg.IPv6, - Interface: cfg.Interface, - RoutingMark: cfg.RoutingMark, - GeoXUrl: GeoXUrl{ - GeoIp: cfg.GeoXUrl.GeoIp, - Mmdb: cfg.GeoXUrl.Mmdb, - ASN: cfg.GeoXUrl.ASN, - GeoSite: cfg.GeoXUrl.GeoSite, - }, - GeoAutoUpdate: cfg.GeoAutoUpdate, - GeoUpdateInterval: cfg.GeoUpdateInterval, - GeodataMode: cfg.GeodataMode, - GeodataLoader: cfg.GeodataLoader, - GeositeMatcher: cfg.GeositeMatcher, - TCPConcurrent: cfg.TCPConcurrent, - FindProcessMode: cfg.FindProcessMode, - GlobalClientFingerprint: cfg.GlobalClientFingerprint, - GlobalUA: cfg.GlobalUA, - ETagSupport: cfg.ETagSupport, - KeepAliveIdle: cfg.KeepAliveIdle, - KeepAliveInterval: cfg.KeepAliveInterval, - DisableKeepAlive: cfg.DisableKeepAlive, - }, nil -} - -func parseController(cfg *RawConfig) (*Controller, error) { - if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) { - return nil, C.Path.ErrNotSafePath(path) - } - return &Controller{ - ExternalController: cfg.ExternalController, - ExternalUI: cfg.ExternalUI, - ExternalUIURL: cfg.ExternalUIURL, - ExternalUIName: cfg.ExternalUIName, - Secret: cfg.Secret, - ExternalControllerPipe: cfg.ExternalControllerPipe, - ExternalControllerUnix: cfg.ExternalControllerUnix, - ExternalControllerTLS: cfg.ExternalControllerTLS, - ExternalDohServer: cfg.ExternalDohServer, - Cors: Cors{ - AllowOrigins: cfg.ExternalControllerCors.AllowOrigins, - AllowPrivateNetwork: cfg.ExternalControllerCors.AllowPrivateNetwork, - }, - }, nil -} - -func parseExperimental(cfg *RawConfig) (*Experimental, error) { - return &Experimental{ - Fingerprints: cfg.Experimental.Fingerprints, - QUICGoDisableGSO: cfg.Experimental.QUICGoDisableGSO, - QUICGoDisableECN: cfg.Experimental.QUICGoDisableECN, - IP4PEnable: cfg.Experimental.IP4PEnable, - }, nil -} - -func parseIPTables(cfg *RawConfig) (*IPTables, error) { - return &IPTables{ - Enable: cfg.IPTables.Enable, - InboundInterface: cfg.IPTables.InboundInterface, - Bypass: cfg.IPTables.Bypass, - DnsRedirect: cfg.IPTables.DnsRedirect, - }, nil -} - -func parseNTP(cfg *RawConfig) (*NTP, error) { - return &NTP{ - Enable: cfg.NTP.Enable, - Server: cfg.NTP.Server, - Port: cfg.NTP.Port, - Interval: cfg.NTP.Interval, - DialerProxy: cfg.NTP.DialerProxy, - WriteToSystem: cfg.NTP.WriteToSystem, - }, nil -} - -func parseProfile(cfg *RawConfig) (*Profile, error) { - return &Profile{ - StoreSelected: cfg.Profile.StoreSelected, - StoreFakeIP: cfg.Profile.StoreFakeIP, - }, nil -} - -func parseTLS(cfg *RawConfig) (*TLS, error) { - return &TLS{ - Certificate: cfg.TLS.Certificate, - PrivateKey: cfg.TLS.PrivateKey, - CustomTrustCert: cfg.TLS.CustomTrustCert, - }, 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) - proxiesConfig := cfg.Proxy - groupsConfig := cfg.ProxyGroup - providersConfig := cfg.ProxyProvider - - var ( - proxyList []string - AllProxies []string - hasGlobal bool - ) - proxiesList := list.New() - groupsList := list.New() - - proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect()) - proxies["REJECT"] = adapter.NewProxy(outbound.NewReject()) - proxies["REJECT-DROP"] = adapter.NewProxy(outbound.NewRejectDrop()) - proxies["COMPATIBLE"] = adapter.NewProxy(outbound.NewCompatible()) - proxies["PASS"] = adapter.NewProxy(outbound.NewPass()) - 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()) - AllProxies = append(AllProxies, proxy.Name()) - proxiesList.PushBack(mapping) - } - - // 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) - } - if groupName == "GLOBAL" { - hasGlobal = true - } - proxyList = append(proxyList, groupName) - groupsList.PushBack(mapping) - } - - // check if any loop exists and sort the ProxyGroups - if err := proxyGroupsDagSort(groupsConfig); err != nil { - return nil, nil, err - } - - var AllProviders []string - // 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 - AllProviders = append(AllProviders, name) - } - - slices.Sort(AllProxies) - slices.Sort(AllProviders) - - // parse proxy group - for idx, mapping := range groupsConfig { - group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap, AllProxies, AllProviders) - 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) - } - - var ps []C.Proxy - for _, v := range proxyList { - if proxies[v].Type() == C.Pass { - continue - } - ps = append(ps, proxies[v]) - } - hc := provider.NewHealthCheck(ps, "", 5000, 0, true, nil) - pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) - providersMap[provider.ReservedName] = pd - - if !hasGlobal { - global := outboundgroup.NewSelector( - &outboundgroup.GroupCommonOption{ - Name: "GLOBAL", - }, - []providerTypes.ProxyProvider{pd}, - ) - proxies["GLOBAL"] = adapter.NewProxy(global) - } - return proxies, providersMap, nil -} - -func parseListeners(cfg *RawConfig) (listeners map[string]C.InboundListener, err error) { - listeners = make(map[string]C.InboundListener) - for index, mapping := range cfg.Listeners { - listener, err := L.ParseListener(mapping) - if err != nil { - return nil, fmt.Errorf("proxy %d: %w", index, err) - } - - if _, exist := mapping[listener.Name()]; exist { - return nil, fmt.Errorf("listener %s is the duplicate name", listener.Name()) - } - - listeners[listener.Name()] = listener - - } - return -} - -func parseRuleProviders(cfg *RawConfig) (ruleProviders map[string]providerTypes.RuleProvider, err error) { - RP.SetTunnel(T.Tunnel) - ruleProviders = map[string]providerTypes.RuleProvider{} - // parse rule provider - for name, mapping := range cfg.RuleProvider { - rp, err := RP.ParseRuleProvider(name, mapping, R.ParseRule) - if err != nil { - return nil, err - } - - ruleProviders[name] = rp - } - return -} - -func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy, ruleProviders map[string]providerTypes.RuleProvider) (subRules map[string][]C.Rule, err error) { - subRules = map[string][]C.Rule{} - for name := range cfg.SubRules { - subRules[name] = make([]C.Rule, 0) - } - for name, rawRules := range cfg.SubRules { - if len(name) == 0 { - return nil, fmt.Errorf("sub-rule name is empty") - } - var rules []C.Rule - rules, err = parseRules(rawRules, proxies, ruleProviders, subRules, fmt.Sprintf("sub-rules[%s]", name)) - if err != nil { - return nil, err - } - subRules[name] = rules - } - - if err = verifySubRule(subRules); err != nil { - return nil, err - } - - return -} - -func verifySubRule(subRules map[string][]C.Rule) error { - for name := range subRules { - err := verifySubRuleCircularReferences(name, subRules, []string{}) - if err != nil { - return err - } - } - return nil -} - -func verifySubRuleCircularReferences(n string, subRules map[string][]C.Rule, arr []string) error { - isInArray := func(v string, array []string) bool { - for _, c := range array { - if v == c { - return true - } - } - return false - } - - arr = append(arr, n) - for i, rule := range subRules[n] { - if rule.RuleType() == C.SubRules { - if _, ok := subRules[rule.Adapter()]; !ok { - return fmt.Errorf("sub-rule[%d:%s] error: [%s] not found", i, n, rule.Adapter()) - } - if isInArray(rule.Adapter(), arr) { - arr = append(arr, rule.Adapter()) - return fmt.Errorf("sub-rule error: circular references [%s]", strings.Join(arr, "->")) - } - - if err := verifySubRuleCircularReferences(rule.Adapter(), subRules, arr); err != nil { - return err - } - } - } - return nil -} - -func parseRules(rulesConfig []string, proxies map[string]C.Proxy, ruleProviders map[string]providerTypes.RuleProvider, subRules map[string][]C.Rule, format string) ([]C.Rule, error) { - var rules []C.Rule - - // parse rules - for idx, line := range rulesConfig { - rule := trimArr(strings.Split(line, ",")) - var ( - payload string - target string - params []string - ruleName = strings.ToUpper(rule[0]) - ) - - l := len(rule) - - if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" || ruleName == "SUB-RULE" || ruleName == "DOMAIN-REGEX" || ruleName == "PROCESS-NAME-REGEX" || ruleName == "PROCESS-PATH-REGEX" { - target = rule[l-1] - payload = strings.Join(rule[1:l-1], ",") - } else { - if l < 2 { - return nil, fmt.Errorf("%s[%d] [%s] error: format invalid", format, idx, line) - } - if l < 4 { - rule = append(rule, make([]string, 4-l)...) - } - if ruleName == "MATCH" { - l = 2 - } - if l >= 3 { - l = 3 - payload = rule[1] - } - target = rule[l-1] - params = rule[l:] - } - if _, ok := proxies[target]; !ok { - if ruleName != "SUB-RULE" { - return nil, fmt.Errorf("%s[%d] [%s] error: proxy [%s] not found", format, idx, line, target) - } else if _, ok = subRules[target]; !ok { - return nil, fmt.Errorf("%s[%d] [%s] error: sub-rule [%s] not found", format, idx, line, target) - } - } - - params = trimArr(params) - parsed, parseErr := R.ParseRule(ruleName, payload, target, params, subRules) - if parseErr != nil { - return nil, fmt.Errorf("%s[%d] [%s] error: %s", format, idx, line, parseErr.Error()) - } - - for _, name := range parsed.ProviderNames() { - if _, ok := ruleProviders[name]; !ok { - return nil, fmt.Errorf("%s[%d] [%s] error: rule set [%s] not found", format, idx, line, name) - } - } - - rules = append(rules, parsed) - } - - return rules, nil -} - -func parseHosts(cfg *RawConfig) (*trie.DomainTrie[resolver.HostValue], error) { - tree := trie.New[resolver.HostValue]() - - // add default hosts - hostValue, _ := resolver.NewHostValueByIPs( - []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1})}) - if err := tree.Insert("localhost", hostValue); err != nil { - log.Errorln("insert localhost to host error: %s", err.Error()) - } - - if len(cfg.Hosts) != 0 { - for domain, anyValue := range cfg.Hosts { - if str, ok := anyValue.(string); ok && str == "lan" { - if addrs, err := net.InterfaceAddrs(); err != nil { - log.Errorln("insert lan to host error: %s", err) - } else { - ips := make([]netip.Addr, 0) - for _, addr := range addrs { - if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && !ipnet.IP.IsLinkLocalUnicast() { - if ip, err := netip.ParseAddr(ipnet.IP.String()); err == nil { - ips = append(ips, ip) - } - } - } - anyValue = ips - } - } - value, err := resolver.NewHostValue(anyValue) - if err != nil { - return nil, fmt.Errorf("%s is not a valid value", anyValue) - } - if value.IsDomain { - node := tree.Search(value.Domain) - for node != nil && node.Data().IsDomain { - if node.Data().Domain == domain { - return nil, fmt.Errorf("%s, there is a cycle in domain name mapping", domain) - } - node = tree.Search(node.Data().Domain) - } - } - _ = tree.Insert(domain, value) - } - } - tree.Optimize() - - return tree, nil -} - -func hostWithDefaultPort(host string, defPort string) (string, error) { - hostname, port, err := net.SplitHostPort(host) - if err != nil { - if !strings.Contains(err.Error(), "missing port in address") { - return "", err - } - host = host + ":" + defPort - if hostname, port, err = net.SplitHostPort(host); err != nil { - return "", err - } - } - - return net.JoinHostPort(hostname, port), nil -} - -func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.NameServer, error) { - var nameservers []dns.NameServer - - for idx, server := range servers { - server = parsePureDNSServer(server) - u, err := url.Parse(server) - if err != nil { - return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) - } - - proxyName := u.Fragment - - var addr, dnsNetType string - params := map[string]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 "http", "https": - addr, err = hostWithDefaultPort(u.Host, "443") - dnsNetType = "https" // DNS over HTTPS - if u.Scheme == "http" { - addr, err = hostWithDefaultPort(u.Host, "80") - } - if err == nil { - proxyName = "" - clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User} - addr = clearURL.String() - if len(u.Fragment) != 0 { - for _, s := range strings.Split(u.Fragment, "&") { - arr := strings.Split(s, "=") - if len(arr) == 0 { - continue - } else if len(arr) == 1 { - proxyName = arr[0] - } else if len(arr) == 2 { - params[arr[0]] = arr[1] - } else { - params[arr[0]] = strings.Join(arr[1:], "=") - } - } - } - } - case "quic": - addr, err = hostWithDefaultPort(u.Host, "853") - dnsNetType = "quic" // DNS over QUIC - case "system": - dnsNetType = "system" // System DNS - case "dhcp": - addr = server[len("dhcp://"):] // some special notation cannot be parsed by url - dnsNetType = "dhcp" // UDP from DHCP - if addr == "system" { // Compatible with old writing "dhcp://system" - dnsNetType = "system" - addr = "" - } - case "rcode": - dnsNetType = "rcode" - addr = u.Host - switch addr { - case "success", - "format_error", - "server_failure", - "name_error", - "not_implemented", - "refused": - default: - err = fmt.Errorf("unsupported RCode type: %s", addr) - } - 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()) - } - - if respectRules && len(proxyName) == 0 { - proxyName = dns.RespectRules - } - - nameserver := dns.NameServer{ - Net: dnsNetType, - Addr: addr, - ProxyName: proxyName, - Params: params, - PreferH3: preferH3, - } - if slices.ContainsFunc(nameservers, nameserver.Equal) { - continue // skip duplicates nameserver - } - - nameservers = append(nameservers, nameserver) - } - return nameservers, nil -} - -func init() { - dns.ParseNameServer = func(servers []string) ([]dns.NameServer, error) { // using by wireguard - return parseNameServer(servers, false, false) - } -} - -func parsePureDNSServer(server string) string { - addPre := func(server string) string { - return "udp://" + server - } - - if server == "system" { - return "system://" - } - - if ip, err := netip.ParseAddr(server); err != nil { - if strings.Contains(server, "://") { - return server - } - return addPre(server) - } else { - if ip.Is4() { - return addPre(server) - } else { - return addPre("[" + server + "]") - } - } -} - -func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], ruleProviders map[string]providerTypes.RuleProvider, respectRules bool, preferH3 bool) ([]dns.Policy, error) { - var policy []dns.Policy - - for pair := nsPolicy.Oldest(); pair != nil; pair = pair.Next() { - k, v := pair.Key, pair.Value - servers, err := utils.ToStringSlice(v) - if err != nil { - return nil, err - } - nameservers, err := parseNameServer(servers, respectRules, preferH3) - if err != nil { - return nil, err - } - kLower := strings.ToLower(k) - if strings.Contains(kLower, ",") { - if strings.Contains(kLower, "geosite:") { - subkeys := strings.Split(k, ":") - subkeys = subkeys[1:] - subkeys = strings.Split(subkeys[0], ",") - for _, subkey := range subkeys { - newKey := "geosite:" + subkey - policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) - } - } else if strings.Contains(kLower, "rule-set:") { - subkeys := strings.Split(k, ":") - subkeys = subkeys[1:] - subkeys = strings.Split(subkeys[0], ",") - for _, subkey := range subkeys { - newKey := "rule-set:" + subkey - policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) - } - } else { - subkeys := strings.Split(k, ",") - for _, subkey := range subkeys { - policy = append(policy, dns.Policy{Domain: subkey, NameServers: nameservers}) - } - } - } else { - if strings.Contains(kLower, "geosite:") { - policy = append(policy, dns.Policy{Domain: "geosite:" + k[8:], NameServers: nameservers}) - } else if strings.Contains(kLower, "rule-set:") { - policy = append(policy, dns.Policy{Domain: "rule-set:" + k[9:], NameServers: nameservers}) - } else { - policy = append(policy, dns.Policy{Domain: k, NameServers: nameservers}) - } - } - } - - for idx, p := range policy { - domain, nameservers := p.Domain, p.NameServers - - if strings.HasPrefix(domain, "rule-set:") { - domainSetName := domain[9:] - matcher, err := parseDomainRuleSet(domainSetName, "dns.nameserver-policy", ruleProviders) - if err != nil { - return nil, err - } - policy[idx] = dns.Policy{Matcher: matcher, NameServers: nameservers} - } else if strings.HasPrefix(domain, "geosite:") { - country := domain[8:] - matcher, err := RC.NewGEOSITE(country, "dns.nameserver-policy") - if err != nil { - return nil, err - } - policy[idx] = dns.Policy{Matcher: matcher, NameServers: nameservers} - } else { - if _, valid := trie.ValidAndSplitDomain(domain); !valid { - return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain) - } - } - } - - return policy, nil -} - -func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], ruleProviders map[string]providerTypes.RuleProvider) (*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") - } - - if cfg.RespectRules && len(cfg.ProxyServerNameserver) == 0 { - return nil, fmt.Errorf("if “respect-rules” is turned on, “proxy-server-nameserver” cannot be empty") - } - - dnsCfg := &DNS{ - Enable: cfg.Enable, - Listen: cfg.Listen, - PreferH3: cfg.PreferH3, - IPv6Timeout: cfg.IPv6Timeout, - IPv6: cfg.IPv6, - UseSystemHosts: cfg.UseSystemHosts, - EnhancedMode: cfg.EnhancedMode, - } - var err error - if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer, cfg.RespectRules, cfg.PreferH3); err != nil { - return nil, err - } - - if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback, cfg.RespectRules, cfg.PreferH3); err != nil { - return nil, err - } - - if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy, ruleProviders, cfg.RespectRules, cfg.PreferH3); err != nil { - return nil, err - } - - if dnsCfg.ProxyServerNameserver, err = parseNameServer(cfg.ProxyServerNameserver, false, cfg.PreferH3); err != nil { - return nil, err - } - - if dnsCfg.DirectNameServer, err = parseNameServer(cfg.DirectNameServer, false, cfg.PreferH3); err != nil { - return nil, err - } - dnsCfg.DirectFollowPolicy = cfg.DirectNameServerFollowPolicy - - if len(cfg.DefaultNameserver) == 0 { - return nil, errors.New("default nameserver should have at least one nameserver") - } - if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver, false, cfg.PreferH3); err != nil { - return nil, err - } - // check default nameserver is pure ip addr - for _, ns := range dnsCfg.DefaultNameserver { - if ns.Net == "system" { - continue - } - host, _, err := net.SplitHostPort(ns.Addr) - if err != nil || net.ParseIP(host) == nil { - u, err := url.Parse(ns.Addr) - if err == nil && net.ParseIP(u.Host) == nil { - if ip, _, err := net.SplitHostPort(u.Host); err != nil || net.ParseIP(ip) == nil { - return nil, errors.New("default nameserver should be pure IP") - } - } - } - } - - fakeIPRange, err := netip.ParsePrefix(cfg.FakeIPRange) - T.SetFakeIPRange(fakeIPRange) - if cfg.EnhancedMode == C.DNSFakeIP { - if err != nil { - return nil, err - } - - var fakeIPTrie *trie.DomainTrie[struct{}] - if len(dnsCfg.Fallback) != 0 { - fakeIPTrie = trie.New[struct{}]() - for _, fb := range dnsCfg.Fallback { - if net.ParseIP(fb.Addr) != nil { - continue - } - _ = fakeIPTrie.Insert(fb.Addr, struct{}{}) - } - } - - // fake ip skip host filter - host, err := parseDomain(cfg.FakeIPFilter, fakeIPTrie, "dns.fake-ip-filter", ruleProviders) - if err != nil { - return nil, err - } - - pool, err := fakeip.New(fakeip.Options{ - IPNet: fakeIPRange, - Size: 1000, - Host: host, - Mode: cfg.FakeIPFilterMode, - Persistence: rawCfg.Profile.StoreFakeIP, - }) - if err != nil { - return nil, err - } - - dnsCfg.FakeIPRange = pool - } - - if len(cfg.Fallback) != 0 { - if cfg.FallbackFilter.GeoIP { - matcher, err := RC.NewGEOIP(cfg.FallbackFilter.GeoIPCode, "dns.fallback-filter.geoip", false, true) - if err != nil { - return nil, fmt.Errorf("load GeoIP dns fallback filter error, %w", err) - } - dnsCfg.FallbackIPFilter = append(dnsCfg.FallbackIPFilter, matcher.DnsFallbackFilter()) - } - if len(cfg.FallbackFilter.IPCIDR) > 0 { - cidrSet := cidr.NewIpCidrSet() - for idx, ipcidr := range cfg.FallbackFilter.IPCIDR { - err = cidrSet.AddIpCidrForString(ipcidr) - if err != nil { - return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %w", idx, err) - } - } - err = cidrSet.Merge() - if err != nil { - return nil, err - } - matcher := cidrSet // dns.fallback-filter.ipcidr - dnsCfg.FallbackIPFilter = append(dnsCfg.FallbackIPFilter, matcher) - } - if len(cfg.FallbackFilter.Domain) > 0 { - domainTrie := trie.New[struct{}]() - for idx, domain := range cfg.FallbackFilter.Domain { - err = domainTrie.Insert(domain, struct{}{}) - if err != nil { - return nil, fmt.Errorf("DNS FallbackDomain[%d] format error: %w", idx, err) - } - } - matcher := domainTrie.NewDomainSet() // dns.fallback-filter.domain - dnsCfg.FallbackDomainFilter = append(dnsCfg.FallbackDomainFilter, matcher) - } - if len(cfg.FallbackFilter.GeoSite) > 0 { - log.Warnln("replace fallback-filter.geosite with nameserver-policy, it will be removed in the future") - for idx, geoSite := range cfg.FallbackFilter.GeoSite { - matcher, err := RC.NewGEOSITE(geoSite, "dns.fallback-filter.geosite") - if err != nil { - return nil, fmt.Errorf("DNS FallbackGeosite[%d] format error: %w", idx, err) - } - dnsCfg.FallbackDomainFilter = append(dnsCfg.FallbackDomainFilter, matcher) - } - } - } - - if cfg.UseHosts { - dnsCfg.Hosts = hosts - } - - if cfg.CacheAlgorithm == "" || cfg.CacheAlgorithm == "lru" { - dnsCfg.CacheAlgorithm = "lru" - } else { - dnsCfg.CacheAlgorithm = "arc" - } - - return dnsCfg, nil -} - -func parseAuthentication(rawRecords []string) []auth.AuthUser { - var 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 -} - -func parseTun(rawTun RawTun, general *General) error { - tunAddressPrefix := T.FakeIPRange() - if !tunAddressPrefix.IsValid() { - tunAddressPrefix = netip.MustParsePrefix("198.18.0.1/16") - } - tunAddressPrefix = netip.PrefixFrom(tunAddressPrefix.Addr(), 30) - - if !general.IPv6 || !verifyIP6() { - rawTun.Inet6Address = nil - } - - general.Tun = LC.Tun{ - Enable: rawTun.Enable, - Device: rawTun.Device, - Stack: rawTun.Stack, - DNSHijack: rawTun.DNSHijack, - AutoRoute: rawTun.AutoRoute, - AutoDetectInterface: rawTun.AutoDetectInterface, - - MTU: rawTun.MTU, - GSO: rawTun.GSO, - GSOMaxSize: rawTun.GSOMaxSize, - Inet4Address: []netip.Prefix{tunAddressPrefix}, - Inet6Address: rawTun.Inet6Address, - IPRoute2TableIndex: rawTun.IPRoute2TableIndex, - IPRoute2RuleIndex: rawTun.IPRoute2RuleIndex, - AutoRedirect: rawTun.AutoRedirect, - AutoRedirectInputMark: rawTun.AutoRedirectInputMark, - AutoRedirectOutputMark: rawTun.AutoRedirectOutputMark, - StrictRoute: rawTun.StrictRoute, - RouteAddress: rawTun.RouteAddress, - RouteAddressSet: rawTun.RouteAddressSet, - RouteExcludeAddress: rawTun.RouteExcludeAddress, - RouteExcludeAddressSet: rawTun.RouteExcludeAddressSet, - IncludeInterface: rawTun.IncludeInterface, - ExcludeInterface: rawTun.ExcludeInterface, - IncludeUID: rawTun.IncludeUID, - IncludeUIDRange: rawTun.IncludeUIDRange, - ExcludeUID: rawTun.ExcludeUID, - ExcludeUIDRange: rawTun.ExcludeUIDRange, - ExcludeSrcPort: rawTun.ExcludeSrcPort, - ExcludeSrcPortRange: rawTun.ExcludeSrcPortRange, - ExcludeDstPort: rawTun.ExcludeDstPort, - ExcludeDstPortRange: rawTun.ExcludeDstPortRange, - IncludeAndroidUser: rawTun.IncludeAndroidUser, - IncludePackage: rawTun.IncludePackage, - ExcludePackage: rawTun.ExcludePackage, - EndpointIndependentNat: rawTun.EndpointIndependentNat, - UDPTimeout: rawTun.UDPTimeout, - FileDescriptor: rawTun.FileDescriptor, - - Inet4RouteAddress: rawTun.Inet4RouteAddress, - Inet6RouteAddress: rawTun.Inet6RouteAddress, - Inet4RouteExcludeAddress: rawTun.Inet4RouteExcludeAddress, - Inet6RouteExcludeAddress: rawTun.Inet6RouteExcludeAddress, - } - - return nil -} - -func parseTuicServer(rawTuic RawTuicServer, general *General) error { - general.TuicServer = LC.TuicServer{ - Enable: rawTuic.Enable, - Listen: rawTuic.Listen, - Token: rawTuic.Token, - Users: rawTuic.Users, - Certificate: rawTuic.Certificate, - PrivateKey: rawTuic.PrivateKey, - CongestionController: rawTuic.CongestionController, - MaxIdleTime: rawTuic.MaxIdleTime, - AuthenticationTimeout: rawTuic.AuthenticationTimeout, - ALPN: rawTuic.ALPN, - MaxUdpRelayPacketSize: rawTuic.MaxUdpRelayPacketSize, - CWND: rawTuic.CWND, - } - return nil -} - -func parseSniffer(snifferRaw RawSniffer, ruleProviders map[string]providerTypes.RuleProvider) (*sniffer.Config, error) { - snifferConfig := &sniffer.Config{ - Enable: snifferRaw.Enable, - ForceDnsMapping: snifferRaw.ForceDnsMapping, - ParsePureIp: snifferRaw.ParsePureIp, - } - loadSniffer := make(map[snifferTypes.Type]sniffer.SnifferConfig) - - if len(snifferRaw.Sniff) != 0 { - for sniffType, sniffConfig := range snifferRaw.Sniff { - find := false - ports, err := utils.NewUnsignedRangesFromList[uint16](sniffConfig.Ports) - if err != nil { - return nil, err - } - overrideDest := snifferRaw.OverrideDest - if sniffConfig.OverrideDest != nil { - overrideDest = *sniffConfig.OverrideDest - } - for _, snifferType := range snifferTypes.List { - if snifferType.String() == strings.ToUpper(sniffType) { - find = true - loadSniffer[snifferType] = sniffer.SnifferConfig{ - Ports: ports, - OverrideDest: overrideDest, - } - } - } - - if !find { - return nil, fmt.Errorf("not find the sniffer[%s]", sniffType) - } - } - } else { - if snifferConfig.Enable && len(snifferRaw.Sniffing) != 0 { - // Deprecated: Use Sniff instead - log.Warnln("Deprecated: Use Sniff instead") - } - globalPorts, err := utils.NewUnsignedRangesFromList[uint16](snifferRaw.Ports) - if err != nil { - return nil, err - } - - for _, snifferName := range snifferRaw.Sniffing { - find := false - for _, snifferType := range snifferTypes.List { - if snifferType.String() == strings.ToUpper(snifferName) { - find = true - loadSniffer[snifferType] = sniffer.SnifferConfig{ - Ports: globalPorts, - OverrideDest: snifferRaw.OverrideDest, - } - } - } - - if !find { - return nil, fmt.Errorf("not find the sniffer[%s]", snifferName) - } - } - } - - snifferConfig.Sniffers = loadSniffer - - forceDomain, err := parseDomain(snifferRaw.ForceDomain, nil, "sniffer.force-domain", ruleProviders) - if err != nil { - return nil, fmt.Errorf("error in force-domain, error:%w", err) - } - snifferConfig.ForceDomain = forceDomain - - skipSrcAddress, err := parseIPCIDR(snifferRaw.SkipSrcAddress, nil, "sniffer.skip-src-address", ruleProviders) - if err != nil { - return nil, fmt.Errorf("error in skip-src-address, error:%w", err) - } - snifferConfig.SkipSrcAddress = skipSrcAddress - - skipDstAddress, err := parseIPCIDR(snifferRaw.SkipDstAddress, nil, "sniffer.skip-src-address", ruleProviders) - if err != nil { - return nil, fmt.Errorf("error in skip-dst-address, error:%w", err) - } - snifferConfig.SkipDstAddress = skipDstAddress - - skipDomain, err := parseDomain(snifferRaw.SkipDomain, nil, "sniffer.skip-domain", ruleProviders) - if err != nil { - return nil, fmt.Errorf("error in skip-domain, error:%w", err) - } - snifferConfig.SkipDomain = skipDomain - - return snifferConfig, nil -} - -func parseIPCIDR(addresses []string, cidrSet *cidr.IpCidrSet, adapterName string, ruleProviders map[string]providerTypes.RuleProvider) (matchers []C.IpMatcher, err error) { - var matcher C.IpMatcher - for _, ipcidr := range addresses { - ipcidrLower := strings.ToLower(ipcidr) - if strings.Contains(ipcidrLower, "geoip:") { - subkeys := strings.Split(ipcidr, ":") - subkeys = subkeys[1:] - subkeys = strings.Split(subkeys[0], ",") - for _, country := range subkeys { - matcher, err = RC.NewGEOIP(country, adapterName, false, false) - if err != nil { - return nil, err - } - matchers = append(matchers, matcher) - } - } else if strings.Contains(ipcidrLower, "rule-set:") { - subkeys := strings.Split(ipcidr, ":") - subkeys = subkeys[1:] - subkeys = strings.Split(subkeys[0], ",") - for _, domainSetName := range subkeys { - matcher, err = parseIPRuleSet(domainSetName, adapterName, ruleProviders) - if err != nil { - return nil, err - } - matchers = append(matchers, matcher) - } - } else { - if cidrSet == nil { - cidrSet = cidr.NewIpCidrSet() - } - err = cidrSet.AddIpCidrForString(ipcidr) - if err != nil { - return nil, err - } - } - } - if !cidrSet.IsEmpty() { - err = cidrSet.Merge() - if err != nil { - return nil, err - } - matcher = cidrSet - matchers = append(matchers, matcher) - } - return -} - -func parseDomain(domains []string, domainTrie *trie.DomainTrie[struct{}], adapterName string, ruleProviders map[string]providerTypes.RuleProvider) (matchers []C.DomainMatcher, err error) { - var matcher C.DomainMatcher - for _, domain := range domains { - domainLower := strings.ToLower(domain) - if strings.Contains(domainLower, "geosite:") { - subkeys := strings.Split(domain, ":") - subkeys = subkeys[1:] - subkeys = strings.Split(subkeys[0], ",") - for _, country := range subkeys { - matcher, err = RC.NewGEOSITE(country, adapterName) - if err != nil { - return nil, err - } - matchers = append(matchers, matcher) - } - } else if strings.Contains(domainLower, "rule-set:") { - subkeys := strings.Split(domain, ":") - subkeys = subkeys[1:] - subkeys = strings.Split(subkeys[0], ",") - for _, domainSetName := range subkeys { - matcher, err = parseDomainRuleSet(domainSetName, adapterName, ruleProviders) - if err != nil { - return nil, err - } - matchers = append(matchers, matcher) - } - } else { - if domainTrie == nil { - domainTrie = trie.New[struct{}]() - } - err = domainTrie.Insert(domain, struct{}{}) - if err != nil { - return nil, err - } - } - } - if !domainTrie.IsEmpty() { - matcher = domainTrie.NewDomainSet() - matchers = append(matchers, matcher) - } - return -} - -func parseIPRuleSet(domainSetName string, adapterName string, ruleProviders map[string]providerTypes.RuleProvider) (C.IpMatcher, error) { - if rp, ok := ruleProviders[domainSetName]; !ok { - return nil, fmt.Errorf("not found rule-set: %s", domainSetName) - } else { - switch rp.Behavior() { - case providerTypes.Domain: - return nil, fmt.Errorf("rule provider type error, except ipcidr,actual %s", rp.Behavior()) - case providerTypes.Classical: - log.Warnln("%s provider is %s, only matching it contain ip rule", rp.Name(), rp.Behavior()) - default: - } - } - return RP.NewRuleSet(domainSetName, adapterName, false, true) -} - -func parseDomainRuleSet(domainSetName string, adapterName string, ruleProviders map[string]providerTypes.RuleProvider) (C.DomainMatcher, error) { - if rp, ok := ruleProviders[domainSetName]; !ok { - return nil, fmt.Errorf("not found rule-set: %s", domainSetName) - } else { - switch rp.Behavior() { - case providerTypes.IPCIDR: - return nil, fmt.Errorf("rule provider type error, except domain,actual %s", rp.Behavior()) - case providerTypes.Classical: - log.Warnln("%s provider is %s, only matching it contain domain rule", rp.Name(), rp.Behavior()) - default: - } - } - return RP.NewRuleSet(domainSetName, adapterName, false, true) -} diff --git a/config/initial.go b/config/initial.go deleted file mode 100644 index 61d128956e..0000000000 --- a/config/initial.go +++ /dev/null @@ -1,32 +0,0 @@ -package config - -import ( - "fmt" - "os" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -// 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() - } - - return nil -} diff --git a/config/utils.go b/config/utils.go deleted file mode 100644 index f87fb34131..0000000000 --- a/config/utils.go +++ /dev/null @@ -1,163 +0,0 @@ -package config - -import ( - "fmt" - "net" - "net/netip" - "strings" - - "github.com/metacubex/mihomo/adapter/outboundgroup" - "github.com/metacubex/mihomo/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) -} - -func verifyIP6() bool { - if iAddrs, err := net.InterfaceAddrs(); err == nil { - for _, addr := range iAddrs { - if prefix, err := netip.ParsePrefix(addr.String()); err == nil { - if addr := prefix.Addr().Unmap(); addr.Is6() && addr.IsGlobalUnicast() { - return true - } - } - } - } - return false -} diff --git a/constant/adapters.go b/constant/adapters.go deleted file mode 100644 index 280552ec33..0000000000 --- a/constant/adapters.go +++ /dev/null @@ -1,344 +0,0 @@ -package constant - -import ( - "context" - "errors" - "fmt" - "net" - "net/netip" - "sync" - "time" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/dialer" -) - -// Adapter Type -const ( - Direct AdapterType = iota - Reject - RejectDrop - Compatible - Pass - Dns - - Relay - Selector - Fallback - URLTest - LoadBalance - - Shadowsocks - ShadowsocksR - Snell - Socks5 - Http - Vmess - Vless - Trojan - Hysteria - Hysteria2 - WireGuard - Tuic - Ssh - Mieru - AnyTLS -) - -const ( - DefaultTCPTimeout = dialer.DefaultTCPTimeout - DefaultUDPTimeout = dialer.DefaultUDPTimeout - DefaultDropTime = 12 * DefaultTCPTimeout - DefaultTLSTimeout = DefaultTCPTimeout - DefaultTestURL = "https://www.gstatic.com/generate_204" -) - -var ErrNotSupport = errors.New("no support") - -type Connection interface { - Chains() Chain - AppendToChains(adapter ProxyAdapter) - RemoteDestination() string -} - -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 { - N.ExtendedConn - Connection -} - -type PacketConn interface { - N.EnhancePacketConn - Connection - // Deprecate WriteWithMetadata because of remote resolve DNS cause TURN failed - // WriteWithMetadata(p []byte, metadata *Metadata) (n int, err error) -} - -type Dialer interface { - DialContext(ctx context.Context, network, address string) (net.Conn, error) - ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) -} - -type ProxyInfo struct { - XUDP bool - TFO bool - MPTCP bool - SMUX bool - Interface string - RoutingMark int - DialerProxy string -} - -type ProxyAdapter interface { - Name() string - Type() AdapterType - Addr() string - SupportUDP() bool - - // ProxyInfo contains some extra information maybe useful for MarshalJSON - ProxyInfo() ProxyInfo - MarshalJSON() ([]byte, error) - - // Deprecated: use DialContextWithDialer and ListenPacketWithDialer instead. - // StreamConn wraps a protocol around net.Conn with Metadata. - // - // Examples: - // conn, _ := net.DialContext(context.Background(), "tcp", "host:port") - // conn, _ = adapter.StreamConnContext(context.Background(), conn, metadata) - // - // It returns a C.Conn with protocol which start with - // a new session (if any) - StreamConnContext(ctx context.Context, 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) (Conn, error) - ListenPacketContext(ctx context.Context, metadata *Metadata) (PacketConn, error) - - // SupportUOT return UDP over TCP support - SupportUOT() bool - - SupportWithDialer() NetWork - DialContextWithDialer(ctx context.Context, dialer Dialer, metadata *Metadata) (Conn, error) - ListenPacketWithDialer(ctx context.Context, dialer Dialer, metadata *Metadata) (PacketConn, error) - - // IsL3Protocol return ProxyAdapter working in L3 (tell dns module not pass the domain to avoid loopback) - IsL3Protocol(metadata *Metadata) bool - - // Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract. - Unwrap(metadata *Metadata, touch bool) Proxy - - // Close releasing associated resources - Close() error -} - -type Group interface { - URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error) - Touch() -} - -type DelayHistory struct { - Time time.Time `json:"time"` - Delay uint16 `json:"delay"` -} - -type ProxyState struct { - Alive bool `json:"alive"` - History []DelayHistory `json:"history"` -} - -type DelayHistoryStoreType int - -type Proxy interface { - ProxyAdapter - Adapter() ProxyAdapter - AliveForTestUrl(url string) bool - DelayHistory() []DelayHistory - ExtraDelayHistories() map[string]ProxyState - LastDelayForTestUrl(url string) uint16 - URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[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 RejectDrop: - return "RejectDrop" - case Compatible: - return "Compatible" - case Pass: - return "Pass" - case Dns: - return "Dns" - 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 Vless: - return "Vless" - case Trojan: - return "Trojan" - case Hysteria: - return "Hysteria" - case Hysteria2: - return "Hysteria2" - case WireGuard: - return "WireGuard" - case Tuic: - return "Tuic" - case Ssh: - return "Ssh" - case Mieru: - return "Mieru" - case AnyTLS: - return "AnyTLS" - 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 - - // Drop call after packet is used, could recycle buffer in this function. - Drop() - - // LocalAddr returns the source IP/Port of packet - LocalAddr() net.Addr -} - -type UDPPacketInAddr interface { - InAddr() net.Addr -} - -// PacketAdapter is a UDP Packet adapter for socks/redir/tun -type PacketAdapter interface { - UDPPacket - // Metadata returns destination metadata - Metadata() *Metadata - // Key is a SNAT key - Key() string -} - -type packetAdapter struct { - UDPPacket - metadata *Metadata - key string -} - -// Metadata returns destination metadata -func (s *packetAdapter) Metadata() *Metadata { - return s.metadata -} - -// Key is a SNAT key -func (s *packetAdapter) Key() string { - return s.key -} - -func NewPacketAdapter(packet UDPPacket, metadata *Metadata) PacketAdapter { - return &packetAdapter{ - packet, - metadata, - packet.LocalAddr().String(), - } -} - -type WriteBack interface { - WriteBack(b []byte, addr net.Addr) (n int, err error) -} - -type WriteBackProxy interface { - WriteBack - UpdateWriteBack(wb WriteBack) -} - -type PacketSender interface { - // Send will send PacketAdapter nonblocking - // the implement must call UDPPacket.Drop() inside Send - Send(PacketAdapter) - // Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy - Process(PacketConn, WriteBackProxy) - // ResolveUDP do a local resolve UDP dns blocking if metadata is not resolved - ResolveUDP(*Metadata) error - // Close stop the Process loop - Close() -} - -type NatTable interface { - GetOrCreate(key string, maker func() PacketSender) (PacketSender, bool) - - Delete(key string) - - GetForLocalConn(lAddr, rAddr string) *net.UDPConn - - AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool - - RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool) - - GetOrCreateLockForLocalConn(lAddr string, key string) (*sync.Cond, bool) - - DeleteForLocalConn(lAddr, key string) - - DeleteLockForLocalConn(lAddr, key string) -} diff --git a/constant/context.go b/constant/context.go deleted file mode 100644 index 11ad701109..0000000000 --- a/constant/context.go +++ /dev/null @@ -1,25 +0,0 @@ -package constant - -import ( - "net" - - N "github.com/metacubex/mihomo/common/net" - - "github.com/gofrs/uuid/v5" -) - -type PlainContext interface { - ID() uuid.UUID -} - -type ConnContext interface { - PlainContext - Metadata() *Metadata - Conn() *N.BufferedConn -} - -type PacketConnContext interface { - PlainContext - Metadata() *Metadata - PacketConn() net.PacketConn -} diff --git a/constant/dns.go b/constant/dns.go deleted file mode 100644 index b0874eef5b..0000000000 --- a/constant/dns.go +++ /dev/null @@ -1,216 +0,0 @@ -package constant - -import ( - "encoding/json" - "errors" - "strings" -) - -// DNSModeMapping is a mapping for EnhancedMode enum -var DNSModeMapping = map[string]DNSMode{ - DNSNormal.String(): DNSNormal, - DNSFakeIP.String(): DNSFakeIP, - DNSMapping.String(): DNSMapping, -} - -const ( - DNSNormal DNSMode = iota - DNSFakeIP - DNSMapping - DNSHosts -) - -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[strings.ToLower(tp)] - if !exist { - return errors.New("invalid mode") - } - *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 - if err := json.Unmarshal(data, &tp); err != nil { - return err - } - mode, exist := DNSModeMapping[strings.ToLower(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()) -} - -// UnmarshalText unserialize EnhancedMode -func (e *DNSMode) UnmarshalText(data []byte) error { - mode, exist := DNSModeMapping[strings.ToLower(string(data))] - if !exist { - return errors.New("invalid mode") - } - *e = mode - return nil -} - -// MarshalText serialize EnhancedMode -func (e DNSMode) MarshalText() ([]byte, error) { - return []byte(e.String()), nil -} - -func (e DNSMode) String() string { - switch e { - case DNSNormal: - return "normal" - case DNSFakeIP: - return "fake-ip" - case DNSMapping: - return "redir-host" - case DNSHosts: - return "hosts" - default: - return "unknown" - } -} - -type DNSPrefer int - -const ( - DualStack DNSPrefer = iota - IPv4Only - IPv6Only - IPv4Prefer - IPv6Prefer -) - -var dnsPreferMap = map[string]DNSPrefer{ - DualStack.String(): DualStack, - IPv4Only.String(): IPv4Only, - IPv6Only.String(): IPv6Only, - IPv4Prefer.String(): IPv4Prefer, - IPv6Prefer.String(): IPv6Prefer, -} - -func (d DNSPrefer) String() string { - switch d { - case DualStack: - return "dual" - case IPv4Only: - return "ipv4" - case IPv6Only: - return "ipv6" - case IPv4Prefer: - return "ipv4-prefer" - case IPv6Prefer: - return "ipv6-prefer" - default: - return "dual" - } -} - -func NewDNSPrefer(prefer string) DNSPrefer { - if p, ok := dnsPreferMap[prefer]; ok { - return p - } else { - return DualStack - } -} - -// FilterModeMapping is a mapping for FilterMode enum -var FilterModeMapping = map[string]FilterMode{ - FilterBlackList.String(): FilterBlackList, - FilterWhiteList.String(): FilterWhiteList, -} - -type FilterMode int - -const ( - FilterBlackList FilterMode = iota - FilterWhiteList -) - -func (e FilterMode) String() string { - switch e { - case FilterBlackList: - return "blacklist" - case FilterWhiteList: - return "whitelist" - default: - return "unknown" - } -} - -func (e FilterMode) MarshalYAML() (interface{}, error) { - return e.String(), nil -} - -func (e *FilterMode) UnmarshalYAML(unmarshal func(interface{}) error) error { - var tp string - if err := unmarshal(&tp); err != nil { - return err - } - mode, exist := FilterModeMapping[strings.ToLower(tp)] - if !exist { - return errors.New("invalid mode") - } - *e = mode - return nil -} - -func (e FilterMode) MarshalJSON() ([]byte, error) { - return json.Marshal(e.String()) -} - -func (e *FilterMode) UnmarshalJSON(data []byte) error { - var tp string - if err := json.Unmarshal(data, &tp); err != nil { - return err - } - mode, exist := FilterModeMapping[strings.ToLower(tp)] - if !exist { - return errors.New("invalid mode") - } - *e = mode - return nil -} - -func (e FilterMode) MarshalText() ([]byte, error) { - return []byte(e.String()), nil -} - -func (e *FilterMode) UnmarshalText(data []byte) error { - mode, exist := FilterModeMapping[strings.ToLower(string(data))] - if !exist { - return errors.New("invalid mode") - } - *e = mode - return nil -} - -type HTTPVersion string - -const ( - // HTTPVersion11 is HTTP/1.1. - HTTPVersion11 HTTPVersion = "http/1.1" - // HTTPVersion2 is HTTP/2. - HTTPVersion2 HTTPVersion = "h2" - // HTTPVersion3 is HTTP/3. - HTTPVersion3 HTTPVersion = "h3" -) diff --git a/constant/features/cmfa.go b/constant/features/cmfa.go deleted file mode 100644 index 9a81d82ffe..0000000000 --- a/constant/features/cmfa.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build cmfa - -package features - -const CMFA = true diff --git a/constant/features/cmfa_stub.go b/constant/features/cmfa_stub.go deleted file mode 100644 index f7ef082741..0000000000 --- a/constant/features/cmfa_stub.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !cmfa - -package features - -const CMFA = false diff --git a/constant/features/low_memory.go b/constant/features/low_memory.go deleted file mode 100644 index 2f0f4d506f..0000000000 --- a/constant/features/low_memory.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build with_low_memory - -package features - -const WithLowMemory = true diff --git a/constant/features/low_memory_stub.go b/constant/features/low_memory_stub.go deleted file mode 100644 index 00b0b94741..0000000000 --- a/constant/features/low_memory_stub.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !with_low_memory - -package features - -const WithLowMemory = false diff --git a/constant/features/no_fake_tcp.go b/constant/features/no_fake_tcp.go deleted file mode 100644 index a9e09728af..0000000000 --- a/constant/features/no_fake_tcp.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build no_fake_tcp - -package features - -const NoFakeTCP = true diff --git a/constant/features/no_fake_tcp_stub.go b/constant/features/no_fake_tcp_stub.go deleted file mode 100644 index f1620a2bc2..0000000000 --- a/constant/features/no_fake_tcp_stub.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !no_fake_tcp - -package features - -const NoFakeTCP = false diff --git a/constant/features/tags.go b/constant/features/tags.go deleted file mode 100644 index 12a5fbdd14..0000000000 --- a/constant/features/tags.go +++ /dev/null @@ -1,17 +0,0 @@ -package features - -func Tags() (tags []string) { - if CMFA { - tags = append(tags, "cmfa") - } - if WithLowMemory { - tags = append(tags, "with_low_memory") - } - if NoFakeTCP { - tags = append(tags, "no_fake_tcp") - } - if WithGVisor { - tags = append(tags, "with_gvisor") - } - return -} diff --git a/constant/features/version.go b/constant/features/version.go deleted file mode 100644 index 1710e218eb..0000000000 --- a/constant/features/version.go +++ /dev/null @@ -1,5 +0,0 @@ -package features - -var WindowsMajorVersion uint32 -var WindowsMinorVersion uint32 -var WindowsBuildNumber uint32 diff --git a/constant/features/version_windows.go b/constant/features/version_windows.go deleted file mode 100644 index 2f75618835..0000000000 --- a/constant/features/version_windows.go +++ /dev/null @@ -1,10 +0,0 @@ -package features - -import "golang.org/x/sys/windows" - -func init() { - version := windows.RtlGetVersion() - WindowsMajorVersion = version.MajorVersion - WindowsMinorVersion = version.MinorVersion - WindowsBuildNumber = version.BuildNumber -} diff --git a/constant/features/with_gvisor.go b/constant/features/with_gvisor.go deleted file mode 100644 index c00cc34e86..0000000000 --- a/constant/features/with_gvisor.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build with_gvisor - -package features - -const WithGVisor = true diff --git a/constant/features/with_gvisor_stub.go b/constant/features/with_gvisor_stub.go deleted file mode 100644 index 5684c1b16e..0000000000 --- a/constant/features/with_gvisor_stub.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !with_gvisor - -package features - -const WithGVisor = false diff --git a/constant/listener.go b/constant/listener.go deleted file mode 100644 index f69b4a9ba8..0000000000 --- a/constant/listener.go +++ /dev/null @@ -1,29 +0,0 @@ -package constant - -import "net" - -type Listener interface { - RawAddress() string - Address() string - Close() error -} - -type MultiAddrListener interface { - Close() error - Config() string - AddrList() (addrList []net.Addr) -} - -type InboundListener interface { - Name() string - Listen(tunnel Tunnel) error - Close() error - Address() string - RawAddress() string - Config() InboundConfig -} - -type InboundConfig interface { - Name() string - Equal(config InboundConfig) bool -} diff --git a/constant/matcher.go b/constant/matcher.go deleted file mode 100644 index 107f843d92..0000000000 --- a/constant/matcher.go +++ /dev/null @@ -1,11 +0,0 @@ -package constant - -import "net/netip" - -type DomainMatcher interface { - MatchDomain(domain string) bool -} - -type IpMatcher interface { - MatchIp(ip netip.Addr) bool -} diff --git a/constant/metadata.go b/constant/metadata.go deleted file mode 100644 index 3a8f4c792c..0000000000 --- a/constant/metadata.go +++ /dev/null @@ -1,345 +0,0 @@ -package constant - -import ( - "encoding/json" - "fmt" - "net" - "net/netip" - "strconv" -) - -// SOCKS address types as defined in RFC 1928 section 5. -const ( - AtypIPv4 AddrType = 1 - AtypDomainName AddrType = 3 - AtypIPv6 AddrType = 4 -) - -const ( - TCP NetWork = iota - UDP - ALLNet - InvalidNet = 0xff -) - -const ( - HTTP Type = iota - HTTPS - SOCKS4 - SOCKS5 - SHADOWSOCKS - VMESS - VLESS - REDIR - TPROXY - TROJAN - TUNNEL - TUN - TUIC - HYSTERIA2 - ANYTLS - INNER -) - -type AddrType byte - -func (a AddrType) String() string { - switch a { - case AtypIPv4: - return "IPv4" - case AtypDomainName: - return "DomainName" - case AtypIPv6: - return "IPv6" - default: - return "Unknown" - } -} - -type NetWork int - -func (n NetWork) String() string { - switch n { - case TCP: - return "tcp" - case UDP: - return "udp" - case ALLNet: - return "all" - default: - return "invalid" - } -} - -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 HTTPS: - return "HTTPS" - case SOCKS4: - return "Socks4" - case SOCKS5: - return "Socks5" - case SHADOWSOCKS: - return "ShadowSocks" - case VMESS: - return "Vmess" - case VLESS: - return "Vless" - case REDIR: - return "Redir" - case TPROXY: - return "TProxy" - case TROJAN: - return "Trojan" - case TUNNEL: - return "Tunnel" - case TUN: - return "Tun" - case TUIC: - return "Tuic" - case HYSTERIA2: - return "Hysteria2" - case ANYTLS: - return "AnyTLS" - case INNER: - return "Inner" - default: - return "Unknown" - } -} - -func ParseType(t string) (*Type, error) { - var res Type - switch t { - case "HTTP": - res = HTTP - case "HTTPS": - res = HTTPS - case "SOCKS4": - res = SOCKS4 - case "SOCKS5": - res = SOCKS5 - case "SHADOWSOCKS": - res = SHADOWSOCKS - case "VMESS": - res = VMESS - case "VLESS": - res = VLESS - case "REDIR": - res = REDIR - case "TPROXY": - res = TPROXY - case "TROJAN": - res = TROJAN - case "TUNNEL": - res = TUNNEL - case "TUN": - res = TUN - case "TUIC": - res = TUIC - case "HYSTERIA2": - res = HYSTERIA2 - case "ANYTLS": - res = ANYTLS - case "INNER": - res = INNER - default: - return nil, fmt.Errorf("unknown type: %s", t) - } - return &res, nil -} - -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 netip.Addr `json:"sourceIP"` - DstIP netip.Addr `json:"destinationIP"` - SrcGeoIP []string `json:"sourceGeoIP"` // can be nil if never queried, empty slice if got no result - DstGeoIP []string `json:"destinationGeoIP"` // can be nil if never queried, empty slice if got no result - SrcIPASN string `json:"sourceIPASN"` - DstIPASN string `json:"destinationIPASN"` - SrcPort uint16 `json:"sourcePort,string"` // `,string` is used to compatible with old version json output - DstPort uint16 `json:"destinationPort,string"` // `,string` is used to compatible with old version json output - InIP netip.Addr `json:"inboundIP"` - InPort uint16 `json:"inboundPort,string"` // `,string` is used to compatible with old version json output - InName string `json:"inboundName"` - InUser string `json:"inboundUser"` - Host string `json:"host"` - DNSMode DNSMode `json:"dnsMode"` - Uid uint32 `json:"uid"` - Process string `json:"process"` - ProcessPath string `json:"processPath"` - SpecialProxy string `json:"specialProxy"` - SpecialRules string `json:"specialRules"` - RemoteDst string `json:"remoteDestination"` - DSCP uint8 `json:"dscp"` - - RawSrcAddr net.Addr `json:"-"` - RawDstAddr net.Addr `json:"-"` - // Only domain rule - SniffHost string `json:"sniffHost"` -} - -func (m *Metadata) RemoteAddress() string { - return net.JoinHostPort(m.String(), strconv.FormatUint(uint64(m.DstPort), 10)) -} - -func (m *Metadata) SourceAddress() string { - return net.JoinHostPort(m.SrcIP.String(), strconv.FormatUint(uint64(m.SrcPort), 10)) -} - -func (m *Metadata) SourceAddrPort() netip.AddrPort { - return netip.AddrPortFrom(m.SrcIP.Unmap(), m.SrcPort) -} - -func (m *Metadata) SourceDetail() string { - if m.Type == INNER { - return fmt.Sprintf("%s", MihomoName) - } - - switch { - case m.Process != "" && m.Uid != 0: - return fmt.Sprintf("%s(%s, uid=%d)", m.SourceAddress(), m.Process, m.Uid) - case m.Uid != 0: - return fmt.Sprintf("%s(uid=%d)", m.SourceAddress(), m.Uid) - case m.Process != "": - return fmt.Sprintf("%s(%s)", m.SourceAddress(), m.Process) - default: - return fmt.Sprintf("%s", m.SourceAddress()) - } -} - -func (m *Metadata) SourceValid() bool { - return m.SrcPort != 0 && m.SrcIP.IsValid() -} - -func (m *Metadata) AddrType() AddrType { - switch true { - case m.Host != "" || !m.DstIP.IsValid(): - return AtypDomainName - case m.DstIP.Is4(): - return AtypIPv4 - default: - return AtypIPv6 - } -} - -func (m *Metadata) Resolved() bool { - return m.DstIP.IsValid() -} - -func (m *Metadata) RuleHost() string { - if len(m.SniffHost) == 0 { - return m.Host - } else { - return m.SniffHost - } -} - -// Pure is used to solve unexpected behavior -// when dialing proxy connection in DNSMapping mode. -func (m *Metadata) Pure() *Metadata { - if (m.DNSMode == DNSMapping || m.DNSMode == DNSHosts) && m.DstIP.IsValid() { - copyM := *m - copyM.Host = "" - return ©M - } - - return m -} - -func (m *Metadata) AddrPort() netip.AddrPort { - return netip.AddrPortFrom(m.DstIP.Unmap(), m.DstPort) -} - -func (m *Metadata) UDPAddr() *net.UDPAddr { - if m.NetWork != UDP || !m.DstIP.IsValid() { - return nil - } - return net.UDPAddrFromAddrPort(m.AddrPort()) -} - -func (m *Metadata) String() string { - if m.Host != "" { - return m.Host - } else if m.DstIP.IsValid() { - return m.DstIP.String() - } else { - return "" - } -} - -func (m *Metadata) Valid() bool { - return m.Host != "" || m.DstIP.IsValid() -} - -func (m *Metadata) SetRemoteAddr(addr net.Addr) error { - if addr == nil { - return nil - } - if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok { - if rawAddr := rawAddr.RawAddr(); rawAddr != nil { - if err := m.SetRemoteAddr(rawAddr); err == nil { - return nil - } - } - } - if addr, ok := addr.(interface{ AddrPort() netip.AddrPort }); ok { // *net.TCPAddr, *net.UDPAddr, M.Socksaddr - if addrPort := addr.AddrPort(); addrPort.Port() != 0 { - m.DstPort = addrPort.Port() - if addrPort.IsValid() { // sing's M.Socksaddr maybe return an invalid AddrPort if it's a DomainName - m.DstIP = addrPort.Addr().Unmap() - return nil - } else { - if addr, ok := addr.(interface{ AddrString() string }); ok { // must be sing's M.Socksaddr - m.Host = addr.AddrString() // actually is M.Socksaddr.Fqdn - return nil - } - } - } - } - return m.SetRemoteAddress(addr.String()) -} - -func (m *Metadata) SetRemoteAddress(rawAddress string) error { - host, port, err := net.SplitHostPort(rawAddress) - if err != nil { - return err - } - - var uint16Port uint16 - if port, err := strconv.ParseUint(port, 10, 16); err == nil { - uint16Port = uint16(port) - } - - if ip, err := netip.ParseAddr(host); err != nil { - m.Host = host - m.DstIP = netip.Addr{} - } else { - m.Host = "" - m.DstIP = ip.Unmap() - } - m.DstPort = uint16Port - - return nil -} - -func (m *Metadata) SwapSrcDst() { - m.SrcIP, m.DstIP = m.DstIP, m.SrcIP - m.SrcPort, m.DstPort = m.DstPort, m.SrcPort - m.SrcIPASN, m.DstIPASN = m.DstIPASN, m.SrcIPASN - m.SrcGeoIP, m.DstGeoIP = m.DstGeoIP, m.SrcGeoIP -} diff --git a/constant/path.go b/constant/path.go deleted file mode 100644 index a7f180b5c5..0000000000 --- a/constant/path.go +++ /dev/null @@ -1,222 +0,0 @@ -package constant - -import ( - "fmt" - "os" - P "path" - "path/filepath" - "strconv" - "strings" - - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/constant/features" -) - -const Name = "mihomo" - -var ( - GeositeName = "GeoSite.dat" - GeoipName = "GeoIP.dat" - ASNName = "ASN.mmdb" -) - -// Path is used to get the configuration path -// -// on Unix systems, `$HOME/.config/mihomo`. -// on Windows, `%USERPROFILE%/.config/mihomo`. -var Path = func() *path { - homeDir, err := os.UserHomeDir() - if err != nil { - homeDir, _ = os.Getwd() - } - allowUnsafePath, _ := strconv.ParseBool(os.Getenv("SKIP_SAFE_PATH_CHECK")) - homeDir = P.Join(homeDir, ".config", Name) - - if _, err = os.Stat(homeDir); err != nil { - if configHome, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok { - homeDir = P.Join(configHome, Name) - } - } - - var safePaths []string - for _, safePath := range filepath.SplitList(os.Getenv("SAFE_PATHS")) { - safePath = strings.TrimSpace(safePath) - if len(safePath) == 0 { - continue - } - safePaths = append(safePaths, safePath) - } - - return &path{homeDir: homeDir, configFile: "config.yaml", allowUnsafePath: allowUnsafePath, safePaths: safePaths} -}() - -type path struct { - homeDir string - configFile string - allowUnsafePath bool - safePaths []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 -} - -// IsSafePath return true if path is a subpath of homedir (or in the SAFE_PATHS environment variable) -func (p *path) IsSafePath(path string) bool { - if p.allowUnsafePath || features.CMFA { - return true - } - path = p.Resolve(path) - for _, safePath := range p.SafePaths() { - if rel, err := filepath.Rel(safePath, path); err == nil { - if filepath.IsLocal(rel) { - return true - } - } - } - return false -} - -func (p *path) SafePaths() []string { - return append([]string{p.homeDir}, p.safePaths...) // add homedir to safePaths -} - -func (p *path) ErrNotSafePath(path string) error { - return ErrNotSafePath{Path: path, SafePaths: p.SafePaths()} -} - -type ErrNotSafePath struct { - Path string - SafePaths []string -} - -func (e ErrNotSafePath) Error() string { - return fmt.Sprintf("path is not subpath of home directory or SAFE_PATHS: %s \n allowed paths: %s", e.Path, e.SafePaths) -} - -func (p *path) GetPathByHash(prefix, name string) string { - hash := utils.MakeHash([]byte(name)) - filename := hash.String() - return filepath.Join(p.HomeDir(), prefix, filename) -} - -func (p *path) MMDB() string { - files, err := os.ReadDir(p.homeDir) - if err != nil { - return "" - } - for _, fi := range files { - if fi.IsDir() { - // 目录则直接跳过 - continue - } else { - if strings.EqualFold(fi.Name(), "Country.mmdb") || - strings.EqualFold(fi.Name(), "geoip.db") || - strings.EqualFold(fi.Name(), "geoip.metadb") { - GeoipName = fi.Name() - return P.Join(p.homeDir, fi.Name()) - } - } - } - return P.Join(p.homeDir, "geoip.metadb") -} - -func (p *path) ASN() string { - files, err := os.ReadDir(p.homeDir) - if err != nil { - return "" - } - for _, fi := range files { - if fi.IsDir() { - // 目录则直接跳过 - continue - } else { - if strings.EqualFold(fi.Name(), "ASN.mmdb") { - ASNName = fi.Name() - return P.Join(p.homeDir, fi.Name()) - } - } - } - return P.Join(p.homeDir, ASNName) -} - -func (p *path) OldCache() string { - return P.Join(p.homeDir, ".cache") -} - -func (p *path) Cache() string { - return P.Join(p.homeDir, "cache.db") -} - -func (p *path) GeoIP() string { - files, err := os.ReadDir(p.homeDir) - if err != nil { - return "" - } - for _, fi := range files { - if fi.IsDir() { - // 目录则直接跳过 - continue - } else { - if strings.EqualFold(fi.Name(), "GeoIP.dat") { - GeoipName = fi.Name() - return P.Join(p.homeDir, fi.Name()) - } - } - } - return P.Join(p.homeDir, "GeoIP.dat") -} - -func (p *path) GeoSite() string { - files, err := os.ReadDir(p.homeDir) - if err != nil { - return "" - } - for _, fi := range files { - if fi.IsDir() { - // 目录则直接跳过 - continue - } else { - if strings.EqualFold(fi.Name(), "GeoSite.dat") { - GeositeName = fi.Name() - return P.Join(p.homeDir, fi.Name()) - } - } - } - return P.Join(p.homeDir, "GeoSite.dat") -} - -func (p *path) GetAssetLocation(file string) string { - return P.Join(p.homeDir, file) -} - -func (p *path) GetExecutableFullPath() string { - exePath, err := os.Executable() - if err != nil { - return "mihomo" - } - res, _ := filepath.EvalSymlinks(exePath) - return res -} diff --git a/constant/path_test.go b/constant/path_test.go deleted file mode 100644 index 82c6b2c563..0000000000 --- a/constant/path_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package constant - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestPath(t *testing.T) { - assert.False(t, (&path{}).IsSafePath("/usr/share/metacubexd/")) - assert.True(t, (&path{ - safePaths: []string{"/usr/share/metacubexd"}, - }).IsSafePath("/usr/share/metacubexd/")) - - assert.False(t, (&path{}).IsSafePath("../metacubexd/")) - assert.True(t, (&path{ - homeDir: "/usr/share/mihomo", - safePaths: []string{"/usr/share/metacubexd"}, - }).IsSafePath("../metacubexd/")) - assert.False(t, (&path{ - homeDir: "/usr/share/mihomo", - safePaths: []string{"/usr/share/ycad"}, - }).IsSafePath("../metacubexd/")) - - assert.False(t, (&path{}).IsSafePath("/opt/mykeys/key1.key")) - assert.True(t, (&path{ - safePaths: []string{"/opt/mykeys"}, - }).IsSafePath("/opt/mykeys/key1.key")) - assert.True(t, (&path{ - safePaths: []string{"/opt/mykeys/"}, - }).IsSafePath("/opt/mykeys/key1.key")) - assert.True(t, (&path{ - safePaths: []string{"/opt/mykeys/key1.key"}, - }).IsSafePath("/opt/mykeys/key1.key")) - - assert.True(t, (&path{}).IsSafePath("key1.key")) - assert.True(t, (&path{}).IsSafePath("./key1.key")) - assert.True(t, (&path{}).IsSafePath("./mykey/key1.key")) - assert.True(t, (&path{}).IsSafePath("./mykey/../key1.key")) - assert.False(t, (&path{}).IsSafePath("./mykey/../../key1.key")) - -} diff --git a/constant/provider/interface.go b/constant/provider/interface.go deleted file mode 100644 index 8c6ee6f8bd..0000000000 --- a/constant/provider/interface.go +++ /dev/null @@ -1,189 +0,0 @@ -package provider - -import ( - "context" - "fmt" - - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/constant" -) - -// Vehicle Type -const ( - File VehicleType = iota - HTTP - Compatible - Inline -) - -// VehicleType defined -type VehicleType int - -func (v VehicleType) String() string { - switch v { - case File: - return "File" - case HTTP: - return "HTTP" - case Compatible: - return "Compatible" - case Inline: - return "Inline" - default: - return "Unknown" - } -} - -type Vehicle interface { - Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) - Write(buf []byte) error - Path() string - Url() string - Proxy() 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 - Count() int - // 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() - Version() uint32 - RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) - HealthCheckURL() string -} - -// RuleProvider interface -type RuleProvider interface { - Provider - Behavior() RuleBehavior - Count() int - Match(*constant.Metadata) bool - ShouldResolveIP() bool - ShouldFindProcess() bool - Strategy() any -} - -// Rule Behavior -const ( - Domain RuleBehavior = iota - IPCIDR - Classical -) - -// RuleBehavior defined -type RuleBehavior int - -func (rt RuleBehavior) String() string { - switch rt { - case Domain: - return "Domain" - case IPCIDR: - return "IPCIDR" - case Classical: - return "Classical" - default: - return "Unknown" - } -} - -func (rt RuleBehavior) Byte() byte { - switch rt { - case Domain: - return 0 - case IPCIDR: - return 1 - case Classical: - return 2 - default: - return 255 - } -} - -func ParseBehavior(s string) (behavior RuleBehavior, err error) { - switch s { - case "domain": - behavior = Domain - case "ipcidr": - behavior = IPCIDR - case "classical": - behavior = Classical - default: - err = fmt.Errorf("unsupported behavior type: %s", s) - } - return -} - -const ( - YamlRule RuleFormat = iota - TextRule - MrsRule -) - -type RuleFormat int - -func (rf RuleFormat) String() string { - switch rf { - case YamlRule: - return "YamlRule" - case TextRule: - return "TextRule" - case MrsRule: - return "MrsRule" - default: - return "Unknown" - } -} - -func ParseRuleFormat(s string) (format RuleFormat, err error) { - switch s { - case "", "yaml": - format = YamlRule - case "text": - format = TextRule - case "mrs": - format = MrsRule - default: - err = fmt.Errorf("unsupported format type: %s", s) - } - return -} - -type Tunnel interface { - Providers() map[string]ProxyProvider - RuleProviders() map[string]RuleProvider - RuleUpdateCallback() *utils.Callback[RuleProvider] -} diff --git a/constant/rule.go b/constant/rule.go deleted file mode 100644 index 31702ddc33..0000000000 --- a/constant/rule.go +++ /dev/null @@ -1,125 +0,0 @@ -package constant - -// Rule Type -const ( - Domain RuleType = iota - DomainSuffix - DomainKeyword - DomainRegex - GEOSITE - GEOIP - SrcGEOIP - IPASN - SrcIPASN - IPCIDR - SrcIPCIDR - IPSuffix - SrcIPSuffix - SrcPort - DstPort - InPort - DSCP - InUser - InName - InType - ProcessName - ProcessPath - ProcessNameRegex - ProcessPathRegex - RuleSet - Network - Uid - SubRules - MATCH - AND - OR - NOT -) - -type RuleType int - -func (rt RuleType) String() string { - switch rt { - case Domain: - return "Domain" - case DomainSuffix: - return "DomainSuffix" - case DomainKeyword: - return "DomainKeyword" - case DomainRegex: - return "DomainRegex" - case GEOSITE: - return "GeoSite" - case GEOIP: - return "GeoIP" - case SrcGEOIP: - return "SrcGeoIP" - case IPASN: - return "IPASN" - case SrcIPASN: - return "SrcIPASN" - case IPCIDR: - return "IPCIDR" - case SrcIPCIDR: - return "SrcIPCIDR" - case IPSuffix: - return "IPSuffix" - case SrcIPSuffix: - return "SrcIPSuffix" - case SrcPort: - return "SrcPort" - case DstPort: - return "DstPort" - case InPort: - return "InPort" - case InUser: - return "InUser" - case InName: - return "InName" - case InType: - return "InType" - case ProcessName: - return "ProcessName" - case ProcessPath: - return "ProcessPath" - case ProcessNameRegex: - return "ProcessNameRegex" - case ProcessPathRegex: - return "ProcessPathRegex" - case MATCH: - return "Match" - case RuleSet: - return "RuleSet" - case Network: - return "Network" - case DSCP: - return "DSCP" - case Uid: - return "Uid" - case SubRules: - return "SubRules" - case AND: - return "AND" - case OR: - return "OR" - case NOT: - return "NOT" - default: - return "Unknown" - } -} - -type Rule interface { - RuleType() RuleType - Match(metadata *Metadata) (bool, string) - Adapter() string - Payload() string - ShouldResolveIP() bool - ShouldFindProcess() bool - ProviderNames() []string -} - -type RuleGroup interface { - Rule - GetRecodeSize() int -} diff --git a/constant/sniffer/sniffer.go b/constant/sniffer/sniffer.go deleted file mode 100644 index 8de4b89644..0000000000 --- a/constant/sniffer/sniffer.go +++ /dev/null @@ -1,40 +0,0 @@ -package sniffer - -import "github.com/metacubex/mihomo/constant" - -type Sniffer interface { - SupportNetwork() constant.NetWork - // SniffData must not change input bytes - SniffData(bytes []byte) (string, error) - Protocol() string - SupportPort(port uint16) bool -} - -type MultiPacketSniffer interface { - WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender -} - -const ( - TLS Type = iota - HTTP - QUIC -) - -var ( - List = []Type{TLS, HTTP, QUIC} -) - -type Type int - -func (rt Type) String() string { - switch rt { - case TLS: - return "TLS" - case HTTP: - return "HTTP" - case QUIC: - return "QUIC" - default: - return "Unknown" - } -} diff --git a/constant/tun.go b/constant/tun.go deleted file mode 100644 index 669f7a2e2b..0000000000 --- a/constant/tun.go +++ /dev/null @@ -1,85 +0,0 @@ -package constant - -import ( - "encoding/json" - "errors" - "strings" -) - -var StackTypeMapping = map[string]TUNStack{ - strings.ToLower(TunGvisor.String()): TunGvisor, - strings.ToLower(TunSystem.String()): TunSystem, - strings.ToLower(TunMixed.String()): TunMixed, -} - -const ( - TunGvisor TUNStack = iota - TunSystem - TunMixed -) - -type TUNStack int - -// UnmarshalYAML unserialize TUNStack with yaml -func (e *TUNStack) UnmarshalYAML(unmarshal func(any) error) error { - var tp string - if err := unmarshal(&tp); err != nil { - return err - } - mode, exist := StackTypeMapping[strings.ToLower(tp)] - if !exist { - return errors.New("invalid tun stack") - } - *e = mode - return nil -} - -// MarshalYAML serialize TUNStack with yaml -func (e TUNStack) MarshalYAML() (any, error) { - return e.String(), nil -} - -// UnmarshalJSON unserialize TUNStack with json -func (e *TUNStack) UnmarshalJSON(data []byte) error { - var tp string - json.Unmarshal(data, &tp) - mode, exist := StackTypeMapping[strings.ToLower(tp)] - if !exist { - return errors.New("invalid tun stack") - } - *e = mode - return nil -} - -// MarshalJSON serialize TUNStack with json -func (e TUNStack) MarshalJSON() ([]byte, error) { - return json.Marshal(e.String()) -} - -// UnmarshalText unserialize TUNStack -func (e *TUNStack) UnmarshalText(data []byte) error { - mode, exist := StackTypeMapping[strings.ToLower(string(data))] - if !exist { - return errors.New("invalid tun stack") - } - *e = mode - return nil -} - -// MarshalText serialize TUNStack with json -func (e TUNStack) MarshalText() ([]byte, error) { - return []byte(e.String()), nil -} - -func (e TUNStack) String() string { - switch e { - case TunGvisor: - return "gVisor" - case TunSystem: - return "System" - case TunMixed: - return "Mixed" - default: - return "unknown" - } -} diff --git a/constant/tunnel.go b/constant/tunnel.go deleted file mode 100644 index 7c9d08e235..0000000000 --- a/constant/tunnel.go +++ /dev/null @@ -1,12 +0,0 @@ -package constant - -import "net" - -type Tunnel interface { - // HandleTCPConn will handle a tcp connection blocking - HandleTCPConn(conn net.Conn, metadata *Metadata) - // HandleUDPPacket will handle a udp packet nonblocking - HandleUDPPacket(packet UDPPacket, metadata *Metadata) - // NatTable return nat table - NatTable() NatTable -} diff --git a/constant/version.go b/constant/version.go deleted file mode 100644 index c71024c28f..0000000000 --- a/constant/version.go +++ /dev/null @@ -1,8 +0,0 @@ -package constant - -var ( - Meta = true - Version = "1.10.0" - BuildTime = "unknown time" - MihomoName = "mihomo" -) diff --git a/context/conn.go b/context/conn.go deleted file mode 100644 index bae07c23d3..0000000000 --- a/context/conn.go +++ /dev/null @@ -1,40 +0,0 @@ -package context - -import ( - "github.com/metacubex/mihomo/common/utils" - "net" - - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - - "github.com/gofrs/uuid/v5" -) - -type ConnContext struct { - id uuid.UUID - metadata *C.Metadata - conn *N.BufferedConn -} - -func NewConnContext(conn net.Conn, metadata *C.Metadata) *ConnContext { - return &ConnContext{ - id: utils.NewUUIDV4(), - metadata: metadata, - conn: N.NewBufferedConn(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() *N.BufferedConn { - return c.conn -} diff --git a/context/dns.go b/context/dns.go deleted file mode 100644 index 1cc2067d8d..0000000000 --- a/context/dns.go +++ /dev/null @@ -1,47 +0,0 @@ -package context - -import ( - "context" - "github.com/metacubex/mihomo/common/utils" - - "github.com/gofrs/uuid/v5" - "github.com/miekg/dns" -) - -const ( - DNSTypeHost = "host" - DNSTypeFakeIP = "fakeip" - DNSTypeRaw = "raw" -) - -type DNSContext struct { - context.Context - - id uuid.UUID - msg *dns.Msg - tp string -} - -func NewDNSContext(ctx context.Context, msg *dns.Msg) *DNSContext { - return &DNSContext{ - Context: ctx, - - id: utils.NewUUIDV4(), - 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 deleted file mode 100644 index feab7666f5..0000000000 --- a/context/packetconn.go +++ /dev/null @@ -1,43 +0,0 @@ -package context - -import ( - "net" - - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - - "github.com/gofrs/uuid/v5" -) - -type PacketConnContext struct { - id uuid.UUID - metadata *C.Metadata - packetConn net.PacketConn -} - -func NewPacketConnContext(metadata *C.Metadata) *PacketConnContext { - return &PacketConnContext{ - id: utils.NewUUIDV4(), - 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 deleted file mode 100644 index abdad3a690..0000000000 --- a/dns/client.go +++ /dev/null @@ -1,107 +0,0 @@ -package dns - -import ( - "context" - "crypto/tls" - "fmt" - "net" - "strings" - - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/log" - - D "github.com/miekg/dns" -) - -type client struct { - *D.Client - port string - host string - dialer *dnsDialer - addr string -} - -var _ dnsClient = (*client)(nil) - -// Address implements dnsClient -func (c *client) Address() string { - if len(c.addr) != 0 { - return c.addr - } - schema := "udp" - if strings.HasPrefix(c.Client.Net, "tcp") { - schema = "tcp" - if strings.HasSuffix(c.Client.Net, "tls") { - schema = "tls" - } - } - - c.addr = fmt.Sprintf("%s://%s", schema, net.JoinHostPort(c.host, c.port)) - return c.addr -} - -func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { - network := "udp" - if strings.HasPrefix(c.Client.Net, "tcp") { - network = "tcp" - } - - addr := net.JoinHostPort(c.host, c.port) - conn, err := c.dialer.DialContext(ctx, network, addr) - if err != nil { - return nil, err - } - defer func() { - _ = 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, ca.GetGlobalTLSConfig(c.Client.TLSConfig)) - } - - dConn := &D.Conn{ - Conn: conn, - UDPSize: c.Client.UDPSize, - TsigSecret: c.Client.TsigSecret, - TsigProvider: c.Client.TsigProvider, - } - - msg, _, err := c.Client.ExchangeWithConn(m, dConn) - - // Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)! - if msg != nil && msg.Truncated && network == "udp" { - tcpClient := *c.Client // copy a client - tcpClient.Net = "tcp" - network = "tcp" - log.Debugln("[DNS] Truncated reply from %s:%s for %s over UDP, retrying over TCP", c.host, c.port, m.Question[0].String()) - dConn.Conn, err = c.dialer.DialContext(ctx, network, addr) - if err != nil { - ch <- result{msg, err} - return - } - defer func() { - _ = conn.Close() - }() - msg, _, err = tcpClient.ExchangeWithConn(m, dConn) - } - - ch <- result{msg, err} - }() - - select { - case <-ctx.Done(): - return nil, ctx.Err() - case ret := <-ch: - return ret.msg, ret.err - } -} - -func (c *client) ResetConnection() {} diff --git a/dns/dhcp.go b/dns/dhcp.go deleted file mode 100644 index fdd9b2f188..0000000000 --- a/dns/dhcp.go +++ /dev/null @@ -1,156 +0,0 @@ -package dns - -import ( - "context" - "net" - "net/netip" - "strings" - "sync" - "time" - - "github.com/metacubex/mihomo/component/dhcp" - "github.com/metacubex/mihomo/component/iface" - 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 netip.Prefix - done chan struct{} - clients []dnsClient - err error -} - -var _ dnsClient = (*dhcpClient)(nil) - -// Address implements dnsClient -func (d *dhcpClient) Address() string { - addrs := make([]string, 0) - for _, c := range d.clients { - addrs = append(addrs, c.Address()) - } - return strings.Join(addrs, ",") -} - -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 - } - - msg, _, err = batchExchange(ctx, clients, m) - return -} - -func (d *dhcpClient) ResetConnection() { - for _, client := range d.clients { - client.ResetConnection() - } -} - -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"), - ProxyName: 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(netip.Addr{}) - if err != nil { - return false, err - } - - if time.Now().Before(d.dnsInvalidate) && d.ifaceAddr == addr { - 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/dialer.go b/dns/dialer.go deleted file mode 100644 index f4d9e128f8..0000000000 --- a/dns/dialer.go +++ /dev/null @@ -1,11 +0,0 @@ -package dns - -// export functions from tunnel module - -import "github.com/metacubex/mihomo/tunnel" - -const RespectRules = tunnel.DnsRespectRules - -type dnsDialer = tunnel.DNSDialer - -var newDNSDialer = tunnel.NewDNSDialer diff --git a/dns/doh.go b/dns/doh.go deleted file mode 100644 index c6f7c67b5d..0000000000 --- a/dns/doh.go +++ /dev/null @@ -1,769 +0,0 @@ -package dns - -import ( - "context" - "crypto/tls" - "encoding/base64" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/netip" - "net/url" - "runtime" - "strconv" - "sync" - "time" - - "github.com/metacubex/mihomo/component/ca" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/quic-go" - "github.com/metacubex/quic-go/http3" - D "github.com/miekg/dns" - "golang.org/x/exp/slices" - "golang.org/x/net/http2" -) - -// Values to configure HTTP and HTTP/2 transport. -const ( - // transportDefaultReadIdleTimeout is the default timeout for pinging - // idle connections in HTTP/2 transport. - transportDefaultReadIdleTimeout = 30 * time.Second - - // transportDefaultIdleConnTimeout is the default timeout for idle - // connections in HTTP transport. - transportDefaultIdleConnTimeout = 5 * time.Minute - - // dohMaxConnsPerHost controls the maximum number of connections for - // each host. Note, that setting it to 1 may cause issues with Go's http - // implementation, see https://github.com/AdguardTeam/dnsproxy/issues/278. - dohMaxConnsPerHost = 2 - dialTimeout = 10 * time.Second - - // dohMaxIdleConns controls the maximum number of connections being idle - // at the same time. - dohMaxIdleConns = 2 - maxElapsedTime = time.Second * 30 -) - -var DefaultHTTPVersions = []C.HTTPVersion{C.HTTPVersion11, C.HTTPVersion2} - -// dnsOverHTTPS is a struct that implements the Upstream interface for the -// DNS-over-HTTPS protocol. -type dnsOverHTTPS struct { - // The Client's Transport typically has internal state (cached TCP - // connections), so Clients should be reused instead of created as - // needed. Clients are safe for concurrent use by multiple goroutines. - client *http.Client - clientMu sync.Mutex - - // quicConfig is the QUIC configuration that is used if HTTP/3 is enabled - // for this upstream. - quicConfig *quic.Config - quicConfigGuard sync.Mutex - - url *url.URL - httpVersions []C.HTTPVersion - dialer *dnsDialer - addr string - skipCertVerify bool - ecsPrefix netip.Prefix - ecsOverride bool -} - -// type check -var _ dnsClient = (*dnsOverHTTPS)(nil) - -// newDoH returns the DNS-over-HTTPS Upstream. -func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) dnsClient { - u, _ := url.Parse(urlString) - httpVersions := DefaultHTTPVersions - if preferH3 { - httpVersions = append(httpVersions, C.HTTPVersion3) - } - - if params["h3"] == "true" { - httpVersions = []C.HTTPVersion{C.HTTPVersion3} - } - - doh := &dnsOverHTTPS{ - url: u, - addr: u.String(), - dialer: newDNSDialer(r, proxyAdapter, proxyName), - quicConfig: &quic.Config{ - KeepAlivePeriod: QUICKeepAlivePeriod, - TokenStore: newQUICTokenStore(), - }, - httpVersions: httpVersions, - } - - if params["skip-cert-verify"] == "true" { - doh.skipCertVerify = true - } - - if ecs := params["ecs"]; ecs != "" { - prefix, err := netip.ParsePrefix(ecs) - if err != nil { - addr, err := netip.ParseAddr(ecs) - if err != nil { - log.Warnln("DOH [%s] config with invalid ecs: %s", doh.addr, ecs) - } else { - doh.ecsPrefix = netip.PrefixFrom(addr, addr.BitLen()) - } - } else { - doh.ecsPrefix = prefix - } - } - - if doh.ecsPrefix.IsValid() { - log.Debugln("DOH [%s] config with ecs: %s", doh.addr, doh.ecsPrefix) - } - - if params["ecs-override"] == "true" { - doh.ecsOverride = true - } - - runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close) - - return doh -} - -// Address implements the Upstream interface for *dnsOverHTTPS. -func (doh *dnsOverHTTPS) Address() string { - return doh.addr -} - -func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { - // Quote from https://www.rfc-editor.org/rfc/rfc8484.html: - // In order to maximize HTTP cache friendliness, DoH clients using media - // formats that include the ID field from the DNS message header, such - // as "application/dns-message", SHOULD use a DNS ID of 0 in every DNS - // request. - m = m.Copy() - id := m.Id - m.Id = 0 - defer func() { - // Restore the original ID to not break compatibility with proxies. - m.Id = id - if msg != nil { - msg.Id = id - } - }() - - if doh.ecsPrefix.IsValid() { - setEdns0Subnet(m, doh.ecsPrefix, doh.ecsOverride) - } - - // Check if there was already an active client before sending the request. - // We'll only attempt to re-connect if there was one. - client, isCached, err := doh.getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to init http client: %w", err) - } - - // Make the first attempt to send the DNS query. - msg, err = doh.exchangeHTTPS(ctx, client, m) - - // Make up to 2 attempts to re-create the HTTP client and send the request - // again. There are several cases (mostly, with QUIC) where this workaround - // is necessary to make HTTP client usable. We need to make 2 attempts in - // the case when the connection was closed (due to inactivity for example) - // AND the server refuses to open a 0-RTT connection. - for i := 0; isCached && doh.shouldRetry(err) && i < 2; i++ { - client, err = doh.resetClient(ctx, err) - if err != nil { - return nil, fmt.Errorf("failed to reset http client: %w", err) - } - - msg, err = doh.exchangeHTTPS(ctx, client, m) - } - - if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { - // If the request failed anyway, make sure we don't use this client. - _, resErr := doh.resetClient(ctx, err) - - return nil, fmt.Errorf("%w (resErr:%v)", err, resErr) - } - - return msg, err -} - -// Close implements the Upstream interface for *dnsOverHTTPS. -func (doh *dnsOverHTTPS) Close() (err error) { - doh.clientMu.Lock() - defer doh.clientMu.Unlock() - - runtime.SetFinalizer(doh, nil) - - if doh.client == nil { - return nil - } - - return doh.closeClient(doh.client) -} - -func (doh *dnsOverHTTPS) ResetConnection() { - doh.clientMu.Lock() - defer doh.clientMu.Unlock() - - if doh.client == nil { - return - } - - _ = doh.closeClient(doh.client) - doh.client = nil -} - -// closeClient cleans up resources used by client if necessary. -func (doh *dnsOverHTTPS) closeClient(client *http.Client) (err error) { - client.CloseIdleConnections() - - if isHTTP3(client) { // HTTP/3 may leak due to keep-alive connections. - return client.Transport.(io.Closer).Close() - } - - return nil -} - -// exchangeHTTPS sends the DNS query to a DoH resolver using the specified -// http.Client instance. -func (doh *dnsOverHTTPS) exchangeHTTPS(ctx context.Context, client *http.Client, req *D.Msg) (resp *D.Msg, err error) { - buf, err := req.Pack() - if err != nil { - return nil, fmt.Errorf("packing message: %w", err) - } - - // It appears, that GET requests are more memory-efficient with Golang - // implementation of HTTP/2. - method := http.MethodGet - if isHTTP3(client) { - // If we're using HTTP/3, use http3.MethodGet0RTT to force using 0-RTT. - method = http3.MethodGet0RTT - } - - requestUrl := *doh.url // don't modify origin url - requestUrl.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf)) - httpReq, err := http.NewRequestWithContext(ctx, method, requestUrl.String(), nil) - if err != nil { - return nil, fmt.Errorf("creating http request to %s: %w", doh.url, err) - } - - httpReq.Header.Set("Accept", "application/dns-message") - httpReq.Header.Set("User-Agent", "") - httpResp, err := client.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("requesting %s: %w", doh.url, err) - } - defer httpResp.Body.Close() - - body, err := io.ReadAll(httpResp.Body) - if err != nil { - return nil, fmt.Errorf("reading %s: %w", doh.url, err) - } - - if httpResp.StatusCode != http.StatusOK { - return nil, - fmt.Errorf( - "expected status %d, got %d from %s", - http.StatusOK, - httpResp.StatusCode, - doh.url, - ) - } - - resp = &D.Msg{} - err = resp.Unpack(body) - if err != nil { - return nil, fmt.Errorf( - "unpacking response from %s: body is %s: %w", - doh.url, - body, - err, - ) - } - - if resp.Id != req.Id { - err = D.ErrId - } - - return resp, err -} - -// shouldRetry checks what error we have received and returns true if we should -// re-create the HTTP client and retry the request. -func (doh *dnsOverHTTPS) shouldRetry(err error) (ok bool) { - if err == nil { - return false - } - - var netErr net.Error - if errors.As(err, &netErr) && netErr.Timeout() { - // If this is a timeout error, trying to forcibly re-create the HTTP - // client instance. This is an attempt to fix an issue with DoH client - // stalling after a network change. - // - // See https://github.com/AdguardTeam/AdGuardHome/issues/3217. - return true - } - - if isQUICRetryError(err) { - return true - } - - return false -} - -// resetClient triggers re-creation of the *http.Client that is used by this -// upstream. This method accepts the error that caused resetting client as -// depending on the error we may also reset the QUIC config. -func (doh *dnsOverHTTPS) resetClient(ctx context.Context, resetErr error) (client *http.Client, err error) { - doh.clientMu.Lock() - defer doh.clientMu.Unlock() - - if errors.Is(resetErr, quic.Err0RTTRejected) { - // Reset the TokenStore only if 0-RTT was rejected. - doh.resetQUICConfig() - } - - oldClient := doh.client - if oldClient != nil { - closeErr := doh.closeClient(oldClient) - if closeErr != nil { - log.Warnln("warning: failed to close the old http client: %v", closeErr) - } - } - - log.Debugln("re-creating the http client due to %v", resetErr) - doh.client, err = doh.createClient(ctx) - - return doh.client, err -} - -// getQUICConfig returns the QUIC config in a thread-safe manner. Note, that -// this method returns a pointer, it is forbidden to change its properties. -func (doh *dnsOverHTTPS) getQUICConfig() (c *quic.Config) { - doh.quicConfigGuard.Lock() - defer doh.quicConfigGuard.Unlock() - - return doh.quicConfig -} - -// resetQUICConfig Re-create the token store to make sure we're not trying to -// use invalid for 0-RTT. -func (doh *dnsOverHTTPS) resetQUICConfig() { - doh.quicConfigGuard.Lock() - defer doh.quicConfigGuard.Unlock() - - doh.quicConfig = doh.quicConfig.Clone() - doh.quicConfig.TokenStore = newQUICTokenStore() -} - -// getClient gets or lazily initializes an HTTP client (and transport) that will -// be used for this DoH resolver. -func (doh *dnsOverHTTPS) getClient(ctx context.Context) (c *http.Client, isCached bool, err error) { - startTime := time.Now() - - doh.clientMu.Lock() - defer doh.clientMu.Unlock() - if doh.client != nil { - return doh.client, true, nil - } - - // Timeout can be exceeded while waiting for the lock. This happens quite - // often on mobile devices. - elapsed := time.Since(startTime) - if elapsed > maxElapsedTime { - return nil, false, fmt.Errorf("timeout exceeded: %s", elapsed) - } - - log.Debugln("creating a new http client") - doh.client, err = doh.createClient(ctx) - - return doh.client, false, err -} - -// createClient creates a new *http.Client instance. The HTTP protocol version -// will depend on whether HTTP3 is allowed and provided by this upstream. Note, -// that we'll attempt to establish a QUIC connection when creating the client in -// order to check whether HTTP3 is supported. -func (doh *dnsOverHTTPS) createClient(ctx context.Context) (*http.Client, error) { - transport, err := doh.createTransport(ctx) - if err != nil { - return nil, fmt.Errorf("[%s] initializing http transport: %w", doh.url.String(), err) - } - - client := &http.Client{ - Transport: transport, - Timeout: DefaultTimeout, - Jar: nil, - } - - doh.client = client - - return doh.client, nil -} - -// createTransport initializes an HTTP transport that will be used specifically -// for this DoH resolver. This HTTP transport ensures that the HTTP requests -// will be sent exactly to the IP address got from the bootstrap resolver. Note, -// that this function will first attempt to establish a QUIC connection (if -// HTTP3 is enabled in the upstream options). If this attempt is successful, -// it returns an HTTP3 transport, otherwise it returns the H1/H2 transport. -func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripper, err error) { - transport := &http.Transport{ - DisableCompression: true, - DialContext: doh.dialer.DialContext, - IdleConnTimeout: transportDefaultIdleConnTimeout, - MaxConnsPerHost: dohMaxConnsPerHost, - MaxIdleConns: dohMaxIdleConns, - } - - if doh.url.Scheme == "http" { - return transport, nil - } - - tlsConfig := ca.GetGlobalTLSConfig( - &tls.Config{ - InsecureSkipVerify: doh.skipCertVerify, - MinVersion: tls.VersionTLS12, - SessionTicketsDisabled: false, - }) - var nextProtos []string - for _, v := range doh.httpVersions { - nextProtos = append(nextProtos, string(v)) - } - tlsConfig.NextProtos = nextProtos - transport.TLSClientConfig = tlsConfig - - if slices.Contains(doh.httpVersions, C.HTTPVersion3) { - // First, we attempt to create an HTTP3 transport. If the probe QUIC - // connection is established successfully, we'll be using HTTP3 for this - // upstream. - transportH3, err := doh.createTransportH3(ctx, tlsConfig) - if err == nil { - log.Debugln("[%s] using HTTP/3 for this upstream: QUIC was faster", doh.url.String()) - return transportH3, nil - } - } - - log.Debugln("[%s] using HTTP/2 for this upstream: %v", doh.url.String(), err) - - if !doh.supportsHTTP() { - return nil, errors.New("HTTP1/1 and HTTP2 are not supported by this upstream") - } - - // Since we have a custom DialContext, we need to use this field to - // make golang http.Client attempt to use HTTP/2. Otherwise, it would - // only be used when negotiated on the TLS level. - transport.ForceAttemptHTTP2 = true - - // Explicitly configure transport to use HTTP/2. - // - // See https://github.com/AdguardTeam/dnsproxy/issues/11. - var transportH2 *http2.Transport - transportH2, err = http2.ConfigureTransports(transport) - if err != nil { - return nil, err - } - - // Enable HTTP/2 pings on idle connections. - transportH2.ReadIdleTimeout = transportDefaultReadIdleTimeout - - return transport, nil -} - -// http3Transport is a wrapper over *http3.RoundTripper that tries to optimize -// its behavior. The main thing that it does is trying to force use a single -// connection to a host instead of creating a new one all the time. It also -// helps mitigate race issues with quic-go. -type http3Transport struct { - baseTransport *http3.RoundTripper - - closed bool - mu sync.RWMutex -} - -// type check -var _ http.RoundTripper = (*http3Transport)(nil) - -// RoundTrip implements the http.RoundTripper interface for *http3Transport. -func (h *http3Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - h.mu.RLock() - defer h.mu.RUnlock() - - if h.closed { - return nil, net.ErrClosed - } - - // Try to use cached connection to the target host if it's available. - resp, err = h.baseTransport.RoundTripOpt(req, http3.RoundTripOpt{OnlyCachedConn: true}) - - if errors.Is(err, http3.ErrNoCachedConn) { - // If there are no cached connection, trigger creating a new one. - resp, err = h.baseTransport.RoundTrip(req) - } - - return resp, err -} - -// type check -var _ io.Closer = (*http3Transport)(nil) - -// Close implements the io.Closer interface for *http3Transport. -func (h *http3Transport) Close() (err error) { - h.mu.Lock() - defer h.mu.Unlock() - - h.closed = true - - return h.baseTransport.Close() -} - -func (h *http3Transport) CloseIdleConnections() { - h.mu.RLock() - defer h.mu.RUnlock() - - h.baseTransport.CloseIdleConnections() -} - -// createTransportH3 tries to create an HTTP/3 transport for this upstream. -// We should be able to fall back to H1/H2 in case if HTTP/3 is unavailable or -// if it is too slow. In order to do that, this method will run two probes -// in parallel (one for TLS, the other one for QUIC) and if QUIC is faster it -// will create the *http3.RoundTripper instance. -func (doh *dnsOverHTTPS) createTransportH3( - ctx context.Context, - tlsConfig *tls.Config, -) (roundTripper http.RoundTripper, err error) { - if !doh.supportsH3() { - return nil, errors.New("HTTP3 support is not enabled") - } - - addr, err := doh.probeH3(ctx, tlsConfig) - if err != nil { - return nil, err - } - - rt := &http3.RoundTripper{ - Dial: func( - ctx context.Context, - - // Ignore the address and always connect to the one that we got - // from the bootstrapper. - _ string, - tlsCfg *tlsC.Config, - cfg *quic.Config, - ) (c quic.EarlyConnection, err error) { - return doh.dialQuic(ctx, addr, tlsCfg, cfg) - }, - DisableCompression: true, - TLSClientConfig: tlsC.UConfig(tlsConfig), - QUICConfig: doh.getQUICConfig(), - } - - return &http3Transport{baseTransport: rt}, nil -} - -func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tlsC.Config, cfg *quic.Config) (quic.EarlyConnection, error) { - ip, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - portInt, err := strconv.Atoi(port) - if err != nil { - return nil, err - } - udpAddr := net.UDPAddr{ - IP: net.ParseIP(ip), - Port: portInt, - } - conn, err := doh.dialer.ListenPacket(ctx, "udp", addr) - if err != nil { - return nil, err - } - transport := quic.Transport{Conn: conn} - transport.SetCreatedConn(true) // auto close conn - transport.SetSingleUse(true) // auto close transport - tlsCfg = tlsCfg.Clone() - if host, _, err := net.SplitHostPort(doh.url.Host); err == nil { - tlsCfg.ServerName = host - } else { - // It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port. - tlsCfg.ServerName = doh.url.Host - } - return transport.DialEarly(ctx, &udpAddr, tlsCfg, cfg) -} - -// probeH3 runs a test to check whether QUIC is faster than TLS for this -// upstream. If the test is successful it will return the address that we -// should use to establish the QUIC connections. -func (doh *dnsOverHTTPS) probeH3( - ctx context.Context, - tlsConfig *tls.Config, -) (addr string, err error) { - // We're using bootstrapped address instead of what's passed to the function - // it does not create an actual connection, but it helps us determine - // what IP is actually reachable (when there are v4/v6 addresses). - rawConn, err := doh.dialer.DialContext(ctx, "udp", doh.url.Host) - if err != nil { - return "", fmt.Errorf("failed to dial: %w", err) - } - addr = rawConn.RemoteAddr().String() - // It's never actually used. - _ = rawConn.Close() - - // Avoid spending time on probing if this upstream only supports HTTP/3. - if doh.supportsH3() && !doh.supportsHTTP() { - return addr, nil - } - - // Use a new *tls.Config with empty session cache for probe connections. - // Surprisingly, this is really important since otherwise it invalidates - // the existing cache. - // TODO(ameshkov): figure out why the sessions cache invalidates here. - probeTLSCfg := tlsConfig.Clone() - probeTLSCfg.ClientSessionCache = nil - - // Do not expose probe connections to the callbacks that are passed to - // the bootstrap options to avoid side-effects. - // TODO(ameshkov): consider exposing, somehow mark that this is a probe. - probeTLSCfg.VerifyPeerCertificate = nil - probeTLSCfg.VerifyConnection = nil - - // Run probeQUIC and probeTLS in parallel and see which one is faster. - chQuic := make(chan error, 1) - chTLS := make(chan error, 1) - go doh.probeQUIC(ctx, addr, tlsC.UConfig(probeTLSCfg), chQuic) - go doh.probeTLS(ctx, probeTLSCfg, chTLS) - - select { - case quicErr := <-chQuic: - if quicErr != nil { - // QUIC failed, return error since HTTP3 was not preferred. - return "", quicErr - } - - // Return immediately, QUIC was faster. - return addr, quicErr - case tlsErr := <-chTLS: - if tlsErr != nil { - // Return immediately, TLS failed. - log.Debugln("probing TLS: %v", tlsErr) - return addr, nil - } - - return "", errors.New("TLS was faster than QUIC, prefer it") - } -} - -// probeQUIC attempts to establish a QUIC connection to the specified address. -// We run probeQUIC and probeTLS in parallel and see which one is faster. -func (doh *dnsOverHTTPS) probeQUIC(ctx context.Context, addr string, tlsConfig *tlsC.Config, ch chan error) { - startTime := time.Now() - conn, err := doh.dialQuic(ctx, addr, tlsConfig, doh.getQUICConfig()) - if err != nil { - ch <- fmt.Errorf("opening QUIC connection to %s: %w", doh.Address(), err) - return - } - - // Ignore the error since there's no way we can use it for anything useful. - _ = conn.CloseWithError(QUICCodeNoError, "") - - ch <- nil - - elapsed := time.Now().Sub(startTime) - log.Debugln("elapsed on establishing a QUIC connection: %s", elapsed) -} - -// probeTLS attempts to establish a TLS connection to the specified address. We -// run probeQUIC and probeTLS in parallel and see which one is faster. -func (doh *dnsOverHTTPS) probeTLS(ctx context.Context, tlsConfig *tls.Config, ch chan error) { - startTime := time.Now() - - conn, err := doh.tlsDial(ctx, "tcp", tlsConfig) - if err != nil { - ch <- fmt.Errorf("opening TLS connection: %w", err) - return - } - - // Ignore the error since there's no way we can use it for anything useful. - _ = conn.Close() - - ch <- nil - - elapsed := time.Now().Sub(startTime) - log.Debugln("elapsed on establishing a TLS connection: %s", elapsed) -} - -// supportsH3 returns true if HTTP/3 is supported by this upstream. -func (doh *dnsOverHTTPS) supportsH3() (ok bool) { - for _, v := range doh.supportedHTTPVersions() { - if v == C.HTTPVersion3 { - return true - } - } - - return false -} - -// supportsHTTP returns true if HTTP/1.1 or HTTP2 is supported by this upstream. -func (doh *dnsOverHTTPS) supportsHTTP() (ok bool) { - for _, v := range doh.supportedHTTPVersions() { - if v == C.HTTPVersion11 || v == C.HTTPVersion2 { - return true - } - } - - return false -} - -// supportedHTTPVersions returns the list of supported HTTP versions. -func (doh *dnsOverHTTPS) supportedHTTPVersions() (v []C.HTTPVersion) { - v = doh.httpVersions - if v == nil { - v = DefaultHTTPVersions - } - - return v -} - -// isHTTP3 checks if the *http.Client is an HTTP/3 client. -func isHTTP3(client *http.Client) (ok bool) { - _, ok = client.Transport.(*http3Transport) - - return ok -} - -// tlsDial is basically the same as tls.DialWithDialer, but we will call our own -// dialContext function to get connection. -func (doh *dnsOverHTTPS) tlsDial(ctx context.Context, network string, config *tls.Config) (*tls.Conn, error) { - // We're using bootstrapped address instead of what's passed - // to the function. - rawConn, err := doh.dialer.DialContext(ctx, network, doh.url.Host) - if err != nil { - return nil, err - } - - // We want the timeout to cover the whole process: TCP connection and - // TLS handshake dialTimeout will be used as connection deadLine. - conn := tls.Client(rawConn, config) - - err = conn.SetDeadline(time.Now().Add(dialTimeout)) - if err != nil { - // Must not happen in normal circumstances. - log.Errorln("cannot set deadline: %v", err) - return nil, err - } - - err = conn.Handshake() - if err != nil { - defer conn.Close() - return nil, err - } - - return conn, nil -} diff --git a/dns/doq.go b/dns/doq.go deleted file mode 100644 index af0a0d4601..0000000000 --- a/dns/doq.go +++ /dev/null @@ -1,471 +0,0 @@ -package dns - -import ( - "context" - "crypto/tls" - "encoding/binary" - "errors" - "fmt" - "net" - "runtime" - "strconv" - "sync" - "time" - - "github.com/metacubex/mihomo/component/ca" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/quic-go" - D "github.com/miekg/dns" -) - -const NextProtoDQ = "doq" -const ( - // QUICCodeNoError is used when the connection or stream needs to be closed, - // but there is no error to signal. - QUICCodeNoError = quic.ApplicationErrorCode(0) - // QUICCodeInternalError signals that the DoQ implementation encountered - // an internal error and is incapable of pursuing the transaction or the - // connection. - QUICCodeInternalError = quic.ApplicationErrorCode(1) - // QUICKeepAlivePeriod is the value that we pass to *quic.Config and that - // controls the period with with keep-alive frames are being sent to the - // connection. We set it to 20s as it would be in the quic-go@v0.27.1 with - // KeepAlive field set to true This value is specified in - // https://pkg.go.dev/github.com/metacubex/quic-go/internal/protocol#MaxKeepAliveInterval. - // - // TODO(ameshkov): Consider making it configurable. - QUICKeepAlivePeriod = time.Second * 20 - DefaultTimeout = time.Second * 5 -) - -// dnsOverQUIC is a struct that implements the Upstream interface for the -// DNS-over-QUIC protocol (spec: https://www.rfc-editor.org/rfc/rfc9250.html). -type dnsOverQUIC struct { - // quicConfig is the QUIC configuration that is used for establishing - // connections to the upstream. This configuration includes the TokenStore - // that needs to be stored for the lifetime of dnsOverQUIC since we can - // re-create the connection. - quicConfig *quic.Config - quicConfigGuard sync.Mutex - - // conn is the current active QUIC connection. It can be closed and - // re-opened when needed. - conn quic.Connection - connMu sync.RWMutex - - // bytesPool is a *sync.Pool we use to store byte buffers in. These byte - // buffers are used to read responses from the upstream. - bytesPool *sync.Pool - bytesPoolGuard sync.Mutex - - addr string - dialer *dnsDialer -} - -// type check -var _ dnsClient = (*dnsOverQUIC)(nil) - -// newDoQ returns the DNS-over-QUIC Upstream. -func newDoQ(resolver *Resolver, addr string, proxyAdapter C.ProxyAdapter, proxyName string) (dnsClient, error) { - doq := &dnsOverQUIC{ - addr: addr, - dialer: newDNSDialer(resolver, proxyAdapter, proxyName), - quicConfig: &quic.Config{ - KeepAlivePeriod: QUICKeepAlivePeriod, - TokenStore: newQUICTokenStore(), - }, - } - - runtime.SetFinalizer(doq, (*dnsOverQUIC).Close) - return doq, nil -} - -// Address implements the Upstream interface for *dnsOverQUIC. -func (doq *dnsOverQUIC) Address() string { return doq.addr } - -func (doq *dnsOverQUIC) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { - // When sending queries over a QUIC connection, the DNS Message ID MUST be - // set to zero. - m = m.Copy() - id := m.Id - m.Id = 0 - defer func() { - // Restore the original ID to not break compatibility with proxies. - m.Id = id - if msg != nil { - msg.Id = id - } - }() - - // Check if there was already an active conn before sending the request. - // We'll only attempt to re-connect if there was one. - hasConnection := doq.hasConnection() - - // Make the first attempt to send the DNS query. - msg, err = doq.exchangeQUIC(ctx, m) - - // Make up to 2 attempts to re-open the QUIC connection and send the request - // again. There are several cases where this workaround is necessary to - // make DoQ usable. We need to make 2 attempts in the case when the - // connection was closed (due to inactivity for example) AND the server - // refuses to open a 0-RTT connection. - for i := 0; hasConnection && doq.shouldRetry(err) && i < 2; i++ { - log.Debugln("re-creating the QUIC connection and retrying due to %v", err) - - // Close the active connection to make sure we'll try to re-connect. - doq.closeConnWithError(err) - - // Retry sending the request. - msg, err = doq.exchangeQUIC(ctx, m) - } - - if err != nil { - // If we're unable to exchange messages, make sure the connection is - // closed and signal about an internal error. - doq.closeConnWithError(err) - } - - return msg, err -} - -// Close implements the Upstream interface for *dnsOverQUIC. -func (doq *dnsOverQUIC) Close() (err error) { - doq.connMu.Lock() - defer doq.connMu.Unlock() - - runtime.SetFinalizer(doq, nil) - - if doq.conn != nil { - err = doq.conn.CloseWithError(QUICCodeNoError, "") - } - - return err -} - -func (doq *dnsOverQUIC) ResetConnection() { - doq.closeConnWithError(nil) -} - -// exchangeQUIC attempts to open a QUIC connection, send the DNS message -// through it and return the response it got from the server. -func (doq *dnsOverQUIC) exchangeQUIC(ctx context.Context, msg *D.Msg) (resp *D.Msg, err error) { - var conn quic.Connection - conn, err = doq.getConnection(ctx, true) - if err != nil { - return nil, err - } - - var buf []byte - buf, err = msg.Pack() - if err != nil { - return nil, fmt.Errorf("failed to pack DNS message for DoQ: %w", err) - } - - var stream quic.Stream - stream, err = doq.openStream(ctx, conn) - if err != nil { - return nil, err - } - - _, err = stream.Write(AddPrefix(buf)) - if err != nil { - return nil, fmt.Errorf("failed to write to a QUIC stream: %w", err) - } - - // The client MUST send the DNS query over the selected stream, and MUST - // indicate through the STREAM FIN mechanism that no further data will - // be sent on that stream. Note, that stream.Close() closes the - // write-direction of the stream, but does not prevent reading from it. - _ = stream.Close() - - return doq.readMsg(stream) -} - -// AddPrefix adds a 2-byte prefix with the DNS message length. -func AddPrefix(b []byte) (m []byte) { - m = make([]byte, 2+len(b)) - binary.BigEndian.PutUint16(m, uint16(len(b))) - copy(m[2:], b) - - return m -} - -// shouldRetry checks what error we received and decides whether it is required -// to re-open the connection and retry sending the request. -func (doq *dnsOverQUIC) shouldRetry(err error) (ok bool) { - return isQUICRetryError(err) -} - -// getBytesPool returns (creates if needed) a pool we store byte buffers in. -func (doq *dnsOverQUIC) getBytesPool() (pool *sync.Pool) { - doq.bytesPoolGuard.Lock() - defer doq.bytesPoolGuard.Unlock() - - if doq.bytesPool == nil { - doq.bytesPool = &sync.Pool{ - New: func() interface{} { - b := make([]byte, MaxMsgSize) - - return &b - }, - } - } - - return doq.bytesPool -} - -// getConnection opens or returns an existing quic.Connection. useCached -// argument controls whether we should try to use the existing cached -// connection. If it is false, we will forcibly create a new connection and -// close the existing one if needed. -func (doq *dnsOverQUIC) getConnection(ctx context.Context, useCached bool) (quic.Connection, error) { - var conn quic.Connection - doq.connMu.RLock() - conn = doq.conn - if conn != nil && useCached { - doq.connMu.RUnlock() - - return conn, nil - } - if conn != nil { - // we're recreating the connection, let's create a new one. - _ = conn.CloseWithError(QUICCodeNoError, "") - } - doq.connMu.RUnlock() - - doq.connMu.Lock() - defer doq.connMu.Unlock() - - var err error - conn, err = doq.openConnection(ctx) - if err != nil { - return nil, err - } - doq.conn = conn - - return conn, nil -} - -// hasConnection returns true if there's an active QUIC connection. -func (doq *dnsOverQUIC) hasConnection() (ok bool) { - doq.connMu.Lock() - defer doq.connMu.Unlock() - - return doq.conn != nil -} - -// getQUICConfig returns the QUIC config in a thread-safe manner. Note, that -// this method returns a pointer, it is forbidden to change its properties. -func (doq *dnsOverQUIC) getQUICConfig() (c *quic.Config) { - doq.quicConfigGuard.Lock() - defer doq.quicConfigGuard.Unlock() - - return doq.quicConfig -} - -// resetQUICConfig re-creates the tokens store as we may need to use a new one -// if we failed to connect. -func (doq *dnsOverQUIC) resetQUICConfig() { - doq.quicConfigGuard.Lock() - defer doq.quicConfigGuard.Unlock() - - doq.quicConfig = doq.quicConfig.Clone() - doq.quicConfig.TokenStore = newQUICTokenStore() -} - -// openStream opens a new QUIC stream for the specified connection. -func (doq *dnsOverQUIC) openStream(ctx context.Context, conn quic.Connection) (quic.Stream, error) { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - stream, err := conn.OpenStreamSync(ctx) - if err == nil { - return stream, nil - } - - // We can get here if the old QUIC connection is not valid anymore. We - // should try to re-create the connection again in this case. - newConn, err := doq.getConnection(ctx, false) - if err != nil { - return nil, err - } - // Open a new stream. - return newConn.OpenStreamSync(ctx) -} - -// openConnection opens a new QUIC connection. -func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connection, err error) { - // we're using bootstrapped address instead of what's passed to the function - // it does not create an actual connection, but it helps us determine - // what IP is actually reachable (when there're v4/v6 addresses). - rawConn, err := doq.dialer.DialContext(ctx, "udp", doq.addr) - if err != nil { - return nil, fmt.Errorf("failed to open a QUIC connection: %w", err) - } - addr := rawConn.RemoteAddr().String() - // It's never actually used - _ = rawConn.Close() - - ip, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - - p, err := strconv.Atoi(port) - udpAddr := net.UDPAddr{IP: net.ParseIP(ip), Port: p} - udp, err := doq.dialer.ListenPacket(ctx, "udp", addr) - if err != nil { - return nil, err - } - - host, _, err := net.SplitHostPort(doq.addr) - if err != nil { - return nil, err - } - - tlsConfig := ca.GetGlobalTLSConfig( - &tls.Config{ - ServerName: host, - InsecureSkipVerify: false, - NextProtos: []string{ - NextProtoDQ, - }, - SessionTicketsDisabled: false, - }) - - transport := quic.Transport{Conn: udp} - transport.SetCreatedConn(true) // auto close conn - transport.SetSingleUse(true) // auto close transport - conn, err = transport.Dial(ctx, &udpAddr, tlsC.UConfig(tlsConfig), doq.getQUICConfig()) - if err != nil { - return nil, fmt.Errorf("opening quic connection to %s: %w", doq.addr, err) - } - - return conn, nil -} - -// closeConnWithError closes the active connection with error to make sure that -// new queries were processed in another connection. We can do that in the case -// of a fatal error. -func (doq *dnsOverQUIC) closeConnWithError(err error) { - doq.connMu.Lock() - defer doq.connMu.Unlock() - - if doq.conn == nil { - // Do nothing, there's no active conn anyways. - return - } - - code := QUICCodeNoError - if err != nil { - code = QUICCodeInternalError - } - - if errors.Is(err, quic.Err0RTTRejected) { - // Reset the TokenStore only if 0-RTT was rejected. - doq.resetQUICConfig() - } - - err = doq.conn.CloseWithError(code, "") - if err != nil { - log.Errorln("failed to close the conn: %v", err) - } - doq.conn = nil -} - -// readMsg reads the incoming DNS message from the QUIC stream. -func (doq *dnsOverQUIC) readMsg(stream quic.Stream) (m *D.Msg, err error) { - pool := doq.getBytesPool() - bufPtr := pool.Get().(*[]byte) - - defer pool.Put(bufPtr) - - respBuf := *bufPtr - n, err := stream.Read(respBuf) - if err != nil && n == 0 { - return nil, fmt.Errorf("reading response from %s: %w", doq.Address(), err) - } - - // All DNS messages (queries and responses) sent over DoQ connections MUST - // be encoded as a 2-octet length field followed by the message content as - // specified in [RFC1035]. - // IMPORTANT: Note, that we ignore this prefix here as this implementation - // does not support receiving multiple messages over a single connection. - m = new(D.Msg) - err = m.Unpack(respBuf[2:]) - if err != nil { - return nil, fmt.Errorf("unpacking response from %s: %w", doq.Address(), err) - } - - return m, nil -} - -// newQUICTokenStore creates a new quic.TokenStore that is necessary to have -// in order to benefit from 0-RTT. -func newQUICTokenStore() (s quic.TokenStore) { - // You can read more on address validation here: - // https://datatracker.ietf.org/doc/html/rfc9000#section-8.1 - // Setting maxOrigins to 1 and tokensPerOrigin to 10 assuming that this is - // more than enough for the way we use it (one connection per upstream). - return quic.NewLRUTokenStore(1, 10) -} - -// isQUICRetryError checks the error and determines whether it may signal that -// we should re-create the QUIC connection. This requirement is caused by -// quic-go issues, see the comments inside this function. -// TODO(ameshkov): re-test when updating quic-go. -func isQUICRetryError(err error) (ok bool) { - var qAppErr *quic.ApplicationError - if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 { - // This error is often returned when the server has been restarted, - // and we try to use the same connection on the client-side. It seems, - // that the old connections aren't closed immediately on the server-side - // and that's why one can run into this. - // In addition to that, quic-go HTTP3 client implementation does not - // clean up dead connections (this one is specific to DoH3 upstream): - // https://github.com/metacubex/quic-go/issues/765 - return true - } - - var qIdleErr *quic.IdleTimeoutError - if errors.As(err, &qIdleErr) { - // This error means that the connection was closed due to being idle. - // In this case we should forcibly re-create the QUIC connection. - // Reproducing is rather simple, stop the server and wait for 30 seconds - // then try to send another request via the same upstream. - return true - } - - var resetErr *quic.StatelessResetError - if errors.As(err, &resetErr) { - // A stateless reset is sent when a server receives a QUIC packet that - // it doesn't know how to decrypt. For instance, it may happen when - // the server was recently rebooted. We should reconnect and try again - // in this case. - return true - } - - var qTransportError *quic.TransportError - if errors.As(err, &qTransportError) && qTransportError.ErrorCode == quic.NoError { - // A transport error with the NO_ERROR error code could be sent by the - // server when it considers that it's time to close the connection. - // For example, Google DNS eventually closes an active connection with - // the NO_ERROR code and "Connection max age expired" message: - // https://github.com/AdguardTeam/dnsproxy/issues/283 - return true - } - - if errors.Is(err, quic.Err0RTTRejected) { - // This error happens when we try to establish a 0-RTT connection with - // a token the server is no more aware of. This can be reproduced by - // restarting the QUIC server (it will clear its tokens cache). The - // next connection attempt will return this error until the client's - // tokens cache is purged. - return true - } - - return false -} diff --git a/dns/edns0_subnet.go b/dns/edns0_subnet.go deleted file mode 100644 index 2ed4f140b5..0000000000 --- a/dns/edns0_subnet.go +++ /dev/null @@ -1,51 +0,0 @@ -package dns - -import ( - "net/netip" - - "github.com/miekg/dns" -) - -func setEdns0Subnet(message *dns.Msg, clientSubnet netip.Prefix, override bool) bool { - var ( - optRecord *dns.OPT - subnetOption *dns.EDNS0_SUBNET - ) -findExists: - for _, record := range message.Extra { - var isOPTRecord bool - if optRecord, isOPTRecord = record.(*dns.OPT); isOPTRecord { - for _, option := range optRecord.Option { - var isEDNS0Subnet bool - if subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET); isEDNS0Subnet { - if !override { - return false - } - break findExists - } - } - } - } - if optRecord == nil { - optRecord = &dns.OPT{ - Hdr: dns.RR_Header{ - Name: ".", - Rrtype: dns.TypeOPT, - }, - } - message.Extra = append(message.Extra, optRecord) - } - if subnetOption == nil { - subnetOption = new(dns.EDNS0_SUBNET) - optRecord.Option = append(optRecord.Option, subnetOption) - } - subnetOption.Code = dns.EDNS0SUBNET - if clientSubnet.Addr().Is4() { - subnetOption.Family = 1 - } else { - subnetOption.Family = 2 - } - subnetOption.SourceNetmask = uint8(clientSubnet.Bits()) - subnetOption.Address = clientSubnet.Addr().AsSlice() - return true -} diff --git a/dns/enhancer.go b/dns/enhancer.go deleted file mode 100644 index a8c0a5ac0d..0000000000 --- a/dns/enhancer.go +++ /dev/null @@ -1,120 +0,0 @@ -package dns - -import ( - "net/netip" - - "github.com/metacubex/mihomo/common/lru" - "github.com/metacubex/mihomo/component/fakeip" - C "github.com/metacubex/mihomo/constant" -) - -type ResolverEnhancer struct { - mode C.DNSMode - fakePool *fakeip.Pool - mapping *lru.LruCache[netip.Addr, string] -} - -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 netip.Addr) bool { - if !h.FakeIPEnabled() { - return false - } - - if pool := h.fakePool; pool != nil { - return pool.Exist(ip) - } - - return false -} - -func (h *ResolverEnhancer) IsFakeIP(ip netip.Addr) bool { - if !h.FakeIPEnabled() { - return false - } - - if pool := h.fakePool; pool != nil { - return pool.IPNet().Contains(ip) && ip != pool.Gateway() && ip != pool.Broadcast() - } - - return false -} - -func (h *ResolverEnhancer) IsFakeBroadcastIP(ip netip.Addr) bool { - if !h.FakeIPEnabled() { - return false - } - - if pool := h.fakePool; pool != nil { - return pool.Broadcast() == ip - } - - return false -} - -func (h *ResolverEnhancer) FindHostByIP(ip netip.Addr) (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); existed { - return host, true - } - } - - return "", false -} - -func (h *ResolverEnhancer) InsertHostByIP(ip netip.Addr, host string) { - if mapping := h.mapping; mapping != nil { - h.mapping.Set(ip, host) - } -} - -func (h *ResolverEnhancer) FlushFakeIP() error { - if h.fakePool != nil { - return h.fakePool.FlushFakeIP() - } - return nil -} - -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 (h *ResolverEnhancer) StoreFakePoolState() { - if h.fakePool != nil { - h.fakePool.StoreState() - } -} - -func NewEnhancer(cfg Config) *ResolverEnhancer { - var fakePool *fakeip.Pool - var mapping *lru.LruCache[netip.Addr, string] - - if cfg.EnhancedMode != C.DNSNormal { - fakePool = cfg.Pool - mapping = lru.New(lru.WithSize[netip.Addr, string](4096)) - } - - return &ResolverEnhancer{ - mode: cfg.EnhancedMode, - fakePool: fakePool, - mapping: mapping, - } -} diff --git a/dns/local.go b/dns/local.go deleted file mode 100644 index 37b5d41b04..0000000000 --- a/dns/local.go +++ /dev/null @@ -1,20 +0,0 @@ -package dns - -import ( - "context" - - D "github.com/miekg/dns" -) - -type LocalServer struct { - handler handler -} - -// ServeMsg implement resolver.LocalServer ResolveMsg -func (s *LocalServer) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { - return handlerWithContext(ctx, s.handler, msg) -} - -func NewLocalServer(resolver *Resolver, mapper *ResolverEnhancer) *LocalServer { - return &LocalServer{handler: NewHandler(resolver, mapper)} -} diff --git a/dns/middleware.go b/dns/middleware.go deleted file mode 100644 index e6461e91a3..0000000000 --- a/dns/middleware.go +++ /dev/null @@ -1,237 +0,0 @@ -package dns - -import ( - "net/netip" - "strings" - "time" - - "github.com/metacubex/mihomo/common/lru" - "github.com/metacubex/mihomo/component/fakeip" - R "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/context" - "github.com/metacubex/mihomo/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 R.Hosts, mapping *lru.LruCache[netip.Addr, string]) 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) - } - - host := strings.TrimRight(q.Name, ".") - handleCName := func(resp *D.Msg, domain string) { - rr := &D.CNAME{} - rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeCNAME, Class: D.ClassINET, Ttl: 10} - rr.Target = domain + "." - resp.Answer = append([]D.RR{rr}, resp.Answer...) - } - record, ok := hosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA) - if !ok { - if record != nil && record.IsDomain { - // replace request domain - newR := r.Copy() - newR.Question[0].Name = record.Domain + "." - resp, err := next(ctx, newR) - if err == nil { - resp.Id = r.Id - resp.Question = r.Question - handleCName(resp, record.Domain) - } - return resp, err - } - return next(ctx, r) - } - - msg := r.Copy() - handleIPs := func() { - for _, ipAddr := range record.IPs { - if ipAddr.Is4() && q.Qtype == D.TypeA { - rr := &D.A{} - rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: 10} - rr.A = ipAddr.AsSlice() - msg.Answer = append(msg.Answer, rr) - if mapping != nil { - mapping.SetWithExpire(ipAddr, host, time.Now().Add(time.Second*10)) - } - } else if q.Qtype == D.TypeAAAA { - rr := &D.AAAA{} - rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: 10} - ip := ipAddr.As16() - rr.AAAA = ip[:] - msg.Answer = append(msg.Answer, rr) - if mapping != nil { - mapping.SetWithExpire(ipAddr, host, time.Now().Add(time.Second*10)) - } - } - } - } - - switch q.Qtype { - case D.TypeA: - handleIPs() - case D.TypeAAAA: - handleIPs() - case D.TypeCNAME: - handleCName(r, record.Domain) - default: - 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 *lru.LruCache[netip.Addr, string]) 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 netip.Addr - var ttl uint32 - - switch a := ans.(type) { - case *D.A: - ip, _ = netip.AddrFromSlice(a.A) - ttl = a.Hdr.Ttl - case *D.AAAA: - ip, _ = netip.AddrFromSlice(a.AAAA) - ttl = a.Hdr.Ttl - default: - continue - } - if !ip.IsValid() { - continue - } - if !ip.IsGlobalUnicast() { - continue - } - ip = ip.Unmap() - - if ttl < 1 { - ttl = 1 - } - - mapping.SetWithExpire(ip, 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.AsSlice() - 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.ExchangeContext(ctx, 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(R.NewHosts(resolver.hosts), mapper.mapping)) - } - - if mapper.mode == C.DNSFakeIP { - middlewares = append(middlewares, withFakeIP(mapper.fakePool)) - } - - if mapper.mode != C.DNSNormal { - middlewares = append(middlewares, withMapping(mapper.mapping)) - } - - return compose(middlewares, withResolver(resolver)) -} diff --git a/dns/patch_android.go b/dns/patch_android.go deleted file mode 100644 index 8e744fcd81..0000000000 --- a/dns/patch_android.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build android && cmfa - -package dns - -import ( - "github.com/metacubex/mihomo/component/resolver" -) - -var systemResolver []dnsClient - -func FlushCacheWithDefaultResolver() { - if r := resolver.DefaultResolver; r != nil { - r.ClearCache() - } - if r := resolver.SystemResolver; r != nil { - r.ClearCache() - } - resolver.ResetConnection() -} - -func UpdateSystemDNS(addr []string) { - if len(addr) == 0 { - systemResolver = nil - } - - ns := make([]NameServer, 0, len(addr)) - for _, d := range addr { - ns = append(ns, NameServer{Addr: d}) - } - - systemResolver = transform(ns, nil) -} - -func (c *systemClient) getDnsClients() ([]dnsClient, error) { - return systemResolver, nil -} - -func (c *systemClient) ResetConnection() { - for _, r := range systemResolver { - r.ResetConnection() - } -} diff --git a/dns/policy.go b/dns/policy.go deleted file mode 100644 index 50dc17199b..0000000000 --- a/dns/policy.go +++ /dev/null @@ -1,34 +0,0 @@ -package dns - -import ( - "github.com/metacubex/mihomo/component/trie" - C "github.com/metacubex/mihomo/constant" -) - -type dnsPolicy interface { - Match(domain string) []dnsClient -} - -type domainTriePolicy struct { - *trie.DomainTrie[[]dnsClient] -} - -func (p domainTriePolicy) Match(domain string) []dnsClient { - record := p.DomainTrie.Search(domain) - if record != nil { - return record.Data() - } - return nil -} - -type domainMatcherPolicy struct { - matcher C.DomainMatcher - dnsClients []dnsClient -} - -func (p domainMatcherPolicy) Match(domain string) []dnsClient { - if p.matcher.MatchDomain(domain) { - return p.dnsClients - } - return nil -} diff --git a/dns/rcode.go b/dns/rcode.go deleted file mode 100644 index 901d1019d3..0000000000 --- a/dns/rcode.go +++ /dev/null @@ -1,52 +0,0 @@ -package dns - -import ( - "context" - "fmt" - - D "github.com/miekg/dns" -) - -func newRCodeClient(addr string) rcodeClient { - var rcode int - switch addr { - case "success": - rcode = D.RcodeSuccess - case "format_error": - rcode = D.RcodeFormatError - case "server_failure": - rcode = D.RcodeServerFailure - case "name_error": - rcode = D.RcodeNameError - case "not_implemented": - rcode = D.RcodeNotImplemented - case "refused": - rcode = D.RcodeRefused - default: - panic(fmt.Errorf("unsupported RCode type: %s", addr)) - } - - return rcodeClient{ - rcode: rcode, - addr: "rcode://" + addr, - } -} - -type rcodeClient struct { - rcode int - addr string -} - -var _ dnsClient = rcodeClient{} - -func (r rcodeClient) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { - m.Response = true - m.Rcode = r.rcode - return m, nil -} - -func (r rcodeClient) Address() string { - return r.addr -} - -func (r rcodeClient) ResetConnection() {} diff --git a/dns/resolver.go b/dns/resolver.go deleted file mode 100644 index 0dfeadd28d..0000000000 --- a/dns/resolver.go +++ /dev/null @@ -1,572 +0,0 @@ -package dns - -import ( - "context" - "errors" - "net/netip" - "time" - - "github.com/metacubex/mihomo/common/arc" - "github.com/metacubex/mihomo/common/lru" - "github.com/metacubex/mihomo/common/singleflight" - "github.com/metacubex/mihomo/component/fakeip" - "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/component/trie" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - D "github.com/miekg/dns" - "github.com/samber/lo" - "golang.org/x/exp/maps" -) - -type dnsClient interface { - ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) - Address() string - ResetConnection() -} - -type dnsCache interface { - GetWithExpire(key string) (*D.Msg, time.Time, bool) - SetWithExpire(key string, value *D.Msg, expire time.Time) - Clear() -} - -type result struct { - Msg *D.Msg - Error error -} - -type Resolver struct { - ipv6 bool - ipv6Timeout time.Duration - hosts *trie.DomainTrie[resolver.HostValue] - main []dnsClient - fallback []dnsClient - fallbackDomainFilters []C.DomainMatcher - fallbackIPFilters []C.IpMatcher - group singleflight.Group[*D.Msg] - cache dnsCache - policy []dnsPolicy - defaultResolver *Resolver -} - -func (r *Resolver) LookupIPPrimaryIPv4(ctx context.Context, host string) (ips []netip.Addr, err error) { - ch := make(chan []netip.Addr, 1) - go func() { - defer close(ch) - ip, err := r.lookupIP(ctx, host, D.TypeAAAA) - if err != nil { - return - } - ch <- ip - }() - - ips, err = r.lookupIP(ctx, host, D.TypeA) - if err == nil { - return - } - - ip, open := <-ch - if !open { - return nil, resolver.ErrIPNotFound - } - - return ip, nil -} - -func (r *Resolver) LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error) { - ch := make(chan []netip.Addr, 1) - go func() { - defer close(ch) - ip, err := r.lookupIP(ctx, host, D.TypeAAAA) - if err != nil { - return - } - - ch <- ip - }() - - ips, err = r.lookupIP(ctx, host, D.TypeA) - var waitIPv6 *time.Timer - if r != nil && r.ipv6Timeout > 0 { - waitIPv6 = time.NewTimer(r.ipv6Timeout) - } else { - waitIPv6 = time.NewTimer(100 * time.Millisecond) - } - defer waitIPv6.Stop() - select { - case ipv6s, open := <-ch: - if !open && err != nil { - return nil, resolver.ErrIPNotFound - } - ips = append(ips, ipv6s...) - case <-waitIPv6.C: - // wait ipv6 result - } - - return ips, nil -} - -// LookupIPv4 request with TypeA -func (r *Resolver) LookupIPv4(ctx context.Context, host string) ([]netip.Addr, error) { - return r.lookupIP(ctx, host, D.TypeA) -} - -// LookupIPv6 request with TypeAAAA -func (r *Resolver) LookupIPv6(ctx context.Context, host string) ([]netip.Addr, error) { - return r.lookupIP(ctx, host, D.TypeAAAA) -} - -func (r *Resolver) shouldIPFallback(ip netip.Addr) bool { - for _, filter := range r.fallbackIPFilters { - if filter.MatchIp(ip) { - return true - } - } - return false -} - -// 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") - } - continueFetch := false - defer func() { - if continueFetch || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - go func() { - ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) - defer cancel() - _, _ = r.exchangeWithoutCache(ctx, m) // ignore result, just for putMsgToCache - }() - } - }() - - q := m.Question[0] - domain := msgToDomain(m) - _, qTypeStr := msgToQtype(m) - cacheM, expireTime, hit := r.cache.GetWithExpire(q.String()) - if hit { - ips := msgToIP(cacheM) - log.Debugln("[DNS] cache hit %s --> %s %s, expire at %s", domain, ips, qTypeStr, expireTime.Format("2006-01-02 15:04:05")) - now := time.Now() - msg = cacheM.Copy() - if expireTime.Before(now) { - setMsgTTL(msg, uint32(1)) // Continue fetch - continueFetch = true - } 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] - - retryNum := 0 - retryMax := 3 - fn := func() (result *D.Msg, err error) { - ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) // reset timeout in singleflight - defer cancel() - cache := false - - defer func() { - if err != nil { - result = &D.Msg{} - result.Opcode = retryNum - retryNum++ - return - } - - msg := result - - if cache { - // OPT RRs MUST NOT be cached, forwarded, or stored in or loaded from master files. - msg.Extra = lo.Filter(msg.Extra, func(rr D.RR, index int) bool { - return rr.Header().Rrtype != D.TypeOPT - }) - putMsgToCache(r.cache, q.String(), q, msg) - } - }() - - isIPReq := isIPRequest(q) - if isIPReq { - cache = true - return r.ipExchange(ctx, m) - } - - if matched := r.matchPolicy(m); len(matched) != 0 { - result, cache, err = batchExchange(ctx, matched, m) - return - } - result, cache, err = batchExchange(ctx, r.main, m) - return - } - - ch := r.group.DoChan(q.String(), fn) - - var result singleflight.Result[*D.Msg] - - select { - case result = <-ch: - break - case <-ctx.Done(): - select { - case result = <-ch: // maybe ctxDone and chFinish in same time, get DoChan's result as much as possible - break - default: - go func() { // start a retrying monitor in background - result := <-ch - ret, err, shared := result.Val, result.Err, result.Shared - if err != nil && !shared && ret.Opcode < retryMax { // retry - r.group.DoChan(q.String(), fn) - } - }() - return nil, ctx.Err() - } - } - - ret, err, shared := result.Val, result.Err, result.Shared - if err != nil && !shared && ret.Opcode < retryMax { // retry - r.group.DoChan(q.String(), fn) - } - - if err == nil { - msg = ret - if shared { - msg = msg.Copy() - } - } - - return -} - -func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient { - if r.policy == nil { - return nil - } - - domain := msgToDomain(m) - if domain == "" { - return nil - } - - for _, policy := range r.policy { - if dnsClients := policy.Match(domain); len(dnsClients) > 0 { - return dnsClients - } - } - return nil -} - -func (r *Resolver) shouldOnlyQueryFallback(m *D.Msg) bool { - if r.fallback == nil || len(r.fallbackDomainFilters) == 0 { - return false - } - - domain := msgToDomain(m) - - if domain == "" { - return false - } - - for _, df := range r.fallbackDomainFilters { - if df.MatchDomain(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 || len(r.fallback) == 0 { // directly return if no fallback servers are available - res := <-msgCh - msg, err = res.Msg, res.Error - return - } - - res := <-msgCh - if res.Error == nil { - if ips := msgToIP(res.Msg); len(ips) != 0 { - shouldNotFallback := lo.EveryBy(ips, func(ip netip.Addr) bool { - return !r.shouldIPFallback(ip) - }) - if shouldNotFallback { - msg, err = res.Msg, res.Error // no need to wait for fallback result - return - } - } - } - - res = <-r.asyncExchange(ctx, r.fallback, m) - msg, err = res.Msg, res.Error - return -} - -func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (ips []netip.Addr, err error) { - ip, err := netip.ParseAddr(host) - if err == nil { - isIPv4 := ip.Is4() || ip.Is4In6() - if dnsType == D.TypeAAAA && !isIPv4 { - return []netip.Addr{ip}, nil - } else if dnsType == D.TypeA && isIPv4 { - return []netip.Addr{ip}, nil - } else { - return []netip.Addr{}, resolver.ErrIPVersion - } - } - - query := &D.Msg{} - query.SetQuestion(D.Fqdn(host), dnsType) - - msg, err := r.ExchangeContext(ctx, query) - if err != nil { - return []netip.Addr{}, err - } - - ips = msgToIP(msg) - ipLength := len(ips) - if ipLength == 0 { - return []netip.Addr{}, resolver.ErrIPNotFound - } - - return -} - -func (r *Resolver) asyncExchange(ctx context.Context, client []dnsClient, msg *D.Msg) <-chan *result { - ch := make(chan *result, 1) - go func() { - res, _, err := batchExchange(ctx, client, msg) - ch <- &result{Msg: res, Error: err} - }() - return ch -} - -// Invalid return this resolver can or can't be used -func (r *Resolver) Invalid() bool { - if r == nil { - return false - } - return len(r.main) > 0 -} - -func (r *Resolver) ClearCache() { - if r != nil && r.cache != nil { - r.cache.Clear() - } -} - -func (r *Resolver) ResetConnection() { - if r != nil { - for _, c := range r.main { - c.ResetConnection() - } - for _, c := range r.fallback { - c.ResetConnection() - } - if dr := r.defaultResolver; dr != nil { - dr.ResetConnection() - } - } -} - -type NameServer struct { - Net string - Addr string - ProxyAdapter C.ProxyAdapter - ProxyName string - Params map[string]string - PreferH3 bool -} - -func (ns NameServer) Equal(ns2 NameServer) bool { - defer func() { - // C.ProxyAdapter compare maybe panic, just ignore - recover() - }() - if ns.Net == ns2.Net && - ns.Addr == ns2.Addr && - ns.ProxyAdapter == ns2.ProxyAdapter && - ns.ProxyName == ns2.ProxyName && - maps.Equal(ns.Params, ns2.Params) && - ns.PreferH3 == ns2.PreferH3 { - return true - } - return false -} - -type Policy struct { - Domain string - Matcher C.DomainMatcher - NameServers []NameServer -} - -type Config struct { - Main, Fallback []NameServer - Default []NameServer - ProxyServer []NameServer - DirectServer []NameServer - DirectFollowPolicy bool - IPv6 bool - IPv6Timeout uint - EnhancedMode C.DNSMode - FallbackIPFilter []C.IpMatcher - FallbackDomainFilter []C.DomainMatcher - Pool *fakeip.Pool - Hosts *trie.DomainTrie[resolver.HostValue] - Policy []Policy - CacheAlgorithm string -} - -func (config Config) newCache() dnsCache { - if config.CacheAlgorithm == "" || config.CacheAlgorithm == "lru" { - return lru.New(lru.WithSize[string, *D.Msg](4096), lru.WithStale[string, *D.Msg](true)) - } else { - return arc.New(arc.WithSize[string, *D.Msg](4096)) - } -} - -type Resolvers struct { - *Resolver - ProxyResolver *Resolver - DirectResolver *Resolver -} - -func (rs Resolvers) ClearCache() { - rs.Resolver.ClearCache() - rs.ProxyResolver.ClearCache() - rs.DirectResolver.ClearCache() -} - -func (rs Resolvers) ResetConnection() { - rs.Resolver.ResetConnection() - rs.ProxyResolver.ResetConnection() - rs.DirectResolver.ResetConnection() -} - -func NewResolver(config Config) (rs Resolvers) { - defaultResolver := &Resolver{ - main: transform(config.Default, nil), - cache: config.newCache(), - ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, - } - - var nameServerCache []struct { - NameServer - dnsClient - } - cacheTransform := func(nameserver []NameServer) (result []dnsClient) { - LOOP: - for _, ns := range nameserver { - for _, nsc := range nameServerCache { - if nsc.NameServer.Equal(ns) { - result = append(result, nsc.dnsClient) - continue LOOP - } - } - // not in cache - dc := transform([]NameServer{ns}, defaultResolver) - if len(dc) > 0 { - dc := dc[0] - nameServerCache = append(nameServerCache, struct { - NameServer - dnsClient - }{NameServer: ns, dnsClient: dc}) - result = append(result, dc) - } - } - return - } - - r := &Resolver{ - ipv6: config.IPv6, - main: cacheTransform(config.Main), - cache: config.newCache(), - hosts: config.Hosts, - ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, - } - r.defaultResolver = defaultResolver - rs.Resolver = r - - if len(config.ProxyServer) != 0 { - rs.ProxyResolver = &Resolver{ - ipv6: config.IPv6, - main: cacheTransform(config.ProxyServer), - cache: config.newCache(), - hosts: config.Hosts, - ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, - } - } - - if len(config.DirectServer) != 0 { - rs.DirectResolver = &Resolver{ - ipv6: config.IPv6, - main: cacheTransform(config.DirectServer), - cache: config.newCache(), - hosts: config.Hosts, - ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, - } - } - - if len(config.Fallback) != 0 { - r.fallback = cacheTransform(config.Fallback) - r.fallbackIPFilters = config.FallbackIPFilter - r.fallbackDomainFilters = config.FallbackDomainFilter - } - - if len(config.Policy) != 0 { - r.policy = make([]dnsPolicy, 0) - - var triePolicy *trie.DomainTrie[[]dnsClient] - insertPolicy := func(policy dnsPolicy) { - if triePolicy != nil { - triePolicy.Optimize() - r.policy = append(r.policy, domainTriePolicy{triePolicy}) - triePolicy = nil - } - if policy != nil { - r.policy = append(r.policy, policy) - } - } - - for _, policy := range config.Policy { - if policy.Matcher != nil { - insertPolicy(domainMatcherPolicy{matcher: policy.Matcher, dnsClients: cacheTransform(policy.NameServers)}) - } else { - if triePolicy == nil { - triePolicy = trie.New[[]dnsClient]() - } - _ = triePolicy.Insert(policy.Domain, cacheTransform(policy.NameServers)) - } - } - insertPolicy(nil) - - if rs.DirectResolver != nil && config.DirectFollowPolicy { - rs.DirectResolver.policy = r.policy - } - } - - return -} - -var ParseNameServer func(servers []string) ([]NameServer, error) // define in config/config.go diff --git a/dns/server.go b/dns/server.go deleted file mode 100644 index caf1c2891a..0000000000 --- a/dns/server.go +++ /dev/null @@ -1,121 +0,0 @@ -package dns - -import ( - stdContext "context" - "errors" - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/sockopt" - "github.com/metacubex/mihomo/context" - "github.com/metacubex/mihomo/log" - - D "github.com/miekg/dns" -) - -var ( - address string - server = &Server{} - - dnsDefaultTTL uint32 = 600 -) - -type Server struct { - handler handler - tcpServer *D.Server - udpServer *D.Server -} - -// ServeDNS implement D.Handler ServeDNS -func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) { - msg, err := handlerWithContext(stdContext.Background(), s.handler, r) - if err != nil { - D.HandleFailed(w, r) - return - } - msg.Compress = true - w.WriteMsg(msg) -} - -func handlerWithContext(stdCtx stdContext.Context, 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(stdCtx, 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.tcpServer != nil { - _ = server.tcpServer.Shutdown() - server.tcpServer = nil - } - - if server.udpServer != nil { - _ = server.udpServer.Shutdown() - server.udpServer = nil - } - - server.handler = nil - 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 - } - - address = addr - handler := NewHandler(resolver, mapper) - server = &Server{handler: handler} - - go func() { - p, err := inbound.ListenPacket("udp", addr) - if err != nil { - log.Errorln("Start DNS server(UDP) error: %s", err.Error()) - return - } - - if err := sockopt.UDPReuseaddr(p); err != nil { - log.Warnln("Failed to Reuse UDP Address: %s", err) - } - - log.Infoln("DNS server(UDP) listening at: %s", p.LocalAddr().String()) - server.udpServer = &D.Server{Addr: addr, PacketConn: p, Handler: server} - _ = server.udpServer.ActivateAndServe() - }() - - go func() { - l, err := inbound.Listen("tcp", addr) - if err != nil { - log.Errorln("Start DNS server(TCP) error: %s", err.Error()) - return - } - - log.Infoln("DNS server(TCP) listening at: %s", l.Addr().String()) - server.tcpServer = &D.Server{Addr: addr, Listener: l, Handler: server} - _ = server.tcpServer.ActivateAndServe() - }() - -} diff --git a/dns/system.go b/dns/system.go deleted file mode 100644 index ab6c010028..0000000000 --- a/dns/system.go +++ /dev/null @@ -1,74 +0,0 @@ -package dns - -import ( - "context" - "fmt" - "strings" - "sync" - "time" - - "github.com/metacubex/mihomo/component/resolver" - - D "github.com/miekg/dns" -) - -const ( - SystemDnsFlushTime = 5 * time.Minute - SystemDnsDeleteTimes = 12 // 12*5 = 60min -) - -type systemDnsClient struct { - disableTimes uint32 - dnsClient -} - -type systemClient struct { - mu sync.Mutex - dnsClients map[string]*systemDnsClient - lastFlush time.Time - defaultNS []dnsClient -} - -func (c *systemClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { - dnsClients, err := c.getDnsClients() - if len(dnsClients) == 0 && len(c.defaultNS) > 0 { - dnsClients = c.defaultNS - err = nil - } - if err != nil { - return - } - msg, _, err = batchExchange(ctx, dnsClients, m) - return -} - -// Address implements dnsClient -func (c *systemClient) Address() string { - dnsClients, _ := c.getDnsClients() - isDefault := "" - if len(dnsClients) == 0 && len(c.defaultNS) > 0 { - dnsClients = c.defaultNS - isDefault = "[defaultNS]" - } - addrs := make([]string, 0, len(dnsClients)) - for _, c := range dnsClients { - addrs = append(addrs, c.Address()) - } - return fmt.Sprintf("system%s(%s)", isDefault, strings.Join(addrs, ",")) -} - -var _ dnsClient = (*systemClient)(nil) - -func newSystemClient() *systemClient { - return &systemClient{ - dnsClients: map[string]*systemDnsClient{}, - } -} - -func init() { - r := NewResolver(Config{}) - c := newSystemClient() - c.defaultNS = transform([]NameServer{{Addr: "114.114.114.114:53"}, {Addr: "8.8.8.8:53"}}, nil) - r.main = []dnsClient{c} - resolver.SystemResolver = r -} diff --git a/dns/system_common.go b/dns/system_common.go deleted file mode 100644 index e6dabdcfff..0000000000 --- a/dns/system_common.go +++ /dev/null @@ -1,73 +0,0 @@ -//go:build !(android && cmfa) - -package dns - -import ( - "net" - "time" - - "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/log" - - "golang.org/x/exp/slices" -) - -func (c *systemClient) getDnsClients() ([]dnsClient, error) { - c.mu.Lock() - defer c.mu.Unlock() - var err error - if time.Since(c.lastFlush) > SystemDnsFlushTime { - var nameservers []string - if nameservers, err = dnsReadConfig(); err == nil { - log.Debugln("[DNS] system dns update to %s", nameservers) - for _, addr := range nameservers { - if resolver.IsSystemDnsBlacklisted(addr) { - continue - } - if _, ok := c.dnsClients[addr]; !ok { - clients := transform( - []NameServer{{ - Addr: net.JoinHostPort(addr, "53"), - Net: "udp", - }}, - nil, - ) - if len(clients) > 0 { - c.dnsClients[addr] = &systemDnsClient{ - disableTimes: 0, - dnsClient: clients[0], - } - } - } - } - available := 0 - for nameserver, sdc := range c.dnsClients { - if slices.Contains(nameservers, nameserver) { - sdc.disableTimes = 0 // enable - available++ - } else { - if sdc.disableTimes > SystemDnsDeleteTimes { - delete(c.dnsClients, nameserver) // drop too old dnsClient - } else { - sdc.disableTimes++ - } - } - } - if available > 0 { - c.lastFlush = time.Now() - } - } - } - dnsClients := make([]dnsClient, 0, len(c.dnsClients)) - for _, sdc := range c.dnsClients { - if sdc.disableTimes == 0 { - dnsClients = append(dnsClients, sdc.dnsClient) - } - } - if len(dnsClients) > 0 { - return dnsClients, nil - } - return nil, err -} - -func (c *systemClient) ResetConnection() {} diff --git a/dns/system_posix.go b/dns/system_posix.go deleted file mode 100644 index 4d07d4ec12..0000000000 --- a/dns/system_posix.go +++ /dev/null @@ -1,43 +0,0 @@ -//go:build !windows - -package dns - -import ( - "bufio" - "fmt" - "net/netip" - "os" - "strings" -) - -const resolvConf = "/etc/resolv.conf" - -func dnsReadConfig() (servers []string, err error) { - file, err := os.Open(resolvConf) - if err != nil { - err = fmt.Errorf("failed to read %s: %w", resolvConf, err) - return - } - defer func() { _ = file.Close() }() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if len(line) > 0 && (line[0] == ';' || line[0] == '#') { - // comment. - continue - } - f := strings.Fields(line) - if len(f) < 1 { - continue - } - switch f[0] { - case "nameserver": // add one name server - if len(f) > 1 { - if addr, err := netip.ParseAddr(f[1]); err == nil { - servers = append(servers, addr.String()) - } - } - } - } - return -} diff --git a/dns/system_windows.go b/dns/system_windows.go deleted file mode 100644 index 6e5fe56eb8..0000000000 --- a/dns/system_windows.go +++ /dev/null @@ -1,96 +0,0 @@ -//go:build windows - -package dns - -import ( - "net/netip" - "os" - "strconv" - "syscall" - "unsafe" - - "golang.org/x/exp/slices" - "golang.org/x/sys/windows" -) - -func dnsReadConfig() (servers []string, err error) { - aas, err := adapterAddresses() - if err != nil { - return - } - for _, aa := range aas { - // Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs. - if aa.OperStatus != windows.IfOperStatusUp { - continue - } - - // Only take interfaces which have at least one gateway - if aa.FirstGatewayAddress == nil { - continue - } - - for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next { - sa, err := dns.Address.Sockaddr.Sockaddr() - if err != nil { - continue - } - var ip netip.Addr - switch sa := sa.(type) { - case *syscall.SockaddrInet4: - ip = netip.AddrFrom4(sa.Addr) - case *syscall.SockaddrInet6: - if sa.Addr[0] == 0xfe && sa.Addr[1] == 0xc0 { - // Ignore these fec0/10 ones. Windows seems to - // populate them as defaults on its misc rando - // interfaces. - continue - } - ip = netip.AddrFrom16(sa.Addr) - if sa.ZoneId != 0 { - ip = ip.WithZone(strconv.FormatInt(int64(sa.ZoneId), 10)) - } - //continue - default: - // Unexpected type. - continue - } - ipStr := ip.String() - if slices.Contains(servers, ipStr) { - continue - } - servers = append(servers, ipStr) - } - } - return -} - -// adapterAddresses returns a list of IP adapter and address -// structures. The structure contains an IP adapter and flattened -// multiple IP addresses including unicast, anycast and multicast -// addresses. -func adapterAddresses() ([]*windows.IpAdapterAddresses, error) { - var b []byte - l := uint32(15000) // recommended initial size - for { - b = make([]byte, l) - const flags = windows.GAA_FLAG_INCLUDE_PREFIX | windows.GAA_FLAG_INCLUDE_GATEWAYS - err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) - if err == nil { - if l == 0 { - return nil, nil - } - break - } - if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW { - return nil, os.NewSyscallError("getadaptersaddresses", err) - } - if l <= uint32(len(b)) { - return nil, os.NewSyscallError("getadaptersaddresses", err) - } - } - var aas []*windows.IpAdapterAddresses - for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next { - aas = append(aas, aa) - } - return aas, nil -} diff --git a/dns/util.go b/dns/util.go deleted file mode 100644 index a4ca98d4f1..0000000000 --- a/dns/util.go +++ /dev/null @@ -1,234 +0,0 @@ -package dns - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net" - "net/netip" - "strings" - "time" - - "github.com/metacubex/mihomo/common/picker" - "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/log" - - D "github.com/miekg/dns" - "github.com/samber/lo" -) - -const ( - MaxMsgSize = 65535 -) - -const serverFailureCacheTTL uint32 = 5 - -func minimalTTL(records []D.RR) uint32 { - rr := lo.MinBy(records, func(r1 D.RR, r2 D.RR) bool { - return r1.Header().Ttl < r2.Header().Ttl - }) - if rr == nil { - return 0 - } - return rr.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 dnsCache, key string, q D.Question, msg *D.Msg) { - // skip dns cache for acme challenge - if q.Qtype == D.TypeTXT && strings.HasPrefix(q.Name, "_acme-challenge.") { - log.Debugln("[DNS] dns cache ignored because of acme challenge for: %s", q.Name) - return - } - - var ttl uint32 - if msg.Rcode == D.RcodeServerFailure { - // [...] a resolver MAY cache a server failure response. - // If it does so it MUST NOT cache it for longer than five (5) minutes [...] - ttl = serverFailureCacheTTL - } else { - ttl = minimalTTL(append(append(msg.Answer, msg.Ns...), msg.Extra...)) - } - 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 || q.Qtype == D.TypeCNAME) -} - -func transform(servers []NameServer, resolver *Resolver) []dnsClient { - ret := make([]dnsClient, 0, len(servers)) - for _, s := range servers { - switch s.Net { - case "https": - ret = append(ret, newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter, s.ProxyName)) - continue - case "dhcp": - ret = append(ret, newDHCPClient(s.Addr)) - continue - case "system": - ret = append(ret, newSystemClient()) - continue - case "rcode": - ret = append(ret, newRCodeClient(s.Addr)) - continue - case "quic": - if doq, err := newDoQ(resolver, s.Addr, s.ProxyAdapter, s.ProxyName); err == nil { - ret = append(ret, doq) - } else { - log.Fatalln("DoQ format error: %v", err) - } - 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, - dialer: newDNSDialer(resolver, s.ProxyAdapter, s.ProxyName), - }) - } - 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) (ips []netip.Addr) { - for _, answer := range msg.Answer { - var ip netip.Addr - switch ans := answer.(type) { - case *D.AAAA: - ip, _ = netip.AddrFromSlice(ans.AAAA) - case *D.A: - ip, _ = netip.AddrFromSlice(ans.A) - default: - continue - } - if !ip.IsValid() { - continue - } - ip = ip.Unmap() - ips = append(ips, ip) - } - return -} - -func msgToDomain(msg *D.Msg) string { - if len(msg.Question) > 0 { - return strings.TrimRight(msg.Question[0].Name, ".") - } - - return "" -} - -func msgToQtype(msg *D.Msg) (uint16, string) { - if len(msg.Question) > 0 { - qType := msg.Question[0].Qtype - return qType, D.Type(qType).String() - } - return 0, "" -} - -func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, cache bool, err error) { - cache = true - fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout) - defer fast.Close() - domain := msgToDomain(m) - qType, qTypeStr := msgToQtype(m) - var noIpMsg *D.Msg - for _, client := range clients { - if _, isRCodeClient := client.(rcodeClient); isRCodeClient { - msg, err = client.ExchangeContext(ctx, m) - return msg, false, err - } - client := client // shadow define client to ensure the value captured by the closure will not be changed in the next loop - fast.Go(func() (*D.Msg, error) { - log.Debugln("[DNS] resolve %s %s from %s", domain, qTypeStr, client.Address()) - m, err := client.ExchangeContext(ctx, m) - if err != nil { - return nil, err - } else if cache && (m.Rcode == D.RcodeServerFailure || m.Rcode == D.RcodeRefused) { - // currently, cache indicates whether this msg was from a RCode client, - // so we would ignore RCode errors from RCode clients. - return nil, errors.New("server failure: " + D.RcodeToString[m.Rcode]) - } - ips := msgToIP(m) - log.Debugln("[DNS] %s --> %s %s from %s", domain, ips, qTypeStr, client.Address()) - switch qType { - case D.TypeAAAA: - if len(ips) == 0 { - noIpMsg = m - return nil, resolver.ErrIPNotFound - } - case D.TypeA: - if len(ips) == 0 { - noIpMsg = m - return nil, resolver.ErrIPNotFound - } - } - return m, nil - }) - } - - msg = fast.Wait() - if msg == nil { - if noIpMsg != nil { - return noIpMsg, false, nil - } - err = errors.New("all DNS requests failed") - if fErr := fast.Error(); fErr != nil { - err = fmt.Errorf("%w, first error: %w", err, fErr) - } - } - return -} diff --git a/docker/file-name.sh b/docker/file-name.sh deleted file mode 100644 index 3b2d61f977..0000000000 --- a/docker/file-name.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -os="mihomo-linux-" -case $TARGETPLATFORM in - "linux/amd64") - arch="amd64-compatible" - ;; - "linux/386") - arch="386" - ;; - "linux/arm64") - arch="arm64" - ;; - "linux/arm/v7") - arch="armv7" - ;; - "riscv64") - arch="riscv64" - ;; - *) - echo "Unknown architecture" - exit 1 - ;; -esac -file_name="$os$arch-$(cat bin/version.txt)" -echo $file_name \ No newline at end of file diff --git a/docs/config.yaml b/docs/config.yaml deleted file mode 100644 index 999bd35f0a..0000000000 --- a/docs/config.yaml +++ /dev/null @@ -1,1396 +0,0 @@ -# port: 7890 # HTTP(S) 代理服务器端口 -# socks-port: 7891 # SOCKS5 代理端口 -mixed-port: 10801 # HTTP(S) 和 SOCKS 代理混合端口 -# redir-port: 7892 # 透明代理端口,用于 Linux 和 MacOS - -# Transparent proxy server port for Linux (TProxy TCP and TProxy UDP) -# tproxy-port: 7893 - -allow-lan: true # 允许局域网连接 -bind-address: "*" # 绑定 IP 地址,仅作用于 allow-lan 为 true,'*'表示所有地址 -authentication: # http,socks 入口的验证用户名,密码 - - "username:password" -skip-auth-prefixes: # 设置跳过验证的 IP 段 - - 127.0.0.1/8 - - ::1/128 -lan-allowed-ips: # 允许连接的 IP 地址段,仅作用于 allow-lan 为 true, 默认值为 0.0.0.0/0 和::/0 - - 0.0.0.0/0 - - ::/0 -lan-disallowed-ips: # 禁止连接的 IP 地址段,黑名单优先级高于白名单,默认值为空 - - 192.168.0.3/32 - -# find-process-mode has 3 values:always, strict, off -# - always, 开启,强制匹配所有进程 -# - strict, 默认,由 mihomo 判断是否开启 -# - off, 不匹配进程,推荐在路由器上使用此模式 -find-process-mode: strict - -mode: rule - -#自定义 geodata url -geox-url: - geoip: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat" - geosite: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat" - mmdb: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb" - -geo-auto-update: false # 是否自动更新 geodata -geo-update-interval: 24 # 更新间隔,单位:小时 - -# Matcher implementation used by GeoSite, available implementations: -# - succinct (default, same as rule-set) -# - mph (from V2Ray, also `hybrid` in Xray) -# geosite-matcher: succinct - -log-level: debug # 日志等级 silent/error/warning/info/debug - -ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录 - -tls: - certificate: string # 证书 PEM 格式,或者 证书的路径 - private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 - custom-certifactes: - - | - -----BEGIN CERTIFICATE----- - format/pem... - -----END CERTIFICATE----- - -external-controller: 0.0.0.0:9093 # RESTful API 监听地址 -external-controller-tls: 0.0.0.0:9443 # RESTful API HTTPS 监听地址,需要配置 tls 部分配置文件 -# secret: "123456" # `Authorization:Bearer ${secret}` - -# RESTful API CORS标头配置 -external-controller-cors: - allow-origins: - - "*" - allow-private-network: true - -# RESTful API Unix socket 监听地址( windows版本大于17063也可以使用,即大于等于1803/RS4版本即可使用 ) -# !!!注意: 从Unix socket访问api接口不会验证secret, 如果开启请自行保证安全问题 !!! -# 测试方法: curl -v --unix-socket "mihomo.sock" http://localhost/ -external-controller-unix: mihomo.sock - -# RESTful API Windows namedpipe 监听地址 -# !!!注意: 从Windows namedpipe访问api接口不会验证secret, 如果开启请自行保证安全问题 !!! -external-controller-pipe: \\.\pipe\mihomo - -# tcp-concurrent: true # TCP 并发连接所有 IP, 将使用最快握手的 TCP - -# 配置 WEB UI 目录,使用 http://{{external-controller}}/ui 访问 -external-ui: /path/to/ui/folder/ -external-ui-name: xd -# 目前支持下载zip,tgz格式的压缩包 -external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip" - -# 在RESTful API端口上开启DOH服务器 -# !!!该URL不会验证secret, 如果开启请自行保证安全问题 !!! -external-doh-server: /dns-query - -# interface-name: en0 # 设置出口网卡 - -# 全局 TLS 指纹,优先低于 proxy 内的 client-fingerprint -# 可选: "chrome","firefox","safari","ios","random","none" options. -# Utls is currently support TLS transport in TCP/grpc/WS/HTTP for VLESS/Vmess and trojan. -global-client-fingerprint: chrome - -# TCP keep alive interval -# disable-keep-alive: false #目前在android端强制为true -# keep-alive-idle: 15 -# keep-alive-interval: 15 - -# routing-mark:6666 # 配置 fwmark 仅用于 Linux -experimental: - # Disable quic-go GSO support. This may result in reduced performance on Linux. - # This is not recommended for most users. - # Only users encountering issues with quic-go's internal implementation should enable this, - # and they should disable it as soon as the issue is resolved. - # This field will be removed when quic-go fixes all their issues in GSO. - # This equivalent to the environment variable QUIC_GO_DISABLE_GSO=1. - #quic-go-disable-gso: true - -# 类似于 /etc/hosts, 仅支持配置单个 IP -hosts: -# '*.mihomo.dev': 127.0.0.1 -# '.dev': 127.0.0.1 -# 'alpha.mihomo.dev': '::1' -# test.com: [1.1.1.1, 2.2.2.2] -# home.lan: lan # lan 为特别字段,将加入本地所有网卡的地址 -# baidu.com: google.com # 只允许配置一个别名 - -profile: # 存储 select 选择记录 - store-selected: false - - # 持久化 fake-ip - store-fake-ip: true - -# Tun 配置 -tun: - enable: false - stack: system # gvisor/mixed - dns-hijack: - - 0.0.0.0:53 # 需要劫持的 DNS - # auto-detect-interface: true # 自动识别出口网卡 - # auto-route: true # 配置路由表 - # mtu: 9000 # 最大传输单元 - # gso: false # 启用通用分段卸载,仅支持 Linux - # gso-max-size: 65536 # 通用分段卸载包的最大大小 - auto-redirect: false # 自动配置 iptables 以重定向 TCP 连接。仅支持 Linux。带有 auto-redirect 的 auto-route 现在可以在路由器上按预期工作,无需干预。 - # strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问 - route-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 不匹配的流量将绕过路由, 仅支持 Linux,且需要 nftables,`auto-route` 和 `auto-redirect` 已启用。 - - ruleset-1 - - ruleset-2 - route-exclude-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 匹配的流量将绕过路由, 仅支持 Linux,且需要 nftables,`auto-route` 和 `auto-redirect` 已启用。 - - ruleset-3 - - ruleset-4 - route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 - - 0.0.0.0/1 - - 128.0.0.0/1 - - "::/1" - - "8000::/1" - # inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由(旧写法) - # - 0.0.0.0/1 - # - 128.0.0.0/1 - # inet6-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由(旧写法) - # - "::/1" - # - "8000::/1" - # endpoint-independent-nat: false # 启用独立于端点的 NAT - # include-interface: # 限制被路由的接口。默认不限制,与 `exclude-interface` 冲突 - # - "lan0" - # exclude-interface: # 排除路由的接口,与 `include-interface` 冲突 - # - "lan1" - # include-uid: # UID 规则仅在 Linux 下被支持,并且需要 auto-route - # - 0 - # include-uid-range: # 限制被路由的的用户范围 - # - 1000:9999 - # exclude-uid: # 排除路由的的用户 - #- 1000 - # exclude-uid-range: # 排除路由的的用户范围 - # - 1000:9999 - - # Android 用户和应用规则仅在 Android 下被支持 - # 并且需要 auto-route - - # include-android-user: # 限制被路由的 Android 用户 - # - 0 - # - 10 - # include-package: # 限制被路由的 Android 应用包名 - # - com.android.chrome - # exclude-package: # 排除被路由的 Android 应用包名 - # - com.android.captiveportallogin - -# 嗅探域名 可选配置 -sniffer: - enable: false - ## 对 redir-host 类型识别的流量进行强制嗅探 - ## 如:Tun、Redir 和 TProxy 并 DNS 为 redir-host 皆属于 - # force-dns-mapping: false - ## 对所有未获取到域名的流量进行强制嗅探 - # parse-pure-ip: false - # 是否使用嗅探结果作为实际访问,默认 true - # 全局配置,优先级低于 sniffer.sniff 实际配置 - override-destination: false - sniff: # TLS 和 QUIC 默认如果不配置 ports 默认嗅探 443 - QUIC: - # ports: [ 443 ] - TLS: - # ports: [443, 8443] - - # 默认嗅探 80 - HTTP: # 需要嗅探的端口 - ports: [80, 8080-8880] - # 可覆盖 sniffer.override-destination - override-destination: true - force-domain: - - +.v2ex.com - # skip-src-address: # 对于来源ip跳过嗅探 - # - 192.168.0.3/32 - # skip-dst-address: # 对于目标ip跳过嗅探 - # - 192.168.0.3/32 - ## 对嗅探结果进行跳过 - # skip-domain: - # - Mijia Cloud - # 需要嗅探协议 - # 已废弃,若 sniffer.sniff 配置则此项无效 - sniffing: - - tls - - http - # 强制对此域名进行嗅探 - - # 仅对白名单中的端口进行嗅探,默认为 443,80 - # 已废弃,若 sniffer.sniff 配置则此项无效 - port-whitelist: - - "80" - - "443" - # - 8000-9999 - -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 - -# DNS 配置 -dns: - cache-algorithm: arc - enable: false # 关闭将使用系统 DNS - prefer-h3: false # 是否开启 DoH 支持 HTTP/3,将并发尝试 - listen: 0.0.0.0:53 # 开启 DNS 服务器监听 - # ipv6: false # false 将返回 AAAA 的空结果 - # ipv6-timeout: 300 # 单位:ms,内部双栈并发时,向上游查询 AAAA 时,等待 AAAA 的时间,默认 100ms - # 用于解析 nameserver,fallback 以及其他 DNS 服务器配置的,DNS 服务域名 - # 只能使用纯 IP 地址,可使用加密 DNS - default-nameserver: - - 114.114.114.114 - - 8.8.8.8 - - tls://1.12.12.12:853 - - tls://223.5.5.5:853 - - system # append DNS server from system configuration. If not found, it would print an error log and skip. - enhanced-mode: fake-ip # or redir-host - - fake-ip-range: 198.18.0.1/16 # fake-ip 池设置 - - # 配置不使用 fake-ip 的域名 - fake-ip-filter: - - '*.lan' - - localhost.ptlogin2.qq.com - # fakeip-filter 为 rule-providers 中的名为 fakeip-filter 规则订阅, - # 且 behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则 - - rule-set:fakeip-filter - # fakeip-filter 为 geosite 中名为 fakeip-filter 的分类(需要自行保证该分类存在) - - geosite:fakeip-filter - # 配置fake-ip-filter的匹配模式,默认为blacklist,即如果匹配成功不返回fake-ip - # 可设置为whitelist,即只有匹配成功才返回fake-ip - fake-ip-filter-mode: blacklist - - # use-hosts: true # 查询 hosts - - # 配置后面的nameserver、fallback和nameserver-policy向dns服务器的连接过程是否遵守遵守rules规则 - # 如果为false(默认值)则这三部分的dns服务器在未特别指定的情况下会直连 - # 如果为true,将会按照rules的规则匹配链接方式(走代理或直连),如果有特别指定则任然以指定值为准 - # 仅当proxy-server-nameserver非空时可以开启此选项, 强烈不建议和prefer-h3一起使用 - # 此外,这三者配置中的dns服务器如果出现域名会采用default-nameserver配置项解析,也请确保正确配置default-nameserver - respect-rules: false - - # DNS 主要域名配置 - # 支持 UDP,TCP,DoT,DoH,DoQ - # 这部分为主要 DNS 配置,影响所有直连,确保使用对大陆解析精准的 DNS - nameserver: - - 114.114.114.114 # default value - - 8.8.8.8 # default value - - tls://223.5.5.5:853 # DNS over TLS - - https://doh.pub/dns-query # DNS over HTTPS - - https://dns.alidns.com/dns-query#h3=true # 强制 HTTP/3,与 perfer-h3 无关,强制开启 DoH 的 HTTP/3 支持,若不支持将无法使用 - - https://mozilla.cloudflare-dns.com/dns-query#DNS&h3=true # 指定策略组和使用 HTTP/3 - - dhcp://en0 # dns from dhcp - - quic://dns.adguard.com:784 # DNS over QUIC - # - '8.8.8.8#RULES' # 效果同respect-rules,但仅对该服务器生效 - # - '8.8.8.8#en0' # 兼容指定 DNS 出口网卡 - - # 当配置 fallback 时,会查询 nameserver 中返回的 IP 是否为 CN,非必要配置 - # 当不是 CN,则使用 fallback 中的 DNS 查询结果 - # 确保配置 fallback 时能够正常查询 - # fallback: - # - tcp://1.1.1.1 - # - 'tcp://1.1.1.1#ProxyGroupName' # 指定 DNS 过代理查询,ProxyGroupName 为策略组名或节点名,过代理配置优先于配置出口网卡,当找不到策略组或节点名则设置为出口网卡 - - # 专用于节点域名解析的 DNS 服务器,非必要配置项,如果不填则遵循nameserver-policy、nameserver和fallback的配置 - # proxy-server-nameserver: - # - https://dns.google/dns-query - # - tls://one.one.one.one - - # 专用于direct出口域名解析的 DNS 服务器,非必要配置项,如果不填则遵循nameserver-policy、nameserver和fallback的配置 - # direct-nameserver: - # - system:// - # direct-nameserver-follow-policy: false # 是否遵循nameserver-policy,默认为不遵守,仅当direct-nameserver不为空时生效 - - # 配置 fallback 使用条件 - # fallback-filter: - # geoip: true # 配置是否使用 geoip - # geoip-code: CN # 当 nameserver 域名的 IP 查询 geoip 库为 CN 时,不使用 fallback 中的 DNS 查询结果 - # 配置强制 fallback,优先于 IP 判断,具体分类自行查看 geosite 库 - # geosite: - # - gfw - # 如果不匹配 ipcidr 则使用 nameservers 中的结果 - # ipcidr: - # - 240.0.0.0/4 - # domain: - # - '+.google.com' - # - '+.facebook.com' - # - '+.youtube.com' - - # 配置查询域名使用的 DNS 服务器 - nameserver-policy: - # 'www.baidu.com': '114.114.114.114' - # '+.internal.crop.com': '10.0.0.1' - "geosite:cn,private,apple": - - https://doh.pub/dns-query - - https://dns.alidns.com/dns-query - "geosite:category-ads-all": rcode://success - "www.baidu.com,+.google.cn": [223.5.5.5, https://dns.alidns.com/dns-query] - ## global,dns 为 rule-providers 中的名为 global 和 dns 规则订阅, - ## 且 behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则 - # "rule-set:global,dns": 8.8.8.8 - -proxies: # socks5 - - name: "socks" - type: socks5 - server: server - port: 443 - # username: username - # password: password - # tls: true - # fingerprint: xxxx - # skip-cert-verify: true - # udp: true - # ip-version: ipv6 - - # http - - name: "http" - type: http - server: server - port: 443 - # username: username - # password: password - # tls: true # https - # skip-cert-verify: true - # sni: custom.com - # fingerprint: xxxx # 同 experimental.fingerprints 使用 sha256 指纹,配置协议独立的指纹,将忽略 experimental.fingerprints - # ip-version: dual - - # 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 - - # Shadowsocks - # cipher支持: - # 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 - # 2022-blake3-aes-128-gcm 2022-blake3-aes-256-gcm 2022-blake3-chacha20-poly1305 - - name: "ss1" - type: ss - server: server - port: 443 - cipher: chacha20-ietf-poly1305 - password: "password" - # udp: true - # udp-over-tcp: false - # ip-version: ipv4 # 设置节点使用 IP 版本,可选:dual,ipv4,ipv6,ipv4-prefer,ipv6-prefer。默认使用 dual - # ipv4:仅使用 IPv4 ipv6:仅使用 IPv6 - # ipv4-prefer:优先使用 IPv4 对于 TCP 会进行双栈解析,并发链接但是优先使用 IPv4 链接, - # UDP 则为双栈解析,获取结果中的第一个 IPv4 - # ipv6-prefer 同 ipv4-prefer - # 现有协议都支持此参数,TCP 效果仅在开启 tcp-concurrent 生效 - smux: - enabled: false - protocol: smux # smux/yamux/h2mux - # max-connections: 4 # Maximum connections. Conflict with max-streams. - # min-streams: 4 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. - # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. - # padding: false # Enable padding. Requires sing-box server version 1.3-beta9 or later. - # statistic: false # 控制是否将底层连接显示在面板中,方便打断底层连接 - # only-tcp: false # 如果设置为 true, smux 的设置将不会对 udp 生效,udp 连接会直接走底层协议 - - - 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 - # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 - # 配置指纹将实现 SSL Pining 效果 - # fingerprint: xxxx - # skip-cert-verify: true - # host: bing.com - # path: "/" - # mux: true - # headers: - # custom: value - # v2ray-http-upgrade: false - # v2ray-http-upgrade-fast-open: false - - - name: "ss4-shadow-tls" - type: ss - server: server - port: 443 - cipher: chacha20-ietf-poly1305 - password: "password" - plugin: shadow-tls - client-fingerprint: chrome - plugin-opts: - host: "cloud.tencent.com" - password: "shadow_tls_password" - version: 2 # support 1/2/3 - # alpn: ["h2","http/1.1"] - - - name: "ss5" - type: ss - server: server - port: 443 - cipher: chacha20-ietf-poly1305 - password: "password" - plugin: gost-plugin - plugin-opts: - mode: websocket - # tls: true # wss - # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 - # 配置指纹将实现 SSL Pining 效果 - # fingerprint: xxxx - # skip-cert-verify: true - # host: bing.com - # path: "/" - # mux: true - # headers: - # custom: value - - - name: "ss-restls-tls13" - type: ss - server: [YOUR_SERVER_IP] - port: 443 - cipher: chacha20-ietf-poly1305 - password: [YOUR_SS_PASSWORD] - client-fingerprint: - chrome # One of: chrome, ios, firefox or safari - # 可以是 chrome, ios, firefox, safari 中的一个 - plugin: restls - plugin-opts: - host: - "www.microsoft.com" # Must be a TLS 1.3 server - # 应当是一个 TLS 1.3 服务器 - password: [YOUR_RESTLS_PASSWORD] - version-hint: "tls13" - # Control your post-handshake traffic through restls-script - # Hide proxy behaviors like "tls in tls". - # see https://github.com/3andne/restls/blob/main/Restls-Script:%20Hide%20Your%20Proxy%20Traffic%20Behavior.md - # 用 restls 剧本来控制握手后的行为,隐藏"tls in tls"等特征 - # 详情:https://github.com/3andne/restls/blob/main/Restls-Script:%20%E9%9A%90%E8%97%8F%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%90%86%E8%A1%8C%E4%B8%BA.md - restls-script: "300?100<1,400~100,350~100,600~100,300~200,300~100" - - - name: "ss-restls-tls12" - type: ss - server: [YOUR_SERVER_IP] - port: 443 - cipher: chacha20-ietf-poly1305 - password: [YOUR_SS_PASSWORD] - client-fingerprint: - chrome # One of: chrome, ios, firefox or safari - # 可以是 chrome, ios, firefox, safari 中的一个 - plugin: restls - plugin-opts: - host: - "vscode.dev" # Must be a TLS 1.2 server - # 应当是一个 TLS 1.2 服务器 - password: [YOUR_RESTLS_PASSWORD] - version-hint: "tls12" - restls-script: "1000?100<1,500~100,350~100,600~100,400~200" - - # vmess - # cipher 支持 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 - # fingerprint: xxxx - # client-fingerprint: chrome # Available: "chrome","firefox","safari","ios","random", currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan. - # 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 - # v2ray-http-upgrade: false - # v2ray-http-upgrade-fast-open: false - - - name: "vmess-h2" - type: vmess - server: server - port: 443 - uuid: uuid - alterId: 32 - cipher: auto - network: h2 - tls: true - # fingerprint: xxxx - 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 - # ip-version: ipv4 # 设置使用 IP 类型偏好,可选:ipv4,ipv6,dual,默认值:dual - - - name: vmess-grpc - server: server - port: 443 - type: vmess - uuid: uuid - alterId: 32 - cipher: auto - network: grpc - tls: true - # fingerprint: xxxx - servername: example.com - # skip-cert-verify: true - grpc-opts: - grpc-service-name: "example" - # ip-version: ipv4 - - # vless - - name: "vless-tcp" - type: vless - server: server - port: 443 - uuid: uuid - network: tcp - servername: example.com # AKA SNI - # flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS - # skip-cert-verify: true - # fingerprint: xxxx - # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" - - - name: "vless-vision" - type: vless - server: server - port: 443 - uuid: uuid - network: tcp - tls: true - udp: true - flow: xtls-rprx-vision - client-fingerprint: chrome - # fingerprint: xxxx - # skip-cert-verify: true - - - name: "vless-reality-vision" - type: vless - server: server - port: 443 - uuid: uuid - network: tcp - tls: true - udp: true - flow: xtls-rprx-vision - servername: www.microsoft.com # REALITY servername - reality-opts: - public-key: xxx - short-id: xxx # optional - client-fingerprint: chrome # cannot be empty - - - name: "vless-reality-grpc" - type: vless - server: server - port: 443 - uuid: uuid - network: grpc - tls: true - udp: true - flow: - # skip-cert-verify: true - client-fingerprint: chrome - servername: testingcf.jsdelivr.net - grpc-opts: - grpc-service-name: "grpc" - reality-opts: - public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE - short-id: 10f897e26c4b9478 - - - name: "vless-ws" - type: vless - server: server - port: 443 - uuid: uuid - udp: true - tls: true - network: ws - # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" - servername: example.com # priority over wss host - # skip-cert-verify: true - # fingerprint: xxxx - ws-opts: - path: "/" - headers: - Host: example.com - # v2ray-http-upgrade: false - # v2ray-http-upgrade-fast-open: false - - # Trojan - - name: "trojan" - type: trojan - server: server - port: 443 - password: yourpsk - # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" - # fingerprint: xxxx - # udp: true - # sni: example.com # aka server name - # alpn: - # - h2 - # - http/1.1 - # skip-cert-verify: true - # ss-opts: # like trojan-go's `shadowsocks` config - # enabled: false - # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 - # password: "example" - - - name: trojan-grpc - server: server - port: 443 - type: trojan - password: "example" - network: grpc - sni: example.com - # skip-cert-verify: true - # fingerprint: xxxx - 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 - # fingerprint: xxxx - udp: true - # ws-opts: - # path: /path - # headers: - # Host: example.com - # v2ray-http-upgrade: false - # v2ray-http-upgrade-fast-open: false - - - name: "trojan-xtls" - type: trojan - server: server - port: 443 - password: yourpsk - flow: "xtls-rprx-direct" # xtls-rprx-origin xtls-rprx-direct - flow-show: true - # udp: true - # sni: example.com # aka server name - # skip-cert-verify: true - # fingerprint: xxxx - - #hysteria - - name: "hysteria" - type: hysteria - server: server.com - port: 443 - # ports: 1000,2000-3000,5000 # port 不可省略 - auth-str: yourpassword - # obfs: obfs_str - # alpn: - # - h3 - protocol: udp # 支持 udp/wechat-video/faketcp - up: "30 Mbps" # 若不写单位,默认为 Mbps - down: "200 Mbps" # 若不写单位,默认为 Mbps - # sni: server.com - # skip-cert-verify: false - # recv-window-conn: 12582912 - # recv-window: 52428800 - # ca: "./my.ca" - # ca-str: "xyz" - # disable-mtu-discovery: false - # fingerprint: xxxx - # fast-open: true # 支持 TCP 快速打开,默认为 false - - #hysteria2 - - name: "hysteria2" - type: hysteria2 - server: server.com - port: 443 - # ports: 1000,2000-3000,5000 # port 不可省略 - # hop-interval: 15 - # up 和 down 均不写或为 0 则使用 BBR 流控 - # up: "30 Mbps" # 若不写单位,默认为 Mbps - # down: "200 Mbps" # 若不写单位,默认为 Mbps - password: yourpassword - # obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander - # obfs-password: yourpassword - # sni: server.com - # skip-cert-verify: false - # fingerprint: xxxx - # alpn: - # - h3 - # ca: "./my.ca" - # ca-str: "xyz" - ###quic-go特殊配置项,不要随意修改除非你知道你在干什么### - # initial-stream-receive-window: 8388608 - # max-stream-receive-window: 8388608 - # initial-connection-receive-window: 20971520 - # max-connection-receive-window: 20971520 - - # wireguard - - name: "wg" - type: wireguard - server: 162.159.192.1 - port: 2480 - ip: 172.16.0.2 - ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5 - public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= - # pre-shared-key: 31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM= - private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= - udp: true - reserved: "U4An" - # 数组格式也是合法的 - # reserved: [209,98,59] - # 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接 - # dialer-proxy: "ss1" - # remote-dns-resolve: true # 强制 dns 远程解析,默认值为 false - # dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效 - # refresh-server-ip-interval: 60 # 重新解析server ip的间隔,单位为秒,默认值为0即仅第一次链接时解析server域名,仅应在server域名对应的IP会发生变化时启用该选项(如家宽ddns) - # 如果 peers 不为空,该段落中的 allowed-ips 不可为空;前面段落的 server,port,public-key,pre-shared-key 均会被忽略,但 private-key 会被保留且只能在顶层指定 - # peers: - # - server: 162.159.192.1 - # port: 2480 - # public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= - # # pre-shared-key: 31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM= - # allowed-ips: ['0.0.0.0/0'] - # reserved: [209,98,59] - # 如果存在则开启AmneziaWG功能 - # amnezia-wg-option: - # jc: 5 - # jmin: 500 - # jmax: 501 - # s1: 30 - # s2: 40 - # h1: 123456 - # h2: 67543 - # h4: 32345 - # h3: 123123 - - # tuic - - name: tuic - server: www.example.com - port: 10443 - type: tuic - # tuicV4 必须填写 token(不可同时填写 uuid 和 password) - token: TOKEN - # tuicV5 必须填写 uuid 和 password(不可同时填写 token) - uuid: 00000000-0000-0000-0000-000000000001 - password: PASSWORD_1 - # ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server' - # heartbeat-interval: 10000 - # alpn: [h3] - disable-sni: true - reduce-rtt: true - request-timeout: 8000 - udp-relay-mode: native # Available: "native", "quic". Default: "native" - # congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic" - # cwnd: 10 # default: 32 - # max-udp-relay-packet-size: 1500 - # fast-open: true - # skip-cert-verify: true - # max-open-streams: 20 # default 100, too many open streams may hurt performance - # sni: example.com - # - # meta 和 sing-box 私有扩展,将 ss-uot 用于 udp 中继,开启此选项后 udp-relay-mode 将失效 - # 警告,与原版 tuic 不兼容!!! - # udp-over-stream: false - # udp-over-stream-version: 1 - - # 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 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 - - - name: "ssh-out" - type: ssh - - server: 127.0.0.1 - port: 22 - username: root - password: password - privateKey: path - - # mieru - - name: mieru - type: mieru - server: 1.2.3.4 - port: 2999 - # port-range: 2090-2099 #(不可同时填写 port 和 port-range) - transport: TCP # 只支持 TCP - udp: true # 支持 UDP over TCP - username: user - password: password - # 可以使用的值包括 MULTIPLEXING_OFF, MULTIPLEXING_LOW, MULTIPLEXING_MIDDLE, MULTIPLEXING_HIGH。其中 MULTIPLEXING_OFF 会关闭多路复用功能。默认值为 MULTIPLEXING_LOW。 - # multiplexing: MULTIPLEXING_LOW - - # anytls - - name: anytls - type: anytls - server: 1.2.3.4 - port: 443 - password: "" - # client-fingerprint: chrome - udp: true - # idle-session-check-interval: 30 # seconds - # idle-session-timeout: 30 # seconds - # min-idle-session: 0 - # sni: "example.com" - # alpn: - # - h2 - # - http/1.1 - # skip-cert-verify: true - -# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理 - - name: "dns-out" - type: dns - - # 配置指定 interface-name 和 fwmark 的 DIRECT - - name: en1-direct - type: direct - interface-name: en1 - routing-mark: 6667 -proxy-groups: - # 代理链,目前 relay 可以支持 udp 的只有 vmess/vless/trojan/ss/ssr/tuic - # wireguard 目前不支持在 relay 中使用,请使用 proxy 中的 dialer-proxy 配置项 - # Traffic: mihomo <-> 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 - # expected-status: 204 # 当健康检查返回状态码与期望值不符时,认为节点不可用 - url: "https://cp.cloudflare.com/generate_204" - interval: 300 - - # fallback 将按照 url 测试结果按照节点顺序选择 - - name: "fallback-auto" - type: fallback - proxies: - - ss1 - - ss2 - - vmess1 - url: "https://cp.cloudflare.com/generate_204" - interval: 300 - - # load-balance 将按照算法随机选择节点 - - name: "load-balance" - type: load-balance - proxies: - - ss1 - - ss2 - - vmess1 - url: "https://cp.cloudflare.com/generate_204" - interval: 300 - # strategy: consistent-hashing # 可选 round-robin 和 sticky-sessions - - # select 用户自行选择节点 - - name: Proxy - type: select - # disable-udp: true - proxies: - - ss1 - - ss2 - - vmess1 - - auto - - - name: UseProvider - type: select - filter: "HK|TW" # 正则表达式,过滤 provider1 中节点名包含 HK 或 TW - use: - - provider1 - proxies: - - Proxy - - DIRECT - -# Mihomo 格式的节点或支持 *ray 的分享格式 -proxy-providers: - provider1: - type: http # http 的 path 可空置,默认储存路径为 homedir 的 proxies 文件夹,文件名为 url 的 md5 - url: "url" - interval: 3600 - path: ./provider1.yaml # 默认只允许存储在 mihomo 的 Home Dir,如果想存储到任意位置,添加环境变量 SKIP_SAFE_PATH_CHECK=1 - proxy: DIRECT - # size-limit: 10240 # 限制下载文件最大为10kb,默认为0即不限制文件大小 - header: - User-Agent: - - "Clash/v1.18.0" - - "mihomo/1.18.3" - # Accept: - # - 'application/vnd.github.v3.raw' - # Authorization: - # - 'token 1231231' - health-check: - enable: true - interval: 600 - # lazy: true - url: https://cp.cloudflare.com/generate_204 - # expected-status: 204 # 当健康检查返回状态码与期望值不符时,认为节点不可用 - override: # 覆写节点加载时的一些配置项 - skip-cert-verify: true - udp: true - # down: "50 Mbps" - # up: "10 Mbps" - # dialer-proxy: proxy - # interface-name: tailscale0 - # routing-mark: 233 - # ip-version: ipv4-prefer - # additional-prefix: "[provider1]" - # additional-suffix: "test" - # # 名字替换,支持正则表达式 - # proxy-name: - # - pattern: "test" - # target: "TEST" - # - pattern: "IPLC-(.*?)倍" - # target: "iplc x $1" - - provider2: - type: inline - dialer-proxy: proxy - payload: - - name: "ss1" - type: ss - server: server - port: 443 - cipher: chacha20-ietf-poly1305 - password: "password" - - test: - type: file - path: /test.yaml - health-check: - enable: true - interval: 36000 - url: https://cp.cloudflare.com/generate_204 -rule-providers: - rule1: - behavior: classical # domain ipcidr - interval: 259200 - path: /path/to/save/file.yaml # 默认只允许存储在 mihomo 的 Home Dir,如果想存储到任意位置,添加环境变量 SKIP_SAFE_PATH_CHECK=1 - type: http # http 的 path 可空置,默认储存路径为 homedir 的 rules 文件夹,文件名为 url 的 md5 - url: "url" - proxy: DIRECT - # size-limit: 10240 # 限制下载文件最大为10kb,默认为0即不限制文件大小 - rule2: - behavior: classical - interval: 259200 - path: /path/to/save/file.yaml - type: file - rule3: - # mrs类型ruleset,目前仅支持domain和ipcidr(即不支持classical), - # - # 对于behavior=domain: - # - format=yaml 可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换到mrs格式 - # - format=text 可以通过“mihomo convert-ruleset domain text XXX.text XXX.mrs”转换到mrs格式 - # - XXX.mrs 可以通过"mihomo convert-ruleset domain mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回yaml格式) - # - # 对于behavior=ipcidr: - # - format=yaml 可以通过“mihomo convert-ruleset ipcidr yaml XXX.yaml XXX.mrs”转换到mrs格式 - # - format=text 可以通过“mihomo convert-ruleset ipcidr text XXX.text XXX.mrs”转换到mrs格式 - # - XXX.mrs 可以通过"mihomo convert-ruleset ipcidr mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回yaml格式) - # - type: http - url: "url" - format: mrs - behavior: domain - path: /path/to/save/file.mrs - rule4: - type: inline - behavior: domain # classical / ipcidr - payload: - - '.blogger.com' - - '*.*.microsoft.com' - - 'books.itunes.apple.com' - -rules: - - RULE-SET,rule1,REJECT - - IP-ASN,1,PROXY - - DOMAIN-REGEX,^abc,DIRECT - - DOMAIN-SUFFIX,baidu.com,DIRECT - - DOMAIN-KEYWORD,google,ss1 - - IP-CIDR,1.1.1.1/32,ss1 - - IP-CIDR6,2409::/64,DIRECT - # 当满足条件是 TCP 或 UDP 流量时,使用名为 sub-rule-name1 的规则集 - - SUB-RULE,(OR,((NETWORK,TCP),(NETWORK,UDP))),sub-rule-name1 - - SUB-RULE,(AND,((NETWORK,UDP))),sub-rule-name2 -# 定义多个子规则集,规则将以分叉匹配,使用 SUB-RULE 使用 -# google.com(not match)--> baidu.com(match) -# / | -# / | -# https://baidu.com --> rule1 --> rule2 --> sub-rule-name1(match tcp) 使用 DIRECT -# -# -# google.com(not match)--> baidu.com(not match) -# / | -# / | -# dns 1.1.1.1 --> rule1 --> rule2 --> sub-rule-name1(match udp) sub-rule-name2(match udp) -# | -# | -# 使用 REJECT <-- 1.1.1.1/32(match) -# - -sub-rules: - sub-rule-name1: - - DOMAIN,google.com,ss1 - - DOMAIN,baidu.com,DIRECT - sub-rule-name2: - - IP-CIDR,1.1.1.1/32,REJECT - - IP-CIDR,8.8.8.8/32,ss1 - - DOMAIN,dns.alidns.com,REJECT - -# 流量入站 -listeners: - - name: socks5-in-1 - type: socks - port: 10808 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - #listen: 0.0.0.0 # 默认监听 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 - # udp: false # 默认 true - # users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: [] - # - username: aaa - # password: aaa - # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key - - - name: http-in-1 - type: http - port: 10809 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - # users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: [] - # - username: aaa - # password: aaa - # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key - - - name: mixed-in-1 - type: mixed # HTTP(S) 和 SOCKS 代理混合 - port: 10810 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - # udp: false # 默认 true - # users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: [] - # - username: aaa - # password: aaa - # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key - - - name: reidr-in-1 - type: redir - port: 10811 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - - - name: tproxy-in-1 - type: tproxy - port: 10812 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - # udp: false # 默认 true - - - name: shadowsocks-in-1 - type: shadowsocks - port: 10813 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg= - cipher: 2022-blake3-aes-256-gcm - # shadow-tls: - # enable: false # 设置为true时开启 - # version: 3 # 支持v1/v2/v3 - # password: password # v2设置项 - # users: # v3设置项 - # - name: 1 - # password: password - # handshake: - # dest: test.com:443 - - - name: vmess-in-1 - type: vmess - port: 10814 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - users: - - username: 1 - uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 - alterId: 1 - # ws-path: "/" # 如果不为空则开启 websocket 传输层 - # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 - # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key - # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) - # reality-config: - # dest: test.com:443 - # private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成 - # short-id: - # - 0123456789abcdef - # server-names: - # - test.com - - - name: tuic-in-1 - type: tuic - port: 10815 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - # token: # tuicV4 填写(可以同时填写 users) - # - TOKEN - # users: # tuicV5 填写(可以同时填写 token) - # 00000000-0000-0000-0000-000000000000: PASSWORD_0 - # 00000000-0000-0000-0000-000000000001: PASSWORD_1 - # certificate: ./server.crt - # private-key: ./server.key - # congestion-controller: bbr - # max-idle-time: 15000 - # authentication-timeout: 1000 - # alpn: - # - h3 - # max-udp-relay-packet-size: 1500 - - - name: tunnel-in-1 - type: tunnel - port: 10816 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - network: [tcp, udp] - target: target.com - - - name: vless-in-1 - type: vless - port: 10817 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - users: - - username: 1 - uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 - flow: xtls-rprx-vision - # ws-path: "/" # 如果不为空则开启 websocket 传输层 - # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 - # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key - # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) - reality-config: - dest: test.com:443 - private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成 - short-id: - - 0123456789abcdef - server-names: - - test.com - ### 注意,对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 的其中一项 ### - - - name: anytls-in-1 - type: anytls - port: 10818 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - users: - username1: password1 - username2: password2 - # "certificate" and "private-key" are required - certificate: ./server.crt - private-key: ./server.key - # padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme - - - name: trojan-in-1 - type: trojan - port: 10819 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - users: - - username: 1 - password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 - # ws-path: "/" # 如果不为空则开启 websocket 传输层 - # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 - # 下面两项如果填写则开启 tls(需要同时填写) - certificate: ./server.crt - private-key: ./server.key - # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) - # reality-config: - # dest: test.com:443 - # private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成 - # short-id: - # - 0123456789abcdef - # server-names: - # - test.com - # ss-option: # like trojan-go's `shadowsocks` config - # enabled: false - # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 - # password: "example" - ### 注意,对于trojan listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 或 “ss-option” 的其中一项 ### - - - name: hysteria2-in-1 - type: hysteria2 - port: 10820 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503 - listen: 0.0.0.0 - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - users: - 00000000-0000-0000-0000-000000000000: PASSWORD_0 - 00000000-0000-0000-0000-000000000001: PASSWORD_1 - # certificate: ./server.crt - # private-key: ./server.key - ## up 和 down 均不写或为 0 则使用 BBR 流控 - # up: "30 Mbps" # 若不写单位,默认为 Mbps - # down: "200 Mbps" # 若不写单位,默认为 Mbps - # obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander - # obfs-password: yourpassword - # max-idle-time: 15000 - # alpn: - # - h3 - # ignore-client-bandwidth: false - # HTTP3 服务器认证失败时的行为 (URL 字符串配置),如果 masquerade 未配置,则返回 404 页 - # masquerade: file:///var/www # 作为文件服务器 - # masquerade: http://127.0.0.1:8080 #作为反向代理 - # masquerade: https://127.0.0.1:8080 #作为反向代理 - - - name: tun-in-1 - type: tun - # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - stack: system # gvisor / mixed - dns-hijack: - - 0.0.0.0:53 # 需要劫持的 DNS - # auto-detect-interface: false # 自动识别出口网卡 - # auto-route: false # 配置路由表 - # mtu: 9000 # 最大传输单元 - inet4-address: # 必须手动设置 ipv4 地址段 - - 198.19.0.1/30 - inet6-address: # 必须手动设置 ipv6 地址段 - - "fdfe:dcba:9877::1/126" - # strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问 - # inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 - # - 0.0.0.0/1 - # - 128.0.0.0/1 - # inet6-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 - # - "::/1" - # - "8000::/1" - # endpoint-independent-nat: false # 启用独立于端点的 NAT - # include-uid: # UID 规则仅在 Linux 下被支持,并且需要 auto-route - # - 0 - # include-uid-range: # 限制被路由的的用户范围 - # - 1000:99999 - # exclude-uid: # 排除路由的的用户 - # - 1000 - # exclude-uid-range: # 排除路由的的用户范围 - # - 1000:99999 - - # Android 用户和应用规则仅在 Android 下被支持 - # 并且需要 auto-route - - # include-android-user: # 限制被路由的 Android 用户 - # - 0 - # - 10 - # include-package: # 限制被路由的 Android 应用包名 - # - com.android.chrome - # exclude-package: # 排除被路由的 Android 应用包名 - # - com.android.captiveportallogin -# 入口配置与 Listener 等价,传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理 -# shadowsocks,vmess 入口配置(传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理) -# ss-config: ss://2022-blake3-aes-256-gcm:vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=@:23456 -# vmess-config: vmess://1:9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68@:12345 - -# tuic 服务器入口(传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理) -# tuic-server: -# enable: true -# listen: 127.0.0.1:10443 -# token: # tuicV4 填写(可以同时填写 users) -# - TOKEN -# users: # tuicV5 填写(可以同时填写 token) -# 00000000-0000-0000-0000-000000000000: PASSWORD_0 -# 00000000-0000-0000-0000-000000000001: PASSWORD_1 -# certificate: ./server.crt -# private-key: ./server.key -# congestion-controller: bbr -# max-idle-time: 15000 -# authentication-timeout: 1000 -# alpn: -# - h3 -# max-udp-relay-packet-size: 1500 diff --git a/docs/logo.png b/docs/logo.png deleted file mode 100755 index c624b94d23..0000000000 Binary files a/docs/logo.png and /dev/null differ diff --git a/examples/basic.py b/examples/basic.py new file mode 100644 index 0000000000..deff02910f --- /dev/null +++ b/examples/basic.py @@ -0,0 +1,44 @@ +import asyncio + +from mihomo import Language, MihomoAPI +from mihomo.models import StarrailInfoParsed +from mihomo.models.v1 import StarrailInfoParsedV1 + +client = MihomoAPI(language=Language.EN) + + +async def v1(): + data: StarrailInfoParsedV1 = await client.fetch_user_v1(800333171) + + print(f"Name: {data.player.name}") + print(f"Level: {data.player.level}") + print(f"Signature: {data.player.signature}") + print(f"Achievements: {data.player_details.achievements}") + print(f"Characters count: {data.player_details.characters}") + print(f"Profile picture url: {client.get_icon_url(data.player.icon)}") + for character in data.characters: + print("-----------") + print(f"Name: {character.name}") + print(f"Rarity: {character.rarity}") + print(f"Level: {character.level}") + print(f"Avatar url: {client.get_icon_url(character.icon)}") + print(f"Preview url: {client.get_icon_url(character.preview)}") + print(f"Portrait url: {client.get_icon_url(character.portrait)}") + + +async def v2(): + data: StarrailInfoParsed = await client.fetch_user(800333171, replace_icon_name_with_url=True) + + print(f"Name: {data.player.name}") + print(f"Level: {data.player.level}") + print(f"Signature: {data.player.signature}") + print(f"Profile picture url: {data.player.avatar.icon}") + for character in data.characters: + print("-----------") + print(f"Name: {character.name}") + print(f"Rarity: {character.rarity}") + print(f"Portrait url: {character.portrait}") + + +asyncio.run(v1()) +asyncio.run(v2()) diff --git a/examples/data_persistence.py b/examples/data_persistence.py new file mode 100644 index 0000000000..7eb933b040 --- /dev/null +++ b/examples/data_persistence.py @@ -0,0 +1,25 @@ +import asyncio +import pickle +import zlib + +from mihomo import Language, MihomoAPI, StarrailInfoParsed + + +async def main(): + client = MihomoAPI(language=Language.EN) + data = await client.fetch_user(800333171) + + # Save + pickle_data = zlib.compress(pickle.dumps(data)) + print(len(pickle_data)) + json_data = data.json(by_alias=True, ensure_ascii=False) + print(len(json_data)) + + # Load + data_from_pickle = pickle.loads(zlib.decompress(pickle_data)) + data_from_json = StarrailInfoParsed.parse_raw(json_data) + print(type(data_from_pickle)) + print(type(data_from_json)) + + +asyncio.run(main()) diff --git a/examples/merge_data.py b/examples/merge_data.py new file mode 100644 index 0000000000..c30d8b6b1f --- /dev/null +++ b/examples/merge_data.py @@ -0,0 +1,19 @@ +import asyncio + +from mihomo import Language, MihomoAPI, tools + + +async def main(): + client = MihomoAPI(language=Language.EN) + old_data = await client.fetch_user(800333171) + + # Change characters in game and wait for the API to refresh + # ... + + new_data = await client.fetch_user(800333171) + data = tools.merge_character_data(new_data, old_data) + + print(data) + + +asyncio.run(main()) diff --git a/flake.lock b/flake.lock deleted file mode 100644 index eba25bf64a..0000000000 --- a/flake.lock +++ /dev/null @@ -1,43 +0,0 @@ -{ - "nodes": { - "nixpkgs": { - "locked": { - "lastModified": 1671072901, - "narHash": "sha256-eyFdLtfxYyZnbJorRiZ2kP2kW4gEU76hLzpZGW9mcZg=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "69ce4fbad877f91d4b9bc4cfedfb0ff1fe5043d5", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "master", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "nixpkgs": "nixpkgs", - "utils": "utils" - } - }, - "utils": { - "locked": { - "lastModified": 1667395993, - "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index afe6e1c1d2..0000000000 --- a/flake.nix +++ /dev/null @@ -1,61 +0,0 @@ -{ - description = "Another Mihomo Kernel"; - - inputs.nixpkgs.url = "github:NixOS/nixpkgs/master"; - - inputs.utils.url = "github:numtide/flake-utils"; - - outputs = { self, nixpkgs, utils }: - utils.lib.eachDefaultSystem - (system: - let - pkgs = import nixpkgs { - inherit system; - overlays = [ self.overlay ]; - }; - in - rec { - packages.default = pkgs.mihomo-meta; - } - ) // - ( - let version = nixpkgs.lib.substring 0 8 self.lastModifiedDate or self.lastModified or "19700101"; in - { - overlay = final: prev: { - - mihomo-meta = final.buildGo119Module { - pname = "mihomo-meta"; - inherit version; - src = ./.; - - vendorSha256 = "sha256-W5oiPtTRin0731QQWr98xZ2Vpk97HYcBtKoi1OKZz+w="; - - # Do not build testing suit - excludedPackages = [ "./test" ]; - - CGO_ENABLED = 0; - - ldflags = [ - "-s" - "-w" - "-X github.com/metacubex/mihomo/constant.Version=dev-${version}" - "-X github.com/metacubex/mihomo/constant.BuildTime=${version}" - ]; - - tags = [ - "with_gvisor" - ]; - - # Network required - doCheck = false; - - postInstall = '' - mv $out/bin/mihomo $out/bin/mihomo-meta - ''; - - }; - }; - } - ); -} - diff --git a/go.mod b/go.mod deleted file mode 100644 index a4ecb0fbd4..0000000000 --- a/go.mod +++ /dev/null @@ -1,117 +0,0 @@ -module github.com/metacubex/mihomo - -go 1.20 - -require ( - github.com/3andne/restls-client-go v0.1.6 - github.com/bahlo/generic-list-go v0.2.0 - github.com/coreos/go-iptables v0.8.0 - github.com/dlclark/regexp2 v1.11.5 - github.com/enfein/mieru/v3 v3.13.0 - github.com/go-chi/chi/v5 v5.2.1 - github.com/go-chi/render v1.0.3 - github.com/gobwas/ws v1.4.0 - github.com/gofrs/uuid/v5 v5.3.2 - github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 - github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20 - github.com/klauspost/cpuid/v2 v2.2.9 // lastest version compatible with golang1.20 - github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 - github.com/mdlayher/netlink v1.7.2 - github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab - github.com/metacubex/bart v0.19.0 - github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 - github.com/metacubex/chacha v0.1.2 - github.com/metacubex/fswatch v0.1.1 - github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 - github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b - github.com/metacubex/randv2 v0.2.0 - github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 - github.com/metacubex/sing-mux v0.3.2 - github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 - github.com/metacubex/sing-shadowsocks v0.2.9 - github.com/metacubex/sing-shadowsocks2 v0.2.3 - github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 - github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6 - github.com/metacubex/sing-vmess v0.2.1 - github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f - github.com/metacubex/smux v0.0.0-20250503055512-501391591dee - github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf - github.com/metacubex/utls v1.7.0-alpha.3 - github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 - github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20 - github.com/mroth/weightedrand/v2 v2.1.0 - github.com/openacid/low v0.1.21 - github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20 - github.com/puzpuzpuz/xsync/v3 v3.5.1 - github.com/sagernet/cors v1.2.1 - github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a - github.com/samber/lo v1.50.0 - github.com/shirou/gopsutil/v4 v4.25.1 // lastest version compatible with golang1.20 - github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.10.0 - github.com/vmihailenco/msgpack/v5 v5.4.1 - github.com/wk8/go-ordered-map/v2 v2.1.8 - gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 - go.uber.org/automaxprocs v1.6.0 - go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.33.0 // lastest version compatible with golang1.20 - golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // lastest version compatible with golang1.20 - golang.org/x/net v0.35.0 // lastest version compatible with golang1.20 - golang.org/x/sync v0.11.0 // lastest version compatible with golang1.20 - golang.org/x/sys v0.30.0 // lastest version compatible with golang1.20 - google.golang.org/protobuf v1.34.2 // lastest version compatible with golang1.20 - gopkg.in/yaml.v3 v3.0.1 - lukechampine.com/blake3 v1.3.0 // lastest version compatible with golang1.20 -) - -require ( - github.com/RyuaNerin/go-krypto v1.3.0 // indirect - github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect - github.com/ajg/form v1.5.1 // indirect - github.com/andybalholm/brotli v1.0.6 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 // indirect - github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect - github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect - github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect - github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gaukas/godicttls v0.0.4 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/google/btree v1.1.3 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect - github.com/hashicorp/yamux v0.1.2 // indirect - github.com/josharian/native v1.1.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mdlayher/socket v0.4.1 // indirect - github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect - github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect - github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // indirect - github.com/pierrec/lz4/v4 v4.1.14 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/quic-go/qpack v0.4.0 // indirect - github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect - github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect - github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect - github.com/vishvananda/netns v0.0.4 // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect - go.uber.org/mock v0.4.0 // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.24.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 0840c49a44..0000000000 --- a/go.sum +++ /dev/null @@ -1,285 +0,0 @@ -github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08= -github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY= -github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg= -github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM= -github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss= -github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= -github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= -github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= -github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= -github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -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.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 h1:W7mpP4uiOAbBOdDnRXT9EUdauFv7bz+ERT5rPIord00= -github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98= -github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= -github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= -github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= -github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= -github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= -github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= -github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY= -github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= -github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= -github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= -github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= -github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -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/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= -github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= -github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0= -github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= -github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= -github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= -github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4= -github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= -github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY= -github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= -github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= -github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= -github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc= -github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= -github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU= -github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI= -github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= -github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= -github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI= -github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= -github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo= -github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA= -github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b h1:8oDU32eJ+RRhl1FodGgPfxQxtoBAiD9D40XG2XtU/SE= -github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b/go.mod h1:9R1NOzCgTcWsdWvOMlmtMuF0uKzuOpsfvEf7U3I8zM0= -github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= -github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= -github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= -github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 h1:m4nSxvw46JEgxMzzmnXams+ebwabcry4Ydep/zNiesQ= -github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= -github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw= -github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= -github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 h1:wfmYgtECbEYo1slMtyo+2kMqscYYDSjU/TVgS3018F4= -github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2/go.mod h1:P1kd57U6XXmXv9PbwWdznUGT0k9bKgFJXF0fEORbIlk= -github.com/metacubex/sing-shadowsocks v0.2.9 h1:2e++13WNN7EGjGtvrGLUzW1xrCdQbW2gIFpgw5GEw00= -github.com/metacubex/sing-shadowsocks v0.2.9/go.mod h1:CJSEGO4FWQAWe+ZiLZxCweGdjRR60A61SIoVjdjQeBA= -github.com/metacubex/sing-shadowsocks2 v0.2.3 h1:v3rNS/5Ywh0NIZ6VU/NmdERQIN5RePzyxCFeQsU4Cx0= -github.com/metacubex/sing-shadowsocks2 v0.2.3/go.mod h1:/WNy/Q8ahLCoPRriWuFZFD0Jy+JNp1MEQl28Zw6SaF8= -github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI= -github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= -github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6 h1:TAwL91XPa6x1QK55CRm+VTzPvLPUfEr/uFDnOZArqEU= -github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U= -github.com/metacubex/sing-vmess v0.2.1 h1:I6gM3VUjtvJ15D805EUbNH+SRBuqzJeFnuIbKYUsWZ0= -github.com/metacubex/sing-vmess v0.2.1/go.mod h1:DsODWItJtOMZNna8Qhheg8r3tUivrcO3vWgaTYKnfTo= -github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU= -github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80= -github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo= -github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE= -github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf h1:LwID1wz4tzypidd412dd4dC1H0m1TgRCQ/XvRvMJDFM= -github.com/metacubex/tfo-go v0.0.0-20250503140532-decbcfccbfdf/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= -github.com/metacubex/utls v1.7.0-alpha.3 h1:cp1cEMUnoifiWrGHRzo+nCwPRveN9yPD8QaRFmfcYxA= -github.com/metacubex/utls v1.7.0-alpha.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU= -github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= -github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= -github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= -github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= -github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= -github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= -github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= -github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= -github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I= -github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do= -github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= -github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= -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/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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= -github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= -github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= -github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= -github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= -github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= -github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= -github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= -github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= -github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= -github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= -github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU= -github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo= -github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8= -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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -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.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= -github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= -gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs= -gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= -gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= -go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= -golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -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-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= -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= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -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= -lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= -lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/hub/executor/concurrent_load_limit.go b/hub/executor/concurrent_load_limit.go deleted file mode 100644 index 45f5ce9f27..0000000000 --- a/hub/executor/concurrent_load_limit.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !386 && !amd64 && !arm64 && !arm64be && !mipsle && !mips - -package executor - -const concurrentCount = 5 diff --git a/hub/executor/concurrent_load_single.go b/hub/executor/concurrent_load_single.go deleted file mode 100644 index 603d38f160..0000000000 --- a/hub/executor/concurrent_load_single.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build mips || mipsle - -package executor - -const concurrentCount = 1 diff --git a/hub/executor/concurrent_load_unlimit.go b/hub/executor/concurrent_load_unlimit.go deleted file mode 100644 index fded7e5649..0000000000 --- a/hub/executor/concurrent_load_unlimit.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build 386 || amd64 || arm64 || arm64be - -package executor - -import "math" - -const concurrentCount = math.MaxInt diff --git a/hub/executor/executor.go b/hub/executor/executor.go deleted file mode 100644 index 386be37a4a..0000000000 --- a/hub/executor/executor.go +++ /dev/null @@ -1,532 +0,0 @@ -package executor - -import ( - "fmt" - "net" - "net/netip" - "os" - "runtime" - "strconv" - "sync" - "time" - _ "unsafe" - - "github.com/metacubex/mihomo/adapter" - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/adapter/outboundgroup" - "github.com/metacubex/mihomo/component/auth" - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/geodata" - mihomoHttp "github.com/metacubex/mihomo/component/http" - "github.com/metacubex/mihomo/component/iface" - "github.com/metacubex/mihomo/component/keepalive" - "github.com/metacubex/mihomo/component/profile" - "github.com/metacubex/mihomo/component/profile/cachefile" - "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/component/resource" - "github.com/metacubex/mihomo/component/sniffer" - tlsC "github.com/metacubex/mihomo/component/tls" - "github.com/metacubex/mihomo/component/trie" - "github.com/metacubex/mihomo/component/updater" - "github.com/metacubex/mihomo/config" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/dns" - "github.com/metacubex/mihomo/listener" - authStore "github.com/metacubex/mihomo/listener/auth" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/inner" - "github.com/metacubex/mihomo/listener/tproxy" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/ntp" - "github.com/metacubex/mihomo/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 without ExternalController -func ApplyConfig(cfg *config.Config, force bool) { - mux.Lock() - defer mux.Unlock() - log.SetLevel(cfg.General.LogLevel) - - tunnel.OnSuspend() - - ca.ResetCertificate() - for _, c := range cfg.TLS.CustomTrustCert { - if err := ca.AddCertificate(c); err != nil { - log.Warnln("%s\nadd error: %s", c, err.Error()) - } - } - - updateExperimental(cfg.Experimental) - updateUsers(cfg.Users) - updateProxies(cfg.Proxies, cfg.Providers) - updateRules(cfg.Rules, cfg.SubRules, cfg.RuleProviders) - updateSniffer(cfg.Sniffer) - updateHosts(cfg.Hosts) - updateGeneral(cfg.General, true) - updateNTP(cfg.NTP) - updateDNS(cfg.DNS, cfg.General.IPv6) - updateListeners(cfg.General, cfg.Listeners, force) - updateTun(cfg.General) // tun should not care "force" - updateIPTables(cfg) - updateTunnels(cfg.Tunnels) - - tunnel.OnInnerLoading() - - initInnerTcp() - loadProvider(cfg.Providers) - updateProfile(cfg) - loadProvider(cfg.RuleProviders) - runtime.GC() - tunnel.OnRunning() - updateUpdater(cfg) - - resolver.ResetConnection() -} - -func initInnerTcp() { - inner.New(tunnel.Tunnel) -} - -func GetGeneral() *config.General { - ports := listener.GetPorts() - var authenticator []string - if auth := authStore.Default.Authenticator(); auth != nil { - authenticator = auth.Users() - } - - general := &config.General{ - Inbound: config.Inbound{ - Port: ports.Port, - SocksPort: ports.SocksPort, - RedirPort: ports.RedirPort, - TProxyPort: ports.TProxyPort, - MixedPort: ports.MixedPort, - Tun: listener.GetTunConf(), - TuicServer: listener.GetTuicConf(), - ShadowSocksConfig: ports.ShadowSocksConfig, - VmessConfig: ports.VmessConfig, - Authentication: authenticator, - SkipAuthPrefixes: inbound.SkipAuthPrefixes(), - LanAllowedIPs: inbound.AllowedIPs(), - LanDisAllowedIPs: inbound.DisAllowedIPs(), - AllowLan: listener.AllowLan(), - BindAddress: listener.BindAddress(), - InboundTfo: inbound.Tfo(), - InboundMPTCP: inbound.MPTCP(), - }, - Mode: tunnel.Mode(), - UnifiedDelay: adapter.UnifiedDelay.Load(), - LogLevel: log.Level(), - IPv6: !resolver.DisableIPv6, - Interface: dialer.DefaultInterface.Load(), - RoutingMark: int(dialer.DefaultRoutingMark.Load()), - GeoXUrl: config.GeoXUrl{ - GeoIp: geodata.GeoIpUrl(), - Mmdb: geodata.MmdbUrl(), - ASN: geodata.ASNUrl(), - GeoSite: geodata.GeoSiteUrl(), - }, - GeoAutoUpdate: updater.GeoAutoUpdate(), - GeoUpdateInterval: updater.GeoUpdateInterval(), - GeodataMode: geodata.GeodataMode(), - GeodataLoader: geodata.LoaderName(), - GeositeMatcher: geodata.SiteMatcherName(), - TCPConcurrent: dialer.GetTcpConcurrent(), - FindProcessMode: tunnel.FindProcessMode(), - Sniffing: tunnel.IsSniffing(), - GlobalClientFingerprint: tlsC.GetGlobalFingerprint(), - GlobalUA: mihomoHttp.UA(), - ETagSupport: resource.ETag(), - KeepAliveInterval: int(keepalive.KeepAliveInterval() / time.Second), - KeepAliveIdle: int(keepalive.KeepAliveIdle() / time.Second), - DisableKeepAlive: keepalive.DisableKeepAlive(), - } - - return general -} - -func updateListeners(general *config.General, listeners map[string]C.InboundListener, force bool) { - listener.PatchInboundListeners(listeners, tunnel.Tunnel, true) - if !force { - return - } - - allowLan := general.AllowLan - listener.SetAllowLan(allowLan) - inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes) - inbound.SetAllowedIPs(general.LanAllowedIPs) - inbound.SetDisAllowedIPs(general.LanDisAllowedIPs) - - bindAddress := general.BindAddress - listener.SetBindAddress(bindAddress) - listener.ReCreateHTTP(general.Port, tunnel.Tunnel) - listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel) - listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel) - listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel) - listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel) - listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel) - listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel) - listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel) -} - -func updateTun(general *config.General) { - listener.ReCreateTun(general.Tun, tunnel.Tunnel) -} - -func updateExperimental(c *config.Experimental) { - if c.QUICGoDisableGSO { - _ = os.Setenv("QUIC_GO_DISABLE_GSO", strconv.FormatBool(true)) - } - if c.QUICGoDisableECN { - _ = os.Setenv("QUIC_GO_DISABLE_ECN", strconv.FormatBool(true)) - } - resolver.SetIP4PEnable(c.IP4PEnable) -} - -func updateNTP(c *config.NTP) { - if c.Enable { - ntp.ReCreateNTPService( - net.JoinHostPort(c.Server, strconv.Itoa(c.Port)), - time.Duration(c.Interval), - c.DialerProxy, - c.WriteToSystem, - ) - } -} - -func updateDNS(c *config.DNS, generalIPv6 bool) { - if !c.Enable { - resolver.DefaultResolver = nil - resolver.DefaultHostMapper = nil - resolver.DefaultLocalServer = nil - resolver.ProxyServerHostResolver = nil - resolver.DirectHostResolver = nil - dns.ReCreateServer("", nil, nil) - return - } - cfg := dns.Config{ - Main: c.NameServer, - Fallback: c.Fallback, - IPv6: c.IPv6 && generalIPv6, - IPv6Timeout: c.IPv6Timeout, - EnhancedMode: c.EnhancedMode, - Pool: c.FakeIPRange, - Hosts: c.Hosts, - FallbackIPFilter: c.FallbackIPFilter, - FallbackDomainFilter: c.FallbackDomainFilter, - Default: c.DefaultNameserver, - Policy: c.NameServerPolicy, - ProxyServer: c.ProxyServerNameserver, - DirectServer: c.DirectNameServer, - DirectFollowPolicy: c.DirectFollowPolicy, - CacheAlgorithm: c.CacheAlgorithm, - } - - 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 - resolver.DefaultLocalServer = dns.NewLocalServer(r.Resolver, m) - resolver.UseSystemHosts = c.UseSystemHosts - - if r.ProxyResolver.Invalid() { - resolver.ProxyServerHostResolver = r.ProxyResolver - } else { - resolver.ProxyServerHostResolver = r.Resolver - } - - if r.DirectResolver.Invalid() { - resolver.DirectHostResolver = r.DirectResolver - } else { - resolver.DirectHostResolver = r.Resolver - } - - dns.ReCreateServer(c.Listen, r.Resolver, m) -} - -func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) { - resolver.DefaultHosts = resolver.NewHosts(tree) -} - -func updateProxies(proxies map[string]C.Proxy, providers map[string]provider.ProxyProvider) { - tunnel.UpdateProxies(proxies, providers) -} - -func updateRules(rules []C.Rule, subRules map[string][]C.Rule, ruleProviders map[string]provider.RuleProvider) { - tunnel.UpdateRules(rules, subRules, ruleProviders) -} - -func loadProvider[P provider.Provider](providers map[string]P) { - load := func(pv P) { - name := pv.Name() - if pv.VehicleType() == provider.Compatible { - log.Infoln("Start initial compatible provider %s", name) - } else { - log.Infoln("Start initial provider %s", name) - } - - if err := pv.Initial(); err != nil { - switch pv.Type() { - case provider.Proxy: - { - log.Errorln("initial proxy provider %s error: %v", name, err) - } - case provider.Rule: - { - log.Errorln("initial rule provider %s error: %v", name, err) - } - } - } - } - - wg := sync.WaitGroup{} - ch := make(chan struct{}, concurrentCount) - for _, pv := range providers { - pv := pv - wg.Add(1) - ch <- struct{}{} - go func() { - defer func() { <-ch; wg.Done() }() - load(pv) - }() - } - wg.Wait() -} - -func updateSniffer(snifferConfig *sniffer.Config) { - dispatcher, err := sniffer.NewDispatcher(snifferConfig) - if err != nil { - log.Warnln("initial sniffer failed, err:%v", err) - } - - tunnel.UpdateSniffer(dispatcher) - - if snifferConfig.Enable { - log.Infoln("Sniffer is loaded and working") - } else { - log.Infoln("Sniffer is closed") - } -} - -func updateTunnels(tunnels []LC.Tunnel) { - listener.PatchTunnel(tunnels, tunnel.Tunnel) -} - -func updateUpdater(cfg *config.Config) { - general := cfg.General - updater.SetGeoAutoUpdate(general.GeoAutoUpdate) - updater.SetGeoUpdateInterval(general.GeoUpdateInterval) - - controller := cfg.Controller - updater.DefaultUiUpdater = updater.NewUiUpdater(controller.ExternalUI, controller.ExternalUIURL, controller.ExternalUIName) - updater.DefaultUiUpdater.AutoDownloadUI() -} - -//go:linkname temporaryUpdateGeneral github.com/metacubex/mihomo/config.temporaryUpdateGeneral -func temporaryUpdateGeneral(general *config.General) func() { - oldGeneral := GetGeneral() - updateGeneral(general, false) - return func() { - updateGeneral(oldGeneral, false) - } -} - -func updateGeneral(general *config.General, logging bool) { - tunnel.SetMode(general.Mode) - tunnel.SetFindProcessMode(general.FindProcessMode) - resolver.DisableIPv6 = !general.IPv6 - - dialer.SetTcpConcurrent(general.TCPConcurrent) - if logging && general.TCPConcurrent { - log.Infoln("Use tcp concurrent") - } - - inbound.SetTfo(general.InboundTfo) - inbound.SetMPTCP(general.InboundMPTCP) - - keepalive.SetKeepAliveIdle(time.Duration(general.KeepAliveIdle) * time.Second) - keepalive.SetKeepAliveInterval(time.Duration(general.KeepAliveInterval) * time.Second) - keepalive.SetDisableKeepAlive(general.DisableKeepAlive) - - adapter.UnifiedDelay.Store(general.UnifiedDelay) - - dialer.DefaultInterface.Store(general.Interface) - dialer.DefaultRoutingMark.Store(int32(general.RoutingMark)) - if logging && general.RoutingMark > 0 { - log.Infoln("Use routing mark: %#x", general.RoutingMark) - } - - iface.FlushCache() - - geodata.SetGeodataMode(general.GeodataMode) - geodata.SetLoader(general.GeodataLoader) - geodata.SetSiteMatcher(general.GeositeMatcher) - geodata.SetGeoIpUrl(general.GeoXUrl.GeoIp) - geodata.SetGeoSiteUrl(general.GeoXUrl.GeoSite) - geodata.SetMmdbUrl(general.GeoXUrl.Mmdb) - geodata.SetASNUrl(general.GeoXUrl.ASN) - mihomoHttp.SetUA(general.GlobalUA) - resource.SetETag(general.ETagSupport) - - tlsC.SetGlobalFingerprint(general.GlobalClientFingerprint) -} - -func updateUsers(users []auth.AuthUser) { - authenticator := auth.NewAuthenticator(users) - authStore.Default.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.(C.Proxy) - if !ok { - continue - } - - selector, ok := outbound.Adapter().(outboundgroup.SelectAble) - if !ok { - continue - } - - selected, exist := mapping[name] - if !exist { - continue - } - - selector.ForceSet(selected) - } -} - -func updateIPTables(cfg *config.Config) { - tproxy.CleanupTProxyIPTables() - - iptables := cfg.IPTables - if runtime.GOOS != "linux" || !iptables.Enable { - return - } - - var err error - defer func() { - if err != nil { - log.Errorln("[IPTABLES] setting iptables failed: %s", err.Error()) - os.Exit(2) - } - }() - - if cfg.General.Tun.Enable { - err = fmt.Errorf("when tun is enabled, iptables cannot be set automatically") - return - } - - var ( - inboundInterface = "lo" - bypass = iptables.Bypass - tProxyPort = cfg.General.TProxyPort - dnsCfg = cfg.DNS - DnsRedirect = iptables.DnsRedirect - - dnsPort netip.AddrPort - ) - - if tProxyPort == 0 { - err = fmt.Errorf("tproxy-port must be greater than zero") - return - } - - if DnsRedirect { - if !dnsCfg.Enable { - err = fmt.Errorf("DNS server must be enable") - return - } - - dnsPort, err = netip.ParseAddrPort(dnsCfg.Listen) - if err != nil { - err = fmt.Errorf("DNS server must be correct") - return - } - } - - if iptables.InboundInterface != "" { - inboundInterface = iptables.InboundInterface - } - - dialer.DefaultRoutingMark.CompareAndSwap(0, 2158) - - err = tproxy.SetTProxyIPTables(inboundInterface, bypass, uint16(tProxyPort), DnsRedirect, dnsPort.Port()) - if err != nil { - return - } - - log.Infoln("[IPTABLES] Setting iptables completed") -} - -func Shutdown() { - listener.Cleanup() - tproxy.CleanupTProxyIPTables() - resolver.StoreFakePoolState() - - log.Warnln("Mihomo shutting down") -} diff --git a/hub/hub.go b/hub/hub.go deleted file mode 100644 index 69f627ffa8..0000000000 --- a/hub/hub.go +++ /dev/null @@ -1,90 +0,0 @@ -package hub - -import ( - "github.com/metacubex/mihomo/config" - "github.com/metacubex/mihomo/hub/executor" - "github.com/metacubex/mihomo/hub/route" - "github.com/metacubex/mihomo/log" -) - -type Option func(*config.Config) - -func WithExternalUI(externalUI string) Option { - return func(cfg *config.Config) { - cfg.Controller.ExternalUI = externalUI - } -} - -func WithExternalController(externalController string) Option { - return func(cfg *config.Config) { - cfg.Controller.ExternalController = externalController - } -} - -func WithExternalControllerUnix(externalControllerUnix string) Option { - return func(cfg *config.Config) { - cfg.Controller.ExternalControllerUnix = externalControllerUnix - } -} - -func WithExternalControllerPipe(externalControllerPipe string) Option { - return func(cfg *config.Config) { - cfg.Controller.ExternalControllerPipe = externalControllerPipe - } -} - -func WithSecret(secret string) Option { - return func(cfg *config.Config) { - cfg.Controller.Secret = secret - } -} - -// ApplyConfig dispatch configure to all parts include ExternalController -func ApplyConfig(cfg *config.Config) { - applyRoute(cfg) - executor.ApplyConfig(cfg, true) -} - -func applyRoute(cfg *config.Config) { - if cfg.Controller.ExternalUI != "" { - route.SetUIPath(cfg.Controller.ExternalUI) - } - route.ReCreateServer(&route.Config{ - Addr: cfg.Controller.ExternalController, - TLSAddr: cfg.Controller.ExternalControllerTLS, - UnixAddr: cfg.Controller.ExternalControllerUnix, - PipeAddr: cfg.Controller.ExternalControllerPipe, - Secret: cfg.Controller.Secret, - Certificate: cfg.TLS.Certificate, - PrivateKey: cfg.TLS.PrivateKey, - DohServer: cfg.Controller.ExternalDohServer, - IsDebug: cfg.General.LogLevel == log.DEBUG, - Cors: route.Cors{ - AllowOrigins: cfg.Controller.Cors.AllowOrigins, - AllowPrivateNetwork: cfg.Controller.Cors.AllowPrivateNetwork, - }, - }) -} - -// Parse call at the beginning of mihomo -func Parse(configBytes []byte, options ...Option) error { - var cfg *config.Config - var err error - - if len(configBytes) != 0 { - cfg, err = executor.ParseWithBytes(configBytes) - } else { - cfg, err = executor.Parse() - } - - if err != nil { - return err - } - - for _, option := range options { - option(cfg) - } - - ApplyConfig(cfg) - return nil -} diff --git a/hub/route/cache.go b/hub/route/cache.go deleted file mode 100644 index f07eb33abb..0000000000 --- a/hub/route/cache.go +++ /dev/null @@ -1,26 +0,0 @@ -package route - -import ( - "net/http" - - "github.com/metacubex/mihomo/component/resolver" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" -) - -func cacheRouter() http.Handler { - r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeIPPool) - return r -} - -func flushFakeIPPool(w http.ResponseWriter, r *http.Request) { - err := resolver.FlushFakeIP() - if err != nil { - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, newError(err.Error())) - return - } - render.NoContent(w, r) -} diff --git a/hub/route/common.go b/hub/route/common.go deleted file mode 100644 index d0053e674b..0000000000 --- a/hub/route/common.go +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index c387c9498c..0000000000 --- a/hub/route/configs.go +++ /dev/null @@ -1,412 +0,0 @@ -package route - -import ( - "net/http" - "net/netip" - "path/filepath" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/component/updater" - "github.com/metacubex/mihomo/config" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/hub/executor" - P "github.com/metacubex/mihomo/listener" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/tunnel" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" -) - -func configRouter() http.Handler { - r := chi.NewRouter() - r.Get("/", getConfigs) - if !embedMode { // disallow update/patch configs in embed mode - r.Put("/", updateConfigs) - r.Post("/geo", updateGeoDatabases) - r.Patch("/", patchConfigs) - } - return r -} - -type configSchema 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"` - Tun *tunSchema `json:"tun"` - TuicServer *tuicServerSchema `json:"tuic-server"` - ShadowSocksConfig *string `json:"ss-config"` - VmessConfig *string `json:"vmess-config"` - TcptunConfig *string `json:"tcptun-config"` - UdptunConfig *string `json:"udptun-config"` - AllowLan *bool `json:"allow-lan"` - SkipAuthPrefixes *[]netip.Prefix `json:"skip-auth-prefixes"` - LanAllowedIPs *[]netip.Prefix `json:"lan-allowed-ips"` - LanDisAllowedIPs *[]netip.Prefix `json:"lan-disallowed-ips"` - BindAddress *string `json:"bind-address"` - Mode *tunnel.TunnelMode `json:"mode"` - LogLevel *log.LogLevel `json:"log-level"` - IPv6 *bool `json:"ipv6"` - Sniffing *bool `json:"sniffing"` - TcpConcurrent *bool `json:"tcp-concurrent"` - InterfaceName *string `json:"interface-name"` -} - -type tunSchema struct { - Enable bool `yaml:"enable" json:"enable"` - Device *string `yaml:"device" json:"device"` - Stack *C.TUNStack `yaml:"stack" json:"stack"` - DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"` - AutoRoute *bool `yaml:"auto-route" json:"auto-route"` - AutoDetectInterface *bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` - - MTU *uint32 `yaml:"mtu" json:"mtu,omitempty"` - GSO *bool `yaml:"gso" json:"gso,omitempty"` - GSOMaxSize *uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` - //Inet4Address *[]netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` - Inet6Address *[]netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` - IPRoute2TableIndex *int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` - IPRoute2RuleIndex *int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` - AutoRedirect *bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` - AutoRedirectInputMark *uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` - AutoRedirectOutputMark *uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` - StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` - RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` - RouteAddressSet *[]string `yaml:"route-address-set" json:"route-address-set,omitempty"` - RouteExcludeAddress *[]netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` - RouteExcludeAddressSet *[]string `yaml:"route-exclude-address-set" json:"route-exclude-address-set,omitempty"` - IncludeInterface *[]string `yaml:"include-interface" json:"include-interface,omitempty"` - ExcludeInterface *[]string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` - IncludeUID *[]uint32 `yaml:"include-uid" json:"include-uid,omitempty"` - IncludeUIDRange *[]string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` - ExcludeUID *[]uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` - ExcludeUIDRange *[]string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` - IncludeAndroidUser *[]int `yaml:"include-android-user" json:"include-android-user,omitempty"` - IncludePackage *[]string `yaml:"include-package" json:"include-package,omitempty"` - ExcludePackage *[]string `yaml:"exclude-package" json:"exclude-package,omitempty"` - EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` - UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` - FileDescriptor *int `yaml:"file-descriptor" json:"file-descriptor"` - - Inet4RouteAddress *[]netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` - Inet6RouteAddress *[]netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` - Inet4RouteExcludeAddress *[]netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"` - Inet6RouteExcludeAddress *[]netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"` -} - -type tuicServerSchema struct { - Enable bool `yaml:"enable" json:"enable"` - Listen *string `yaml:"listen" json:"listen"` - Token *[]string `yaml:"token" json:"token"` - Users *map[string]string `yaml:"users" json:"users,omitempty"` - Certificate *string `yaml:"certificate" json:"certificate"` - PrivateKey *string `yaml:"private-key" json:"private-key"` - CongestionController *string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` - MaxIdleTime *int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` - AuthenticationTimeout *int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` - ALPN *[]string `yaml:"alpn" json:"alpn,omitempty"` - MaxUdpRelayPacketSize *int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` - CWND *int `yaml:"cwnd" json:"cwnd,omitempty"` -} - -func getConfigs(w http.ResponseWriter, r *http.Request) { - general := executor.GetGeneral() - render.JSON(w, r, general) -} - -func pointerOrDefault[T any](p *T, def T) T { - if p != nil { - return *p - } - return def -} - -func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { - if p != nil { - def.Enable = p.Enable - if p.Device != nil { - def.Device = *p.Device - } - if p.Stack != nil { - def.Stack = *p.Stack - } - if p.DNSHijack != nil { - def.DNSHijack = *p.DNSHijack - } - if p.AutoRoute != nil { - def.AutoRoute = *p.AutoRoute - } - if p.AutoDetectInterface != nil { - def.AutoDetectInterface = *p.AutoDetectInterface - } - if p.MTU != nil { - def.MTU = *p.MTU - } - if p.GSO != nil { - def.GSO = *p.GSO - } - if p.GSOMaxSize != nil { - def.GSOMaxSize = *p.GSOMaxSize - } - //if p.Inet4Address != nil { - // def.Inet4Address = *p.Inet4Address - //} - if p.Inet6Address != nil { - def.Inet6Address = *p.Inet6Address - } - if p.IPRoute2TableIndex != nil { - def.IPRoute2TableIndex = *p.IPRoute2TableIndex - } - if p.IPRoute2RuleIndex != nil { - def.IPRoute2RuleIndex = *p.IPRoute2RuleIndex - } - if p.AutoRedirect != nil { - def.AutoRedirect = *p.AutoRedirect - } - if p.AutoRedirectInputMark != nil { - def.AutoRedirectInputMark = *p.AutoRedirectInputMark - } - if p.AutoRedirectOutputMark != nil { - def.AutoRedirectOutputMark = *p.AutoRedirectOutputMark - } - if p.StrictRoute != nil { - def.StrictRoute = *p.StrictRoute - } - if p.RouteAddress != nil { - def.RouteAddress = *p.RouteAddress - } - if p.RouteAddressSet != nil { - def.RouteAddressSet = *p.RouteAddressSet - } - if p.RouteExcludeAddress != nil { - def.RouteExcludeAddress = *p.RouteExcludeAddress - } - if p.RouteExcludeAddressSet != nil { - def.RouteExcludeAddressSet = *p.RouteExcludeAddressSet - } - if p.Inet4RouteAddress != nil { - def.Inet4RouteAddress = *p.Inet4RouteAddress - } - if p.Inet6RouteAddress != nil { - def.Inet6RouteAddress = *p.Inet6RouteAddress - } - if p.Inet4RouteExcludeAddress != nil { - def.Inet4RouteExcludeAddress = *p.Inet4RouteExcludeAddress - } - if p.Inet6RouteExcludeAddress != nil { - def.Inet6RouteExcludeAddress = *p.Inet6RouteExcludeAddress - } - if p.IncludeInterface != nil { - def.IncludeInterface = *p.IncludeInterface - } - if p.ExcludeInterface != nil { - def.ExcludeInterface = *p.ExcludeInterface - } - if p.IncludeUID != nil { - def.IncludeUID = *p.IncludeUID - } - if p.IncludeUIDRange != nil { - def.IncludeUIDRange = *p.IncludeUIDRange - } - if p.ExcludeUID != nil { - def.ExcludeUID = *p.ExcludeUID - } - if p.ExcludeUIDRange != nil { - def.ExcludeUIDRange = *p.ExcludeUIDRange - } - if p.IncludeAndroidUser != nil { - def.IncludeAndroidUser = *p.IncludeAndroidUser - } - if p.IncludePackage != nil { - def.IncludePackage = *p.IncludePackage - } - if p.ExcludePackage != nil { - def.ExcludePackage = *p.ExcludePackage - } - if p.EndpointIndependentNat != nil { - def.EndpointIndependentNat = *p.EndpointIndependentNat - } - if p.UDPTimeout != nil { - def.UDPTimeout = *p.UDPTimeout - } - if p.FileDescriptor != nil { - def.FileDescriptor = *p.FileDescriptor - } - } - return def -} - -func pointerOrDefaultTuicServer(p *tuicServerSchema, def LC.TuicServer) LC.TuicServer { - if p != nil { - def.Enable = p.Enable - if p.Listen != nil { - def.Listen = *p.Listen - } - if p.Token != nil { - def.Token = *p.Token - } - if p.Users != nil { - def.Users = *p.Users - } - if p.Certificate != nil { - def.Certificate = *p.Certificate - } - if p.PrivateKey != nil { - def.PrivateKey = *p.PrivateKey - } - if p.CongestionController != nil { - def.CongestionController = *p.CongestionController - } - if p.MaxIdleTime != nil { - def.MaxIdleTime = *p.MaxIdleTime - } - if p.AuthenticationTimeout != nil { - def.AuthenticationTimeout = *p.AuthenticationTimeout - } - if p.ALPN != nil { - def.ALPN = *p.ALPN - } - if p.MaxUdpRelayPacketSize != nil { - def.MaxUdpRelayPacketSize = *p.MaxUdpRelayPacketSize - } - if p.CWND != nil { - def.CWND = *p.CWND - } - } - return def -} - -func patchConfigs(w http.ResponseWriter, r *http.Request) { - general := &configSchema{} - if err := render.DecodeJSON(r.Body, &general); err != nil { - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, ErrBadRequest) - return - } - - if general.AllowLan != nil { - P.SetAllowLan(*general.AllowLan) - } - - if general.SkipAuthPrefixes != nil { - inbound.SetSkipAuthPrefixes(*general.SkipAuthPrefixes) - } - - if general.LanAllowedIPs != nil { - inbound.SetAllowedIPs(*general.LanAllowedIPs) - } - - if general.LanDisAllowedIPs != nil { - inbound.SetDisAllowedIPs(*general.LanDisAllowedIPs) - } - - if general.BindAddress != nil { - P.SetBindAddress(*general.BindAddress) - } - - if general.Sniffing != nil { - tunnel.SetSniffing(*general.Sniffing) - } - - if general.TcpConcurrent != nil { - dialer.SetTcpConcurrent(*general.TcpConcurrent) - } - - if general.InterfaceName != nil { - dialer.DefaultInterface.Store(*general.InterfaceName) - } - - ports := P.GetPorts() - - P.ReCreateHTTP(pointerOrDefault(general.Port, ports.Port), tunnel.Tunnel) - P.ReCreateSocks(pointerOrDefault(general.SocksPort, ports.SocksPort), tunnel.Tunnel) - P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tunnel.Tunnel) - P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tunnel.Tunnel) - P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tunnel.Tunnel) - P.ReCreateTun(pointerOrDefaultTun(general.Tun, P.LastTunConf), tunnel.Tunnel) - P.ReCreateShadowSocks(pointerOrDefault(general.ShadowSocksConfig, ports.ShadowSocksConfig), tunnel.Tunnel) - P.ReCreateVmess(pointerOrDefault(general.VmessConfig, ports.VmessConfig), tunnel.Tunnel) - P.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, P.LastTuicConf), tunnel.Tunnel) - - if general.Mode != nil { - tunnel.SetMode(*general.Mode) - } - - if general.LogLevel != nil { - log.SetLevel(*general.LogLevel) - } - - if general.IPv6 != nil { - resolver.DisableIPv6 = !*general.IPv6 - } - - 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 == "" { // default path unneeded any safe check - req.Path = C.Path.Config() - } else { - if !filepath.IsAbs(req.Path) { - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, newError("path is not a absolute path")) - return - } - - if !C.Path.IsSafePath(req.Path) { - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, newError(C.Path.ErrNotSafePath(req.Path).Error())) - 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) -} - -func updateGeoDatabases(w http.ResponseWriter, r *http.Request) { - err := updater.UpdateGeoDatabases() - if err != nil { - log.Errorln("[GEO] update GEO databases failed: %v", err) - render.Status(r, http.StatusInternalServerError) - render.JSON(w, r, newError(err.Error())) - return - } - - render.NoContent(w, r) -} diff --git a/hub/route/connections.go b/hub/route/connections.go deleted file mode 100644 index e0ff242648..0000000000 --- a/hub/route/connections.go +++ /dev/null @@ -1,89 +0,0 @@ -package route - -import ( - "bytes" - "encoding/json" - "net/http" - "strconv" - "time" - - "github.com/metacubex/mihomo/tunnel/statistic" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" -) - -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 !(r.Header.Get("Upgrade") == "websocket") { - snapshot := statistic.DefaultManager.Snapshot() - render.JSON(w, r, snapshot) - return - } - - conn, _, _, err := ws.UpgradeHTTP(r, w) - 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 := &bytes.Buffer{} - sendSnapshot := func() error { - buf.Reset() - snapshot := statistic.DefaultManager.Snapshot() - if err := json.NewEncoder(buf).Encode(snapshot); err != nil { - return err - } - - return wsutil.WriteMessage(conn, ws.StateServerSide, ws.OpText, 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") - if c := statistic.DefaultManager.Get(id); c != nil { - _ = c.Close() - } - render.NoContent(w, r) -} - -func closeAllConnections(w http.ResponseWriter, r *http.Request) { - statistic.DefaultManager.Range(func(c statistic.Tracker) bool { - _ = c.Close() - return true - }) - render.NoContent(w, r) -} diff --git a/hub/route/ctxkeys.go b/hub/route/ctxkeys.go deleted file mode 100644 index 6883b208f8..0000000000 --- a/hub/route/ctxkeys.go +++ /dev/null @@ -1,14 +0,0 @@ -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 "mihomo context key " + string(c) -} diff --git a/hub/route/dns.go b/hub/route/dns.go deleted file mode 100644 index 1762c94719..0000000000 --- a/hub/route/dns.go +++ /dev/null @@ -1,82 +0,0 @@ -package route - -import ( - "context" - "math" - "net/http" - - "github.com/metacubex/mihomo/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/doh.go b/hub/route/doh.go deleted file mode 100644 index bb5416c436..0000000000 --- a/hub/route/doh.go +++ /dev/null @@ -1,63 +0,0 @@ -package route - -import ( - "context" - "encoding/base64" - "io" - "net/http" - - "github.com/metacubex/mihomo/component/resolver" - - "github.com/go-chi/render" -) - -func dohRouter() http.Handler { - return http.HandlerFunc(dohHandler) -} - -func dohHandler(w http.ResponseWriter, r *http.Request) { - if resolver.DefaultResolver == nil { - render.Status(r, http.StatusInternalServerError) - render.PlainText(w, r, "DNS section is disabled") - return - } - - var dnsData []byte - var err error - switch r.Method { - case "GET": - dnsData, err = base64.RawURLEncoding.DecodeString(r.URL.Query().Get("dns")) - case "POST": - if r.Header.Get("Content-Type") != "application/dns-message" { - render.Status(r, http.StatusInternalServerError) - render.PlainText(w, r, "invalid content-type") - return - } - reader := io.LimitReader(r.Body, 65535) // according to rfc8484, the maximum size of the DNS message is 65535 bytes - dnsData, err = io.ReadAll(reader) - _ = r.Body.Close() - default: - render.Status(r, http.StatusMethodNotAllowed) - render.PlainText(w, r, "method not allowed") - return - } - if err != nil { - render.Status(r, http.StatusInternalServerError) - render.PlainText(w, r, err.Error()) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) - defer cancel() - - dnsData, err = resolver.RelayDnsPacket(ctx, dnsData, dnsData) - if err != nil { - render.Status(r, http.StatusInternalServerError) - render.PlainText(w, r, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/dns-message") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(dnsData) -} diff --git a/hub/route/errors.go b/hub/route/errors.go deleted file mode 100644 index b469e107f0..0000000000 --- a/hub/route/errors.go +++ /dev/null @@ -1,22 +0,0 @@ -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/external.go b/hub/route/external.go deleted file mode 100644 index d2f0635822..0000000000 --- a/hub/route/external.go +++ /dev/null @@ -1,21 +0,0 @@ -package route - -import "github.com/go-chi/chi/v5" - -type externalRouter func(r chi.Router) - -var externalRouters = make([]externalRouter, 0) - -func Register(route ...externalRouter) { - externalRouters = append(externalRouters, route...) -} - -func addExternalRouters(r chi.Router) { - if len(externalRouters) == 0 { - return - } - - for _, caller := range externalRouters { - caller(r) - } -} diff --git a/hub/route/groups.go b/hub/route/groups.go deleted file mode 100644 index 873a94df51..0000000000 --- a/hub/route/groups.go +++ /dev/null @@ -1,94 +0,0 @@ -package route - -import ( - "context" - "net/http" - "strconv" - "time" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - - "github.com/metacubex/mihomo/adapter/outboundgroup" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/profile/cachefile" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/tunnel" -) - -func groupRouter() http.Handler { - r := chi.NewRouter() - r.Get("/", getGroups) - - r.Route("/{name}", func(r chi.Router) { - r.Use(parseProxyName, findProxyByName) - r.Get("/", getGroup) - r.Get("/delay", getGroupDelay) - }) - return r -} - -func getGroups(w http.ResponseWriter, r *http.Request) { - var gs []C.Proxy - for _, p := range tunnel.Proxies() { - if _, ok := p.Adapter().(C.Group); ok { - gs = append(gs, p) - } - } - render.JSON(w, r, render.M{ - "proxies": gs, - }) -} - -func getGroup(w http.ResponseWriter, r *http.Request) { - proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) - if _, ok := proxy.Adapter().(C.Group); ok { - render.JSON(w, r, proxy) - return - } - render.Status(r, http.StatusNotFound) - render.JSON(w, r, ErrNotFound) -} - -func getGroupDelay(w http.ResponseWriter, r *http.Request) { - proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) - group, ok := proxy.Adapter().(C.Group) - if !ok { - render.Status(r, http.StatusNotFound) - render.JSON(w, r, ErrNotFound) - return - } - - if selectAble, ok := proxy.Adapter().(outboundgroup.SelectAble); ok && proxy.Type() != C.Selector { - selectAble.ForceSet("") - cachefile.Cache().SetSelected(proxy.Name(), "") - } - - query := r.URL.Query() - url := query.Get("url") - timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32) - if err != nil { - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, ErrBadRequest) - return - } - - expectedStatus, err := utils.NewUnsignedRanges[uint16](query.Get("expected")) - if err != nil { - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, ErrBadRequest) - return - } - - ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond*time.Duration(timeout)) - defer cancel() - - dm, err := group.URLTest(ctx, url, expectedStatus) - if err != nil { - render.Status(r, http.StatusGatewayTimeout) - render.JSON(w, r, newError(err.Error())) - return - } - - render.JSON(w, r, dm) -} diff --git a/hub/route/patch_android.go b/hub/route/patch_android.go deleted file mode 100644 index 5eba279646..0000000000 --- a/hub/route/patch_android.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build android && cmfa - -package route - -func init() { - SetEmbedMode(true) // set embed mode default -} diff --git a/hub/route/provider.go b/hub/route/provider.go deleted file mode 100644 index a8611a7948..0000000000 --- a/hub/route/provider.go +++ /dev/null @@ -1,162 +0,0 @@ -package route - -import ( - "context" - "net/http" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/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)) - }) -} - -func ruleProviderRouter() http.Handler { - r := chi.NewRouter() - r.Get("/", getRuleProviders) - r.Route("/{name}", func(r chi.Router) { - r.Use(parseRuleProviderName, findRuleProviderByName) - r.Put("/", updateRuleProvider) - }) - return r -} - -func getRuleProviders(w http.ResponseWriter, r *http.Request) { - ruleProviders := tunnel.RuleProviders() - render.JSON(w, r, render.M{ - "providers": ruleProviders, - }) -} - -func updateRuleProvider(w http.ResponseWriter, r *http.Request) { - provider := r.Context().Value(CtxKeyProvider).(provider.RuleProvider) - if err := provider.Update(); err != nil { - render.Status(r, http.StatusServiceUnavailable) - render.JSON(w, r, newError(err.Error())) - return - } - render.NoContent(w, r) -} - -func parseRuleProviderName(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - name := getEscapeParam(r, "name") - ctx := context.WithValue(r.Context(), CtxKeyProviderName, name) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func findRuleProviderByName(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - name := r.Context().Value(CtxKeyProviderName).(string) - providers := tunnel.RuleProviders() - 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)) - }) -} diff --git a/hub/route/proxies.go b/hub/route/proxies.go deleted file mode 100644 index ba4e03f902..0000000000 --- a/hub/route/proxies.go +++ /dev/null @@ -1,160 +0,0 @@ -package route - -import ( - "context" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/metacubex/mihomo/adapter/outboundgroup" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/profile/cachefile" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/tunnel" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" -) - -var ( - SwitchProxiesCallback func(sGroup string, sProxy string) -) - -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) - r.Delete("/", unfixedProxy) - }) - 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.ProxiesWithProviders() - 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.ProxiesWithProviders() - 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).(C.Proxy) - selector, ok := proxy.Adapter().(outboundgroup.SelectAble) - 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) - if SwitchProxiesCallback != nil { - // refresh tray menu - go SwitchProxiesCallback(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 - } - - expectedStatus, err := utils.NewUnsignedRanges[uint16](query.Get("expected")) - 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, err := proxy.URLTest(ctx, url, expectedStatus) - 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) - if err != nil && delay != 0 { - render.JSON(w, r, err) - } else { - render.JSON(w, r, newError("An error occurred in the delay test")) - } - return - } - - render.JSON(w, r, render.M{ - "delay": delay, - }) -} - -func unfixedProxy(w http.ResponseWriter, r *http.Request) { - proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) - if selectAble, ok := proxy.Adapter().(outboundgroup.SelectAble); ok && proxy.Type() != C.Selector { - selectAble.ForceSet("") - cachefile.Cache().SetSelected(proxy.Name(), "") - render.NoContent(w, r) - return - } - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, ErrBadRequest) -} diff --git a/hub/route/restart.go b/hub/route/restart.go deleted file mode 100644 index 49d7e51703..0000000000 --- a/hub/route/restart.go +++ /dev/null @@ -1,67 +0,0 @@ -package route - -import ( - "fmt" - "net/http" - "os" - "os/exec" - "runtime" - "syscall" - - "github.com/metacubex/mihomo/hub/executor" - "github.com/metacubex/mihomo/log" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" -) - -func restartRouter() http.Handler { - r := chi.NewRouter() - r.Post("/", restart) - return r -} - -func restart(w http.ResponseWriter, r *http.Request) { - // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/home/controlupdate.go#L108 - execPath, err := os.Executable() - if err != nil { - render.Status(r, http.StatusInternalServerError) - render.JSON(w, r, newError(fmt.Sprintf("getting path: %s", err))) - return - } - - render.JSON(w, r, render.M{"status": "ok"}) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - - // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/home/controlupdate.go#L180 - // The background context is used because the underlying functions wrap it - // with timeout and shut down the server, which handles current request. It - // also should be done in a separate goroutine for the same reason. - go restartExecutable(execPath) -} - -func restartExecutable(execPath string) { - var err error - executor.Shutdown() - if runtime.GOOS == "windows" { - cmd := exec.Command(execPath, os.Args[1:]...) - log.Infoln("restarting: %q %q", execPath, os.Args[1:]) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err = cmd.Start() - if err != nil { - log.Fatalln("restarting: %s", err) - } - - os.Exit(0) - } - - log.Infoln("restarting: %q %q", execPath, os.Args[1:]) - err = syscall.Exec(execPath, os.Args, os.Environ()) - if err != nil { - log.Fatalln("restarting: %s", err) - } -} diff --git a/hub/route/rules.go b/hub/route/rules.go deleted file mode 100644 index 43d33299a2..0000000000 --- a/hub/route/rules.go +++ /dev/null @@ -1,46 +0,0 @@ -package route - -import ( - "github.com/metacubex/mihomo/constant" - "net/http" - - "github.com/metacubex/mihomo/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"` - Size int `json:"size"` -} - -func getRules(w http.ResponseWriter, r *http.Request) { - rawRules := tunnel.Rules() - rules := []Rule{} - for _, rule := range rawRules { - r := Rule{ - Type: rule.RuleType().String(), - Payload: rule.Payload(), - Proxy: rule.Adapter(), - Size: -1, - } - if rule.RuleType() == constant.GEOIP || rule.RuleType() == constant.GEOSITE { - r.Size = rule.(constant.RuleGroup).GetRecodeSize() - } - rules = append(rules, r) - - } - - render.JSON(w, r, render.M{ - "rules": rules, - }) -} diff --git a/hub/route/server.go b/hub/route/server.go deleted file mode 100644 index 2ccd8596d7..0000000000 --- a/hub/route/server.go +++ /dev/null @@ -1,533 +0,0 @@ -package route - -import ( - "bytes" - "crypto/subtle" - "crypto/tls" - "encoding/json" - "net" - "net/http" - "os" - "path/filepath" - "runtime/debug" - "strings" - "syscall" - "time" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/ca" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/tunnel/statistic" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/render" - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" - "github.com/sagernet/cors" -) - -var ( - uiPath = "" - - httpServer *http.Server - tlsServer *http.Server - unixServer *http.Server - pipeServer *http.Server - - embedMode = false -) - -func SetEmbedMode(embed bool) { - embedMode = embed -} - -type Traffic struct { - Up int64 `json:"up"` - Down int64 `json:"down"` -} - -type Memory struct { - Inuse uint64 `json:"inuse"` - OSLimit uint64 `json:"oslimit"` // maybe we need it in the future -} - -type Config struct { - Addr string - TLSAddr string - UnixAddr string - PipeAddr string - Secret string - Certificate string - PrivateKey string - DohServer string - IsDebug bool - Cors Cors -} - -type Cors struct { - AllowOrigins []string - AllowPrivateNetwork bool -} - -func (c Cors) Apply(r chi.Router) { - r.Use(cors.New(cors.Options{ - AllowedOrigins: c.AllowOrigins, - AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, - AllowedHeaders: []string{"Content-Type", "Authorization"}, - AllowPrivateNetwork: c.AllowPrivateNetwork, - MaxAge: 300, - }).Handler) -} - -func ReCreateServer(cfg *Config) { - go start(cfg) - go startTLS(cfg) - go startUnix(cfg) - if inbound.SupportNamedPipe { - go startPipe(cfg) - } -} - -func SetUIPath(path string) { - uiPath = C.Path.Resolve(path) -} - -func router(isDebug bool, secret string, dohServer string, cors Cors) *chi.Mux { - r := chi.NewRouter() - cors.Apply(r) - if isDebug { - r.Mount("/debug", func() http.Handler { - r := chi.NewRouter() - r.Put("/gc", func(w http.ResponseWriter, r *http.Request) { - debug.FreeOSMemory() - }) - handler := middleware.Profiler - r.Mount("/", handler()) - return r - }()) - } - r.Group(func(r chi.Router) { - if secret != "" { - r.Use(authentication(secret)) - } - r.Get("/", hello) - r.Get("/logs", getLogs) - r.Get("/traffic", traffic) - r.Get("/memory", memory) - r.Get("/version", version) - r.Mount("/configs", configRouter()) - r.Mount("/proxies", proxyRouter()) - r.Mount("/group", groupRouter()) - r.Mount("/rules", ruleRouter()) - r.Mount("/connections", connectionRouter()) - r.Mount("/providers/proxies", proxyProviderRouter()) - r.Mount("/providers/rules", ruleProviderRouter()) - r.Mount("/cache", cacheRouter()) - r.Mount("/dns", dnsRouter()) - if !embedMode { // disallow restart in embed mode - r.Mount("/restart", restartRouter()) - } - r.Mount("/upgrade", upgradeRouter()) - addExternalRouters(r) - - }) - - 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) - }) - }) - } - if len(dohServer) > 0 && dohServer[0] == '/' { - r.Mount(dohServer, dohRouter()) - } - - return r -} - -func start(cfg *Config) { - // first stop existing server - if httpServer != nil { - _ = httpServer.Close() - httpServer = nil - } - - // handle addr - if len(cfg.Addr) > 0 { - l, err := inbound.Listen("tcp", cfg.Addr) - if err != nil { - log.Errorln("External controller listen error: %s", err) - return - } - log.Infoln("RESTful API listening at: %s", l.Addr().String()) - - server := &http.Server{ - Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), - } - httpServer = server - if err = server.Serve(l); err != nil { - log.Errorln("External controller serve error: %s", err) - } - } -} - -func startTLS(cfg *Config) { - // first stop existing server - if tlsServer != nil { - _ = tlsServer.Close() - tlsServer = nil - } - - // handle tlsAddr - if len(cfg.TLSAddr) > 0 { - c, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path) - if err != nil { - log.Errorln("External controller tls listen error: %s", err) - return - } - - l, err := inbound.Listen("tcp", cfg.TLSAddr) - if err != nil { - log.Errorln("External controller tls listen error: %s", err) - return - } - - log.Infoln("RESTful API tls listening at: %s", l.Addr().String()) - server := &http.Server{ - Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{c}, - }, - } - tlsServer = server - if err = server.ServeTLS(l, "", ""); err != nil { - log.Errorln("External controller tls serve error: %s", err) - } - } -} - -func startUnix(cfg *Config) { - // first stop existing server - if unixServer != nil { - _ = unixServer.Close() - unixServer = nil - } - - // handle addr - if len(cfg.UnixAddr) > 0 { - addr := C.Path.Resolve(cfg.UnixAddr) - - dir := filepath.Dir(addr) - if _, err := os.Stat(dir); os.IsNotExist(err) { - if err := os.MkdirAll(dir, 0o755); err != nil { - log.Errorln("External controller unix listen error: %s", err) - return - } - } - - // https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ - // - // Note: As mentioned above in the ‘security’ section, when a socket binds a socket to a valid pathname address, - // a socket file is created within the filesystem. On Linux, the application is expected to unlink - // (see the notes section in the man page for AF_UNIX) before any other socket can be bound to the same address. - // The same applies to Windows unix sockets, except that, DeleteFile (or any other file delete API) - // should be used to delete the socket file prior to calling bind with the same path. - _ = syscall.Unlink(addr) - - l, err := inbound.Listen("unix", addr) - if err != nil { - log.Errorln("External controller unix listen error: %s", err) - return - } - _ = os.Chmod(addr, 0o666) - log.Infoln("RESTful API unix listening at: %s", l.Addr().String()) - - server := &http.Server{ - Handler: router(cfg.IsDebug, "", cfg.DohServer, cfg.Cors), - } - unixServer = server - if err = server.Serve(l); err != nil { - log.Errorln("External controller unix serve error: %s", err) - } - } -} - -func startPipe(cfg *Config) { - // first stop existing server - if pipeServer != nil { - _ = pipeServer.Close() - pipeServer = nil - } - - // handle addr - if len(cfg.PipeAddr) > 0 { - if !strings.HasPrefix(cfg.PipeAddr, "\\\\.\\pipe\\") { // windows namedpipe must start with "\\.\pipe\" - log.Errorln("External controller pipe listen error: windows namedpipe must start with \"\\\\.\\pipe\\\"") - return - } - - l, err := inbound.ListenNamedPipe(cfg.PipeAddr) - if err != nil { - log.Errorln("External controller pipe listen error: %s", err) - return - } - log.Infoln("RESTful API pipe listening at: %s", l.Addr().String()) - - server := &http.Server{ - Handler: router(cfg.IsDebug, "", cfg.DohServer, cfg.Cors), - } - pipeServer = server - if err = server.Serve(l); err != nil { - log.Errorln("External controller pipe serve error: %s", err) - } - } -} - -func safeEuqal(a, b string) bool { - aBuf := utils.ImmutableBytesFromString(a) - bBuf := utils.ImmutableBytesFromString(b) - return subtle.ConstantTimeCompare(aBuf, bBuf) == 1 -} - -func authentication(secret string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - // Browser websocket not support custom header - if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" { - token := r.URL.Query().Get("token") - if !safeEuqal(token, secret) { - 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, secret) - 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": "mihomo"}) -} - -func traffic(w http.ResponseWriter, r *http.Request) { - var wsConn net.Conn - if r.Header.Get("Upgrade") == "websocket" { - var err error - wsConn, _, _, err = ws.UpgradeHTTP(r, w) - 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 := &bytes.Buffer{} - 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 = wsutil.WriteMessage(wsConn, ws.StateServerSide, ws.OpText, buf.Bytes()) - } - - if err != nil { - break - } - } -} - -func memory(w http.ResponseWriter, r *http.Request) { - var wsConn net.Conn - if r.Header.Get("Upgrade") == "websocket" { - var err error - wsConn, _, _, err = ws.UpgradeHTTP(r, w) - 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 := &bytes.Buffer{} - var err error - first := true - for range tick.C { - buf.Reset() - - inuse := t.Memory() - // make chat.js begin with zero - // this is shit var,but we need output 0 for first time - if first { - inuse = 0 - first = false - } - if err := json.NewEncoder(buf).Encode(Memory{ - Inuse: inuse, - OSLimit: 0, - }); err != nil { - break - } - if wsConn == nil { - _, err = w.Write(buf.Bytes()) - w.(http.Flusher).Flush() - } else { - err = wsutil.WriteMessage(wsConn, ws.StateServerSide, ws.OpText, buf.Bytes()) - } - - if err != nil { - break - } - } -} - -type Log struct { - Type string `json:"type"` - Payload string `json:"payload"` -} -type LogStructuredField struct { - Key string `json:"key"` - Value string `json:"value"` -} -type LogStructured struct { - Time string `json:"time"` - Level string `json:"level"` - Message string `json:"message"` - Fields []LogStructuredField `json:"fields"` -} - -func getLogs(w http.ResponseWriter, r *http.Request) { - levelText := r.URL.Query().Get("level") - if levelText == "" { - levelText = "info" - } - - formatText := r.URL.Query().Get("format") - isStructured := false - if formatText == "structured" { - isStructured = true - } - - level, ok := log.LogLevelMapping[levelText] - if !ok { - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, ErrBadRequest) - return - } - - var wsConn net.Conn - if r.Header.Get("Upgrade") == "websocket" { - var err error - wsConn, _, _, err = ws.UpgradeHTTP(r, w) - 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 logM := range sub { - select { - case ch <- logM: - default: - } - } - close(ch) - }() - - for logM := range ch { - if logM.LogLevel < level { - continue - } - buf.Reset() - - if !isStructured { - if err := json.NewEncoder(buf).Encode(Log{ - Type: logM.Type(), - Payload: logM.Payload, - }); err != nil { - break - } - } else { - newLevel := logM.Type() - if newLevel == "warning" { - newLevel = "warn" - } - if err := json.NewEncoder(buf).Encode(LogStructured{ - Time: time.Now().Format(time.TimeOnly), - Level: newLevel, - Message: logM.Payload, - Fields: []LogStructuredField{}, - }); err != nil { - break - } - } - - var err error - if wsConn == nil { - _, err = w.Write(buf.Bytes()) - w.(http.Flusher).Flush() - } else { - err = wsutil.WriteMessage(wsConn, ws.StateServerSide, ws.OpText, buf.Bytes()) - } - - if err != nil { - break - } - } -} - -func version(w http.ResponseWriter, r *http.Request) { - render.JSON(w, r, render.M{"meta": C.Meta, "version": C.Version}) -} diff --git a/hub/route/upgrade.go b/hub/route/upgrade.go deleted file mode 100644 index beee77a219..0000000000 --- a/hub/route/upgrade.go +++ /dev/null @@ -1,64 +0,0 @@ -package route - -import ( - "fmt" - "net/http" - "os" - - "github.com/metacubex/mihomo/component/updater" - "github.com/metacubex/mihomo/log" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" -) - -func upgradeRouter() http.Handler { - r := chi.NewRouter() - r.Post("/ui", updateUI) - if !embedMode { // disallow upgrade core/geo in embed mode - r.Post("/", upgradeCore) - r.Post("/geo", updateGeoDatabases) - } - return r -} - -func upgradeCore(w http.ResponseWriter, r *http.Request) { - // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/home/controlupdate.go#L108 - log.Infoln("start update") - execPath, err := os.Executable() - if err != nil { - render.Status(r, http.StatusInternalServerError) - render.JSON(w, r, newError(fmt.Sprintf("getting path: %s", err))) - return - } - - err = updater.UpdateCore(execPath) - if err != nil { - log.Warnln("%s", err) - render.Status(r, http.StatusInternalServerError) - render.JSON(w, r, newError(fmt.Sprintf("%s", err))) - return - } - - render.JSON(w, r, render.M{"status": "ok"}) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - - go restartExecutable(execPath) -} - -func updateUI(w http.ResponseWriter, r *http.Request) { - err := updater.DefaultUiUpdater.DownloadUI() - if err != nil { - log.Warnln("%s", err) - render.Status(r, http.StatusInternalServerError) - render.JSON(w, r, newError(fmt.Sprintf("%s", err))) - return - } - - render.JSON(w, r, render.M{"status": "ok"}) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } -} diff --git a/listener/anytls/server.go b/listener/anytls/server.go deleted file mode 100644 index db0a5503fd..0000000000 --- a/listener/anytls/server.go +++ /dev/null @@ -1,193 +0,0 @@ -package anytls - -import ( - "context" - "crypto/sha256" - "crypto/tls" - "encoding/binary" - "errors" - "net" - "strings" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/buf" - "github.com/metacubex/mihomo/component/ca" - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/transport/anytls/padding" - "github.com/metacubex/mihomo/transport/anytls/session" - - "github.com/metacubex/sing/common/auth" - "github.com/metacubex/sing/common/bufio" - M "github.com/metacubex/sing/common/metadata" -) - -type Listener struct { - closed bool - config LC.AnyTLSServer - listeners []net.Listener - tlsConfig *tls.Config - userMap map[[32]byte]string - padding atomic.TypedValue[*padding.PaddingFactory] -} - -func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-ANYTLS"), - inbound.WithSpecialRules(""), - } - } - - tlsConfig := &tls.Config{} - if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - - sl = &Listener{ - config: config, - tlsConfig: tlsConfig, - userMap: make(map[[32]byte]string), - } - - for user, password := range config.Users { - sl.userMap[sha256.Sum256([]byte(password))] = user - } - - if len(config.PaddingScheme) > 0 { - if !padding.UpdatePaddingScheme([]byte(config.PaddingScheme), &sl.padding) { - return nil, errors.New("incorrect padding scheme format") - } - } else { - padding.UpdatePaddingScheme(padding.DefaultPaddingScheme, &sl.padding) - } - - // Using sing handler can automatically handle UoT - h, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.ANYTLS, - Additions: additions, - }) - if err != nil { - return nil, err - } - - for _, addr := range strings.Split(config.Listen, ",") { - addr := addr - - //TCP - l, err := inbound.Listen("tcp", addr) - if err != nil { - return nil, err - } - if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) - } else { - return nil, errors.New("disallow using AnyTLS without certificates config") - } - sl.listeners = append(sl.listeners, l) - - go func() { - for { - c, err := l.Accept() - if err != nil { - if sl.closed { - break - } - continue - } - go sl.HandleConn(c, h) - } - }() - } - - return sl, nil -} - -func (l *Listener) Close() error { - l.closed = true - var retErr error - for _, lis := range l.listeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - return retErr -} - -func (l *Listener) Config() string { - return l.config.String() -} - -func (l *Listener) AddrList() (addrList []net.Addr) { - for _, lis := range l.listeners { - addrList = append(addrList, lis.Addr()) - } - return -} - -func (l *Listener) HandleConn(conn net.Conn, h *sing.ListenerHandler) { - ctx := context.TODO() - defer conn.Close() - - b := buf.NewPacket() - defer b.Release() - - _, err := b.ReadOnceFrom(conn) - if err != nil { - return - } - conn = bufio.NewCachedConn(conn, b) - - by, err := b.ReadBytes(32) - if err != nil { - return - } - var passwordSha256 [32]byte - copy(passwordSha256[:], by) - if user, ok := l.userMap[passwordSha256]; ok { - ctx = auth.ContextWithUser(ctx, user) - } else { - return - } - by, err = b.ReadBytes(2) - if err != nil { - return - } - paddingLen := binary.BigEndian.Uint16(by) - if paddingLen > 0 { - _, err = b.ReadBytes(int(paddingLen)) - if err != nil { - return - } - } - - session := session.NewServerSession(conn, func(stream *session.Stream) { - defer stream.Close() - - destination, err := M.SocksaddrSerializer.ReadAddrPort(stream) - if err != nil { - return - } - - // It seems that mihomo does not implement a connection error reporting mechanism, so we report success directly. - err = stream.HandshakeSuccess() - if err != nil { - return - } - - h.NewConnection(ctx, stream, M.Metadata{ - Source: M.SocksaddrFromNet(conn.RemoteAddr()), - Destination: destination, - }) - }, &l.padding) - session.Run() - session.Close() -} diff --git a/listener/auth/auth.go b/listener/auth/auth.go deleted file mode 100644 index 9e7632e868..0000000000 --- a/listener/auth/auth.go +++ /dev/null @@ -1,33 +0,0 @@ -package auth - -import ( - "github.com/metacubex/mihomo/component/auth" -) - -type authStore struct { - authenticator auth.Authenticator -} - -func (a *authStore) Authenticator() auth.Authenticator { - return a.authenticator -} - -func (a *authStore) SetAuthenticator(authenticator auth.Authenticator) { - a.authenticator = authenticator -} - -func NewAuthStore(authenticator auth.Authenticator) auth.AuthStore { - return &authStore{authenticator} -} - -var Default auth.AuthStore = NewAuthStore(nil) - -type nilAuthStore struct{} - -func (a *nilAuthStore) Authenticator() auth.Authenticator { - return nil -} - -func (a *nilAuthStore) SetAuthenticator(authenticator auth.Authenticator) {} - -var Nil auth.AuthStore = (*nilAuthStore)(nil) // always return nil, even call SetAuthenticator() with a non-nil authenticator diff --git a/listener/config/anytls.go b/listener/config/anytls.go deleted file mode 100644 index adbafa60fa..0000000000 --- a/listener/config/anytls.go +++ /dev/null @@ -1,19 +0,0 @@ -package config - -import ( - "encoding/json" -) - -type AnyTLSServer struct { - Enable bool `yaml:"enable" json:"enable"` - Listen string `yaml:"listen" json:"listen"` - Users map[string]string `yaml:"users" json:"users,omitempty"` - Certificate string `yaml:"certificate" json:"certificate"` - PrivateKey string `yaml:"private-key" json:"private-key"` - PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"` -} - -func (t AnyTLSServer) String() string { - b, _ := json.Marshal(t) - return string(b) -} diff --git a/listener/config/auth.go b/listener/config/auth.go deleted file mode 100644 index a99f87fb2d..0000000000 --- a/listener/config/auth.go +++ /dev/null @@ -1,16 +0,0 @@ -package config - -import ( - "github.com/metacubex/mihomo/component/auth" - "github.com/metacubex/mihomo/listener/reality" -) - -// AuthServer for http/socks/mixed server -type AuthServer struct { - Enable bool - Listen string - AuthStore auth.AuthStore - Certificate string - PrivateKey string - RealityConfig reality.Config -} diff --git a/listener/config/hysteria2.go b/listener/config/hysteria2.go deleted file mode 100644 index e26fbaa8aa..0000000000 --- a/listener/config/hysteria2.go +++ /dev/null @@ -1,37 +0,0 @@ -package config - -import ( - "github.com/metacubex/mihomo/listener/sing" - - "encoding/json" -) - -type Hysteria2Server struct { - Enable bool `yaml:"enable" json:"enable"` - Listen string `yaml:"listen" json:"listen"` - Users map[string]string `yaml:"users" json:"users,omitempty"` - Obfs string `yaml:"obfs" json:"obfs,omitempty"` - ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"` - Certificate string `yaml:"certificate" json:"certificate"` - PrivateKey string `yaml:"private-key" json:"private-key"` - MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` - ALPN []string `yaml:"alpn" json:"alpn,omitempty"` - Up string `yaml:"up" json:"up,omitempty"` - Down string `yaml:"down" json:"down,omitempty"` - IgnoreClientBandwidth bool `yaml:"ignore-client-bandwidth" json:"ignore-client-bandwidth,omitempty"` - Masquerade string `yaml:"masquerade" json:"masquerade,omitempty"` - CWND int `yaml:"cwnd" json:"cwnd,omitempty"` - UdpMTU int `yaml:"udp-mtu" json:"udp-mtu,omitempty"` - MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` - - // quic-go special config - InitialStreamReceiveWindow uint64 `yaml:"initial-stream-receive-window" json:"initial-stream-receive-window,omitempty"` - MaxStreamReceiveWindow uint64 `yaml:"max-stream-receive-window" json:"max-stream-receive-window,omitempty"` - InitialConnectionReceiveWindow uint64 `yaml:"initial-connection-receive-window" json:"initial-connection-receive-window,omitempty"` - MaxConnectionReceiveWindow uint64 `yaml:"max-connection-receive-window" json:"max-connection-receive-window,omitempty"` -} - -func (h Hysteria2Server) String() string { - b, _ := json.Marshal(h) - return string(b) -} diff --git a/listener/config/shadowsocks.go b/listener/config/shadowsocks.go deleted file mode 100644 index 442743ef78..0000000000 --- a/listener/config/shadowsocks.go +++ /dev/null @@ -1,22 +0,0 @@ -package config - -import ( - "github.com/metacubex/mihomo/listener/sing" - - "encoding/json" -) - -type ShadowsocksServer struct { - Enable bool - Listen string - Password string - Cipher string - Udp bool - MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` - ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` -} - -func (t ShadowsocksServer) String() string { - b, _ := json.Marshal(t) - return string(b) -} diff --git a/listener/config/shadowtls.go b/listener/config/shadowtls.go deleted file mode 100644 index b427ad6283..0000000000 --- a/listener/config/shadowtls.go +++ /dev/null @@ -1,22 +0,0 @@ -package config - -type ShadowTLS struct { - Enable bool - Version int - Password string - Users []ShadowTLSUser - Handshake ShadowTLSHandshakeOptions - HandshakeForServerName map[string]ShadowTLSHandshakeOptions - StrictMode bool - WildcardSNI string -} - -type ShadowTLSUser struct { - Name string - Password string -} - -type ShadowTLSHandshakeOptions struct { - Dest string - Proxy string -} diff --git a/listener/config/trojan.go b/listener/config/trojan.go deleted file mode 100644 index 28b6fe7c32..0000000000 --- a/listener/config/trojan.go +++ /dev/null @@ -1,38 +0,0 @@ -package config - -import ( - "encoding/json" - - "github.com/metacubex/mihomo/listener/reality" - "github.com/metacubex/mihomo/listener/sing" -) - -type TrojanUser struct { - Username string - Password string -} - -type TrojanServer struct { - Enable bool - Listen string - Users []TrojanUser - WsPath string - GrpcServiceName string - Certificate string - PrivateKey string - RealityConfig reality.Config - MuxOption sing.MuxOption - TrojanSSOption TrojanSSOption -} - -// TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 -type TrojanSSOption struct { - Enabled bool - Method string - Password string -} - -func (t TrojanServer) String() string { - b, _ := json.Marshal(t) - return string(b) -} diff --git a/listener/config/tuic.go b/listener/config/tuic.go deleted file mode 100644 index 14a4680927..0000000000 --- a/listener/config/tuic.go +++ /dev/null @@ -1,29 +0,0 @@ -package config - -import ( - "github.com/metacubex/mihomo/listener/sing" - - "encoding/json" -) - -type TuicServer struct { - Enable bool `yaml:"enable" json:"enable"` - Listen string `yaml:"listen" json:"listen"` - Token []string `yaml:"token" json:"token,omitempty"` - Users map[string]string `yaml:"users" json:"users,omitempty"` - Certificate string `yaml:"certificate" json:"certificate"` - PrivateKey string `yaml:"private-key" json:"private-key"` - CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` - MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` - AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` - ALPN []string `yaml:"alpn" json:"alpn,omitempty"` - MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` - MaxDatagramFrameSize int `yaml:"max-datagram-frame-size" json:"max-datagram-frame-size,omitempty"` - CWND int `yaml:"cwnd" json:"cwnd,omitempty"` - MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` -} - -func (t TuicServer) String() string { - b, _ := json.Marshal(t) - return string(b) -} diff --git a/listener/config/tun.go b/listener/config/tun.go deleted file mode 100644 index 4e12056d68..0000000000 --- a/listener/config/tun.go +++ /dev/null @@ -1,211 +0,0 @@ -package config - -import ( - "net/netip" - - C "github.com/metacubex/mihomo/constant" - - "go4.org/netipx" - "golang.org/x/exp/slices" -) - -func StringSliceToNetipPrefixSlice(ss []string) ([]netip.Prefix, error) { - lps := make([]netip.Prefix, 0, len(ss)) - for _, s := range ss { - prefix, err := netip.ParsePrefix(s) - if err != nil { - return nil, err - } - lps = append(lps, prefix) - } - return lps, nil -} - -type Tun struct { - Enable bool `yaml:"enable" json:"enable"` - Device string `yaml:"device" json:"device"` - Stack C.TUNStack `yaml:"stack" json:"stack"` - DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"` - AutoRoute bool `yaml:"auto-route" json:"auto-route"` - AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` - - MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` - GSO bool `yaml:"gso" json:"gso,omitempty"` - GSOMaxSize uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` - Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` - Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` - IPRoute2TableIndex int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` - IPRoute2RuleIndex int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` - AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` - AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` - AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` - StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` - RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` - RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"` - RouteExcludeAddress []netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` - RouteExcludeAddressSet []string `yaml:"route-exclude-address-set" json:"route-exclude-address-set,omitempty"` - IncludeInterface []string `yaml:"include-interface" json:"include-interface,omitempty"` - ExcludeInterface []string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` - IncludeUID []uint32 `yaml:"include-uid" json:"include-uid,omitempty"` - IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` - ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` - ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` - ExcludeSrcPort []uint16 `yaml:"exclude-src-port" json:"exclude-src-port,omitempty"` - ExcludeSrcPortRange []string `yaml:"exclude-src-port-range" json:"exclude-src-port-range,omitempty"` - ExcludeDstPort []uint16 `yaml:"exclude-dst-port" json:"exclude-dst-port,omitempty"` - ExcludeDstPortRange []string `yaml:"exclude-dst-port-range" json:"exclude-dst-port-range,omitempty"` - IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` - IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` - ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` - EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` - UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` - FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` - - Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` - Inet6RouteAddress []netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` - Inet4RouteExcludeAddress []netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"` - Inet6RouteExcludeAddress []netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"` -} - -func (t *Tun) Sort() { - slices.Sort(t.DNSHijack) - - slices.SortFunc(t.Inet4Address, netipx.ComparePrefix) - slices.SortFunc(t.Inet6Address, netipx.ComparePrefix) - slices.SortFunc(t.RouteAddress, netipx.ComparePrefix) - slices.Sort(t.RouteAddressSet) - slices.SortFunc(t.RouteExcludeAddress, netipx.ComparePrefix) - slices.Sort(t.RouteExcludeAddressSet) - slices.Sort(t.IncludeInterface) - slices.Sort(t.ExcludeInterface) - slices.Sort(t.IncludeUID) - slices.Sort(t.IncludeUIDRange) - slices.Sort(t.ExcludeUID) - slices.Sort(t.ExcludeUIDRange) - slices.Sort(t.IncludeAndroidUser) - slices.Sort(t.IncludePackage) - slices.Sort(t.ExcludePackage) - - slices.SortFunc(t.Inet4RouteAddress, netipx.ComparePrefix) - slices.SortFunc(t.Inet6RouteAddress, netipx.ComparePrefix) - slices.SortFunc(t.Inet4RouteExcludeAddress, netipx.ComparePrefix) - slices.SortFunc(t.Inet6RouteExcludeAddress, netipx.ComparePrefix) -} - -func (t *Tun) Equal(other Tun) bool { - if t.Enable != other.Enable { - return false - } - if t.Device != other.Device { - return false - } - if t.Stack != other.Stack { - return false - } - if !slices.Equal(t.DNSHijack, other.DNSHijack) { - return false - } - if t.AutoRoute != other.AutoRoute { - return false - } - if t.AutoDetectInterface != other.AutoDetectInterface { - return false - } - - if t.MTU != other.MTU { - return false - } - if t.GSO != other.GSO { - return false - } - if t.GSOMaxSize != other.GSOMaxSize { - return false - } - if !slices.Equal(t.Inet4Address, other.Inet4Address) { - return false - } - if !slices.Equal(t.Inet6Address, other.Inet6Address) { - return false - } - if t.IPRoute2TableIndex != other.IPRoute2TableIndex { - return false - } - if t.IPRoute2RuleIndex != other.IPRoute2RuleIndex { - return false - } - if t.AutoRedirect != other.AutoRedirect { - return false - } - if t.AutoRedirectInputMark != other.AutoRedirectInputMark { - return false - } - if t.AutoRedirectOutputMark != other.AutoRedirectOutputMark { - return false - } - if t.StrictRoute != other.StrictRoute { - return false - } - if !slices.Equal(t.RouteAddress, other.RouteAddress) { - return false - } - if !slices.Equal(t.RouteAddressSet, other.RouteAddressSet) { - return false - } - if !slices.Equal(t.RouteExcludeAddress, other.RouteExcludeAddress) { - return false - } - if !slices.Equal(t.RouteExcludeAddressSet, other.RouteExcludeAddressSet) { - return false - } - if !slices.Equal(t.IncludeInterface, other.IncludeInterface) { - return false - } - if !slices.Equal(t.ExcludeInterface, other.ExcludeInterface) { - return false - } - if !slices.Equal(t.IncludeUID, other.IncludeUID) { - return false - } - if !slices.Equal(t.IncludeUIDRange, other.IncludeUIDRange) { - return false - } - if !slices.Equal(t.ExcludeUID, other.ExcludeUID) { - return false - } - if !slices.Equal(t.ExcludeUIDRange, other.ExcludeUIDRange) { - return false - } - if !slices.Equal(t.IncludeAndroidUser, other.IncludeAndroidUser) { - return false - } - if !slices.Equal(t.IncludePackage, other.IncludePackage) { - return false - } - if !slices.Equal(t.ExcludePackage, other.ExcludePackage) { - return false - } - if t.EndpointIndependentNat != other.EndpointIndependentNat { - return false - } - if t.UDPTimeout != other.UDPTimeout { - return false - } - if t.FileDescriptor != other.FileDescriptor { - return false - } - - if !slices.Equal(t.Inet4RouteAddress, other.Inet4RouteAddress) { - return false - } - if !slices.Equal(t.Inet6RouteAddress, other.Inet6RouteAddress) { - return false - } - if !slices.Equal(t.Inet4RouteExcludeAddress, other.Inet4RouteExcludeAddress) { - return false - } - if !slices.Equal(t.Inet6RouteExcludeAddress, other.Inet6RouteExcludeAddress) { - return false - } - - return true -} diff --git a/listener/config/tunnel.go b/listener/config/tunnel.go deleted file mode 100644 index eadee4b176..0000000000 --- a/listener/config/tunnel.go +++ /dev/null @@ -1,69 +0,0 @@ -package config - -import ( - "fmt" - "net" - "strings" - - "github.com/samber/lo" -) - -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) != 3 && 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, - }) - if len(parts) == 4 { - t.Proxy = parts[3] - } - return nil -} diff --git a/listener/config/vless.go b/listener/config/vless.go deleted file mode 100644 index d656db9fdc..0000000000 --- a/listener/config/vless.go +++ /dev/null @@ -1,31 +0,0 @@ -package config - -import ( - "encoding/json" - - "github.com/metacubex/mihomo/listener/reality" - "github.com/metacubex/mihomo/listener/sing" -) - -type VlessUser struct { - Username string - UUID string - Flow string -} - -type VlessServer struct { - Enable bool - Listen string - Users []VlessUser - WsPath string - GrpcServiceName string - Certificate string - PrivateKey string - RealityConfig reality.Config - MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` -} - -func (t VlessServer) String() string { - b, _ := json.Marshal(t) - return string(b) -} diff --git a/listener/config/vmess.go b/listener/config/vmess.go deleted file mode 100644 index 264b772c40..0000000000 --- a/listener/config/vmess.go +++ /dev/null @@ -1,31 +0,0 @@ -package config - -import ( - "encoding/json" - - "github.com/metacubex/mihomo/listener/reality" - "github.com/metacubex/mihomo/listener/sing" -) - -type VmessUser struct { - Username string - UUID string - AlterID int -} - -type VmessServer struct { - Enable bool - Listen string - Users []VmessUser - WsPath string - GrpcServiceName string - Certificate string - PrivateKey string - RealityConfig reality.Config - MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` -} - -func (t VmessServer) String() string { - b, _ := json.Marshal(t) - return string(b) -} diff --git a/listener/http/client.go b/listener/http/client.go deleted file mode 100644 index 0f084fca7b..0000000000 --- a/listener/http/client.go +++ /dev/null @@ -1,46 +0,0 @@ -package http - -import ( - "context" - "errors" - "net" - "net/http" - "time" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" -) - -func newClient(srcConn net.Conn, tunnel C.Tunnel, additions []inbound.Addition) *http.Client { // additions using slice let caller can change its value (without size) after newClient return - return &http.Client{ - Transport: &http.Transport{ - // from http.DefaultTransport - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - DisableCompression: true, // prevents the Transport add "Accept-Encoding: gzip" - 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 := N.Pipe() - - go tunnel.HandleTCPConn(inbound.NewHTTP(dstAddr, srcConn, right, additions...)) - - 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 deleted file mode 100644 index c33eb6f1bc..0000000000 --- a/listener/http/hack.go +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 5c08cd458a..0000000000 --- a/listener/http/proxy.go +++ /dev/null @@ -1,175 +0,0 @@ -package http - -import ( - "context" - "fmt" - "io" - "net" - "net/http" - "strings" - "sync" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/auth" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type bodyWrapper struct { - io.ReadCloser - once sync.Once - onHitEOF func() -} - -func (b *bodyWrapper) Read(p []byte) (n int, err error) { - n, err = b.ReadCloser.Read(p) - if err == io.EOF && b.onHitEOF != nil { - b.once.Do(b.onHitEOF) - } - return n, err -} - -func HandleConn(c net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { - additions = append(additions, inbound.Placeholder) // Add a placeholder for InUser - inUserIdx := len(additions) - 1 - client := newClient(c, tunnel, additions) - defer client.CloseIdleConnections() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - peekMutex := sync.Mutex{} - - conn := N.NewBufferedConn(c) - - authenticator := store.Authenticator() - keepAlive := true - trusted := authenticator == nil // disable authenticate if lru is nil - lastUser := "" - - for keepAlive { - peekMutex.Lock() - request, err := ReadRequest(conn.Reader()) - peekMutex.Unlock() - 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 - - var user string - resp, user = authenticate(request, authenticator) // always call authenticate function to get user - trusted = trusted || resp == nil - additions[inUserIdx] = inbound.WithInUser(user) - - 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 - } - - tunnel.HandleTCPConn(inbound.NewHTTPS(request, conn, additions...)) - - return // hijack connection - } - - host := request.Header.Get("Host") - if host != "" { - request.Host = host - } - - request.RequestURI = "" - - if isUpgradeRequest(request) { - handleUpgrade(conn, request, tunnel, additions...) - - return // hijack connection - } - - // ensure there is a client with correct additions - // when the authenticated user changed, outbound client should close idle connections - if user != lastUser { - client.CloseIdleConnections() - lastUser = user - } - - removeHopByHopHeaders(request.Header) - removeExtraHTTPHostPort(request) - - if request.URL.Scheme == "" || request.URL.Host == "" { - resp = responseWith(request, http.StatusBadRequest) - } else { - request = request.WithContext(ctx) - - startBackgroundRead := func() { - go func() { - peekMutex.Lock() - defer peekMutex.Unlock() - _, err := conn.Peek(1) - if err != nil { - cancel() - } - }() - } - if request.Body == nil || request.Body == http.NoBody { - startBackgroundRead() - } else { - request.Body = &bodyWrapper{ReadCloser: request.Body, onHitEOF: startBackgroundRead} - } - 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, authenticator auth.Authenticator) (resp *http.Response, user string) { - credential := parseBasicProxyAuthorization(request) - if credential == "" && authenticator != nil { - resp = responseWith(request, http.StatusProxyAuthRequired) - resp.Header.Set("Proxy-Authenticate", "Basic") - return - } - user, pass, err := decodeBasicProxyAuthorization(credential) - authed := authenticator == nil || (err == nil && authenticator.Verify(user, pass)) - if !authed { - log.Infoln("Auth failed from %s", request.RemoteAddr) - return responseWith(request, http.StatusForbidden), user - } - log.Debugln("Auth success from %s -> %s", request.RemoteAddr, user) - return -} - -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 deleted file mode 100644 index 52483081eb..0000000000 --- a/listener/http/server.go +++ /dev/null @@ -1,123 +0,0 @@ -package http - -import ( - "crypto/tls" - "errors" - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/component/ca" - C "github.com/metacubex/mihomo/constant" - authStore "github.com/metacubex/mihomo/listener/auth" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/reality" -) - -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, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - return NewWithConfig(LC.AuthServer{Enable: true, Listen: addr, AuthStore: authStore.Default}, tunnel, additions...) -} - -// NewWithAuthenticate -// never change type traits because it's used in CMFA -func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) { - store := authStore.Default - if !authenticate { - store = authStore.Nil - } - return NewWithConfig(LC.AuthServer{Enable: true, Listen: addr, AuthStore: store}, tunnel, additions...) -} - -func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - isDefault := false - if len(additions) == 0 { - isDefault = true - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-HTTP"), - inbound.WithSpecialRules(""), - } - } - - l, err := inbound.Listen("tcp", config.Listen) - if err != nil { - return nil, err - } - - tlsConfig := &tls.Config{} - var realityBuilder *reality.Builder - - if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { - return nil, errors.New("certificate is unavailable in reality") - } - realityBuilder, err = config.RealityConfig.Build(tunnel) - if err != nil { - return nil, err - } - } - - if realityBuilder != nil { - l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) - } - - hl := &Listener{ - listener: l, - addr: config.Listen, - } - - go func() { - for { - conn, err := hl.listener.Accept() - if err != nil { - if hl.closed { - break - } - continue - } - - store := config.AuthStore - if isDefault || store == authStore.Default { // only apply on default listener - if !inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) { - _ = conn.Close() - continue - } - if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) { - store = authStore.Nil - } - } - go HandleConn(conn, tunnel, store, additions...) - } - }() - - return hl, nil -} diff --git a/listener/http/upgrade.go b/listener/http/upgrade.go deleted file mode 100644 index ac67ef6889..0000000000 --- a/listener/http/upgrade.go +++ /dev/null @@ -1,89 +0,0 @@ -package http - -import ( - "context" - "crypto/tls" - "net" - "net/http" - "strings" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/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, tunnel C.Tunnel, additions ...inbound.Addition) { - 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 := N.Pipe() - - go tunnel.HandleTCPConn(inbound.NewHTTP(dstAddr, conn, right, additions...)) - - var bufferedLeft *N.BufferedConn - if request.TLS != nil { - tlsConn := tls.Client(left, &tls.Config{ - ServerName: request.URL.Hostname(), - }) - - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() - if tlsConn.HandshakeContext(ctx) != nil { - _ = left.Close() - return - } - - bufferedLeft = N.NewBufferedConn(tlsConn) - } else { - bufferedLeft = N.NewBufferedConn(left) - } - defer func() { - _ = 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 deleted file mode 100644 index e0793ff3bb..0000000000 --- a/listener/http/utils.go +++ /dev/null @@ -1,86 +0,0 @@ -package http - -import ( - "encoding/base64" - "errors" - "net" - "net/http" - "net/netip" - "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 - if ip, err := netip.ParseAddr(pHost); err == nil && ip.Is6() { - // RFC 2617 Sec 3.2.2, for IPv6 literal - // addresses the Host header needs to follow the RFC 2732 grammar for "host" - host = "[" + host + "]" - } - } - - 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/inbound/anytls.go b/listener/inbound/anytls.go deleted file mode 100644 index 6f1e635084..0000000000 --- a/listener/inbound/anytls.go +++ /dev/null @@ -1,82 +0,0 @@ -package inbound - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/listener/anytls" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/log" -) - -type AnyTLSOption struct { - BaseOption - Users map[string]string `inbound:"users,omitempty"` - Certificate string `inbound:"certificate"` - PrivateKey string `inbound:"private-key"` - PaddingScheme string `inbound:"padding-scheme,omitempty"` -} - -func (o AnyTLSOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type AnyTLS struct { - *Base - config *AnyTLSOption - l C.MultiAddrListener - vs LC.AnyTLSServer -} - -func NewAnyTLS(options *AnyTLSOption) (*AnyTLS, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &AnyTLS{ - Base: base, - config: options, - vs: LC.AnyTLSServer{ - Enable: true, - Listen: base.RawAddress(), - Users: options.Users, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - PaddingScheme: options.PaddingScheme, - }, - }, nil -} - -// Config implements constant.InboundListener -func (v *AnyTLS) Config() C.InboundConfig { - return v.config -} - -// Address implements constant.InboundListener -func (v *AnyTLS) Address() string { - var addrList []string - if v.l != nil { - for _, addr := range v.l.AddrList() { - addrList = append(addrList, addr.String()) - } - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (v *AnyTLS) Listen(tunnel C.Tunnel) error { - var err error - v.l, err = anytls.New(v.vs, tunnel, v.Additions()...) - if err != nil { - return err - } - log.Infoln("AnyTLS[%s] proxy listening at: %s", v.Name(), v.Address()) - return nil -} - -// Close implements constant.InboundListener -func (v *AnyTLS) Close() error { - return v.l.Close() -} - -var _ C.InboundListener = (*AnyTLS)(nil) diff --git a/listener/inbound/anytls_test.go b/listener/inbound/anytls_test.go deleted file mode 100644 index 9d172890d7..0000000000 --- a/listener/inbound/anytls_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package inbound_test - -import ( - "net/netip" - "testing" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/listener/inbound" - - "github.com/stretchr/testify/assert" -) - -func testInboundAnyTLS(t *testing.T, inboundOptions inbound.AnyTLSOption, outboundOptions outbound.AnyTLSOption) { - t.Parallel() - inboundOptions.BaseOption = inbound.BaseOption{ - NameStr: "anytls_inbound", - Listen: "127.0.0.1", - Port: "0", - } - inboundOptions.Users = map[string]string{"test": userUUID} - in, err := inbound.NewAnyTLS(&inboundOptions) - if !assert.NoError(t, err) { - return - } - - tunnel := NewHttpTestTunnel() - defer tunnel.Close() - - err = in.Listen(tunnel) - if !assert.NoError(t, err) { - return - } - defer in.Close() - - addrPort, err := netip.ParseAddrPort(in.Address()) - if !assert.NoError(t, err) { - return - } - - outboundOptions.Name = "anytls_outbound" - outboundOptions.Server = addrPort.Addr().String() - outboundOptions.Port = int(addrPort.Port()) - outboundOptions.Password = userUUID - - out, err := outbound.NewAnyTLS(outboundOptions) - if !assert.NoError(t, err) { - return - } - defer out.Close() - - tunnel.DoTest(t, out) -} - -func TestInboundAnyTLS_TLS(t *testing.T) { - inboundOptions := inbound.AnyTLSOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.AnyTLSOption{ - Fingerprint: tlsFingerprint, - } - testInboundAnyTLS(t, inboundOptions, outboundOptions) -} diff --git a/listener/inbound/auth.go b/listener/inbound/auth.go deleted file mode 100644 index 85e7249455..0000000000 --- a/listener/inbound/auth.go +++ /dev/null @@ -1,31 +0,0 @@ -package inbound - -import ( - "github.com/metacubex/mihomo/component/auth" - authStore "github.com/metacubex/mihomo/listener/auth" -) - -type AuthUser struct { - Username string `inbound:"username"` - Password string `inbound:"password"` -} - -type AuthUsers []AuthUser - -func (a AuthUsers) GetAuthStore() auth.AuthStore { - if a != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array - if len(a) == 0 { - return authStore.Nil - } - users := make([]auth.AuthUser, len(a)) - for i, user := range a { - users[i] = auth.AuthUser{ - User: user.Username, - Pass: user.Password, - } - } - authenticator := auth.NewAuthenticator(users) - return authStore.NewAuthStore(authenticator) - } - return authStore.Default -} diff --git a/listener/inbound/base.go b/listener/inbound/base.go deleted file mode 100644 index d4377986f7..0000000000 --- a/listener/inbound/base.go +++ /dev/null @@ -1,117 +0,0 @@ -package inbound - -import ( - "encoding/json" - "net" - "net/netip" - "strconv" - "strings" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" -) - -type Base struct { - config *BaseOption - name string - specialRules string - listenAddr netip.Addr - ports utils.IntRanges[uint16] -} - -func NewBase(options *BaseOption) (*Base, error) { - if options.Listen == "" { - options.Listen = "0.0.0.0" - } - addr, err := netip.ParseAddr(options.Listen) - if err != nil { - return nil, err - } - ports, err := utils.NewUnsignedRanges[uint16](options.Port) - if err != nil { - return nil, err - } - return &Base{ - name: options.Name(), - listenAddr: addr, - specialRules: options.SpecialRules, - ports: ports, - config: options, - }, nil -} - -// Config implements constant.InboundListener -func (b *Base) Config() C.InboundConfig { - return b.config -} - -// Address implements constant.InboundListener -func (b *Base) Address() string { - return b.RawAddress() -} - -// Close implements constant.InboundListener -func (*Base) Close() error { - return nil -} - -// Name implements constant.InboundListener -func (b *Base) Name() string { - return b.name -} - -// RawAddress implements constant.InboundListener -func (b *Base) RawAddress() string { - if len(b.ports) == 0 { - return net.JoinHostPort(b.listenAddr.String(), "0") - } - address := make([]string, 0, len(b.ports)) - b.ports.Range(func(port uint16) bool { - address = append(address, net.JoinHostPort(b.listenAddr.String(), strconv.Itoa(int(port)))) - return true - }) - return strings.Join(address, ",") -} - -// Listen implements constant.InboundListener -func (*Base) Listen(tunnel C.Tunnel) error { - return nil -} - -func (b *Base) Additions() []inbound.Addition { - return b.config.Additions() -} - -var _ C.InboundListener = (*Base)(nil) - -type BaseOption struct { - NameStr string `inbound:"name"` - Listen string `inbound:"listen,omitempty"` - Port string `inbound:"port,omitempty"` - SpecialRules string `inbound:"rule,omitempty"` - SpecialProxy string `inbound:"proxy,omitempty"` -} - -func (o BaseOption) Name() string { - return o.NameStr -} - -func (o BaseOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -func (o BaseOption) Additions() []inbound.Addition { - return []inbound.Addition{ - inbound.WithInName(o.NameStr), - inbound.WithSpecialRules(o.SpecialRules), - inbound.WithSpecialProxy(o.SpecialProxy), - } -} - -var _ C.InboundConfig = (*BaseOption)(nil) - -func optionToString(option any) string { - str, _ := json.Marshal(option) - return string(str) -} diff --git a/listener/inbound/common_test.go b/listener/inbound/common_test.go deleted file mode 100644 index 7c5718dc06..0000000000 --- a/listener/inbound/common_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package inbound_test - -import ( - "context" - "crypto/rand" - "crypto/tls" - "encoding/base64" - "fmt" - "io" - "net" - "net/http" - "net/netip" - "sync" - "testing" - "time" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/generater" - C "github.com/metacubex/mihomo/constant" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - "github.com/stretchr/testify/assert" -) - -var httpPath = "/inbound_test" -var httpData = make([]byte, 10240) -var remoteAddr = netip.MustParseAddr("1.2.3.4") -var userUUID = utils.NewUUIDV4().String() -var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256) -var tlsConfigCert, _ = tls.X509KeyPair([]byte(tlsCertificate), []byte(tlsPrivateKey)) -var tlsConfig = &tls.Config{Certificates: []tls.Certificate{tlsConfigCert}, NextProtos: []string{"h2", "http/1.1"}} -var tlsClientConfig, _ = ca.GetTLSConfig(nil, tlsFingerprint, "", "") -var realityPrivateKey, realityPublickey string -var realityDest = "itunes.apple.com" -var realityShortid = "10f897e26c4b9478" -var realityRealDial = false - -func init() { - rand.Read(httpData) - privateKey, err := generater.GeneratePrivateKey() - if err != nil { - panic(err) - } - publicKey := privateKey.PublicKey() - realityPrivateKey = base64.RawURLEncoding.EncodeToString(privateKey[:]) - realityPublickey = base64.RawURLEncoding.EncodeToString(publicKey[:]) -} - -type TestTunnel struct { - HandleTCPConnFn func(conn net.Conn, metadata *C.Metadata) - HandleUDPPacketFn func(packet C.UDPPacket, metadata *C.Metadata) - NatTableFn func() C.NatTable - CloseFn func() error - DoTestFn func(t *testing.T, proxy C.ProxyAdapter) -} - -func (tt *TestTunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) { - tt.HandleTCPConnFn(conn, metadata) -} - -func (tt *TestTunnel) HandleUDPPacket(packet C.UDPPacket, metadata *C.Metadata) { - tt.HandleUDPPacketFn(packet, metadata) -} - -func (tt *TestTunnel) NatTable() C.NatTable { - return tt.NatTableFn() -} - -func (tt *TestTunnel) Close() error { - return tt.CloseFn() -} - -func (tt *TestTunnel) DoTest(t *testing.T, proxy C.ProxyAdapter) { - tt.DoTestFn(t, proxy) -} - -type TestTunnelListener struct { - ch chan net.Conn - ctx context.Context - cancel context.CancelFunc - addr net.Addr -} - -func (t *TestTunnelListener) Accept() (net.Conn, error) { - select { - case conn, ok := <-t.ch: - if !ok { - return nil, net.ErrClosed - } - return conn, nil - case <-t.ctx.Done(): - return nil, t.ctx.Err() - } -} - -func (t *TestTunnelListener) Close() error { - t.cancel() - return nil -} - -func (t *TestTunnelListener) Addr() net.Addr { - return t.addr -} - -type WaitCloseConn struct { - net.Conn - ch chan struct{} - once sync.Once -} - -func (c *WaitCloseConn) Close() error { - err := c.Conn.Close() - c.once.Do(func() { - close(c.ch) - }) - return err -} - -var _ C.Tunnel = (*TestTunnel)(nil) -var _ net.Listener = (*TestTunnelListener)(nil) - -func NewHttpTestTunnel() *TestTunnel { - ctx, cancel := context.WithCancel(context.Background()) - ln := &TestTunnelListener{ch: make(chan net.Conn), ctx: ctx, cancel: cancel, addr: net.TCPAddrFromAddrPort(netip.AddrPortFrom(remoteAddr, 0))} - - r := chi.NewRouter() - r.Get(httpPath, func(w http.ResponseWriter, r *http.Request) { - render.Data(w, r, httpData) - }) - go http.Serve(ln, r) - testFn := func(t *testing.T, proxy C.ProxyAdapter, proto string) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s://%s%s", proto, remoteAddr, httpPath), nil) - if !assert.NoError(t, err) { - return - } - req = req.WithContext(ctx) - - var dstPort uint16 = 80 - if proto == "https" { - dstPort = 443 - } - metadata := &C.Metadata{ - NetWork: C.TCP, - DstIP: remoteAddr, - DstPort: dstPort, - } - instance, err := proxy.DialContext(ctx, metadata) - if !assert.NoError(t, err) { - return - } - defer instance.Close() - - transport := &http.Transport{ - DialContext: func(context.Context, 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, - // for our self-signed cert - TLSClientConfig: tlsClientConfig.Clone(), - // open http2 - ForceAttemptHTTP2: true, - } - - client := http.Client{ - Timeout: 30 * time.Second, - Transport: transport, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } - - defer client.CloseIdleConnections() - - resp, err := client.Do(req) - if !assert.NoError(t, err) { - return - } - - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - data, err := io.ReadAll(resp.Body) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, httpData, data) - } - tunnel := &TestTunnel{ - HandleTCPConnFn: func(conn net.Conn, metadata *C.Metadata) { - defer conn.Close() - if metadata.DstIP != remoteAddr && metadata.Host != realityDest { - return // not match, just return - } - c := &WaitCloseConn{ - Conn: conn, - ch: make(chan struct{}), - } - if metadata.DstPort == 443 { - tlsConn := tls.Server(c, tlsConfig.Clone()) - if metadata.Host == realityDest { // ignore the tls handshake error for realityDest - if realityRealDial { - rconn, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress()) - if err != nil { - panic(err) - } - N.Relay(rconn, tlsConn) - return - } - ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) - defer cancel() - if err := tlsConn.HandshakeContext(ctx); err != nil { - return - } - } - ln.ch <- tlsConn - } else { - ln.ch <- c - } - <-c.ch - }, - CloseFn: ln.Close, - DoTestFn: func(t *testing.T, proxy C.ProxyAdapter) { - // Sequential testing for debugging - t.Run("Sequential", func(t *testing.T) { - testFn(t, proxy, "http") - testFn(t, proxy, "https") - }) - - // Concurrent testing to detect stress - t.Run("Concurrent", func(t *testing.T) { - wg := sync.WaitGroup{} - const num = 50 - for i := 0; i < num; i++ { - wg.Add(1) - go func() { - testFn(t, proxy, "https") - defer wg.Done() - }() - } - for i := 0; i < num; i++ { - wg.Add(1) - go func() { - testFn(t, proxy, "http") - defer wg.Done() - }() - } - wg.Wait() - }) - }, - } - return tunnel -} diff --git a/listener/inbound/http.go b/listener/inbound/http.go deleted file mode 100644 index 8a4df00813..0000000000 --- a/listener/inbound/http.go +++ /dev/null @@ -1,96 +0,0 @@ -package inbound - -import ( - "errors" - "fmt" - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/http" - "github.com/metacubex/mihomo/log" -) - -type HTTPOption struct { - BaseOption - Users AuthUsers `inbound:"users,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` -} - -func (o HTTPOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type HTTP struct { - *Base - config *HTTPOption - l []*http.Listener -} - -func NewHTTP(options *HTTPOption) (*HTTP, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &HTTP{ - Base: base, - config: options, - }, nil -} - -// Config implements constant.InboundListener -func (h *HTTP) Config() C.InboundConfig { - return h.config -} - -// Address implements constant.InboundListener -func (h *HTTP) Address() string { - var addrList []string - for _, l := range h.l { - addrList = append(addrList, l.Address()) - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (h *HTTP) Listen(tunnel C.Tunnel) error { - for _, addr := range strings.Split(h.RawAddress(), ",") { - l, err := http.NewWithConfig( - LC.AuthServer{ - Enable: true, - Listen: addr, - AuthStore: h.config.Users.GetAuthStore(), - Certificate: h.config.Certificate, - PrivateKey: h.config.PrivateKey, - RealityConfig: h.config.RealityConfig.Build(), - }, - tunnel, - h.Additions()..., - ) - if err != nil { - return err - } - h.l = append(h.l, l) - } - log.Infoln("HTTP[%s] proxy listening at: %s", h.Name(), h.Address()) - return nil -} - -// Close implements constant.InboundListener -func (h *HTTP) Close() error { - var errs []error - for _, l := range h.l { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - return nil -} - -var _ C.InboundListener = (*HTTP)(nil) diff --git a/listener/inbound/hysteria2.go b/listener/inbound/hysteria2.go deleted file mode 100644 index 62d19c829e..0000000000 --- a/listener/inbound/hysteria2.go +++ /dev/null @@ -1,113 +0,0 @@ -package inbound - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing_hysteria2" - "github.com/metacubex/mihomo/log" -) - -type Hysteria2Option struct { - BaseOption - Users map[string]string `inbound:"users,omitempty"` - Obfs string `inbound:"obfs,omitempty"` - ObfsPassword string `inbound:"obfs-password,omitempty"` - Certificate string `inbound:"certificate"` - PrivateKey string `inbound:"private-key"` - MaxIdleTime int `inbound:"max-idle-time,omitempty"` - ALPN []string `inbound:"alpn,omitempty"` - Up string `inbound:"up,omitempty"` - Down string `inbound:"down,omitempty"` - IgnoreClientBandwidth bool `inbound:"ignore-client-bandwidth,omitempty"` - Masquerade string `inbound:"masquerade,omitempty"` - CWND int `inbound:"cwnd,omitempty"` - UdpMTU int `inbound:"udp-mtu,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` - - // quic-go special config - InitialStreamReceiveWindow uint64 `inbound:"initial-stream-receive-window,omitempty"` - MaxStreamReceiveWindow uint64 `inbound:"max-stream-receive-window,omitempty"` - InitialConnectionReceiveWindow uint64 `inbound:"initial-connection-receive-window,omitempty"` - MaxConnectionReceiveWindow uint64 `inbound:"max-connection-receive-window,omitempty"` -} - -func (o Hysteria2Option) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Hysteria2 struct { - *Base - config *Hysteria2Option - l *sing_hysteria2.Listener - ts LC.Hysteria2Server -} - -func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &Hysteria2{ - Base: base, - config: options, - ts: LC.Hysteria2Server{ - Enable: true, - Listen: base.RawAddress(), - Users: options.Users, - Obfs: options.Obfs, - ObfsPassword: options.ObfsPassword, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - MaxIdleTime: options.MaxIdleTime, - ALPN: options.ALPN, - Up: options.Up, - Down: options.Down, - IgnoreClientBandwidth: options.IgnoreClientBandwidth, - Masquerade: options.Masquerade, - CWND: options.CWND, - UdpMTU: options.UdpMTU, - MuxOption: options.MuxOption.Build(), - // quic-go special config - InitialStreamReceiveWindow: options.InitialStreamReceiveWindow, - MaxStreamReceiveWindow: options.MaxStreamReceiveWindow, - InitialConnectionReceiveWindow: options.InitialConnectionReceiveWindow, - MaxConnectionReceiveWindow: options.MaxConnectionReceiveWindow, - }, - }, nil -} - -// Config implements constant.InboundListener -func (t *Hysteria2) Config() C.InboundConfig { - return t.config -} - -// Address implements constant.InboundListener -func (t *Hysteria2) Address() string { - var addrList []string - if t.l != nil { - for _, addr := range t.l.AddrList() { - addrList = append(addrList, addr.String()) - } - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (t *Hysteria2) Listen(tunnel C.Tunnel) error { - var err error - t.l, err = sing_hysteria2.New(t.ts, tunnel, t.Additions()...) - if err != nil { - return err - } - log.Infoln("Hysteria2[%s] proxy listening at: %s", t.Name(), t.Address()) - return nil -} - -// Close implements constant.InboundListener -func (t *Hysteria2) Close() error { - return t.l.Close() -} - -var _ C.InboundListener = (*Hysteria2)(nil) diff --git a/listener/inbound/hysteria2_test.go b/listener/inbound/hysteria2_test.go deleted file mode 100644 index 2926a9e6ca..0000000000 --- a/listener/inbound/hysteria2_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package inbound_test - -import ( - "net/netip" - "testing" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/listener/inbound" - - "github.com/stretchr/testify/assert" -) - -func testInboundHysteria2(t *testing.T, inboundOptions inbound.Hysteria2Option, outboundOptions outbound.Hysteria2Option) { - t.Parallel() - inboundOptions.BaseOption = inbound.BaseOption{ - NameStr: "hysteria2_inbound", - Listen: "127.0.0.1", - Port: "0", - } - inboundOptions.Users = map[string]string{"test": userUUID} - in, err := inbound.NewHysteria2(&inboundOptions) - if !assert.NoError(t, err) { - return - } - - tunnel := NewHttpTestTunnel() - defer tunnel.Close() - - err = in.Listen(tunnel) - if !assert.NoError(t, err) { - return - } - defer in.Close() - - addrPort, err := netip.ParseAddrPort(in.Address()) - if !assert.NoError(t, err) { - return - } - - outboundOptions.Name = "hysteria2_outbound" - outboundOptions.Server = addrPort.Addr().String() - outboundOptions.Port = int(addrPort.Port()) - outboundOptions.Password = userUUID - - out, err := outbound.NewHysteria2(outboundOptions) - if !assert.NoError(t, err) { - return - } - defer out.Close() - - tunnel.DoTest(t, out) -} - -func TestInboundHysteria2_TLS(t *testing.T) { - inboundOptions := inbound.Hysteria2Option{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.Hysteria2Option{ - Fingerprint: tlsFingerprint, - } - testInboundHysteria2(t, inboundOptions, outboundOptions) -} - -func TestInboundHysteria2_Salamander(t *testing.T) { - inboundOptions := inbound.Hysteria2Option{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - Obfs: "salamander", - ObfsPassword: userUUID, - } - outboundOptions := outbound.Hysteria2Option{ - Fingerprint: tlsFingerprint, - Obfs: "salamander", - ObfsPassword: userUUID, - } - testInboundHysteria2(t, inboundOptions, outboundOptions) -} - -func TestInboundHysteria2_Brutal(t *testing.T) { - inboundOptions := inbound.Hysteria2Option{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - Up: "30 Mbps", - Down: "200 Mbps", - } - outboundOptions := outbound.Hysteria2Option{ - Fingerprint: tlsFingerprint, - Up: "30 Mbps", - Down: "200 Mbps", - } - testInboundHysteria2(t, inboundOptions, outboundOptions) -} diff --git a/listener/inbound/mixed.go b/listener/inbound/mixed.go deleted file mode 100644 index 20c61f2eec..0000000000 --- a/listener/inbound/mixed.go +++ /dev/null @@ -1,114 +0,0 @@ -package inbound - -import ( - "errors" - "fmt" - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/mixed" - "github.com/metacubex/mihomo/listener/socks" - "github.com/metacubex/mihomo/log" -) - -type MixedOption struct { - BaseOption - Users AuthUsers `inbound:"users,omitempty"` - UDP bool `inbound:"udp,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` -} - -func (o MixedOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Mixed struct { - *Base - config *MixedOption - l []*mixed.Listener - lUDP []*socks.UDPListener - udp bool -} - -func NewMixed(options *MixedOption) (*Mixed, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &Mixed{ - Base: base, - config: options, - udp: options.UDP, - }, nil -} - -// Config implements constant.InboundListener -func (m *Mixed) Config() C.InboundConfig { - return m.config -} - -// Address implements constant.InboundListener -func (m *Mixed) Address() string { - var addrList []string - for _, l := range m.l { - addrList = append(addrList, l.Address()) - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (m *Mixed) Listen(tunnel C.Tunnel) error { - for _, addr := range strings.Split(m.RawAddress(), ",") { - l, err := mixed.NewWithConfig( - LC.AuthServer{ - Enable: true, - Listen: addr, - AuthStore: m.config.Users.GetAuthStore(), - Certificate: m.config.Certificate, - PrivateKey: m.config.PrivateKey, - RealityConfig: m.config.RealityConfig.Build(), - }, - tunnel, - m.Additions()..., - ) - if err != nil { - return err - } - m.l = append(m.l, l) - if m.udp { - lUDP, err := socks.NewUDP(addr, tunnel, m.Additions()...) - if err != nil { - return err - } - m.lUDP = append(m.lUDP, lUDP) - } - } - log.Infoln("Mixed(http+socks)[%s] proxy listening at: %s", m.Name(), m.Address()) - return nil -} - -// Close implements constant.InboundListener -func (m *Mixed) Close() error { - var errs []error - for _, l := range m.l { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) - } - } - for _, l := range m.lUDP { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close udp listener %s err: %w", l.Address(), err)) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - return nil -} - -var _ C.InboundListener = (*Mixed)(nil) diff --git a/listener/inbound/mux.go b/listener/inbound/mux.go deleted file mode 100644 index c4068e10ba..0000000000 --- a/listener/inbound/mux.go +++ /dev/null @@ -1,25 +0,0 @@ -package inbound - -import "github.com/metacubex/mihomo/listener/sing" - -type MuxOption struct { - Padding bool `inbound:"padding,omitempty"` - Brutal BrutalOptions `inbound:"brutal,omitempty"` -} - -type BrutalOptions struct { - Enabled bool `inbound:"enabled,omitempty"` - Up string `inbound:"up,omitempty"` - Down string `inbound:"down,omitempty"` -} - -func (m MuxOption) Build() sing.MuxOption { - return sing.MuxOption{ - Padding: m.Padding, - Brutal: sing.BrutalOptions{ - Enabled: m.Brutal.Enabled, - Up: m.Brutal.Up, - Down: m.Brutal.Down, - }, - } -} diff --git a/listener/inbound/mux_test.go b/listener/inbound/mux_test.go deleted file mode 100644 index 4e676e6d96..0000000000 --- a/listener/inbound/mux_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package inbound_test - -import ( - "testing" - - "github.com/metacubex/mihomo/adapter/outbound" - - "github.com/stretchr/testify/assert" -) - -var singMuxProtocolList = []string{"h2mux", "smux"} // don't test "yamux" because it has some confused bugs - -// notCloseProxyAdapter is a proxy adapter that does not close the underlying outbound.ProxyAdapter. -// The outbound.SingMux will close the underlying outbound.ProxyAdapter when it is closed, but we don't want to close it. -// The underlying outbound.ProxyAdapter should only be closed by the caller of testSingMux. -type notCloseProxyAdapter struct { - outbound.ProxyAdapter -} - -func (n *notCloseProxyAdapter) Close() error { - return nil -} - -func testSingMux(t *testing.T, tunnel *TestTunnel, out outbound.ProxyAdapter) { - t.Run("singmux", func(t *testing.T) { - for _, protocol := range singMuxProtocolList { - protocol := protocol - t.Run(protocol, func(t *testing.T) { - t.Parallel() - singMuxOption := outbound.SingMuxOption{ - Enabled: true, - Protocol: protocol, - } - out, err := outbound.NewSingMux(singMuxOption, ¬CloseProxyAdapter{out}) - if !assert.NoError(t, err) { - return - } - defer out.Close() - - tunnel.DoTest(t, out) - }) - } - }) -} diff --git a/listener/inbound/reality.go b/listener/inbound/reality.go deleted file mode 100644 index 7653cd389d..0000000000 --- a/listener/inbound/reality.go +++ /dev/null @@ -1,23 +0,0 @@ -package inbound - -import "github.com/metacubex/mihomo/listener/reality" - -type RealityConfig struct { - Dest string `inbound:"dest"` - PrivateKey string `inbound:"private-key"` - ShortID []string `inbound:"short-id"` - ServerNames []string `inbound:"server-names"` - MaxTimeDifference int `inbound:"max-time-difference,omitempty"` - Proxy string `inbound:"proxy,omitempty"` -} - -func (c RealityConfig) Build() reality.Config { - return reality.Config{ - Dest: c.Dest, - PrivateKey: c.PrivateKey, - ShortID: c.ShortID, - ServerNames: c.ServerNames, - MaxTimeDifference: c.MaxTimeDifference, - Proxy: c.Proxy, - } -} diff --git a/listener/inbound/redir.go b/listener/inbound/redir.go deleted file mode 100644 index 2981ddfc01..0000000000 --- a/listener/inbound/redir.go +++ /dev/null @@ -1,80 +0,0 @@ -package inbound - -import ( - "errors" - "fmt" - "strings" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/listener/redir" - "github.com/metacubex/mihomo/log" -) - -type RedirOption struct { - BaseOption -} - -func (o RedirOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Redir struct { - *Base - config *RedirOption - l []*redir.Listener -} - -func NewRedir(options *RedirOption) (*Redir, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &Redir{ - Base: base, - config: options, - }, nil -} - -// Config implements constant.InboundListener -func (r *Redir) Config() C.InboundConfig { - return r.config -} - -// Address implements constant.InboundListener -func (r *Redir) Address() string { - var addrList []string - for _, l := range r.l { - addrList = append(addrList, l.Address()) - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (r *Redir) Listen(tunnel C.Tunnel) error { - for _, addr := range strings.Split(r.RawAddress(), ",") { - l, err := redir.New(addr, tunnel, r.Additions()...) - if err != nil { - return err - } - r.l = append(r.l, l) - } - log.Infoln("Redir[%s] proxy listening at: %s", r.Name(), r.Address()) - return nil -} - -// Close implements constant.InboundListener -func (r *Redir) Close() error { - var errs []error - for _, l := range r.l { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close redir listener %s err: %w", l.Address(), err)) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - return nil -} - -var _ C.InboundListener = (*Redir)(nil) diff --git a/listener/inbound/shadowsocks.go b/listener/inbound/shadowsocks.go deleted file mode 100644 index 994f4c597c..0000000000 --- a/listener/inbound/shadowsocks.go +++ /dev/null @@ -1,84 +0,0 @@ -package inbound - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing_shadowsocks" - "github.com/metacubex/mihomo/log" -) - -type ShadowSocksOption struct { - BaseOption - Password string `inbound:"password"` - Cipher string `inbound:"cipher"` - UDP bool `inbound:"udp,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` - ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` -} - -func (o ShadowSocksOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type ShadowSocks struct { - *Base - config *ShadowSocksOption - l C.MultiAddrListener - ss LC.ShadowsocksServer -} - -func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &ShadowSocks{ - Base: base, - config: options, - ss: LC.ShadowsocksServer{ - Enable: true, - Listen: base.RawAddress(), - Password: options.Password, - Cipher: options.Cipher, - Udp: options.UDP, - MuxOption: options.MuxOption.Build(), - ShadowTLS: options.ShadowTLS.Build(), - }, - }, nil -} - -// Config implements constant.InboundListener -func (s *ShadowSocks) Config() C.InboundConfig { - return s.config -} - -// Address implements constant.InboundListener -func (s *ShadowSocks) Address() string { - var addrList []string - if s.l != nil { - for _, addr := range s.l.AddrList() { - addrList = append(addrList, addr.String()) - } - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (s *ShadowSocks) Listen(tunnel C.Tunnel) error { - var err error - s.l, err = sing_shadowsocks.New(s.ss, tunnel, s.Additions()...) - if err != nil { - return err - } - log.Infoln("ShadowSocks[%s] proxy listening at: %s", s.Name(), s.Address()) - return nil -} - -// Close implements constant.InboundListener -func (s *ShadowSocks) Close() error { - return s.l.Close() -} - -var _ C.InboundListener = (*ShadowSocks)(nil) diff --git a/listener/inbound/shadowsocks_test.go b/listener/inbound/shadowsocks_test.go deleted file mode 100644 index 7a26eecaf4..0000000000 --- a/listener/inbound/shadowsocks_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package inbound_test - -import ( - "crypto/rand" - "encoding/base64" - "net" - "net/netip" - "strings" - "testing" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/listener/inbound" - shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" - - shadowsocks "github.com/metacubex/sing-shadowsocks" - "github.com/metacubex/sing-shadowsocks/shadowaead" - "github.com/metacubex/sing-shadowsocks/shadowaead_2022" - "github.com/metacubex/sing-shadowsocks/shadowstream" - "github.com/stretchr/testify/assert" -) - -var noneList = []string{shadowsocks.MethodNone} -var shadowsocksCipherLists = [][]string{noneList, shadowaead.List, shadowaead_2022.List, shadowstream.List} -var shadowsocksCipherShortLists = [][]string{noneList, shadowaead.List[:5]} // for test shadowTLS -var shadowsocksPassword32 string -var shadowsocksPassword16 string - -func init() { - passwordBytes := make([]byte, 32) - rand.Read(passwordBytes) - shadowsocksPassword32 = base64.StdEncoding.EncodeToString(passwordBytes) - shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16]) -} - -func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, cipherLists [][]string) { - t.Parallel() - for _, cipherList := range cipherLists { - for i, cipher := range cipherList { - enableSingMux := i == 0 - cipher := cipher - t.Run(cipher, func(t *testing.T) { - inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value - inboundOptions.Cipher = cipher - outboundOptions.Cipher = cipher - testInboundShadowSocks0(t, inboundOptions, outboundOptions, enableSingMux) - }) - } - } -} - -func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, enableSingMux bool) { - t.Parallel() - password := shadowsocksPassword32 - if strings.Contains(inboundOptions.Cipher, "-128-") { - password = shadowsocksPassword16 - } - inboundOptions.BaseOption = inbound.BaseOption{ - NameStr: "shadowsocks_inbound", - Listen: "127.0.0.1", - Port: "0", - } - inboundOptions.Password = password - in, err := inbound.NewShadowSocks(&inboundOptions) - if !assert.NoError(t, err) { - return - } - - tunnel := NewHttpTestTunnel() - defer tunnel.Close() - - err = in.Listen(tunnel) - if !assert.NoError(t, err) { - return - } - defer in.Close() - - addrPort, err := netip.ParseAddrPort(in.Address()) - if !assert.NoError(t, err) { - return - } - - outboundOptions.Name = "shadowsocks_outbound" - outboundOptions.Server = addrPort.Addr().String() - outboundOptions.Port = int(addrPort.Port()) - outboundOptions.Password = password - - out, err := outbound.NewShadowSocks(outboundOptions) - if !assert.NoError(t, err) { - return - } - defer out.Close() - - tunnel.DoTest(t, out) - - if enableSingMux { - testSingMux(t, tunnel, out) - } -} - -func TestInboundShadowSocks_Basic(t *testing.T) { - inboundOptions := inbound.ShadowSocksOption{} - outboundOptions := outbound.ShadowSocksOption{} - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherLists) -} - -func testInboundShadowSocksShadowTls(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) { - t.Parallel() - t.Run("Conn", func(t *testing.T) { - inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists) - }) - t.Run("UConn", func(t *testing.T) { - inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value - outboundOptions.ClientFingerprint = "chrome" - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists) - }) -} - -func TestInboundShadowSocks_ShadowTlsv1(t *testing.T) { - inboundOptions := inbound.ShadowSocksOption{ - ShadowTLS: inbound.ShadowTLS{ - Enable: true, - Version: 1, - Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, - }, - } - outboundOptions := outbound.ShadowSocksOption{ - Plugin: shadowtls.Mode, - PluginOpts: map[string]any{"host": realityDest, "fingerprint": tlsFingerprint, "version": 1}, - } - testInboundShadowSocksShadowTls(t, inboundOptions, outboundOptions) -} - -func TestInboundShadowSocks_ShadowTlsv2(t *testing.T) { - inboundOptions := inbound.ShadowSocksOption{ - ShadowTLS: inbound.ShadowTLS{ - Enable: true, - Version: 2, - Password: shadowsocksPassword16, - Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, - }, - } - outboundOptions := outbound.ShadowSocksOption{ - Plugin: shadowtls.Mode, - PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 2}, - } - outboundOptions.PluginOpts["alpn"] = []string{"http/1.1"} // shadowtls v2 work confuse with http/2 server, so we set alpn to http/1.1 to pass the test - testInboundShadowSocksShadowTls(t, inboundOptions, outboundOptions) -} - -func TestInboundShadowSocks_ShadowTlsv3(t *testing.T) { - inboundOptions := inbound.ShadowSocksOption{ - ShadowTLS: inbound.ShadowTLS{ - Enable: true, - Version: 3, - Users: []inbound.ShadowTLSUser{{Name: "test", Password: shadowsocksPassword16}}, - Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, - }, - } - outboundOptions := outbound.ShadowSocksOption{ - Plugin: shadowtls.Mode, - PluginOpts: map[string]any{"host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 3}, - } - testInboundShadowSocksShadowTls(t, inboundOptions, outboundOptions) -} diff --git a/listener/inbound/shadowtls.go b/listener/inbound/shadowtls.go deleted file mode 100644 index d71adbf0e4..0000000000 --- a/listener/inbound/shadowtls.go +++ /dev/null @@ -1,58 +0,0 @@ -package inbound - -import ( - "github.com/metacubex/mihomo/common/utils" - LC "github.com/metacubex/mihomo/listener/config" -) - -type ShadowTLS struct { - Enable bool `inbound:"enable"` - Version int `inbound:"version,omitempty"` - Password string `inbound:"password,omitempty"` - Users []ShadowTLSUser `inbound:"users,omitempty"` - Handshake ShadowTLSHandshakeOptions `inbound:"handshake,omitempty"` - HandshakeForServerName map[string]ShadowTLSHandshakeOptions `inbound:"handshake-for-server-name,omitempty"` - StrictMode bool `inbound:"strict-mode,omitempty"` - WildcardSNI string `inbound:"wildcard-sni,omitempty"` -} - -type ShadowTLSUser struct { - Name string `inbound:"name,omitempty"` - Password string `inbound:"password,omitempty"` -} - -type ShadowTLSHandshakeOptions struct { - Dest string `inbound:"dest"` - Proxy string `inbound:"proxy,omitempty"` -} - -func (c ShadowTLS) Build() LC.ShadowTLS { - handshakeForServerName := make(map[string]LC.ShadowTLSHandshakeOptions) - for k, v := range c.HandshakeForServerName { - handshakeForServerName[k] = v.Build() - } - return LC.ShadowTLS{ - Enable: c.Enable, - Version: c.Version, - Password: c.Password, - Users: utils.Map(c.Users, ShadowTLSUser.Build), - Handshake: c.Handshake.Build(), - HandshakeForServerName: handshakeForServerName, - StrictMode: c.StrictMode, - WildcardSNI: c.WildcardSNI, - } -} - -func (c ShadowTLSUser) Build() LC.ShadowTLSUser { - return LC.ShadowTLSUser{ - Name: c.Name, - Password: c.Password, - } -} - -func (c ShadowTLSHandshakeOptions) Build() LC.ShadowTLSHandshakeOptions { - return LC.ShadowTLSHandshakeOptions{ - Dest: c.Dest, - Proxy: c.Proxy, - } -} diff --git a/listener/inbound/socks.go b/listener/inbound/socks.go deleted file mode 100644 index 6cb9782cf0..0000000000 --- a/listener/inbound/socks.go +++ /dev/null @@ -1,114 +0,0 @@ -package inbound - -import ( - "errors" - "fmt" - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/socks" - "github.com/metacubex/mihomo/log" -) - -type SocksOption struct { - BaseOption - Users AuthUsers `inbound:"users,omitempty"` - UDP bool `inbound:"udp,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` -} - -func (o SocksOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Socks struct { - *Base - config *SocksOption - udp bool - stl []*socks.Listener - sul []*socks.UDPListener -} - -func NewSocks(options *SocksOption) (*Socks, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &Socks{ - Base: base, - config: options, - udp: options.UDP, - }, nil -} - -// Config implements constant.InboundListener -func (s *Socks) Config() C.InboundConfig { - return s.config -} - -// Close implements constant.InboundListener -func (s *Socks) Close() error { - var errs []error - for _, l := range s.stl { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) - } - } - for _, l := range s.sul { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close udp listener %s err: %w", l.Address(), err)) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - return nil -} - -// Address implements constant.InboundListener -func (s *Socks) Address() string { - var addrList []string - for _, l := range s.stl { - addrList = append(addrList, l.Address()) - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (s *Socks) Listen(tunnel C.Tunnel) error { - for _, addr := range strings.Split(s.RawAddress(), ",") { - stl, err := socks.NewWithConfig( - LC.AuthServer{ - Enable: true, - Listen: addr, - AuthStore: s.config.Users.GetAuthStore(), - Certificate: s.config.Certificate, - PrivateKey: s.config.PrivateKey, - RealityConfig: s.config.RealityConfig.Build(), - }, - tunnel, - s.Additions()..., - ) - if err != nil { - return err - } - s.stl = append(s.stl, stl) - if s.udp { - sul, err := socks.NewUDP(addr, tunnel, s.Additions()...) - if err != nil { - return err - } - s.sul = append(s.sul, sul) - } - } - - log.Infoln("SOCKS[%s] proxy listening at: %s", s.Name(), s.Address()) - return nil -} - -var _ C.InboundListener = (*Socks)(nil) diff --git a/listener/inbound/tproxy.go b/listener/inbound/tproxy.go deleted file mode 100644 index 66a50f7558..0000000000 --- a/listener/inbound/tproxy.go +++ /dev/null @@ -1,98 +0,0 @@ -package inbound - -import ( - "errors" - "fmt" - "strings" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/listener/tproxy" - "github.com/metacubex/mihomo/log" -) - -type TProxyOption struct { - BaseOption - UDP bool `inbound:"udp,omitempty"` -} - -func (o TProxyOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type TProxy struct { - *Base - config *TProxyOption - lUDP []*tproxy.UDPListener - lTCP []*tproxy.Listener - udp bool -} - -func NewTProxy(options *TProxyOption) (*TProxy, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &TProxy{ - Base: base, - config: options, - udp: options.UDP, - }, nil - -} - -// Config implements constant.InboundListener -func (t *TProxy) Config() C.InboundConfig { - return t.config -} - -// Address implements constant.InboundListener -func (t *TProxy) Address() string { - var addrList []string - for _, l := range t.lTCP { - addrList = append(addrList, l.Address()) - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (t *TProxy) Listen(tunnel C.Tunnel) error { - for _, addr := range strings.Split(t.RawAddress(), ",") { - lTCP, err := tproxy.New(addr, tunnel, t.Additions()...) - if err != nil { - return err - } - t.lTCP = append(t.lTCP, lTCP) - if t.udp { - lUDP, err := tproxy.NewUDP(addr, tunnel, t.Additions()...) - if err != nil { - return err - } - t.lUDP = append(t.lUDP, lUDP) - } - } - log.Infoln("TProxy[%s] proxy listening at: %s", t.Name(), t.Address()) - return nil -} - -// Close implements constant.InboundListener -func (t *TProxy) Close() error { - var errs []error - for _, l := range t.lTCP { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) - } - } - for _, l := range t.lUDP { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close udp listener %s err: %w", l.Address(), err)) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - return nil -} - -var _ C.InboundListener = (*TProxy)(nil) diff --git a/listener/inbound/trojan.go b/listener/inbound/trojan.go deleted file mode 100644 index 44c56b0b43..0000000000 --- a/listener/inbound/trojan.go +++ /dev/null @@ -1,113 +0,0 @@ -package inbound - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/trojan" - "github.com/metacubex/mihomo/log" -) - -type TrojanOption struct { - BaseOption - Users []TrojanUser `inbound:"users"` - WsPath string `inbound:"ws-path,omitempty"` - GrpcServiceName string `inbound:"grpc-service-name,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` - SSOption TrojanSSOption `inbound:"ss-option,omitempty"` -} - -type TrojanUser struct { - Username string `inbound:"username,omitempty"` - Password string `inbound:"password"` -} - -// TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 -type TrojanSSOption struct { - Enabled bool `inbound:"enabled,omitempty"` - Method string `inbound:"method,omitempty"` - Password string `inbound:"password,omitempty"` -} - -func (o TrojanOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Trojan struct { - *Base - config *TrojanOption - l C.MultiAddrListener - vs LC.TrojanServer -} - -func NewTrojan(options *TrojanOption) (*Trojan, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - users := make([]LC.TrojanUser, len(options.Users)) - for i, v := range options.Users { - users[i] = LC.TrojanUser{ - Username: v.Username, - Password: v.Password, - } - } - return &Trojan{ - Base: base, - config: options, - vs: LC.TrojanServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, - WsPath: options.WsPath, - GrpcServiceName: options.GrpcServiceName, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - RealityConfig: options.RealityConfig.Build(), - MuxOption: options.MuxOption.Build(), - TrojanSSOption: LC.TrojanSSOption{ - Enabled: options.SSOption.Enabled, - Method: options.SSOption.Method, - Password: options.SSOption.Password, - }, - }, - }, nil -} - -// Config implements constant.InboundListener -func (v *Trojan) Config() C.InboundConfig { - return v.config -} - -// Address implements constant.InboundListener -func (v *Trojan) Address() string { - var addrList []string - if v.l != nil { - for _, addr := range v.l.AddrList() { - addrList = append(addrList, addr.String()) - } - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (v *Trojan) Listen(tunnel C.Tunnel) error { - var err error - v.l, err = trojan.New(v.vs, tunnel, v.Additions()...) - if err != nil { - return err - } - log.Infoln("Trojan[%s] proxy listening at: %s", v.Name(), v.Address()) - return nil -} - -// Close implements constant.InboundListener -func (v *Trojan) Close() error { - return v.l.Close() -} - -var _ C.InboundListener = (*Trojan)(nil) diff --git a/listener/inbound/trojan_test.go b/listener/inbound/trojan_test.go deleted file mode 100644 index 320081f8c6..0000000000 --- a/listener/inbound/trojan_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package inbound_test - -import ( - "net" - "net/netip" - "testing" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/listener/inbound" - "github.com/stretchr/testify/assert" -) - -func testInboundTrojan(t *testing.T, inboundOptions inbound.TrojanOption, outboundOptions outbound.TrojanOption) { - t.Parallel() - inboundOptions.BaseOption = inbound.BaseOption{ - NameStr: "trojan_inbound", - Listen: "127.0.0.1", - Port: "0", - } - inboundOptions.Users = []inbound.TrojanUser{ - {Username: "test", Password: userUUID}, - } - in, err := inbound.NewTrojan(&inboundOptions) - if !assert.NoError(t, err) { - return - } - - tunnel := NewHttpTestTunnel() - defer tunnel.Close() - - err = in.Listen(tunnel) - if !assert.NoError(t, err) { - return - } - defer in.Close() - - addrPort, err := netip.ParseAddrPort(in.Address()) - if !assert.NoError(t, err) { - return - } - - outboundOptions.Name = "trojan_outbound" - outboundOptions.Server = addrPort.Addr().String() - outboundOptions.Port = int(addrPort.Port()) - outboundOptions.Password = userUUID - - out, err := outbound.NewTrojan(outboundOptions) - if !assert.NoError(t, err) { - return - } - defer out.Close() - - tunnel.DoTest(t, out) - - testSingMux(t, tunnel, out) -} - -func TestInboundTrojan_TLS(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.TrojanOption{ - Fingerprint: tlsFingerprint, - } - testInboundTrojan(t, inboundOptions, outboundOptions) -} - -func TestInboundTrojan_Wss1(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - WsPath: "/ws", - } - outboundOptions := outbound.TrojanOption{ - Fingerprint: tlsFingerprint, - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, - } - testInboundTrojan(t, inboundOptions, outboundOptions) -} - -func TestInboundTrojan_Wss2(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - WsPath: "/ws", - GrpcServiceName: "GunService", - } - outboundOptions := outbound.TrojanOption{ - Fingerprint: tlsFingerprint, - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, - } - testInboundTrojan(t, inboundOptions, outboundOptions) -} - -func TestInboundTrojan_Grpc1(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - GrpcServiceName: "GunService", - } - outboundOptions := outbound.TrojanOption{ - Fingerprint: tlsFingerprint, - Network: "grpc", - GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, - } - testInboundTrojan(t, inboundOptions, outboundOptions) -} - -func TestInboundTrojan_Grpc2(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - WsPath: "/ws", - GrpcServiceName: "GunService", - } - outboundOptions := outbound.TrojanOption{ - Fingerprint: tlsFingerprint, - Network: "grpc", - GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, - } - testInboundTrojan(t, inboundOptions, outboundOptions) -} - -func TestInboundTrojan_Reality(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - RealityConfig: inbound.RealityConfig{ - Dest: net.JoinHostPort(realityDest, "443"), - PrivateKey: realityPrivateKey, - ShortID: []string{realityShortid}, - ServerNames: []string{realityDest}, - }, - } - outboundOptions := outbound.TrojanOption{ - SNI: realityDest, - RealityOpts: outbound.RealityOptions{ - PublicKey: realityPublickey, - ShortID: realityShortid, - }, - ClientFingerprint: "chrome", - } - testInboundTrojan(t, inboundOptions, outboundOptions) -} - -func TestInboundTrojan_Reality_Grpc(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - RealityConfig: inbound.RealityConfig{ - Dest: net.JoinHostPort(realityDest, "443"), - PrivateKey: realityPrivateKey, - ShortID: []string{realityShortid}, - ServerNames: []string{realityDest}, - }, - GrpcServiceName: "GunService", - } - outboundOptions := outbound.TrojanOption{ - SNI: realityDest, - RealityOpts: outbound.RealityOptions{ - PublicKey: realityPublickey, - ShortID: realityShortid, - }, - ClientFingerprint: "chrome", - Network: "grpc", - GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, - } - testInboundTrojan(t, inboundOptions, outboundOptions) -} - -func TestInboundTrojan_TLS_TrojanSS(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - SSOption: inbound.TrojanSSOption{ - Enabled: true, - Method: "", - Password: "password", - }, - } - outboundOptions := outbound.TrojanOption{ - Fingerprint: tlsFingerprint, - SSOpts: outbound.TrojanSSOption{ - Enabled: true, - Method: "", - Password: "password", - }, - } - testInboundTrojan(t, inboundOptions, outboundOptions) -} - -func TestInboundTrojan_Wss_TrojanSS(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - SSOption: inbound.TrojanSSOption{ - Enabled: true, - Method: "", - Password: "password", - }, - WsPath: "/ws", - } - outboundOptions := outbound.TrojanOption{ - Fingerprint: tlsFingerprint, - SSOpts: outbound.TrojanSSOption{ - Enabled: true, - Method: "", - Password: "password", - }, - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, - } - testInboundTrojan(t, inboundOptions, outboundOptions) -} diff --git a/listener/inbound/tuic.go b/listener/inbound/tuic.go deleted file mode 100644 index e9c2f21bc1..0000000000 --- a/listener/inbound/tuic.go +++ /dev/null @@ -1,96 +0,0 @@ -package inbound - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/tuic" - "github.com/metacubex/mihomo/log" -) - -type TuicOption struct { - BaseOption - Token []string `inbound:"token,omitempty"` - Users map[string]string `inbound:"users,omitempty"` - Certificate string `inbound:"certificate"` - PrivateKey string `inbound:"private-key"` - CongestionController string `inbound:"congestion-controller,omitempty"` - MaxIdleTime int `inbound:"max-idle-time,omitempty"` - AuthenticationTimeout int `inbound:"authentication-timeout,omitempty"` - ALPN []string `inbound:"alpn,omitempty"` - MaxUdpRelayPacketSize int `inbound:"max-udp-relay-packet-size,omitempty"` - CWND int `inbound:"cwnd,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` -} - -func (o TuicOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Tuic struct { - *Base - config *TuicOption - l *tuic.Listener - ts LC.TuicServer -} - -func NewTuic(options *TuicOption) (*Tuic, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &Tuic{ - Base: base, - config: options, - ts: LC.TuicServer{ - Enable: true, - Listen: base.RawAddress(), - Token: options.Token, - Users: options.Users, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - CongestionController: options.CongestionController, - MaxIdleTime: options.MaxIdleTime, - AuthenticationTimeout: options.AuthenticationTimeout, - ALPN: options.ALPN, - MaxUdpRelayPacketSize: options.MaxUdpRelayPacketSize, - CWND: options.CWND, - MuxOption: options.MuxOption.Build(), - }, - }, nil -} - -// Config implements constant.InboundListener -func (t *Tuic) Config() C.InboundConfig { - return t.config -} - -// Address implements constant.InboundListener -func (t *Tuic) Address() string { - var addrList []string - if t.l != nil { - for _, addr := range t.l.AddrList() { - addrList = append(addrList, addr.String()) - } - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (t *Tuic) Listen(tunnel C.Tunnel) error { - var err error - t.l, err = tuic.New(t.ts, tunnel, t.Additions()...) - if err != nil { - return err - } - log.Infoln("Tuic[%s] proxy listening at: %s", t.Name(), t.Address()) - return nil -} - -// Close implements constant.InboundListener -func (t *Tuic) Close() error { - return t.l.Close() -} - -var _ C.InboundListener = (*Tuic)(nil) diff --git a/listener/inbound/tuic_test.go b/listener/inbound/tuic_test.go deleted file mode 100644 index 24865d8339..0000000000 --- a/listener/inbound/tuic_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package inbound_test - -import ( - "net/netip" - "testing" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/listener/inbound" - - "github.com/stretchr/testify/assert" -) - -var tuicCCs = []string{"cubic", "new_reno", "bbr"} - -func testInboundTuic(t *testing.T, inboundOptions inbound.TuicOption, outboundOptions outbound.TuicOption) { - t.Parallel() - inboundOptions.Users = map[string]string{userUUID: userUUID} - inboundOptions.Token = []string{userUUID} - - for _, tuicCC := range tuicCCs { - tuicCC := tuicCC - t.Run(tuicCC, func(t *testing.T) { - t.Parallel() - t.Run("v4", func(t *testing.T) { - inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value - outboundOptions.Token = userUUID - outboundOptions.CongestionController = tuicCC - inboundOptions.CongestionController = tuicCC - testInboundTuic0(t, inboundOptions, outboundOptions) - }) - t.Run("v5", func(t *testing.T) { - inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value - outboundOptions.UUID = userUUID - outboundOptions.Password = userUUID - outboundOptions.CongestionController = tuicCC - inboundOptions.CongestionController = tuicCC - testInboundTuic0(t, inboundOptions, outboundOptions) - }) - }) - } -} - -func testInboundTuic0(t *testing.T, inboundOptions inbound.TuicOption, outboundOptions outbound.TuicOption) { - t.Parallel() - inboundOptions.BaseOption = inbound.BaseOption{ - NameStr: "tuic_inbound", - Listen: "127.0.0.1", - Port: "0", - } - in, err := inbound.NewTuic(&inboundOptions) - if !assert.NoError(t, err) { - return - } - - tunnel := NewHttpTestTunnel() - defer tunnel.Close() - - err = in.Listen(tunnel) - if !assert.NoError(t, err) { - return - } - defer in.Close() - - addrPort, err := netip.ParseAddrPort(in.Address()) - if !assert.NoError(t, err) { - return - } - - outboundOptions.Name = "tuic_outbound" - outboundOptions.Server = addrPort.Addr().String() - outboundOptions.Port = int(addrPort.Port()) - - out, err := outbound.NewTuic(outboundOptions) - if !assert.NoError(t, err) { - return - } - defer out.Close() - - tunnel.DoTest(t, out) -} - -func TestInboundTuic_TLS(t *testing.T) { - inboundOptions := inbound.TuicOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - AuthenticationTimeout: 5000, - } - outboundOptions := outbound.TuicOption{ - Fingerprint: tlsFingerprint, - } - testInboundTuic(t, inboundOptions, outboundOptions) -} diff --git a/listener/inbound/tun.go b/listener/inbound/tun.go deleted file mode 100644 index 270bf3fa73..0000000000 --- a/listener/inbound/tun.go +++ /dev/null @@ -1,189 +0,0 @@ -package inbound - -import ( - "errors" - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing_tun" - "github.com/metacubex/mihomo/log" -) - -type TunOption struct { - BaseOption - Device string `inbound:"device,omitempty"` - Stack string `inbound:"stack,omitempty"` - DNSHijack []string `inbound:"dns-hijack,omitempty"` - AutoRoute bool `inbound:"auto-route,omitempty"` - AutoDetectInterface bool `inbound:"auto-detect-interface,omitempty"` - - MTU uint32 `inbound:"mtu,omitempty"` - GSO bool `inbound:"gso,omitempty"` - GSOMaxSize uint32 `inbound:"gso-max-size,omitempty"` - Inet4Address []string `inbound:"inet4-address,omitempty"` - Inet6Address []string `inbound:"inet6-address,omitempty"` - IPRoute2TableIndex int `inbound:"iproute2-table-index,omitempty"` - IPRoute2RuleIndex int `inbound:"iproute2-rule-index,omitempty"` - AutoRedirect bool `inbound:"auto-redirect,omitempty"` - AutoRedirectInputMark uint32 `inbound:"auto-redirect-input-mark,omitempty"` - AutoRedirectOutputMark uint32 `inbound:"auto-redirect-output-mark,omitempty"` - StrictRoute bool `inbound:"strict-route,omitempty"` - RouteAddress []string `inbound:"route-address,omitempty"` - RouteAddressSet []string `inbound:"route-address-set,omitempty"` - RouteExcludeAddress []string `inbound:"route-exclude-address,omitempty"` - RouteExcludeAddressSet []string `inbound:"route-exclude-address-set,omitempty"` - IncludeInterface []string `inbound:"include-interface,omitempty"` - ExcludeInterface []string `inbound:"exclude-interface,omitempty"` - IncludeUID []uint32 `inbound:"include-uid,omitempty"` - IncludeUIDRange []string `inbound:"include-uid-range,omitempty"` - ExcludeUID []uint32 `inbound:"exclude-uid,omitempty"` - ExcludeUIDRange []string `inbound:"exclude-uid-range,omitempty"` - ExcludeSrcPort []uint16 `inbound:"exclude-src-port,omitempty"` - ExcludeSrcPortRange []string `inbound:"exclude-src-port-range,omitempty"` - ExcludeDstPort []uint16 `inbound:"exclude-dst-port,omitempty"` - ExcludeDstPortRange []string `inbound:"exclude-dst-port-range,omitempty"` - IncludeAndroidUser []int `inbound:"include-android-user,omitempty"` - IncludePackage []string `inbound:"include-package,omitempty"` - ExcludePackage []string `inbound:"exclude-package,omitempty"` - EndpointIndependentNat bool `inbound:"endpoint-independent-nat,omitempty"` - UDPTimeout int64 `inbound:"udp-timeout,omitempty"` - FileDescriptor int `inbound:"file-descriptor,omitempty"` - - Inet4RouteAddress []string `inbound:"inet4-route-address,omitempty"` - Inet6RouteAddress []string `inbound:"inet6-route-address,omitempty"` - Inet4RouteExcludeAddress []string `inbound:"inet4-route-exclude-address,omitempty"` - Inet6RouteExcludeAddress []string `inbound:"inet6-route-exclude-address,omitempty"` -} - -func (o TunOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Tun struct { - *Base - config *TunOption - l *sing_tun.Listener - tun LC.Tun -} - -func NewTun(options *TunOption) (*Tun, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - stack, exist := C.StackTypeMapping[strings.ToLower(options.Stack)] - if !exist { - return nil, errors.New("invalid tun stack") - } - - routeAddress, err := LC.StringSliceToNetipPrefixSlice(options.RouteAddress) - if err != nil { - return nil, err - } - routeExcludeAddress, err := LC.StringSliceToNetipPrefixSlice(options.RouteExcludeAddress) - if err != nil { - return nil, err - } - - inet4Address, err := LC.StringSliceToNetipPrefixSlice(options.Inet4Address) - if err != nil { - return nil, err - } - inet6Address, err := LC.StringSliceToNetipPrefixSlice(options.Inet6Address) - if err != nil { - return nil, err - } - inet4RouteAddress, err := LC.StringSliceToNetipPrefixSlice(options.Inet4RouteAddress) - if err != nil { - return nil, err - } - inet6RouteAddress, err := LC.StringSliceToNetipPrefixSlice(options.Inet6RouteAddress) - if err != nil { - return nil, err - } - inet4RouteExcludeAddress, err := LC.StringSliceToNetipPrefixSlice(options.Inet4RouteExcludeAddress) - if err != nil { - return nil, err - } - inet6RouteExcludeAddress, err := LC.StringSliceToNetipPrefixSlice(options.Inet6RouteExcludeAddress) - if err != nil { - return nil, err - } - return &Tun{ - Base: base, - config: options, - tun: LC.Tun{ - Enable: true, - Device: options.Device, - Stack: stack, - DNSHijack: options.DNSHijack, - AutoRoute: options.AutoRoute, - AutoDetectInterface: options.AutoDetectInterface, - MTU: options.MTU, - GSO: options.GSO, - GSOMaxSize: options.GSOMaxSize, - Inet4Address: inet4Address, - Inet6Address: inet6Address, - IPRoute2TableIndex: options.IPRoute2TableIndex, - IPRoute2RuleIndex: options.IPRoute2RuleIndex, - AutoRedirect: options.AutoRedirect, - AutoRedirectInputMark: options.AutoRedirectInputMark, - AutoRedirectOutputMark: options.AutoRedirectOutputMark, - StrictRoute: options.StrictRoute, - RouteAddress: routeAddress, - RouteAddressSet: options.RouteAddressSet, - RouteExcludeAddress: routeExcludeAddress, - RouteExcludeAddressSet: options.RouteExcludeAddressSet, - IncludeInterface: options.IncludeInterface, - ExcludeInterface: options.ExcludeInterface, - IncludeUID: options.IncludeUID, - IncludeUIDRange: options.IncludeUIDRange, - ExcludeUID: options.ExcludeUID, - ExcludeUIDRange: options.ExcludeUIDRange, - ExcludeSrcPort: options.ExcludeSrcPort, - ExcludeSrcPortRange: options.ExcludeSrcPortRange, - ExcludeDstPort: options.ExcludeDstPort, - ExcludeDstPortRange: options.ExcludeDstPortRange, - IncludeAndroidUser: options.IncludeAndroidUser, - IncludePackage: options.IncludePackage, - ExcludePackage: options.ExcludePackage, - EndpointIndependentNat: options.EndpointIndependentNat, - UDPTimeout: options.UDPTimeout, - FileDescriptor: options.FileDescriptor, - - Inet4RouteAddress: inet4RouteAddress, - Inet6RouteAddress: inet6RouteAddress, - Inet4RouteExcludeAddress: inet4RouteExcludeAddress, - Inet6RouteExcludeAddress: inet6RouteExcludeAddress, - }, - }, nil -} - -// Config implements constant.InboundListener -func (t *Tun) Config() C.InboundConfig { - return t.config -} - -// Address implements constant.InboundListener -func (t *Tun) Address() string { - return t.l.Address() -} - -// Listen implements constant.InboundListener -func (t *Tun) Listen(tunnel C.Tunnel) error { - var err error - t.l, err = sing_tun.New(t.tun, tunnel, t.Additions()...) - if err != nil { - return err - } - log.Infoln("Tun[%s] proxy listening at: %s", t.Name(), t.Address()) - return nil -} - -// Close implements constant.InboundListener -func (t *Tun) Close() error { - return t.l.Close() -} - -var _ C.InboundListener = (*Tun)(nil) diff --git a/listener/inbound/tunnel.go b/listener/inbound/tunnel.go deleted file mode 100644 index 0b3d5b230e..0000000000 --- a/listener/inbound/tunnel.go +++ /dev/null @@ -1,106 +0,0 @@ -package inbound - -import ( - "errors" - "fmt" - "strings" - - C "github.com/metacubex/mihomo/constant" - LT "github.com/metacubex/mihomo/listener/tunnel" - "github.com/metacubex/mihomo/log" -) - -type TunnelOption struct { - BaseOption - Network []string `inbound:"network"` - Target string `inbound:"target"` -} - -func (o TunnelOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Tunnel struct { - *Base - config *TunnelOption - ttl []*LT.Listener - tul []*LT.PacketConn -} - -func NewTunnel(options *TunnelOption) (*Tunnel, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - return &Tunnel{ - Base: base, - config: options, - }, nil -} - -// Config implements constant.InboundListener -func (t *Tunnel) Config() C.InboundConfig { - return t.config -} - -// Close implements constant.InboundListener -func (t *Tunnel) Close() error { - var errs []error - for _, l := range t.ttl { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close tcp listener %s err: %w", l.Address(), err)) - } - } - for _, l := range t.tul { - err := l.Close() - if err != nil { - errs = append(errs, fmt.Errorf("close udp listener %s err: %w", l.Address(), err)) - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - return nil -} - -// Address implements constant.InboundListener -func (t *Tunnel) Address() string { - var addrList []string - for _, l := range t.ttl { - addrList = append(addrList, "tcp://"+l.Address()) - } - for _, l := range t.tul { - addrList = append(addrList, "udp://"+l.Address()) - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (t *Tunnel) Listen(tunnel C.Tunnel) error { - for _, addr := range strings.Split(t.RawAddress(), ",") { - for _, network := range t.config.Network { - switch network { - case "tcp": - ttl, err := LT.New(addr, t.config.Target, t.config.SpecialProxy, tunnel, t.Additions()...) - if err != nil { - return err - } - t.ttl = append(t.ttl, ttl) - case "udp": - tul, err := LT.NewUDP(addr, t.config.Target, t.config.SpecialProxy, tunnel, t.Additions()...) - if err != nil { - return err - } - t.tul = append(t.tul, tul) - default: - log.Warnln("unknown network type: %s, passed", network) - continue - } - } - } - log.Infoln("Tunnel[%s](%s)proxy listening at: %s", t.Name(), t.config.Target, t.Address()) - return nil -} - -var _ C.InboundListener = (*Tunnel)(nil) diff --git a/listener/inbound/vless.go b/listener/inbound/vless.go deleted file mode 100644 index 0cbf214fb5..0000000000 --- a/listener/inbound/vless.go +++ /dev/null @@ -1,102 +0,0 @@ -package inbound - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing_vless" - "github.com/metacubex/mihomo/log" -) - -type VlessOption struct { - BaseOption - Users []VlessUser `inbound:"users"` - WsPath string `inbound:"ws-path,omitempty"` - GrpcServiceName string `inbound:"grpc-service-name,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` -} - -type VlessUser struct { - Username string `inbound:"username,omitempty"` - UUID string `inbound:"uuid"` - Flow string `inbound:"flow,omitempty"` -} - -func (o VlessOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Vless struct { - *Base - config *VlessOption - l C.MultiAddrListener - vs LC.VlessServer -} - -func NewVless(options *VlessOption) (*Vless, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - users := make([]LC.VlessUser, len(options.Users)) - for i, v := range options.Users { - users[i] = LC.VlessUser{ - Username: v.Username, - UUID: v.UUID, - Flow: v.Flow, - } - } - return &Vless{ - Base: base, - config: options, - vs: LC.VlessServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, - WsPath: options.WsPath, - GrpcServiceName: options.GrpcServiceName, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - RealityConfig: options.RealityConfig.Build(), - MuxOption: options.MuxOption.Build(), - }, - }, nil -} - -// Config implements constant.InboundListener -func (v *Vless) Config() C.InboundConfig { - return v.config -} - -// Address implements constant.InboundListener -func (v *Vless) Address() string { - var addrList []string - if v.l != nil { - for _, addr := range v.l.AddrList() { - addrList = append(addrList, addr.String()) - } - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (v *Vless) Listen(tunnel C.Tunnel) error { - var err error - v.l, err = sing_vless.New(v.vs, tunnel, v.Additions()...) - if err != nil { - return err - } - log.Infoln("Vless[%s] proxy listening at: %s", v.Name(), v.Address()) - return nil -} - -// Close implements constant.InboundListener -func (v *Vless) Close() error { - return v.l.Close() -} - -var _ C.InboundListener = (*Vless)(nil) diff --git a/listener/inbound/vless_test.go b/listener/inbound/vless_test.go deleted file mode 100644 index f19cad348f..0000000000 --- a/listener/inbound/vless_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package inbound_test - -import ( - "net" - "net/netip" - "testing" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/listener/inbound" - "github.com/stretchr/testify/assert" -) - -func testInboundVless(t *testing.T, inboundOptions inbound.VlessOption, outboundOptions outbound.VlessOption) { - t.Parallel() - inboundOptions.BaseOption = inbound.BaseOption{ - NameStr: "vless_inbound", - Listen: "127.0.0.1", - Port: "0", - } - inboundOptions.Users = []inbound.VlessUser{ - {Username: "test", UUID: userUUID, Flow: "xtls-rprx-vision"}, - } - in, err := inbound.NewVless(&inboundOptions) - if !assert.NoError(t, err) { - return - } - - tunnel := NewHttpTestTunnel() - defer tunnel.Close() - - err = in.Listen(tunnel) - if !assert.NoError(t, err) { - return - } - defer in.Close() - - addrPort, err := netip.ParseAddrPort(in.Address()) - if !assert.NoError(t, err) { - return - } - - outboundOptions.Name = "vless_outbound" - outboundOptions.Server = addrPort.Addr().String() - outboundOptions.Port = int(addrPort.Port()) - outboundOptions.UUID = userUUID - - out, err := outbound.NewVless(outboundOptions) - if !assert.NoError(t, err) { - return - } - defer out.Close() - - tunnel.DoTest(t, out) - - testSingMux(t, tunnel, out) -} - -func TestInboundVless_TLS(t *testing.T) { - inboundOptions := inbound.VlessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.VlessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) -} - -func TestInboundVless_Wss1(t *testing.T) { - inboundOptions := inbound.VlessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - WsPath: "/ws", - } - outboundOptions := outbound.VlessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, - } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) -} - -func TestInboundVless_Wss2(t *testing.T) { - inboundOptions := inbound.VlessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - WsPath: "/ws", - GrpcServiceName: "GunService", - } - outboundOptions := outbound.VlessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, - } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) -} - -func TestInboundVless_Grpc1(t *testing.T) { - inboundOptions := inbound.VlessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - GrpcServiceName: "GunService", - } - outboundOptions := outbound.VlessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "grpc", - GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, - } - testInboundVless(t, inboundOptions, outboundOptions) -} - -func TestInboundVless_Grpc2(t *testing.T) { - inboundOptions := inbound.VlessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - WsPath: "/ws", - GrpcServiceName: "GunService", - } - outboundOptions := outbound.VlessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "grpc", - GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, - } - testInboundVless(t, inboundOptions, outboundOptions) -} - -func TestInboundVless_Reality(t *testing.T) { - inboundOptions := inbound.VlessOption{ - RealityConfig: inbound.RealityConfig{ - Dest: net.JoinHostPort(realityDest, "443"), - PrivateKey: realityPrivateKey, - ShortID: []string{realityShortid}, - ServerNames: []string{realityDest}, - }, - } - outboundOptions := outbound.VlessOption{ - TLS: true, - ServerName: realityDest, - RealityOpts: outbound.RealityOptions{ - PublicKey: realityPublickey, - ShortID: realityShortid, - }, - ClientFingerprint: "chrome", - } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) -} - -func TestInboundVless_Reality_Grpc(t *testing.T) { - inboundOptions := inbound.VlessOption{ - RealityConfig: inbound.RealityConfig{ - Dest: net.JoinHostPort(realityDest, "443"), - PrivateKey: realityPrivateKey, - ShortID: []string{realityShortid}, - ServerNames: []string{realityDest}, - }, - GrpcServiceName: "GunService", - } - outboundOptions := outbound.VlessOption{ - TLS: true, - ServerName: realityDest, - RealityOpts: outbound.RealityOptions{ - PublicKey: realityPublickey, - ShortID: realityShortid, - }, - ClientFingerprint: "chrome", - Network: "grpc", - GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, - } - testInboundVless(t, inboundOptions, outboundOptions) -} diff --git a/listener/inbound/vmess.go b/listener/inbound/vmess.go deleted file mode 100644 index 2212a75db7..0000000000 --- a/listener/inbound/vmess.go +++ /dev/null @@ -1,102 +0,0 @@ -package inbound - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing_vmess" - "github.com/metacubex/mihomo/log" -) - -type VmessOption struct { - BaseOption - Users []VmessUser `inbound:"users"` - WsPath string `inbound:"ws-path,omitempty"` - GrpcServiceName string `inbound:"grpc-service-name,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` -} - -type VmessUser struct { - Username string `inbound:"username,omitempty"` - UUID string `inbound:"uuid"` - AlterID int `inbound:"alterId"` -} - -func (o VmessOption) Equal(config C.InboundConfig) bool { - return optionToString(o) == optionToString(config) -} - -type Vmess struct { - *Base - config *VmessOption - l C.MultiAddrListener - vs LC.VmessServer -} - -func NewVmess(options *VmessOption) (*Vmess, error) { - base, err := NewBase(&options.BaseOption) - if err != nil { - return nil, err - } - users := make([]LC.VmessUser, len(options.Users)) - for i, v := range options.Users { - users[i] = LC.VmessUser{ - Username: v.Username, - UUID: v.UUID, - AlterID: v.AlterID, - } - } - return &Vmess{ - Base: base, - config: options, - vs: LC.VmessServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, - WsPath: options.WsPath, - GrpcServiceName: options.GrpcServiceName, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - RealityConfig: options.RealityConfig.Build(), - MuxOption: options.MuxOption.Build(), - }, - }, nil -} - -// Config implements constant.InboundListener -func (v *Vmess) Config() C.InboundConfig { - return v.config -} - -// Address implements constant.InboundListener -func (v *Vmess) Address() string { - var addrList []string - if v.l != nil { - for _, addr := range v.l.AddrList() { - addrList = append(addrList, addr.String()) - } - } - return strings.Join(addrList, ",") -} - -// Listen implements constant.InboundListener -func (v *Vmess) Listen(tunnel C.Tunnel) error { - var err error - v.l, err = sing_vmess.New(v.vs, tunnel, v.Additions()...) - if err != nil { - return err - } - log.Infoln("Vmess[%s] proxy listening at: %s", v.Name(), v.Address()) - return nil -} - -// Close implements constant.InboundListener -func (v *Vmess) Close() error { - return v.l.Close() -} - -var _ C.InboundListener = (*Vmess)(nil) diff --git a/listener/inbound/vmess_test.go b/listener/inbound/vmess_test.go deleted file mode 100644 index 57af5b0b90..0000000000 --- a/listener/inbound/vmess_test.go +++ /dev/null @@ -1,257 +0,0 @@ -package inbound_test - -import ( - "net" - "net/netip" - "testing" - - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/listener/inbound" - "github.com/stretchr/testify/assert" -) - -func testInboundVMess(t *testing.T, inboundOptions inbound.VmessOption, outboundOptions outbound.VmessOption) { - t.Parallel() - inboundOptions.BaseOption = inbound.BaseOption{ - NameStr: "vmess_inbound", - Listen: "127.0.0.1", - Port: "0", - } - inboundOptions.Users = []inbound.VmessUser{ - {Username: "test", UUID: userUUID, AlterID: 0}, - } - in, err := inbound.NewVmess(&inboundOptions) - if !assert.NoError(t, err) { - return - } - - tunnel := NewHttpTestTunnel() - defer tunnel.Close() - - err = in.Listen(tunnel) - if !assert.NoError(t, err) { - return - } - defer in.Close() - - addrPort, err := netip.ParseAddrPort(in.Address()) - if !assert.NoError(t, err) { - return - } - - outboundOptions.Name = "vmess_outbound" - outboundOptions.Server = addrPort.Addr().String() - outboundOptions.Port = int(addrPort.Port()) - outboundOptions.UUID = userUUID - outboundOptions.AlterID = 0 - outboundOptions.Cipher = "auto" - - out, err := outbound.NewVmess(outboundOptions) - if !assert.NoError(t, err) { - return - } - defer out.Close() - - tunnel.DoTest(t, out) - - testSingMux(t, tunnel, out) -} - -func TestInboundVMess_Basic(t *testing.T) { - inboundOptions := inbound.VmessOption{} - outboundOptions := outbound.VmessOption{} - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_TLS(t *testing.T) { - inboundOptions := inbound.VmessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.VmessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Ws(t *testing.T) { - inboundOptions := inbound.VmessOption{ - WsPath: "/ws", - } - outboundOptions := outbound.VmessOption{ - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Ws_ed1(t *testing.T) { - inboundOptions := inbound.VmessOption{ - WsPath: "/ws", - } - outboundOptions := outbound.VmessOption{ - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws?ed=2048", - }, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Ws_ed2(t *testing.T) { - inboundOptions := inbound.VmessOption{ - WsPath: "/ws", - } - outboundOptions := outbound.VmessOption{ - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - MaxEarlyData: 2048, - EarlyDataHeaderName: "Sec-WebSocket-Protocol", - }, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Ws_Upgrade1(t *testing.T) { - inboundOptions := inbound.VmessOption{ - WsPath: "/ws", - } - outboundOptions := outbound.VmessOption{ - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - V2rayHttpUpgrade: true, - }, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Ws_Upgrade2(t *testing.T) { - inboundOptions := inbound.VmessOption{ - WsPath: "/ws", - } - outboundOptions := outbound.VmessOption{ - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - V2rayHttpUpgrade: true, - V2rayHttpUpgradeFastOpen: true, - }, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Wss1(t *testing.T) { - inboundOptions := inbound.VmessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - WsPath: "/ws", - } - outboundOptions := outbound.VmessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Wss2(t *testing.T) { - inboundOptions := inbound.VmessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - WsPath: "/ws", - GrpcServiceName: "GunService", - } - outboundOptions := outbound.VmessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Grpc1(t *testing.T) { - inboundOptions := inbound.VmessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - GrpcServiceName: "GunService", - } - outboundOptions := outbound.VmessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "grpc", - GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Grpc2(t *testing.T) { - inboundOptions := inbound.VmessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - WsPath: "/ws", - GrpcServiceName: "GunService", - } - outboundOptions := outbound.VmessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "grpc", - GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Reality(t *testing.T) { - inboundOptions := inbound.VmessOption{ - RealityConfig: inbound.RealityConfig{ - Dest: net.JoinHostPort(realityDest, "443"), - PrivateKey: realityPrivateKey, - ShortID: []string{realityShortid}, - ServerNames: []string{realityDest}, - }, - } - outboundOptions := outbound.VmessOption{ - TLS: true, - ServerName: realityDest, - RealityOpts: outbound.RealityOptions{ - PublicKey: realityPublickey, - ShortID: realityShortid, - }, - ClientFingerprint: "chrome", - } - testInboundVMess(t, inboundOptions, outboundOptions) -} - -func TestInboundVMess_Reality_Grpc(t *testing.T) { - inboundOptions := inbound.VmessOption{ - RealityConfig: inbound.RealityConfig{ - Dest: net.JoinHostPort(realityDest, "443"), - PrivateKey: realityPrivateKey, - ShortID: []string{realityShortid}, - ServerNames: []string{realityDest}, - }, - GrpcServiceName: "GunService", - } - outboundOptions := outbound.VmessOption{ - TLS: true, - ServerName: realityDest, - RealityOpts: outbound.RealityOptions{ - PublicKey: realityPublickey, - ShortID: realityShortid, - }, - ClientFingerprint: "chrome", - Network: "grpc", - GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, - } - testInboundVMess(t, inboundOptions, outboundOptions) -} diff --git a/listener/inner/tcp.go b/listener/inner/tcp.go deleted file mode 100644 index 95753fe9c4..0000000000 --- a/listener/inner/tcp.go +++ /dev/null @@ -1,42 +0,0 @@ -package inner - -import ( - "errors" - "net" - - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" -) - -var tunnel C.Tunnel - -func New(t C.Tunnel) { - tunnel = t -} - -func GetTunnel() C.Tunnel { - return tunnel -} - -func HandleTcp(tunnel C.Tunnel, address string, proxy string) (conn net.Conn, err error) { - if tunnel == nil { - return nil, errors.New("tunnel uninitialized") - } - // executor Parsed - conn1, conn2 := N.Pipe() - - metadata := &C.Metadata{} - metadata.NetWork = C.TCP - metadata.Type = C.INNER - metadata.DNSMode = C.DNSNormal - metadata.Process = C.MihomoName - if proxy != "" { - metadata.SpecialProxy = proxy - } - if err = metadata.SetRemoteAddress(address); err != nil { - return nil, err - } - - go tunnel.HandleTCPConn(conn2, metadata) - return conn1, nil -} diff --git a/listener/listener.go b/listener/listener.go deleted file mode 100644 index 37bcbebf34..0000000000 --- a/listener/listener.go +++ /dev/null @@ -1,727 +0,0 @@ -package listener - -import ( - "fmt" - "net" - "strconv" - "strings" - "sync" - - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/http" - "github.com/metacubex/mihomo/listener/mixed" - "github.com/metacubex/mihomo/listener/redir" - embedSS "github.com/metacubex/mihomo/listener/shadowsocks" - "github.com/metacubex/mihomo/listener/sing_shadowsocks" - "github.com/metacubex/mihomo/listener/sing_tun" - "github.com/metacubex/mihomo/listener/sing_vmess" - "github.com/metacubex/mihomo/listener/socks" - "github.com/metacubex/mihomo/listener/tproxy" - "github.com/metacubex/mihomo/listener/tuic" - LT "github.com/metacubex/mihomo/listener/tunnel" - "github.com/metacubex/mihomo/log" - - "github.com/samber/lo" -) - -var ( - allowLan = false - bindAddress = "*" - - socksListener *socks.Listener - socksUDPListener *socks.UDPListener - httpListener *http.Listener - redirListener *redir.Listener - redirUDPListener *tproxy.UDPListener - tproxyListener *tproxy.Listener - tproxyUDPListener *tproxy.UDPListener - mixedListener *mixed.Listener - mixedUDPLister *socks.UDPListener - tunnelTCPListeners = map[string]*LT.Listener{} - tunnelUDPListeners = map[string]*LT.PacketConn{} - inboundListeners = map[string]C.InboundListener{} - tunLister *sing_tun.Listener - shadowSocksListener C.MultiAddrListener - vmessListener *sing_vmess.Listener - tuicListener *tuic.Listener - - // lock for recreate function - socksMux sync.Mutex - httpMux sync.Mutex - redirMux sync.Mutex - tproxyMux sync.Mutex - mixedMux sync.Mutex - tunnelMux sync.Mutex - inboundMux sync.Mutex - tunMux sync.Mutex - ssMux sync.Mutex - vmessMux sync.Mutex - tuicMux sync.Mutex - - LastTunConf LC.Tun - LastTuicConf LC.TuicServer -) - -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"` - ShadowSocksConfig string `json:"ss-config"` - VmessConfig string `json:"vmess-config"` -} - -func GetTunConf() LC.Tun { - if tunLister == nil { - return LastTunConf - } - return tunLister.Config() -} - -func GetTuicConf() LC.TuicServer { - if tuicListener == nil { - return LC.TuicServer{Enable: false} - } - return tuicListener.Config() -} - -func AllowLan() bool { - return allowLan -} - -func BindAddress() string { - return bindAddress -} - -func SetAllowLan(al bool) { - allowLan = al -} - -func SetBindAddress(host string) { - bindAddress = host -} - -func ReCreateHTTP(port int, tunnel C.Tunnel) { - httpMux.Lock() - defer httpMux.Unlock() - - var err error - defer func() { - if err != nil { - log.Errorln("Start HTTP server error: %s", err.Error()) - } - }() - - addr := genAddr(bindAddress, port, allowLan) - - if httpListener != nil { - if httpListener.RawAddress() == addr { - return - } - httpListener.Close() - httpListener = nil - } - - if portIsZero(addr) { - return - } - - httpListener, err = http.New(addr, tunnel) - if err != nil { - log.Errorln("Start HTTP server error: %s", err.Error()) - return - } - - log.Infoln("HTTP proxy listening at: %s", httpListener.Address()) -} - -func ReCreateSocks(port int, tunnel C.Tunnel) { - socksMux.Lock() - defer socksMux.Unlock() - - var err error - defer func() { - if err != nil { - log.Errorln("Start SOCKS server error: %s", err.Error()) - } - }() - - addr := genAddr(bindAddress, port, allowLan) - - shouldTCPIgnore := false - shouldUDPIgnore := false - - if socksListener != nil { - if socksListener.RawAddress() != addr { - socksListener.Close() - socksListener = nil - } else { - shouldTCPIgnore = true - } - } - - if socksUDPListener != nil { - if socksUDPListener.RawAddress() != addr { - socksUDPListener.Close() - socksUDPListener = nil - } else { - shouldUDPIgnore = true - } - } - - if shouldTCPIgnore && shouldUDPIgnore { - return - } - - if portIsZero(addr) { - return - } - - tcpListener, err := socks.New(addr, tunnel) - if err != nil { - return - } - - udpListener, err := socks.NewUDP(addr, tunnel) - if err != nil { - tcpListener.Close() - return - } - - socksListener = tcpListener - socksUDPListener = udpListener - - log.Infoln("SOCKS proxy listening at: %s", socksListener.Address()) -} - -func ReCreateRedir(port int, tunnel C.Tunnel) { - redirMux.Lock() - defer redirMux.Unlock() - - var err error - defer func() { - if err != nil { - log.Errorln("Start Redir server error: %s", err.Error()) - } - }() - - addr := genAddr(bindAddress, port, allowLan) - - if redirListener != nil { - if redirListener.RawAddress() == addr { - return - } - redirListener.Close() - redirListener = nil - } - - if redirUDPListener != nil { - if redirUDPListener.RawAddress() == addr { - return - } - redirUDPListener.Close() - redirUDPListener = nil - } - - if portIsZero(addr) { - return - } - - redirListener, err = redir.New(addr, tunnel) - if err != nil { - return - } - - redirUDPListener, err = tproxy.NewUDP(addr, tunnel) - if err != nil { - log.Warnln("Failed to start Redir UDP Listener: %s", err) - } - - log.Infoln("Redirect proxy listening at: %s", redirListener.Address()) -} - -func ReCreateShadowSocks(shadowSocksConfig string, tunnel C.Tunnel) { - ssMux.Lock() - defer ssMux.Unlock() - - var err error - defer func() { - if err != nil { - log.Errorln("Start ShadowSocks server error: %s", err.Error()) - } - }() - - var ssConfig LC.ShadowsocksServer - if addr, cipher, password, err := embedSS.ParseSSURL(shadowSocksConfig); err == nil { - ssConfig = LC.ShadowsocksServer{ - Enable: len(shadowSocksConfig) > 0, - Listen: addr, - Password: password, - Cipher: cipher, - Udp: true, - } - } - - shouldIgnore := false - - if shadowSocksListener != nil { - if shadowSocksListener.Config() != ssConfig.String() { - shadowSocksListener.Close() - shadowSocksListener = nil - } else { - shouldIgnore = true - } - } - - if shouldIgnore { - return - } - - if !ssConfig.Enable { - return - } - - listener, err := sing_shadowsocks.New(ssConfig, tunnel) - if err != nil { - return - } - - shadowSocksListener = listener - - for _, addr := range shadowSocksListener.AddrList() { - log.Infoln("ShadowSocks proxy listening at: %s", addr.String()) - } - return -} - -func ReCreateVmess(vmessConfig string, tunnel C.Tunnel) { - vmessMux.Lock() - defer vmessMux.Unlock() - - var err error - defer func() { - if err != nil { - log.Errorln("Start Vmess server error: %s", err.Error()) - } - }() - - var vsConfig LC.VmessServer - if addr, username, password, err := sing_vmess.ParseVmessURL(vmessConfig); err == nil { - vsConfig = LC.VmessServer{ - Enable: len(vmessConfig) > 0, - Listen: addr, - Users: []LC.VmessUser{{Username: username, UUID: password, AlterID: 1}}, - } - } - - shouldIgnore := false - - if vmessListener != nil { - if vmessListener.Config() != vsConfig.String() { - vmessListener.Close() - vmessListener = nil - } else { - shouldIgnore = true - } - } - - if shouldIgnore { - return - } - - if !vsConfig.Enable { - return - } - - listener, err := sing_vmess.New(vsConfig, tunnel) - if err != nil { - return - } - - vmessListener = listener - - for _, addr := range vmessListener.AddrList() { - log.Infoln("Vmess proxy listening at: %s", addr.String()) - } - return -} - -func ReCreateTuic(config LC.TuicServer, tunnel C.Tunnel) { - tuicMux.Lock() - defer func() { - LastTuicConf = config - tuicMux.Unlock() - }() - shouldIgnore := false - - var err error - defer func() { - if err != nil { - log.Errorln("Start Tuic server error: %s", err.Error()) - } - }() - - if tuicListener != nil { - if tuicListener.Config().String() != config.String() { - tuicListener.Close() - tuicListener = nil - } else { - shouldIgnore = true - } - } - - if shouldIgnore { - return - } - - if !config.Enable { - return - } - - listener, err := tuic.New(config, tunnel) - if err != nil { - return - } - - tuicListener = listener - - for _, addr := range tuicListener.AddrList() { - log.Infoln("Tuic proxy listening at: %s", addr.String()) - } - return -} - -func ReCreateTProxy(port int, tunnel C.Tunnel) { - tproxyMux.Lock() - defer tproxyMux.Unlock() - - var err error - defer func() { - if err != nil { - log.Errorln("Start TProxy server error: %s", err.Error()) - } - }() - - addr := genAddr(bindAddress, port, allowLan) - - if tproxyListener != nil { - if tproxyListener.RawAddress() == addr { - return - } - tproxyListener.Close() - tproxyListener = nil - } - - if tproxyUDPListener != nil { - if tproxyUDPListener.RawAddress() == addr { - return - } - tproxyUDPListener.Close() - tproxyUDPListener = nil - } - - if portIsZero(addr) { - return - } - - tproxyListener, err = tproxy.New(addr, tunnel) - if err != nil { - return - } - - tproxyUDPListener, err = tproxy.NewUDP(addr, tunnel) - if err != nil { - log.Warnln("Failed to start TProxy UDP Listener: %s", err) - } - - log.Infoln("TProxy server listening at: %s", tproxyListener.Address()) -} - -func ReCreateMixed(port int, tunnel C.Tunnel) { - mixedMux.Lock() - defer mixedMux.Unlock() - - var err error - defer func() { - if err != nil { - log.Errorln("Start Mixed(http+socks) server error: %s", err.Error()) - } - }() - - addr := genAddr(bindAddress, port, allowLan) - - shouldTCPIgnore := false - shouldUDPIgnore := false - - if mixedListener != nil { - if mixedListener.RawAddress() != addr { - mixedListener.Close() - mixedListener = nil - } else { - shouldTCPIgnore = true - } - } - if mixedUDPLister != nil { - if mixedUDPLister.RawAddress() != addr { - mixedUDPLister.Close() - mixedUDPLister = nil - } else { - shouldUDPIgnore = true - } - } - - if shouldTCPIgnore && shouldUDPIgnore { - return - } - - if portIsZero(addr) { - return - } - - mixedListener, err = mixed.New(addr, tunnel) - if err != nil { - return - } - - mixedUDPLister, err = socks.NewUDP(addr, tunnel) - if err != nil { - mixedListener.Close() - return - } - - log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address()) -} - -func ReCreateTun(tunConf LC.Tun, tunnel C.Tunnel) { - tunConf.Sort() - - tunMux.Lock() - defer func() { - LastTunConf = tunConf - tunMux.Unlock() - }() - - var err error - defer func() { - if err != nil { - log.Errorln("Start TUN listening error: %s", err.Error()) - tunConf.Enable = false - } - }() - - if tunConf.Equal(LastTunConf) { - if tunLister != nil { // some default value in dialer maybe changed when config reload, reset at here - tunLister.OnReload() - } - return - } - - closeTunListener() - - if !tunConf.Enable { - return - } - - lister, err := sing_tun.New(tunConf, tunnel) - if err != nil { - return - } - tunLister = lister - - log.Infoln("[TUN] Tun adapter listening at: %s", tunLister.Address()) -} - -func PatchTunnel(tunnels []LC.Tunnel, tunnel C.Tunnel) { - 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 LC.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 := LT.New(elm.addr, elm.target, elm.proxy, tunnel) - 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 := LT.NewUDP(elm.addr, elm.target, elm.proxy, tunnel) - 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 PatchInboundListeners(newListenerMap map[string]C.InboundListener, tunnel C.Tunnel, dropOld bool) { - inboundMux.Lock() - defer inboundMux.Unlock() - - for name, newListener := range newListenerMap { - if oldListener, ok := inboundListeners[name]; ok { - if !oldListener.Config().Equal(newListener.Config()) { - _ = oldListener.Close() - } else { - continue - } - } - if err := newListener.Listen(tunnel); err != nil { - log.Errorln("Listener %s listen err: %s", name, err.Error()) - continue - } - inboundListeners[name] = newListener - } - - if dropOld { - for name, oldListener := range inboundListeners { - if _, ok := newListenerMap[name]; !ok { - _ = oldListener.Close() - delete(inboundListeners, name) - } - } - } -} - -// GetPorts return the ports of proxy servers -func GetPorts() *Ports { - ports := &Ports{} - - if httpListener != nil { - _, portStr, _ := net.SplitHostPort(httpListener.Address()) - port, _ := strconv.Atoi(portStr) - ports.Port = port - } - - if socksListener != nil { - _, portStr, _ := net.SplitHostPort(socksListener.Address()) - port, _ := strconv.Atoi(portStr) - ports.SocksPort = port - } - - if redirListener != nil { - _, portStr, _ := net.SplitHostPort(redirListener.Address()) - port, _ := strconv.Atoi(portStr) - ports.RedirPort = port - } - - if tproxyListener != nil { - _, portStr, _ := net.SplitHostPort(tproxyListener.Address()) - port, _ := strconv.Atoi(portStr) - ports.TProxyPort = port - } - - if mixedListener != nil { - _, portStr, _ := net.SplitHostPort(mixedListener.Address()) - port, _ := strconv.Atoi(portStr) - ports.MixedPort = port - } - - if shadowSocksListener != nil { - ports.ShadowSocksConfig = shadowSocksListener.Config() - } - - if vmessListener != nil { - ports.VmessConfig = vmessListener.Config() - } - - return ports -} - -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 closeTunListener() { - if tunLister != nil { - tunLister.Close() - tunLister = nil - } -} - -func Cleanup() { - closeTunListener() -} diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go deleted file mode 100644 index 6893bb5a15..0000000000 --- a/listener/mixed/mixed.go +++ /dev/null @@ -1,134 +0,0 @@ -package mixed - -import ( - "crypto/tls" - "errors" - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/auth" - "github.com/metacubex/mihomo/component/ca" - C "github.com/metacubex/mihomo/constant" - authStore "github.com/metacubex/mihomo/listener/auth" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/http" - "github.com/metacubex/mihomo/listener/reality" - "github.com/metacubex/mihomo/listener/socks" - "github.com/metacubex/mihomo/transport/socks4" - "github.com/metacubex/mihomo/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, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - return NewWithConfig(LC.AuthServer{Enable: true, Listen: addr, AuthStore: authStore.Default}, tunnel, additions...) -} - -func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - isDefault := false - if len(additions) == 0 { - isDefault = true - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-MIXED"), - inbound.WithSpecialRules(""), - } - } - - l, err := inbound.Listen("tcp", config.Listen) - if err != nil { - return nil, err - } - - tlsConfig := &tls.Config{} - var realityBuilder *reality.Builder - - if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { - return nil, errors.New("certificate is unavailable in reality") - } - realityBuilder, err = config.RealityConfig.Build(tunnel) - if err != nil { - return nil, err - } - } - - if realityBuilder != nil { - l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) - } - - ml := &Listener{ - listener: l, - addr: config.Listen, - } - go func() { - for { - c, err := ml.listener.Accept() - if err != nil { - if ml.closed { - break - } - continue - } - store := config.AuthStore - if isDefault || store == authStore.Default { // only apply on default listener - if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { - _ = c.Close() - continue - } - if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) { - store = authStore.Nil - } - } - go handleConn(c, tunnel, store, additions...) - } - }() - - return ml, nil -} - -func handleConn(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { - bufConn := N.NewBufferedConn(conn) - head, err := bufConn.Peek(1) - if err != nil { - return - } - - switch head[0] { - case socks4.Version: - socks.HandleSocks4(bufConn, tunnel, store, additions...) - case socks5.Version: - socks.HandleSocks5(bufConn, tunnel, store, additions...) - default: - http.HandleConn(bufConn, tunnel, store, additions...) - } -} diff --git a/listener/parse.go b/listener/parse.go deleted file mode 100644 index adc206c10c..0000000000 --- a/listener/parse.go +++ /dev/null @@ -1,134 +0,0 @@ -package listener - -import ( - "fmt" - - "github.com/metacubex/mihomo/common/structure" - C "github.com/metacubex/mihomo/constant" - IN "github.com/metacubex/mihomo/listener/inbound" -) - -func ParseListener(mapping map[string]any) (C.InboundListener, error) { - decoder := structure.NewDecoder(structure.Option{TagName: "inbound", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer}) - proxyType, existType := mapping["type"].(string) - if !existType { - return nil, fmt.Errorf("missing type") - } - - var ( - listener C.InboundListener - err error - ) - switch proxyType { - case "socks": - socksOption := &IN.SocksOption{UDP: true} - err = decoder.Decode(mapping, socksOption) - if err != nil { - return nil, err - } - listener, err = IN.NewSocks(socksOption) - case "http": - httpOption := &IN.HTTPOption{} - err = decoder.Decode(mapping, httpOption) - if err != nil { - return nil, err - } - listener, err = IN.NewHTTP(httpOption) - case "tproxy": - tproxyOption := &IN.TProxyOption{UDP: true} - err = decoder.Decode(mapping, tproxyOption) - if err != nil { - return nil, err - } - listener, err = IN.NewTProxy(tproxyOption) - case "redir": - redirOption := &IN.RedirOption{} - err = decoder.Decode(mapping, redirOption) - if err != nil { - return nil, err - } - listener, err = IN.NewRedir(redirOption) - case "mixed": - mixedOption := &IN.MixedOption{UDP: true} - err = decoder.Decode(mapping, mixedOption) - if err != nil { - return nil, err - } - listener, err = IN.NewMixed(mixedOption) - case "tunnel": - tunnelOption := &IN.TunnelOption{} - err = decoder.Decode(mapping, tunnelOption) - if err != nil { - return nil, err - } - listener, err = IN.NewTunnel(tunnelOption) - case "tun": - tunOption := &IN.TunOption{ - Stack: C.TunGvisor.String(), - DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query - } - err = decoder.Decode(mapping, tunOption) - if err != nil { - return nil, err - } - listener, err = IN.NewTun(tunOption) - case "shadowsocks": - shadowsocksOption := &IN.ShadowSocksOption{UDP: true} - err = decoder.Decode(mapping, shadowsocksOption) - if err != nil { - return nil, err - } - listener, err = IN.NewShadowSocks(shadowsocksOption) - case "vmess": - vmessOption := &IN.VmessOption{} - err = decoder.Decode(mapping, vmessOption) - if err != nil { - return nil, err - } - listener, err = IN.NewVmess(vmessOption) - case "vless": - vlessOption := &IN.VlessOption{} - err = decoder.Decode(mapping, vlessOption) - if err != nil { - return nil, err - } - listener, err = IN.NewVless(vlessOption) - case "trojan": - trojanOption := &IN.TrojanOption{} - err = decoder.Decode(mapping, trojanOption) - if err != nil { - return nil, err - } - listener, err = IN.NewTrojan(trojanOption) - case "hysteria2": - hysteria2Option := &IN.Hysteria2Option{} - err = decoder.Decode(mapping, hysteria2Option) - if err != nil { - return nil, err - } - listener, err = IN.NewHysteria2(hysteria2Option) - case "tuic": - tuicOption := &IN.TuicOption{ - MaxIdleTime: 15000, - AuthenticationTimeout: 1000, - ALPN: []string{"h3"}, - MaxUdpRelayPacketSize: 1500, - CongestionController: "bbr", - } - err = decoder.Decode(mapping, tuicOption) - if err != nil { - return nil, err - } - listener, err = IN.NewTuic(tuicOption) - case "anytls": - anytlsOption := &IN.AnyTLSOption{} - err = decoder.Decode(mapping, anytlsOption) - if err != nil { - return nil, err - } - listener, err = IN.NewAnyTLS(anytlsOption) - default: - return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) - } - return listener, err -} diff --git a/listener/reality/reality.go b/listener/reality/reality.go deleted file mode 100644 index 036bcf2828..0000000000 --- a/listener/reality/reality.go +++ /dev/null @@ -1,106 +0,0 @@ -package reality - -import ( - "context" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - "net" - "time" - - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/listener/inner" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/ntp" - - utls "github.com/metacubex/utls" -) - -type Conn = utls.Conn - -type Config struct { - Dest string - PrivateKey string - ShortID []string - ServerNames []string - MaxTimeDifference int - Proxy string -} - -func (c Config) Build(tunnel C.Tunnel) (*Builder, error) { - realityConfig := &utls.RealityConfig{} - realityConfig.SessionTicketsDisabled = true - realityConfig.Type = "tcp" - realityConfig.Dest = c.Dest - realityConfig.Time = ntp.Now - realityConfig.ServerNames = make(map[string]bool) - realityConfig.Log = log.Debugln - for _, it := range c.ServerNames { - realityConfig.ServerNames[it] = true - } - privateKey, err := base64.RawURLEncoding.DecodeString(c.PrivateKey) - if err != nil { - return nil, fmt.Errorf("decode private key: %w", err) - } - if len(privateKey) != 32 { - return nil, errors.New("invalid private key") - } - realityConfig.PrivateKey = privateKey - - realityConfig.MaxTimeDiff = time.Duration(c.MaxTimeDifference) * time.Microsecond - - realityConfig.ShortIds = make(map[[8]byte]bool) - for i, shortIDString := range c.ShortID { - var shortID [8]byte - decodedLen := hex.DecodedLen(len(shortIDString)) - if decodedLen > 8 { - return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString) - } - decodedLen, err = hex.Decode(shortID[:], []byte(shortIDString)) - if err != nil { - return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err) - } - if decodedLen > 8 { - return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString) - } - realityConfig.ShortIds[shortID] = true - } - - realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { - return inner.HandleTcp(tunnel, address, c.Proxy) - } - - return &Builder{realityConfig}, nil -} - -type Builder struct { - realityConfig *utls.RealityConfig -} - -func (b Builder) NewListener(l net.Listener) net.Listener { - return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) { - c, err := utls.RealityServer(ctx, conn, b.realityConfig) - if err != nil { - return nil, err - } - // Due to low implementation quality, the reality server intercepted half-close and caused memory leaks. - // We fixed it by calling Close() directly. - return realityConnWrapper{c}, nil - }, func(a any) { - log.Errorln("reality server panic: %s", a) - }) -} - -type realityConnWrapper struct { - *utls.Conn -} - -func (c realityConnWrapper) Upstream() any { - return c.Conn -} - -func (c realityConnWrapper) CloseWrite() error { - return c.Close() -} diff --git a/listener/redir/tcp.go b/listener/redir/tcp.go deleted file mode 100644 index 47363182d0..0000000000 --- a/listener/redir/tcp.go +++ /dev/null @@ -1,75 +0,0 @@ -package redir - -import ( - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/component/keepalive" - C "github.com/metacubex/mihomo/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, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-REDIR"), - inbound.WithSpecialRules(""), - } - } - - 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, tunnel, additions...) - } - }() - - return rl, nil -} - -func handleRedir(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - target, err := parserPacket(conn) - if err != nil { - conn.Close() - return - } - keepalive.TCPKeepAlive(conn) - tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.REDIR, additions...)) -} diff --git a/listener/redir/tcp_darwin.go b/listener/redir/tcp_darwin.go deleted file mode 100644 index 6e1821bbd3..0000000000 --- a/listener/redir/tcp_darwin.go +++ /dev/null @@ -1,58 +0,0 @@ -package redir - -import ( - "net" - "syscall" - "unsafe" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index 9eb199f078..0000000000 --- a/listener/redir/tcp_freebsd.go +++ /dev/null @@ -1,66 +0,0 @@ -package redir - -import ( - "encoding/binary" - "errors" - "net" - "net/netip" - "syscall" - "unsafe" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index fce7467866..0000000000 --- a/listener/redir/tcp_linux.go +++ /dev/null @@ -1,64 +0,0 @@ -package redir - -import ( - "encoding/binary" - "errors" - "net" - "net/netip" - "syscall" - "unsafe" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index 32f692df8e..0000000000 --- a/listener/redir/tcp_linux_386.go +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index b8c7de8af8..0000000000 --- a/listener/redir/tcp_linux_other.go +++ /dev/null @@ -1,14 +0,0 @@ -//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 deleted file mode 100644 index ae3bebfd5b..0000000000 --- a/listener/redir/tcp_other.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !darwin && !linux && !freebsd - -package redir - -import ( - "errors" - "net" - - "github.com/metacubex/mihomo/transport/socks5" -) - -func parserPacket(conn net.Conn) (socks5.Addr, error) { - return nil, errors.New("system not support yet") -} diff --git a/listener/shadowsocks/tcp.go b/listener/shadowsocks/tcp.go deleted file mode 100644 index 8ae269ed9d..0000000000 --- a/listener/shadowsocks/tcp.go +++ /dev/null @@ -1,132 +0,0 @@ -package shadowsocks - -import ( - "net" - "strings" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/transport/shadowsocks/core" - "github.com/metacubex/mihomo/transport/socks5" -) - -type Listener struct { - closed bool - config LC.ShadowsocksServer - listeners []net.Listener - udpListeners []*UDPListener - pickCipher core.Cipher - handler *sing.ListenerHandler -} - -var _listener *Listener - -func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - pickCipher, err := core.PickCipher(config.Cipher, nil, config.Password) - if err != nil { - return nil, err - } - - h, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.SHADOWSOCKS, - Additions: additions, - MuxOption: config.MuxOption, - }) - if err != nil { - return nil, err - } - - sl := &Listener{false, config, nil, nil, pickCipher, h} - _listener = sl - - for _, addr := range strings.Split(config.Listen, ",") { - addr := addr - - if config.Udp { - //UDP - ul, err := NewUDP(addr, pickCipher, tunnel, additions...) - if err != nil { - return nil, err - } - sl.udpListeners = append(sl.udpListeners, ul) - } - - //TCP - l, err := inbound.Listen("tcp", addr) - if err != nil { - return nil, err - } - sl.listeners = append(sl.listeners, l) - - go func() { - for { - c, err := l.Accept() - if err != nil { - if sl.closed { - break - } - continue - } - go sl.HandleConn(c, tunnel, additions...) - } - }() - } - - return sl, nil -} - -func (l *Listener) Close() error { - var retErr error - for _, lis := range l.listeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - for _, lis := range l.udpListeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - return retErr -} - -func (l *Listener) Config() string { - return l.config.String() -} - -func (l *Listener) AddrList() (addrList []net.Addr) { - for _, lis := range l.listeners { - addrList = append(addrList, lis.Addr()) - } - for _, lis := range l.udpListeners { - addrList = append(addrList, lis.LocalAddr()) - } - return -} - -func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - conn = l.pickCipher.StreamConn(conn) - conn = N.NewDeadlineConn(conn) // embed ss can't handle readDeadline correctly - - target, err := socks5.ReadAddr0(conn) - if err != nil { - _ = conn.Close() - return - } - l.handler.HandleSocket(target, conn, additions...) - //tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.SHADOWSOCKS, additions...)) -} - -func HandleShadowSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) bool { - if _listener != nil && _listener.pickCipher != nil { - go _listener.HandleConn(conn, tunnel, additions...) - return true - } - return false -} diff --git a/listener/shadowsocks/udp.go b/listener/shadowsocks/udp.go deleted file mode 100644 index bdb6739ed7..0000000000 --- a/listener/shadowsocks/udp.go +++ /dev/null @@ -1,79 +0,0 @@ -package shadowsocks - -import ( - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/sockopt" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/shadowsocks/core" - "github.com/metacubex/mihomo/transport/socks5" -) - -type UDPListener struct { - packetConn net.PacketConn - closed bool -} - -func NewUDP(addr string, pickCipher core.Cipher, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { - l, err := inbound.ListenPacket("udp", addr) - if err != nil { - return nil, err - } - - if err := sockopt.UDPReuseaddr(l); err != nil { - log.Warnln("Failed to Reuse UDP Address: %s", err) - } - - sl := &UDPListener{l, false} - conn := pickCipher.PacketConn(N.NewEnhancePacketConn(l)) - go func() { - for { - data, put, remoteAddr, err := conn.WaitReadFrom() - if err != nil { - if put != nil { - put() - } - if sl.closed { - break - } - continue - } - handleSocksUDP(conn, tunnel, data, put, remoteAddr, additions...) - } - }() - - return sl, nil -} - -func (l *UDPListener) Close() error { - l.closed = true - return l.packetConn.Close() -} - -func (l *UDPListener) LocalAddr() net.Addr { - return l.packetConn.LocalAddr() -} - -func handleSocksUDP(pc net.PacketConn, tunnel C.Tunnel, buf []byte, put func(), addr net.Addr, additions ...inbound.Addition) { - tgtAddr := socks5.SplitAddr(buf) - if tgtAddr == nil { - // Unresolved UDP packet, return buffer to the pool - if put != nil { - put() - } - return - } - target := tgtAddr - payload := buf[len(tgtAddr):] - - packet := &packet{ - pc: pc, - rAddr: addr, - payload: payload, - put: put, - } - tunnel.HandleUDPPacket(inbound.NewPacket(target, packet, C.SHADOWSOCKS, additions...)) -} diff --git a/listener/shadowsocks/utils.go b/listener/shadowsocks/utils.go deleted file mode 100644 index 5d6a2977ee..0000000000 --- a/listener/shadowsocks/utils.go +++ /dev/null @@ -1,62 +0,0 @@ -package shadowsocks - -import ( - "bytes" - "errors" - "net" - "net/url" - - "github.com/metacubex/mihomo/transport/socks5" -) - -type packet struct { - pc net.PacketConn - rAddr net.Addr - payload []byte - put func() -} - -func (c *packet) Data() []byte { - return c.payload -} - -// WriteBack wirtes UDP packet with source(ip, port) = `addr` -func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { - if addr == nil { - err = errors.New("address is invalid") - return - } - packet := bytes.Join([][]byte{socks5.ParseAddrToSocksAddr(addr), b}, []byte{}) - 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() { - if c.put != nil { - c.put() - c.put = nil - } - c.payload = nil -} - -func (c *packet) InAddr() net.Addr { - return c.pc.LocalAddr() -} - -func ParseSSURL(s string) (addr, cipher, password string, err error) { - u, err := url.Parse(s) - if err != nil { - return - } - - addr = u.Host - if u.User != nil { - cipher = u.User.Username() - password, _ = u.User.Password() - } - return -} diff --git a/listener/sing/context.go b/listener/sing/context.go deleted file mode 100644 index 0193cb881f..0000000000 --- a/listener/sing/context.go +++ /dev/null @@ -1,31 +0,0 @@ -package sing - -import ( - "context" - "golang.org/x/exp/slices" - - "github.com/metacubex/mihomo/adapter/inbound" - - "github.com/metacubex/sing/common/auth" -) - -type contextKey string - -var ctxKeyAdditions = contextKey("Additions") - -func WithAdditions(ctx context.Context, additions ...inbound.Addition) context.Context { - return context.WithValue(ctx, ctxKeyAdditions, additions) -} - -func getAdditions(ctx context.Context) (additions []inbound.Addition) { - if v := ctx.Value(ctxKeyAdditions); v != nil { - if a, ok := v.([]inbound.Addition); ok { - additions = a - } - } - if user, ok := auth.UserFromContext[string](ctx); ok { - additions = slices.Clone(additions) - additions = append(additions, inbound.WithInUser(user)) - } - return -} diff --git a/listener/sing/dialer.go b/listener/sing/dialer.go deleted file mode 100644 index d3998d6e9c..0000000000 --- a/listener/sing/dialer.go +++ /dev/null @@ -1,35 +0,0 @@ -package sing - -import ( - "context" - "fmt" - "net" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/listener/inner" - - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" -) - -type Dialer struct { - t C.Tunnel - proxy string -} - -func (d Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - if network != "tcp" && network != "tcp4" && network != "tcp6" { - return nil, fmt.Errorf("unsupported network %s", network) - } - return inner.HandleTcp(d.t, destination.String(), d.proxy) -} - -func (d Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return nil, fmt.Errorf("unsupported ListenPacket") -} - -var _ N.Dialer = (*Dialer)(nil) - -func NewDialer(t C.Tunnel, proxy string) (d *Dialer) { - return &Dialer{t, proxy} -} diff --git a/listener/sing/sing.go b/listener/sing/sing.go deleted file mode 100644 index a2ce9b0acc..0000000000 --- a/listener/sing/sing.go +++ /dev/null @@ -1,268 +0,0 @@ -package sing - -import ( - "context" - "errors" - "net" - "net/netip" - "sync" - "time" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/adapter/outbound" - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - mux "github.com/metacubex/sing-mux" - vmess "github.com/metacubex/sing-vmess" - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - "github.com/metacubex/sing/common/bufio/deadline" - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/sing/common/metadata" - "github.com/metacubex/sing/common/network" - "github.com/metacubex/sing/common/uot" -) - -const UDPTimeout = 5 * time.Minute - -type ListenerConfig struct { - Tunnel C.Tunnel - Type C.Type - Additions []inbound.Addition - UDPTimeout time.Duration - MuxOption MuxOption -} - -type MuxOption struct { - Padding bool `yaml:"padding" json:"padding,omitempty"` - Brutal BrutalOptions `yaml:"brutal" json:"brutal,omitempty"` -} - -type BrutalOptions struct { - Enabled bool `yaml:"enabled" json:"enabled"` - Up string `yaml:"up" json:"up,omitempty"` - Down string `yaml:"down" json:"down,omitempty"` -} - -type ListenerHandler struct { - ListenerConfig - muxService *mux.Service -} - -func UpstreamMetadata(metadata M.Metadata) M.Metadata { - return M.Metadata{ - Source: metadata.Source, - Destination: metadata.Destination, - } -} - -func ConvertMetadata(metadata *C.Metadata) M.Metadata { - return M.Metadata{ - Protocol: metadata.Type.String(), - Source: M.SocksaddrFrom(metadata.SrcIP, metadata.SrcPort), - Destination: M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort), - } -} - -func NewListenerHandler(lc ListenerConfig) (h *ListenerHandler, err error) { - h = &ListenerHandler{ListenerConfig: lc} - h.muxService, err = mux.NewService(mux.ServiceOptions{ - NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { - return ctx - }, - Logger: log.SingInfoToDebugLogger, // convert sing-mux info log to debug - Handler: h, - Padding: lc.MuxOption.Padding, - Brutal: mux.BrutalOptions{ - Enabled: lc.MuxOption.Brutal.Enabled, - SendBPS: outbound.StringToBps(lc.MuxOption.Brutal.Up), - ReceiveBPS: outbound.StringToBps(lc.MuxOption.Brutal.Down), - }, - }) - return -} - -func (h *ListenerHandler) IsSpecialFqdn(fqdn string) bool { - switch fqdn { - case mux.Destination.Fqdn, - vmess.MuxDestination.Fqdn, - uot.MagicAddress, - uot.LegacyMagicAddress: - return true - default: - return false - } -} - -func (h *ListenerHandler) ParseSpecialFqdn(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - switch metadata.Destination.Fqdn { - case mux.Destination.Fqdn: - return h.muxService.NewConnection(ctx, conn, UpstreamMetadata(metadata)) - case vmess.MuxDestination.Fqdn: - return vmess.HandleMuxConnection(ctx, conn, h) - case uot.MagicAddress: - request, err := uot.ReadRequest(conn) - if err != nil { - return E.Cause(err, "read UoT request") - } - metadata.Destination = request.Destination - return h.NewPacketConnection(ctx, uot.NewConn(conn, *request), metadata) - case uot.LegacyMagicAddress: - metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} - return h.NewPacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata) - } - return errors.New("not special fqdn") -} - -func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - if h.IsSpecialFqdn(metadata.Destination.Fqdn) { - return h.ParseSpecialFqdn(ctx, conn, metadata) - } - - if deadline.NeedAdditionalReadDeadline(conn) { - conn = N.NewDeadlineConn(conn) // conn from sing should check NeedAdditionalReadDeadline - } - - cMetadata := &C.Metadata{ - NetWork: C.TCP, - Type: h.Type, - } - if metadata.Source.IsIP() && metadata.Source.Fqdn == "" { - cMetadata.RawSrcAddr = metadata.Source.Unwrap().TCPAddr() - } - if metadata.Destination.IsIP() && metadata.Destination.Fqdn == "" { - cMetadata.RawDstAddr = metadata.Destination.Unwrap().TCPAddr() - } - inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(metadata.Destination), inbound.WithSrcAddr(metadata.Source), inbound.WithInAddr(conn.LocalAddr())) - inbound.ApplyAdditions(cMetadata, h.Additions...) - inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...) - - h.Tunnel.HandleTCPConn(conn, cMetadata) // this goroutine must exit after conn unused - return nil -} - -func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error { - defer func() { _ = conn.Close() }() - mutex := sync.Mutex{} - conn2 := bufio.NewNetPacketConn(conn) // a new interface to set nil in defer - defer func() { - mutex.Lock() // this goroutine must exit after all conn.WritePacket() is not running - defer mutex.Unlock() - conn2 = nil - }() - rwOptions := network.ReadWaitOptions{} - readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) - if isReadWaiter { - readWaiter.InitializeReadWaiter(rwOptions) - } - for { - var ( - buff *buf.Buffer - dest M.Socksaddr - err error - ) - if isReadWaiter { - buff, dest, err = readWaiter.WaitReadPacket() - } else { - buff = rwOptions.NewPacketBuffer() - dest, err = conn.ReadPacket(buff) - if buff != nil { - rwOptions.PostReturn(buff) - } - } - if err != nil { - buff.Release() - if ShouldIgnorePacketError(err) { - break - } - return err - } - cPacket := &packet{ - conn: &conn2, - mutex: &mutex, - rAddr: metadata.Source.UDPAddr(), - lAddr: conn.LocalAddr(), - buff: buff, - } - - cMetadata := &C.Metadata{ - NetWork: C.UDP, - Type: h.Type, - } - if metadata.Source.IsIP() && metadata.Source.Fqdn == "" { - cMetadata.RawSrcAddr = metadata.Source.Unwrap().UDPAddr() - } - if dest.IsIP() && dest.Fqdn == "" { - cMetadata.RawDstAddr = dest.Unwrap().UDPAddr() - } - inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(dest), inbound.WithSrcAddr(metadata.Source), inbound.WithInAddr(conn.LocalAddr())) - inbound.ApplyAdditions(cMetadata, h.Additions...) - inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...) - - h.Tunnel.HandleUDPPacket(cPacket, cMetadata) - } - return nil -} - -func (h *ListenerHandler) NewError(ctx context.Context, err error) { - log.Warnln("%s listener get error: %+v", h.Type.String(), err) -} - -func (h *ListenerHandler) TypeMutation(typ C.Type) *ListenerHandler { - handler := *h - handler.Type = typ - return &handler -} - -func ShouldIgnorePacketError(err error) bool { - // ignore simple error - if E.IsTimeout(err) || E.IsClosed(err) || E.IsCanceled(err) { - return true - } - return false -} - -type packet struct { - conn *network.NetPacketConn - mutex *sync.Mutex - rAddr net.Addr - lAddr net.Addr - buff *buf.Buffer -} - -func (c *packet) Data() []byte { - return c.buff.Bytes() -} - -// WriteBack wirtes UDP packet with source(ip, port) = `addr` -func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { - if addr == nil { - err = errors.New("address is invalid") - return - } - - c.mutex.Lock() - defer c.mutex.Unlock() - conn := *c.conn - if conn == nil { - err = errors.New("writeBack to closed connection") - return - } - - return conn.WriteTo(b, addr) -} - -// LocalAddr returns the source IP/Port of UDP Packet -func (c *packet) LocalAddr() net.Addr { - return c.rAddr -} - -func (c *packet) Drop() { - c.buff.Release() -} - -func (c *packet) InAddr() net.Addr { - return c.lAddr -} diff --git a/listener/sing/util.go b/listener/sing/util.go deleted file mode 100644 index 306301621b..0000000000 --- a/listener/sing/util.go +++ /dev/null @@ -1,24 +0,0 @@ -package sing - -import ( - "context" - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/transport/socks5" -) - -// HandleSocket like inbound.NewSocket combine with Tunnel.HandleTCPConn but also handel specialFqdn -func (h *ListenerHandler) HandleSocket(target socks5.Addr, conn net.Conn, _additions ...inbound.Addition) { - conn, metadata := inbound.NewSocket(target, conn, h.Type, h.Additions...) - if h.IsSpecialFqdn(metadata.Host) { - _ = h.ParseSpecialFqdn( - WithAdditions(context.Background(), _additions...), - conn, - ConvertMetadata(metadata), - ) - } else { - inbound.ApplyAdditions(metadata, _additions...) - h.Tunnel.HandleTCPConn(conn, metadata) - } -} diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go deleted file mode 100644 index 0090926aba..0000000000 --- a/listener/sing_hysteria2/server.go +++ /dev/null @@ -1,201 +0,0 @@ -package sing_hysteria2 - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net" - "net/http" - "net/http/httputil" - "net/url" - "strings" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/adapter/outbound" - "github.com/metacubex/mihomo/common/sockopt" - "github.com/metacubex/mihomo/component/ca" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/sing-quic/hysteria2" - - "github.com/metacubex/quic-go" - E "github.com/metacubex/sing/common/exceptions" -) - -type Listener struct { - closed bool - config LC.Hysteria2Server - udpListeners []net.PacketConn - services []*hysteria2.Service[string] -} - -func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - var sl *Listener - var err error - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-HYSTERIA2"), - inbound.WithSpecialRules(""), - } - } - - h, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.HYSTERIA2, - Additions: additions, - MuxOption: config.MuxOption, - }) - if err != nil { - return nil, err - } - - sl = &Listener{false, config, nil, nil} - - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS13, - Certificates: []tls.Certificate{cert}, - } - if len(config.ALPN) > 0 { - tlsConfig.NextProtos = config.ALPN - } else { - tlsConfig.NextProtos = []string{"h3"} - } - - var salamanderPassword string - if len(config.Obfs) > 0 { - if config.ObfsPassword == "" { - return nil, errors.New("missing obfs password") - } - switch config.Obfs { - case hysteria2.ObfsTypeSalamander: - salamanderPassword = config.ObfsPassword - default: - return nil, fmt.Errorf("unknown obfs type: %s", config.Obfs) - } - } - var masqueradeHandler http.Handler - if config.Masquerade != "" { - masqueradeURL, err := url.Parse(config.Masquerade) - if err != nil { - return nil, E.Cause(err, "parse masquerade URL") - } - switch masqueradeURL.Scheme { - case "file": - masqueradeHandler = http.FileServer(http.Dir(masqueradeURL.Path)) - case "http", "https": - masqueradeHandler = &httputil.ReverseProxy{ - Rewrite: func(r *httputil.ProxyRequest) { - r.SetURL(masqueradeURL) - r.Out.Host = r.In.Host - }, - ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { - w.WriteHeader(http.StatusBadGateway) - }, - } - default: - return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme) - } - } - - if config.UdpMTU == 0 { - // "1200" from quic-go's MaxDatagramSize - // "-3" from quic-go's DatagramFrame.MaxDataLen - config.UdpMTU = 1200 - 3 - } - - quicConfig := &quic.Config{ - InitialStreamReceiveWindow: config.InitialStreamReceiveWindow, - MaxStreamReceiveWindow: config.MaxStreamReceiveWindow, - InitialConnectionReceiveWindow: config.InitialConnectionReceiveWindow, - MaxConnectionReceiveWindow: config.MaxConnectionReceiveWindow, - } - - service, err := hysteria2.NewService[string](hysteria2.ServiceOptions{ - Context: context.Background(), - Logger: log.SingLogger, - SendBPS: outbound.StringToBps(config.Up), - ReceiveBPS: outbound.StringToBps(config.Down), - SalamanderPassword: salamanderPassword, - TLSConfig: tlsC.UConfig(tlsConfig), - QUICConfig: quicConfig, - IgnoreClientBandwidth: config.IgnoreClientBandwidth, - UDPTimeout: sing.UDPTimeout, - Handler: h, - MasqueradeHandler: masqueradeHandler, - CWND: config.CWND, - UdpMTU: config.UdpMTU, - }) - if err != nil { - return nil, err - } - - userNameList := make([]string, 0, len(config.Users)) - userPasswordList := make([]string, 0, len(config.Users)) - for name, password := range config.Users { - userNameList = append(userNameList, name) - userPasswordList = append(userPasswordList, password) - } - service.UpdateUsers(userNameList, userPasswordList) - - for _, addr := range strings.Split(config.Listen, ",") { - addr := addr - _service := *service - service := &_service // make a copy - - ul, err := inbound.ListenPacket("udp", addr) - if err != nil { - return nil, err - } - - if err := sockopt.UDPReuseaddr(ul); err != nil { - log.Warnln("Failed to Reuse UDP Address: %s", err) - } - - sl.udpListeners = append(sl.udpListeners, ul) - sl.services = append(sl.services, service) - - go func() { - _ = service.Start(ul) - }() - } - - return sl, nil -} - -func (l *Listener) Close() error { - l.closed = true - var retErr error - for _, service := range l.services { - err := service.Close() - if err != nil { - retErr = err - } - } - for _, lis := range l.udpListeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - return retErr -} - -func (l *Listener) Config() string { - return l.config.String() -} - -func (l *Listener) AddrList() (addrList []net.Addr) { - for _, lis := range l.udpListeners { - addrList = append(addrList, lis.LocalAddr()) - } - return -} diff --git a/listener/sing_shadowsocks/server.go b/listener/sing_shadowsocks/server.go deleted file mode 100644 index 52b062ed02..0000000000 --- a/listener/sing_shadowsocks/server.go +++ /dev/null @@ -1,274 +0,0 @@ -package sing_shadowsocks - -import ( - "context" - "fmt" - "net" - "strings" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/sockopt" - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - embedSS "github.com/metacubex/mihomo/listener/shadowsocks" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/ntp" - - shadowsocks "github.com/metacubex/sing-shadowsocks" - "github.com/metacubex/sing-shadowsocks/shadowaead" - "github.com/metacubex/sing-shadowsocks/shadowaead_2022" - shadowtls "github.com/metacubex/sing-shadowtls" - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - M "github.com/metacubex/sing/common/metadata" - "github.com/metacubex/sing/common/network" -) - -type Listener struct { - closed bool - config LC.ShadowsocksServer - listeners []net.Listener - udpListeners []net.PacketConn - service shadowsocks.Service - shadowTLS *shadowtls.Service -} - -var _listener *Listener - -// shadowTLSService is a wrapper for shadowsocks.Service to support shadowTLS. -type shadowTLSService struct { - shadowsocks.Service - shadowTLS *shadowtls.Service -} - -func (s *shadowTLSService) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - if s.shadowTLS != nil { - return s.shadowTLS.NewConnection(ctx, conn, metadata) - } - return s.Service.NewConnection(ctx, conn, metadata) -} - -func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addition) (C.MultiAddrListener, error) { - var sl *Listener - var err error - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-SHADOWSOCKS"), - inbound.WithSpecialRules(""), - } - defer func() { - _listener = sl - }() - } - - udpTimeout := int64(sing.UDPTimeout.Seconds()) - - h, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.SHADOWSOCKS, - Additions: additions, - MuxOption: config.MuxOption, - }) - if err != nil { - return nil, err - } - - sl = &Listener{} - sl.config = config - - switch { - case config.Cipher == shadowsocks.MethodNone: - sl.service = shadowsocks.NewNoneService(udpTimeout, h) - case common.Contains(shadowaead.List, config.Cipher): - sl.service, err = shadowaead.NewService(config.Cipher, nil, config.Password, udpTimeout, h) - case common.Contains(shadowaead_2022.List, config.Cipher): - sl.service, err = shadowaead_2022.NewServiceWithPassword(config.Cipher, config.Password, udpTimeout, h, ntp.Now) - default: - err = fmt.Errorf("shadowsocks: unsupported method: %s", config.Cipher) - return embedSS.New(config, tunnel, additions...) - } - if err != nil { - return nil, err - } - - if config.ShadowTLS.Enable { - buildHandshake := func(handshake LC.ShadowTLSHandshakeOptions) (handshakeConfig shadowtls.HandshakeConfig) { - handshakeConfig.Server = M.ParseSocksaddr(handshake.Dest) - handshakeConfig.Dialer = sing.NewDialer(tunnel, handshake.Proxy) - return - } - var handshakeForServerName map[string]shadowtls.HandshakeConfig - if config.ShadowTLS.Version > 1 { - handshakeForServerName = make(map[string]shadowtls.HandshakeConfig) - for serverName, serverOptions := range config.ShadowTLS.HandshakeForServerName { - handshakeForServerName[serverName] = buildHandshake(serverOptions) - } - } - var wildcardSNI shadowtls.WildcardSNI - switch config.ShadowTLS.WildcardSNI { - case "authed": - wildcardSNI = shadowtls.WildcardSNIAuthed - case "all": - wildcardSNI = shadowtls.WildcardSNIAll - default: - wildcardSNI = shadowtls.WildcardSNIOff - } - var shadowTLS *shadowtls.Service - shadowTLS, err = shadowtls.NewService(shadowtls.ServiceConfig{ - Version: config.ShadowTLS.Version, - Password: config.ShadowTLS.Password, - Users: common.Map(config.ShadowTLS.Users, func(it LC.ShadowTLSUser) shadowtls.User { - return shadowtls.User{Name: it.Name, Password: it.Password} - }), - Handshake: buildHandshake(config.ShadowTLS.Handshake), - HandshakeForServerName: handshakeForServerName, - StrictMode: config.ShadowTLS.StrictMode, - WildcardSNI: wildcardSNI, - Handler: sl.service, - Logger: log.SingLogger, - }) - if err != nil { - return nil, err - } - sl.service = &shadowTLSService{ - Service: sl.service, - shadowTLS: shadowTLS, - } - } - - for _, addr := range strings.Split(config.Listen, ",") { - addr := addr - - if config.Udp { - //UDP - ul, err := inbound.ListenPacket("udp", addr) - if err != nil { - return nil, err - } - - if err := sockopt.UDPReuseaddr(ul); err != nil { - log.Warnln("Failed to Reuse UDP Address: %s", err) - } - - sl.udpListeners = append(sl.udpListeners, ul) - - go func() { - conn := bufio.NewPacketConn(ul) - rwOptions := network.ReadWaitOptions{ - FrontHeadroom: network.CalculateFrontHeadroom(sl.service), - RearHeadroom: network.CalculateRearHeadroom(sl.service), - MTU: network.CalculateMTU(conn, sl.service), - } - readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) - if isReadWaiter { - readWaiter.InitializeReadWaiter(rwOptions) - } - for { - var ( - buff *buf.Buffer - dest M.Socksaddr - err error - ) - buff = nil // clear last loop status, avoid repeat release - if isReadWaiter { - buff, dest, err = readWaiter.WaitReadPacket() - } else { - buff = rwOptions.NewPacketBuffer() - dest, err = conn.ReadPacket(buff) - if buff != nil { - rwOptions.PostReturn(buff) - } - } - if err != nil { - buff.Release() - if sl.closed { - break - } - continue - } - _ = sl.service.NewPacket(context.TODO(), conn, buff, M.Metadata{ - Protocol: "shadowsocks", - Source: dest, - }) - } - }() - } - - //TCP - l, err := inbound.Listen("tcp", addr) - if err != nil { - return nil, err - } - sl.listeners = append(sl.listeners, l) - - go func() { - for { - c, err := l.Accept() - if err != nil { - if sl.closed { - break - } - continue - } - - go sl.HandleConn(c, tunnel) - } - }() - } - - return sl, nil -} - -func (l *Listener) Close() error { - l.closed = true - var retErr error - for _, lis := range l.listeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - for _, lis := range l.udpListeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - return retErr -} - -func (l *Listener) Config() string { - return l.config.String() -} - -func (l *Listener) AddrList() (addrList []net.Addr) { - for _, lis := range l.listeners { - addrList = append(addrList, lis.Addr()) - } - for _, lis := range l.udpListeners { - addrList = append(addrList, lis.LocalAddr()) - } - return -} - -func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - ctx := sing.WithAdditions(context.TODO(), additions...) - err := l.service.NewConnection(ctx, conn, M.Metadata{ - Protocol: "shadowsocks", - Source: M.SocksaddrFromNet(conn.RemoteAddr()), - }) - if err != nil { - _ = conn.Close() - return - } -} - -func HandleShadowSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) bool { - if _listener != nil && _listener.service != nil { - go _listener.HandleConn(conn, tunnel, additions...) - return true - } - return embedSS.HandleShadowSocks(conn, tunnel, additions...) -} diff --git a/listener/sing_tun/dns.go b/listener/sing_tun/dns.go deleted file mode 100644 index ab56584808..0000000000 --- a/listener/sing_tun/dns.go +++ /dev/null @@ -1,133 +0,0 @@ -package sing_tun - -import ( - "context" - "net" - "net/netip" - "sync" - "time" - - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - M "github.com/metacubex/sing/common/metadata" - "github.com/metacubex/sing/common/network" -) - -type ListenerHandler struct { - *sing.ListenerHandler - DnsAdds []netip.AddrPort -} - -func (h *ListenerHandler) ShouldHijackDns(targetAddr netip.AddrPort) bool { - if targetAddr.Addr().IsLoopback() && targetAddr.Port() == 53 { // cause by system stack - return true - } - for _, addrPort := range h.DnsAdds { - if addrPort == targetAddr || (addrPort.Addr().IsUnspecified() && targetAddr.Port() == 53) { - return true - } - } - return false -} - -func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - if h.ShouldHijackDns(metadata.Destination.AddrPort()) { - log.Debugln("[DNS] hijack tcp:%s", metadata.Destination.String()) - return resolver.RelayDnsConn(ctx, conn, resolver.DefaultDnsReadTimeout) - } - return h.ListenerHandler.NewConnection(ctx, conn, metadata) -} - -func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error { - if h.ShouldHijackDns(metadata.Destination.AddrPort()) { - log.Debugln("[DNS] hijack udp:%s from %s", metadata.Destination.String(), metadata.Source.String()) - defer func() { _ = conn.Close() }() - mutex := sync.Mutex{} - conn2 := conn // a new interface to set nil in defer - defer func() { - mutex.Lock() // this goroutine must exit after all conn.WritePacket() is not running - defer mutex.Unlock() - conn2 = nil - }() - rwOptions := network.ReadWaitOptions{ - FrontHeadroom: network.CalculateFrontHeadroom(conn), - RearHeadroom: network.CalculateRearHeadroom(conn), - MTU: resolver.SafeDnsPacketSize, - } - readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) - if isReadWaiter { - readWaiter.InitializeReadWaiter(rwOptions) - } - for { - var ( - readBuff *buf.Buffer - dest M.Socksaddr - err error - ) - _ = conn.SetReadDeadline(time.Now().Add(resolver.DefaultDnsReadTimeout)) - readBuff = nil // clear last loop status, avoid repeat release - if isReadWaiter { - readBuff, dest, err = readWaiter.WaitReadPacket() - } else { - readBuff = rwOptions.NewPacketBuffer() - dest, err = conn.ReadPacket(readBuff) - if readBuff != nil { - rwOptions.PostReturn(readBuff) - } - } - if err != nil { - if readBuff != nil { - readBuff.Release() - } - if sing.ShouldIgnorePacketError(err) { - break - } - return err - } - go func() { - ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDnsRelayTimeout) - defer cancel() - inData := readBuff.Bytes() - writeBuff := readBuff - writeBuff.Resize(writeBuff.Start(), 0) - if len(writeBuff.FreeBytes()) < resolver.SafeDnsPacketSize { // only create a new buffer when space don't enough - writeBuff = rwOptions.NewPacketBuffer() - } - msg, err := resolver.RelayDnsPacket(ctx, inData, writeBuff.FreeBytes()) - if writeBuff != readBuff { - readBuff.Release() - } - if err != nil { - writeBuff.Release() - return - } - writeBuff.Truncate(len(msg)) - mutex.Lock() - defer mutex.Unlock() - conn := conn2 - if conn == nil { - writeBuff.Release() - return - } - err = conn.WritePacket(writeBuff, dest) // WritePacket will release writeBuff - if err != nil { - writeBuff.Release() - return - } - }() - } - return nil - } - return h.ListenerHandler.NewPacketConnection(ctx, conn, metadata) -} - -func (h *ListenerHandler) TypeMutation(typ C.Type) *ListenerHandler { - handle := *h - handle.ListenerHandler = h.ListenerHandler.TypeMutation(typ) - return &handle -} diff --git a/listener/sing_tun/iface.go b/listener/sing_tun/iface.go deleted file mode 100644 index ddb6c8410c..0000000000 --- a/listener/sing_tun/iface.go +++ /dev/null @@ -1,77 +0,0 @@ -package sing_tun - -import ( - "net" - "net/netip" - - "github.com/metacubex/mihomo/component/iface" - - "github.com/metacubex/sing/common/control" -) - -type defaultInterfaceFinder struct{} - -var DefaultInterfaceFinder control.InterfaceFinder = (*defaultInterfaceFinder)(nil) - -func (f *defaultInterfaceFinder) Update() error { - iface.FlushCache() - _, err := iface.Interfaces() - return err -} - -func (f *defaultInterfaceFinder) Interfaces() []control.Interface { - ifaces, err := iface.Interfaces() - if err != nil { - return nil - } - interfaces := make([]control.Interface, 0, len(ifaces)) - for _, _interface := range ifaces { - interfaces = append(interfaces, control.Interface(*_interface)) - } - - return interfaces -} - -func (f *defaultInterfaceFinder) ByName(name string) (*control.Interface, error) { - netInterface, err := iface.ResolveInterface(name) - if err == nil { - return (*control.Interface)(netInterface), nil - } - if _, err := net.InterfaceByName(name); err == nil { - err = f.Update() - if err != nil { - return nil, err - } - return f.ByName(name) - } - return nil, err -} - -func (f *defaultInterfaceFinder) ByIndex(index int) (*control.Interface, error) { - ifaces, err := iface.Interfaces() - if err != nil { - return nil, err - } - for _, netInterface := range ifaces { - if netInterface.Index == index { - return (*control.Interface)(netInterface), nil - } - } - _, err = net.InterfaceByIndex(index) - if err == nil { - err = f.Update() - if err != nil { - return nil, err - } - return f.ByIndex(index) - } - return nil, iface.ErrIfaceNotFound -} - -func (f *defaultInterfaceFinder) ByAddr(addr netip.Addr) (*control.Interface, error) { - netInterface, err := iface.ResolveInterfaceByAddr(addr) - if err != nil { - return nil, err - } - return (*control.Interface)(netInterface), nil -} diff --git a/listener/sing_tun/redirect_linux.go b/listener/sing_tun/redirect_linux.go deleted file mode 100644 index 6ef6fc968a..0000000000 --- a/listener/sing_tun/redirect_linux.go +++ /dev/null @@ -1,3 +0,0 @@ -package sing_tun - -const supportRedirect = true diff --git a/listener/sing_tun/redirect_stub.go b/listener/sing_tun/redirect_stub.go deleted file mode 100644 index d711af3c70..0000000000 --- a/listener/sing_tun/redirect_stub.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !linux - -package sing_tun - -const supportRedirect = false diff --git a/listener/sing_tun/server.go b/listener/sing_tun/server.go deleted file mode 100644 index 6998f0c1d2..0000000000 --- a/listener/sing_tun/server.go +++ /dev/null @@ -1,646 +0,0 @@ -package sing_tun - -import ( - "context" - "fmt" - "io" - "net" - "net/netip" - "os" - "runtime" - "strconv" - "strings" - "sync" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/iface" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/log" - "golang.org/x/exp/constraints" - - tun "github.com/metacubex/sing-tun" - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/control" - E "github.com/metacubex/sing/common/exceptions" - F "github.com/metacubex/sing/common/format" - "github.com/metacubex/sing/common/ranges" - - "go4.org/netipx" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" -) - -var InterfaceName = "Meta" -var EnforceBindInterface = false - -type Listener struct { - closed bool - options LC.Tun - handler *ListenerHandler - tunName string - addrStr string - - tunIf tun.Tun - tunStack tun.Stack - - networkUpdateMonitor tun.NetworkUpdateMonitor - defaultInterfaceMonitor tun.DefaultInterfaceMonitor - packageManager tun.PackageManager - autoRedirect tun.AutoRedirect - autoRedirectOutputMark int32 - - cDialerInterfaceFinder dialer.InterfaceFinder - - ruleUpdateCallbackCloser io.Closer - ruleUpdateMutex sync.Mutex - routeAddressMap map[string]*netipx.IPSet - routeExcludeAddressMap map[string]*netipx.IPSet - routeAddressSet []*netipx.IPSet - routeExcludeAddressSet []*netipx.IPSet - - dnsServerIp []string -} - -var emptyAddressSet = []*netipx.IPSet{{}} - -func CalculateInterfaceName(name string) (tunName string) { - if runtime.GOOS == "darwin" { - tunName = "utun" - } else if name != "" { - tunName = name - return - } else { - tunName = "tun" - } - interfaces, err := net.Interfaces() - if err != nil { - return - } - tunIndex := 0 - indexArr := make([]int, 0, len(interfaces)) - for _, netInterface := range interfaces { - if strings.HasPrefix(netInterface.Name, tunName) { - index, parseErr := strconv.ParseInt(netInterface.Name[len(tunName):], 10, 16) - if parseErr == nil { - indexArr = append(indexArr, int(index)) - } - } - } - slices.Sort(indexArr) - indexArr = slices.Compact(indexArr) - for _, index := range indexArr { - if index == tunIndex { - tunIndex += 1 - } else { // indexArr already sorted and distinct, so this tunIndex nobody used - break - } - } - tunName = F.ToString(tunName, tunIndex) - return -} - -func checkTunName(tunName string) (ok bool) { - defer func() { - if !ok { - log.Warnln("[TUN] Unsupported tunName(%s) in %s, force regenerate by ourselves.", tunName, runtime.GOOS) - } - }() - if runtime.GOOS == "darwin" { - if len(tunName) <= 4 { - return false - } - if tunName[:4] != "utun" { - return false - } - if _, parseErr := strconv.ParseInt(tunName[4:], 10, 16); parseErr != nil { - return false - } - } - return true -} - -func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Listener, err error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-TUN"), - inbound.WithSpecialRules(""), - } - } - ctx := context.TODO() - rpTunnel := tunnel.(provider.Tunnel) - if options.GSOMaxSize == 0 { - options.GSOMaxSize = 65536 - } - if !supportRedirect { - options.AutoRedirect = false - } - tunName := options.Device - if options.FileDescriptor == 0 && (tunName == "" || !checkTunName(tunName)) { - tunName = CalculateInterfaceName(InterfaceName) - options.Device = tunName - } - routeAddress := options.RouteAddress - if len(options.Inet4RouteAddress) > 0 { - routeAddress = append(routeAddress, options.Inet4RouteAddress...) - } - if len(options.Inet6RouteAddress) > 0 { - routeAddress = append(routeAddress, options.Inet6RouteAddress...) - } - inet4RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { - return it.Addr().Is4() - }) - inet6RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { - return it.Addr().Is6() - }) - routeExcludeAddress := options.RouteExcludeAddress - if len(options.Inet4RouteExcludeAddress) > 0 { - routeExcludeAddress = append(routeExcludeAddress, options.Inet4RouteExcludeAddress...) - } - if len(options.Inet6RouteExcludeAddress) > 0 { - routeExcludeAddress = append(routeExcludeAddress, options.Inet6RouteExcludeAddress...) - } - inet4RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { - return it.Addr().Is4() - }) - inet6RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { - return it.Addr().Is6() - }) - tunMTU := options.MTU - if tunMTU == 0 { - tunMTU = 9000 - } - var udpTimeout int64 - if options.UDPTimeout != 0 { - udpTimeout = options.UDPTimeout - } else { - udpTimeout = int64(sing.UDPTimeout.Seconds()) - } - tableIndex := options.IPRoute2TableIndex - if tableIndex == 0 { - tableIndex = tun.DefaultIPRoute2TableIndex - } - ruleIndex := options.IPRoute2RuleIndex - if ruleIndex == 0 { - ruleIndex = tun.DefaultIPRoute2RuleIndex - } - inputMark := options.AutoRedirectInputMark - if inputMark == 0 { - inputMark = tun.DefaultAutoRedirectInputMark - } - outputMark := options.AutoRedirectOutputMark - if outputMark == 0 { - outputMark = tun.DefaultAutoRedirectOutputMark - } - includeUID := uidToRange(options.IncludeUID) - if len(options.IncludeUIDRange) > 0 { - var err error - includeUID, err = parseRange(includeUID, options.IncludeUIDRange) - if err != nil { - return nil, E.Cause(err, "parse include_uid_range") - } - } - excludeUID := uidToRange(options.ExcludeUID) - if len(options.ExcludeUIDRange) > 0 { - var err error - excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange) - if err != nil { - return nil, E.Cause(err, "parse exclude_uid_range") - } - } - excludeSrcPort := uidToRange(options.ExcludeSrcPort) - if len(options.ExcludeSrcPortRange) > 0 { - var err error - excludeSrcPort, err = parseRange(excludeSrcPort, options.ExcludeSrcPortRange) - if err != nil { - return nil, E.Cause(err, "parse exclude_src_port_range") - } - } - excludeDstPort := uidToRange(options.ExcludeDstPort) - if len(options.ExcludeDstPortRange) > 0 { - var err error - excludeDstPort, err = parseRange(excludeDstPort, options.ExcludeDstPortRange) - if err != nil { - return nil, E.Cause(err, "parse exclude_dst_port_range") - } - } - - var dnsAdds []netip.AddrPort - - for _, d := range options.DNSHijack { - if _, after, ok := strings.Cut(d, "://"); ok { - d = after - } - d = strings.Replace(d, "any", "0.0.0.0", 1) - addrPort, err := netip.ParseAddrPort(d) - if err != nil { - return nil, fmt.Errorf("parse dns-hijack url error: %w", err) - } - - dnsAdds = append(dnsAdds, addrPort) - } - - var dnsServerIp []string - for _, a := range options.Inet4Address { - addrPort := netip.AddrPortFrom(a.Addr().Next(), 53) - dnsServerIp = append(dnsServerIp, a.Addr().Next().String()) - dnsAdds = append(dnsAdds, addrPort) - } - for _, a := range options.Inet6Address { - addrPort := netip.AddrPortFrom(a.Addr().Next(), 53) - dnsServerIp = append(dnsServerIp, a.Addr().Next().String()) - dnsAdds = append(dnsAdds, addrPort) - } - - h, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.TUN, - Additions: additions, - }) - if err != nil { - return nil, err - } - - handler := &ListenerHandler{ - ListenerHandler: h, - DnsAdds: dnsAdds, - } - l = &Listener{ - closed: false, - options: options, - handler: handler, - tunName: tunName, - } - defer func() { - if err != nil { - l.Close() - l = nil - } - }() - - interfaceFinder := DefaultInterfaceFinder - - var networkUpdateMonitor tun.NetworkUpdateMonitor - var defaultInterfaceMonitor tun.DefaultInterfaceMonitor - if options.AutoRoute || options.AutoDetectInterface { // don't start NetworkUpdateMonitor because netlink banned by google on Android14+ - networkUpdateMonitor, err = tun.NewNetworkUpdateMonitor(log.SingLogger) - if err != nil { - err = E.Cause(err, "create NetworkUpdateMonitor") - return - } - l.networkUpdateMonitor = networkUpdateMonitor - err = networkUpdateMonitor.Start() - if err != nil { - err = E.Cause(err, "start NetworkUpdateMonitor") - return - } - - overrideAndroidVPN := true - if disable, _ := strconv.ParseBool(os.Getenv("DISABLE_OVERRIDE_ANDROID_VPN")); disable { - overrideAndroidVPN = false - } - defaultInterfaceMonitor, err = tun.NewDefaultInterfaceMonitor(networkUpdateMonitor, log.SingLogger, tun.DefaultInterfaceMonitorOptions{InterfaceFinder: interfaceFinder, OverrideAndroidVPN: overrideAndroidVPN}) - if err != nil { - err = E.Cause(err, "create DefaultInterfaceMonitor") - return - } - l.defaultInterfaceMonitor = defaultInterfaceMonitor - defaultInterfaceMonitor.RegisterCallback(func(defaultInterface *control.Interface, event int) { - if defaultInterface != nil { - log.Warnln("[TUN] default interface changed by monitor, => %s", defaultInterface.Name) - } else { - log.Errorln("[TUN] default interface lost by monitor") - } - iface.FlushCache() - resolver.ResetConnection() // reset resolver's connection after default interface changed - }) - err = defaultInterfaceMonitor.Start() - if err != nil { - err = E.Cause(err, "start DefaultInterfaceMonitor") - return - } - - if options.AutoDetectInterface { - l.cDialerInterfaceFinder = &cDialerInterfaceFinder{ - tunName: tunName, - defaultInterfaceMonitor: defaultInterfaceMonitor, - } - if !dialer.DefaultInterfaceFinder.CompareAndSwap(nil, l.cDialerInterfaceFinder) { - err = E.New("not allowed two tun listener using auto-detect-interface") - return - } - } - } - - tunOptions := tun.Options{ - Name: tunName, - MTU: tunMTU, - GSO: options.GSO, - Inet4Address: options.Inet4Address, - Inet6Address: options.Inet6Address, - AutoRoute: options.AutoRoute, - IPRoute2TableIndex: tableIndex, - IPRoute2RuleIndex: ruleIndex, - AutoRedirectInputMark: inputMark, - AutoRedirectOutputMark: outputMark, - StrictRoute: options.StrictRoute, - Inet4RouteAddress: inet4RouteAddress, - Inet6RouteAddress: inet6RouteAddress, - Inet4RouteExcludeAddress: inet4RouteExcludeAddress, - Inet6RouteExcludeAddress: inet6RouteExcludeAddress, - IncludeInterface: options.IncludeInterface, - ExcludeInterface: options.ExcludeInterface, - IncludeUID: includeUID, - ExcludeUID: excludeUID, - ExcludeSrcPort: excludeSrcPort, - ExcludeDstPort: excludeDstPort, - IncludeAndroidUser: options.IncludeAndroidUser, - IncludePackage: options.IncludePackage, - ExcludePackage: options.ExcludePackage, - FileDescriptor: options.FileDescriptor, - InterfaceMonitor: defaultInterfaceMonitor, - } - - if options.AutoRedirect { - l.routeAddressMap = make(map[string]*netipx.IPSet) - l.routeExcludeAddressMap = make(map[string]*netipx.IPSet) - - if !options.AutoRoute { - return nil, E.New("`auto-route` is required by `auto-redirect`") - } - disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) - l.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ - TunOptions: &tunOptions, - Context: ctx, - Handler: handler.TypeMutation(C.REDIR), - Logger: log.SingLogger, - NetworkMonitor: l.networkUpdateMonitor, - InterfaceFinder: interfaceFinder, - TableName: "mihomo", - DisableNFTables: dErr == nil && disableNFTables, - RouteAddressSet: &l.routeAddressSet, - RouteExcludeAddressSet: &l.routeExcludeAddressSet, - }) - if err != nil { - err = E.Cause(err, "initialize auto redirect") - return - } - - var markMode bool - for _, routeAddressSet := range options.RouteAddressSet { - rp, loaded := rpTunnel.RuleProviders()[routeAddressSet] - if !loaded { - err = E.New("parse route-address-set: rule-set not found: ", routeAddressSet) - return - } - l.updateRule(rp, false, false) - markMode = true - } - for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { - rp, loaded := rpTunnel.RuleProviders()[routeExcludeAddressSet] - if !loaded { - err = E.New("parse route-exclude_address-set: rule-set not found: ", routeExcludeAddressSet) - return - } - l.updateRule(rp, true, false) - markMode = true - } - if markMode { - tunOptions.AutoRedirectMarkMode = true - } - - } - - err = l.buildAndroidRules(&tunOptions) - if err != nil { - err = E.Cause(err, "build android rules") - return - } - tunIf, err := tunNew(tunOptions) - if err != nil { - err = E.Cause(err, "configure tun interface") - return - } - - l.dnsServerIp = dnsServerIp - // after tun.New sing-tun has set DNS to TUN interface - resolver.AddSystemDnsBlacklist(dnsServerIp...) - - stackOptions := tun.StackOptions{ - Context: ctx, - Tun: tunIf, - TunOptions: tunOptions, - EndpointIndependentNat: options.EndpointIndependentNat, - UDPTimeout: udpTimeout, - Handler: handler, - Logger: log.SingLogger, - InterfaceFinder: interfaceFinder, - EnforceBindInterface: EnforceBindInterface, - } - - if options.FileDescriptor > 0 { - if tunName, err := getTunnelName(int32(options.FileDescriptor)); err != nil { - stackOptions.TunOptions.Name = tunName - stackOptions.ForwarderBindInterface = true - } - } - l.tunIf = tunIf - - tunStack, err := tun.NewStack(strings.ToLower(options.Stack.String()), stackOptions) - if err != nil { - return - } - - err = tunStack.Start() - if err != nil { - return - } - l.tunStack = tunStack - - if l.autoRedirect != nil { - if len(l.options.RouteAddressSet) > 0 && len(l.routeAddressSet) == 0 { - l.routeAddressSet = emptyAddressSet // without this we can't call UpdateRouteAddressSet after Start - } - if len(l.options.RouteExcludeAddressSet) > 0 && len(l.routeExcludeAddressSet) == 0 { - l.routeExcludeAddressSet = emptyAddressSet // without this we can't call UpdateRouteAddressSet after Start - } - err = l.autoRedirect.Start() - if err != nil { - err = E.Cause(err, "auto redirect") - return - } - if tunOptions.AutoRedirectMarkMode { - l.autoRedirectOutputMark = int32(outputMark) - if !dialer.DefaultRoutingMark.CompareAndSwap(0, l.autoRedirectOutputMark) { - err = E.New("not allowed setting global routing-mark when working with autoRedirectMarkMode") - return - } - l.autoRedirect.UpdateRouteAddressSet() - l.ruleUpdateCallbackCloser = rpTunnel.RuleUpdateCallback().Register(l.ruleUpdateCallback) - } - } - - //l.openAndroidHotspot(tunOptions) - - if !l.options.AutoDetectInterface { - resolver.ResetConnection() - } - - if options.FileDescriptor != 0 { - tunName = fmt.Sprintf("%s(fd=%d)", tunName, options.FileDescriptor) - } - l.addrStr = fmt.Sprintf("%s(%s,%s), mtu: %d, auto route: %v, auto redir: %v, ip stack: %s", - tunName, tunOptions.Inet4Address, tunOptions.Inet6Address, tunMTU, options.AutoRoute, options.AutoRedirect, options.Stack) - return -} - -func (l *Listener) ruleUpdateCallback(ruleProvider provider.RuleProvider) { - name := ruleProvider.Name() - if slices.Contains(l.options.RouteAddressSet, name) { - l.updateRule(ruleProvider, false, true) - return - } - if slices.Contains(l.options.RouteExcludeAddressSet, name) { - l.updateRule(ruleProvider, true, true) - return - } -} - -type toIpCidr interface { - ToIpCidr() *netipx.IPSet -} - -func (l *Listener) updateRule(ruleProvider provider.RuleProvider, exclude bool, update bool) { - l.ruleUpdateMutex.Lock() - defer l.ruleUpdateMutex.Unlock() - name := ruleProvider.Name() - switch rp := ruleProvider.Strategy().(type) { - case toIpCidr: - if !exclude { - ipCidr := rp.ToIpCidr() - if ipCidr != nil { - l.routeAddressMap[name] = ipCidr - } else { - delete(l.routeAddressMap, name) - } - l.routeAddressSet = maps.Values(l.routeAddressMap) - } else { - ipCidr := rp.ToIpCidr() - if ipCidr != nil { - l.routeExcludeAddressMap[name] = ipCidr - } else { - delete(l.routeExcludeAddressMap, name) - } - l.routeExcludeAddressSet = maps.Values(l.routeExcludeAddressMap) - } - default: - return - } - if update && l.autoRedirect != nil { - l.autoRedirect.UpdateRouteAddressSet() - } -} - -func (l *Listener) OnReload() { - if l.autoRedirectOutputMark != 0 { - dialer.DefaultRoutingMark.CompareAndSwap(0, l.autoRedirectOutputMark) - } - if l.cDialerInterfaceFinder != nil { - dialer.DefaultInterfaceFinder.CompareAndSwap(nil, l.cDialerInterfaceFinder) - } -} - -type cDialerInterfaceFinder struct { - tunName string - defaultInterfaceMonitor tun.DefaultInterfaceMonitor -} - -func (d *cDialerInterfaceFinder) DefaultInterfaceName(destination netip.Addr) string { - if netInterface, _ := DefaultInterfaceFinder.ByAddr(destination); netInterface != nil { - return netInterface.Name - } - if netInterface := d.defaultInterfaceMonitor.DefaultInterface(); netInterface != nil { - return netInterface.Name - } - return "" -} - -func (d *cDialerInterfaceFinder) FindInterfaceName(destination netip.Addr) string { - for _, dest := range []netip.Addr{destination, netip.IPv4Unspecified(), netip.IPv6Unspecified()} { - autoDetectInterfaceName := d.DefaultInterfaceName(dest) - if autoDetectInterfaceName == d.tunName { - log.Warnln("[TUN] Auto detect interface for %s get same name with tun", destination.String()) - } else if autoDetectInterfaceName == "" || autoDetectInterfaceName == "" { - log.Warnln("[TUN] Auto detect interface for %s get empty name.", destination.String()) - } else { - log.Debugln("[TUN] Auto detect interface for %s --> %s", destination, autoDetectInterfaceName) - return autoDetectInterfaceName - } - } - log.Warnln("[TUN] Auto detect interface for %s failed, return '' to avoid lookback", destination) - return "" -} - -func uidToRange[T constraints.Integer](uidList []T) []ranges.Range[T] { - return common.Map(uidList, func(uid T) ranges.Range[T] { - return ranges.NewSingle(uid) - }) -} - -func parseRange[T constraints.Integer](uidRanges []ranges.Range[T], rangeList []string) ([]ranges.Range[T], error) { - for _, uidRange := range rangeList { - if !strings.Contains(uidRange, ":") { - return nil, E.New("missing ':' in range: ", uidRange) - } - subIndex := strings.Index(uidRange, ":") - if subIndex == 0 { - return nil, E.New("missing range start: ", uidRange) - } else if subIndex == len(uidRange)-1 { - return nil, E.New("missing range end: ", uidRange) - } - var start, end uint64 - var err error - start, err = strconv.ParseUint(uidRange[:subIndex], 0, 32) - if err != nil { - return nil, E.Cause(err, "parse range start") - } - end, err = strconv.ParseUint(uidRange[subIndex+1:], 0, 32) - if err != nil { - return nil, E.Cause(err, "parse range end") - } - uidRanges = append(uidRanges, ranges.New(T(start), T(end))) - } - return uidRanges, nil -} - -func (l *Listener) Close() error { - l.closed = true - resolver.RemoveSystemDnsBlacklist(l.dnsServerIp...) - if l.autoRedirectOutputMark != 0 { - dialer.DefaultRoutingMark.CompareAndSwap(l.autoRedirectOutputMark, 0) - } - if l.cDialerInterfaceFinder != nil { - dialer.DefaultInterfaceFinder.CompareAndSwap(l.cDialerInterfaceFinder, nil) - } - return common.Close( - l.ruleUpdateCallbackCloser, - l.tunStack, - l.tunIf, - l.autoRedirect, - l.defaultInterfaceMonitor, - l.networkUpdateMonitor, - l.packageManager, - ) -} - -func (l *Listener) Config() LC.Tun { - return l.options -} - -func (l *Listener) Address() string { - return l.addrStr -} diff --git a/listener/sing_tun/server_android.go b/listener/sing_tun/server_android.go deleted file mode 100644 index d8240534ed..0000000000 --- a/listener/sing_tun/server_android.go +++ /dev/null @@ -1,102 +0,0 @@ -//go:build android && !cmfa - -package sing_tun - -import ( - "errors" - "runtime" - "sync" - - "github.com/metacubex/mihomo/component/process" - "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/sing-tun" - "github.com/sagernet/netlink" - "golang.org/x/sys/unix" -) - -type packageManagerCallback struct{} - -func (cb *packageManagerCallback) OnPackagesUpdated(packageCount int, sharedCount int) {} - -func newPackageManager() (tun.PackageManager, error) { - packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ - Callback: &packageManagerCallback{}, - Logger: log.SingLogger, - }) - if err != nil { - return nil, err - } - err = packageManager.Start() - if err != nil { - return nil, err - } - return packageManager, nil -} - -var ( - globalPM tun.PackageManager - pmOnce sync.Once - pmErr error -) - -func getPackageManager() (tun.PackageManager, error) { - pmOnce.Do(func() { - globalPM, pmErr = newPackageManager() - }) - return globalPM, pmErr -} - -func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { - packageManager, err := getPackageManager() - if err != nil { - return err - } - tunOptions.BuildAndroidRules(packageManager, l.handler) - return nil -} - -func findPackageName(metadata *constant.Metadata) (string, error) { - packageManager, err := getPackageManager() - if err != nil { - return "", err - } - uid := metadata.Uid - if sharedPackage, loaded := packageManager.SharedPackageByID(uid % 100000); loaded { - return sharedPackage, nil - } - if packageName, loaded := packageManager.PackageByID(uid % 100000); loaded { - return packageName, nil - } - return "", errors.New("package not found") -} - -func init() { - if !features.CMFA { - process.DefaultPackageNameResolver = findPackageName - } -} - -func (l *Listener) openAndroidHotspot(tunOptions tun.Options) { - if runtime.GOOS == "android" && tunOptions.AutoRoute { - priority := 9000 - if len(tunOptions.ExcludedRanges()) > 0 { - priority++ - } - if tunOptions.InterfaceMonitor.AndroidVPNEnabled() { - priority++ - } - it := netlink.NewRule() - it.Priority = priority - it.IifName = tunOptions.Name - it.Table = 254 //main - it.Family = unix.AF_INET - it.SuppressPrefixlen = 0 - err := netlink.RuleAdd(it) - if err != nil { - log.Warnln("[TUN] add AndroidHotspot rule error") - } - } -} diff --git a/listener/sing_tun/server_notandroid.go b/listener/sing_tun/server_notandroid.go deleted file mode 100644 index 10fd3997b4..0000000000 --- a/listener/sing_tun/server_notandroid.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !android || cmfa - -package sing_tun - -import ( - tun "github.com/metacubex/sing-tun" -) - -func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { - return nil -} -func (l *Listener) openAndroidHotspot(tunOptions tun.Options) {} diff --git a/listener/sing_tun/server_notwindows.go b/listener/sing_tun/server_notwindows.go deleted file mode 100644 index eda79dc093..0000000000 --- a/listener/sing_tun/server_notwindows.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !windows - -package sing_tun - -import ( - tun "github.com/metacubex/sing-tun" -) - -func tunNew(options tun.Options) (tun.Tun, error) { - return tun.New(options) -} diff --git a/listener/sing_tun/server_windows.go b/listener/sing_tun/server_windows.go deleted file mode 100644 index 325cf09635..0000000000 --- a/listener/sing_tun/server_windows.go +++ /dev/null @@ -1,36 +0,0 @@ -package sing_tun - -import ( - "time" - - "github.com/metacubex/mihomo/constant/features" - "github.com/metacubex/mihomo/log" - - tun "github.com/metacubex/sing-tun" -) - -func tunNew(options tun.Options) (tunIf tun.Tun, err error) { - maxRetry := 3 - for i := 0; i < maxRetry; i++ { - timeBegin := time.Now() - tunIf, err = tun.New(options) - if err == nil { - return - } - timeEnd := time.Now() - if timeEnd.Sub(timeBegin) < 1*time.Second { // retrying for "Cannot create a file when that file already exists." - return - } - log.Warnln("Start Tun interface timeout: %s [retrying %d/%d]", err, i+1, maxRetry) - } - return -} - -func init() { - tun.TunnelType = InterfaceName - - if features.WindowsMajorVersion < 10 { - // to resolve "bind: The requested address is not valid in its context" - EnforceBindInterface = true - } -} diff --git a/listener/sing_tun/tun_name_darwin.go b/listener/sing_tun/tun_name_darwin.go deleted file mode 100644 index 5b4686ea1b..0000000000 --- a/listener/sing_tun/tun_name_darwin.go +++ /dev/null @@ -1,11 +0,0 @@ -package sing_tun - -import "golang.org/x/sys/unix" - -func getTunnelName(fd int32) (string, error) { - return unix.GetsockoptString( - int(fd), - 2, /* #define SYSPROTO_CONTROL 2 */ - 2, /* #define UTUN_OPT_IFNAME 2 */ - ) -} diff --git a/listener/sing_tun/tun_name_linux.go b/listener/sing_tun/tun_name_linux.go deleted file mode 100644 index 9ae9800be2..0000000000 --- a/listener/sing_tun/tun_name_linux.go +++ /dev/null @@ -1,25 +0,0 @@ -package sing_tun - -import ( - "fmt" - "golang.org/x/sys/unix" - "syscall" - "unsafe" -) - -const ifReqSize = unix.IFNAMSIZ + 64 - -func getTunnelName(fd int32) (string, error) { - var ifr [ifReqSize]byte - var errno syscall.Errno - _, _, errno = unix.Syscall( - unix.SYS_IOCTL, - uintptr(fd), - uintptr(unix.TUNGETIFF), - uintptr(unsafe.Pointer(&ifr[0])), - ) - if errno != 0 { - return "", fmt.Errorf("failed to get name of TUN device: %w", errno) - } - return unix.ByteSliceToString(ifr[:]), nil -} diff --git a/listener/sing_tun/tun_name_other.go b/listener/sing_tun/tun_name_other.go deleted file mode 100644 index c47c8cbee4..0000000000 --- a/listener/sing_tun/tun_name_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !(darwin || linux) - -package sing_tun - -import "os" - -func getTunnelName(fd int32) (string, error) { - return "", os.ErrInvalid -} diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go deleted file mode 100644 index 97a62fe491..0000000000 --- a/listener/sing_vless/server.go +++ /dev/null @@ -1,201 +0,0 @@ -package sing_vless - -import ( - "context" - "crypto/tls" - "errors" - "net" - "net/http" - "reflect" - "strings" - "unsafe" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/component/ca" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/reality" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/gun" - mihomoVMess "github.com/metacubex/mihomo/transport/vmess" - - "github.com/metacubex/sing-vmess/vless" - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/metadata" -) - -func init() { - vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) { - tlsConn, loaded := common.Cast[*reality.Conn](conn) // *utls.Conn - if !loaded { - return - } - return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), unsafe.Pointer(tlsConn) - }) - - vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) { - tlsConn, loaded := common.Cast[*tlsC.UConn](conn) // *utls.UConn - if !loaded { - return - } - return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn) - }) -} - -type Listener struct { - closed bool - config LC.VlessServer - listeners []net.Listener - service *vless.Service[string] -} - -func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-VLESS"), - inbound.WithSpecialRules(""), - } - } - h, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.VLESS, - Additions: additions, - MuxOption: config.MuxOption, - }) - if err != nil { - return nil, err - } - - service := vless.NewService[string](log.SingLogger, h) - service.UpdateUsers( - common.Map(config.Users, func(it LC.VlessUser) string { - return it.Username - }), - common.Map(config.Users, func(it LC.VlessUser) string { - return it.UUID - }), - common.Map(config.Users, func(it LC.VlessUser) string { - return it.Flow - })) - - sl = &Listener{false, config, nil, service} - - tlsConfig := &tls.Config{} - var realityBuilder *reality.Builder - var httpHandler http.Handler - - if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { - return nil, errors.New("certificate is unavailable in reality") - } - realityBuilder, err = config.RealityConfig.Build(tunnel) - if err != nil { - return nil, err - } - } - if config.WsPath != "" { - httpMux := http.NewServeMux() - httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { - conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - sl.HandleConn(conn, tunnel, additions...) - }) - httpHandler = httpMux - tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") - } - if config.GrpcServiceName != "" { - httpHandler = gun.NewServerHandler(gun.ServerOption{ - ServiceName: config.GrpcServiceName, - ConnHandler: func(conn net.Conn) { - sl.HandleConn(conn, tunnel, additions...) - }, - HttpHandler: httpHandler, - }) - tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1 - } - - for _, addr := range strings.Split(config.Listen, ",") { - addr := addr - - //TCP - l, err := inbound.Listen("tcp", addr) - if err != nil { - return nil, err - } - if realityBuilder != nil { - l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) - } else { - return nil, errors.New("disallow using Vless without both certificates/reality config") - } - sl.listeners = append(sl.listeners, l) - - go func() { - if httpHandler != nil { - _ = http.Serve(l, httpHandler) - return - } - for { - c, err := l.Accept() - if err != nil { - if sl.closed { - break - } - continue - } - - go sl.HandleConn(c, tunnel) - } - }() - } - - return sl, nil -} - -func (l *Listener) Close() error { - l.closed = true - var retErr error - for _, lis := range l.listeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - return retErr -} - -func (l *Listener) Config() string { - return l.config.String() -} - -func (l *Listener) AddrList() (addrList []net.Addr) { - for _, lis := range l.listeners { - addrList = append(addrList, lis.Addr()) - } - return -} - -func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - ctx := sing.WithAdditions(context.TODO(), additions...) - err := l.service.NewConnection(ctx, conn, metadata.Metadata{ - Protocol: "vless", - Source: metadata.SocksaddrFromNet(conn.RemoteAddr()), - }) - if err != nil { - _ = conn.Close() - return - } -} diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go deleted file mode 100644 index b5a9378fd6..0000000000 --- a/listener/sing_vmess/server.go +++ /dev/null @@ -1,218 +0,0 @@ -package sing_vmess - -import ( - "context" - "crypto/tls" - "errors" - "net" - "net/http" - "net/url" - "strings" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/component/ca" - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/reality" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/ntp" - "github.com/metacubex/mihomo/transport/gun" - mihomoVMess "github.com/metacubex/mihomo/transport/vmess" - - vmess "github.com/metacubex/sing-vmess" - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/metadata" -) - -type Listener struct { - closed bool - config LC.VmessServer - listeners []net.Listener - service *vmess.Service[string] -} - -var _listener *Listener - -func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-VMESS"), - inbound.WithSpecialRules(""), - } - defer func() { - _listener = sl - }() - } - h, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.VMESS, - Additions: additions, - MuxOption: config.MuxOption, - }) - if err != nil { - return nil, err - } - - service := vmess.NewService[string](h, vmess.ServiceWithDisableHeaderProtection(), vmess.ServiceWithTimeFunc(ntp.Now)) - err = service.UpdateUsers( - common.Map(config.Users, func(it LC.VmessUser) string { - return it.Username - }), - common.Map(config.Users, func(it LC.VmessUser) string { - return it.UUID - }), - common.Map(config.Users, func(it LC.VmessUser) int { - return it.AlterID - })) - if err != nil { - return nil, err - } - - err = service.Start() - if err != nil { - return nil, err - } - - sl = &Listener{false, config, nil, service} - - tlsConfig := &tls.Config{} - var realityBuilder *reality.Builder - var httpHandler http.Handler - - if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { - return nil, errors.New("certificate is unavailable in reality") - } - realityBuilder, err = config.RealityConfig.Build(tunnel) - if err != nil { - return nil, err - } - } - if config.WsPath != "" { - httpMux := http.NewServeMux() - httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { - conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - sl.HandleConn(conn, tunnel, additions...) - }) - httpHandler = httpMux - tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") - } - if config.GrpcServiceName != "" { - httpHandler = gun.NewServerHandler(gun.ServerOption{ - ServiceName: config.GrpcServiceName, - ConnHandler: func(conn net.Conn) { - sl.HandleConn(conn, tunnel, additions...) - }, - HttpHandler: httpHandler, - }) - tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1 - } - - for _, addr := range strings.Split(config.Listen, ",") { - addr := addr - - //TCP - l, err := inbound.Listen("tcp", addr) - if err != nil { - return nil, err - } - if realityBuilder != nil { - l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) - } - sl.listeners = append(sl.listeners, l) - - go func() { - if httpHandler != nil { - _ = http.Serve(l, httpHandler) - return - } - for { - c, err := l.Accept() - if err != nil { - if sl.closed { - break - } - continue - } - - go sl.HandleConn(c, tunnel) - } - }() - } - - return sl, nil -} - -func (l *Listener) Close() error { - l.closed = true - var retErr error - for _, lis := range l.listeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - err := l.service.Close() - if err != nil { - retErr = err - } - return retErr -} - -func (l *Listener) Config() string { - return l.config.String() -} - -func (l *Listener) AddrList() (addrList []net.Addr) { - for _, lis := range l.listeners { - addrList = append(addrList, lis.Addr()) - } - return -} - -func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - ctx := sing.WithAdditions(context.TODO(), additions...) - err := l.service.NewConnection(ctx, conn, metadata.Metadata{ - Protocol: "vmess", - Source: metadata.SocksaddrFromNet(conn.RemoteAddr()), - }) - if err != nil { - _ = conn.Close() - return - } -} - -func HandleVmess(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) bool { - if _listener != nil && _listener.service != nil { - go _listener.HandleConn(conn, tunnel, additions...) - return true - } - return false -} - -func ParseVmessURL(s string) (addr, username, password string, err error) { - u, err := url.Parse(s) - if err != nil { - return - } - - addr = u.Host - if u.User != nil { - username = u.User.Username() - password, _ = u.User.Password() - } - return -} diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go deleted file mode 100644 index ab4086a38b..0000000000 --- a/listener/socks/tcp.go +++ /dev/null @@ -1,161 +0,0 @@ -package socks - -import ( - "crypto/tls" - "errors" - "io" - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/auth" - "github.com/metacubex/mihomo/component/ca" - C "github.com/metacubex/mihomo/constant" - authStore "github.com/metacubex/mihomo/listener/auth" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/reality" - "github.com/metacubex/mihomo/transport/socks4" - "github.com/metacubex/mihomo/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, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - return NewWithConfig(LC.AuthServer{Enable: true, Listen: addr, AuthStore: authStore.Default}, tunnel, additions...) -} - -func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - isDefault := false - if len(additions) == 0 { - isDefault = true - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-SOCKS"), - inbound.WithSpecialRules(""), - } - } - - l, err := inbound.Listen("tcp", config.Listen) - if err != nil { - return nil, err - } - - tlsConfig := &tls.Config{} - var realityBuilder *reality.Builder - - if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { - return nil, errors.New("certificate is unavailable in reality") - } - realityBuilder, err = config.RealityConfig.Build(tunnel) - if err != nil { - return nil, err - } - } - - if realityBuilder != nil { - l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) - } - - sl := &Listener{ - listener: l, - addr: config.Listen, - } - go func() { - for { - c, err := l.Accept() - if err != nil { - if sl.closed { - break - } - continue - } - store := config.AuthStore - if isDefault || store == authStore.Default { // only apply on default listener - if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { - _ = c.Close() - continue - } - if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) { - store = authStore.Nil - } - } - go handleSocks(c, tunnel, store, additions...) - } - }() - - return sl, nil -} - -func handleSocks(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { - bufConn := N.NewBufferedConn(conn) - head, err := bufConn.Peek(1) - if err != nil { - conn.Close() - return - } - - switch head[0] { - case socks4.Version: - HandleSocks4(bufConn, tunnel, store, additions...) - case socks5.Version: - HandleSocks5(bufConn, tunnel, store, additions...) - default: - conn.Close() - } -} - -func HandleSocks4(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { - authenticator := store.Authenticator() - addr, _, user, err := socks4.ServerHandshake(conn, authenticator) - if err != nil { - conn.Close() - return - } - additions = append(additions, inbound.WithInUser(user)) - tunnel.HandleTCPConn(inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...)) -} - -func HandleSocks5(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { - authenticator := store.Authenticator() - target, command, user, err := socks5.ServerHandshake(conn, authenticator) - if err != nil { - conn.Close() - return - } - if command == socks5.CmdUDPAssociate { - defer conn.Close() - io.Copy(io.Discard, conn) - return - } - additions = append(additions, inbound.WithInUser(user)) - tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.SOCKS5, additions...)) -} diff --git a/listener/socks/udp.go b/listener/socks/udp.go deleted file mode 100644 index cb4fa37294..0000000000 --- a/listener/socks/udp.go +++ /dev/null @@ -1,92 +0,0 @@ -package socks - -import ( - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/sockopt" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/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, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-SOCKS"), - inbound.WithSpecialRules(""), - } - } - l, err := inbound.ListenPacket("udp", addr) - if err != nil { - return nil, err - } - - if err := sockopt.UDPReuseaddr(l); err != nil { - log.Warnln("Failed to Reuse UDP Address: %s", err) - } - - sl := &UDPListener{ - packetConn: l, - addr: addr, - } - conn := N.NewEnhancePacketConn(l) - go func() { - for { - data, put, remoteAddr, err := conn.WaitReadFrom() - if err != nil { - if put != nil { - put() - } - if sl.closed { - break - } - continue - } - handleSocksUDP(l, tunnel, data, put, remoteAddr, additions...) - } - }() - - return sl, nil -} - -func handleSocksUDP(pc net.PacketConn, tunnel C.Tunnel, buf []byte, put func(), addr net.Addr, additions ...inbound.Addition) { - target, payload, err := socks5.DecodeUDPPacket(buf) - if err != nil { - // Unresolved UDP packet, return buffer to the pool - if put != nil { - put() - } - return - } - packet := &packet{ - pc: pc, - rAddr: addr, - payload: payload, - put: put, - } - tunnel.HandleUDPPacket(inbound.NewPacket(target, packet, C.SOCKS5, additions...)) -} diff --git a/listener/socks/utils.go b/listener/socks/utils.go deleted file mode 100644 index d113d45c9e..0000000000 --- a/listener/socks/utils.go +++ /dev/null @@ -1,44 +0,0 @@ -package socks - -import ( - "net" - - "github.com/metacubex/mihomo/transport/socks5" -) - -type packet struct { - pc net.PacketConn - rAddr net.Addr - payload []byte - put func() -} - -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() { - if c.put != nil { - c.put() - c.put = nil - } - c.payload = nil -} - -func (c *packet) InAddr() net.Addr { - return c.pc.LocalAddr() -} diff --git a/listener/tproxy/packet.go b/listener/tproxy/packet.go deleted file mode 100644 index b038d954f8..0000000000 --- a/listener/tproxy/packet.go +++ /dev/null @@ -1,120 +0,0 @@ -package tproxy - -import ( - "errors" - "fmt" - "net" - "net/netip" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/pool" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type packet struct { - pc net.PacketConn - lAddr netip.AddrPort - buf []byte - tunnel C.Tunnel -} - -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 := createOrGetLocalConn(addr, c.LocalAddr(), c.tunnel) - if err != nil { - n = 0 - return - } - n, err = tc.Write(b) - 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) - c.buf = nil -} - -func (c *packet) InAddr() net.Addr { - return c.pc.LocalAddr() -} - -// this function listen at rAddr and write to lAddr -// for here, rAddr is the ip/port client want to access -// lAddr is the ip/port client opened -func createOrGetLocalConn(rAddr, lAddr net.Addr, tunnel C.Tunnel) (*net.UDPConn, error) { - remote := rAddr.String() - local := lAddr.String() - natTable := tunnel.NatTable() - localConn := natTable.GetForLocalConn(local, remote) - // localConn not exist - if localConn == nil { - cond, loaded := natTable.GetOrCreateLockForLocalConn(local, remote) - if loaded { - cond.L.Lock() - cond.Wait() - // we should get localConn here - localConn = natTable.GetForLocalConn(local, remote) - if localConn == nil { - return nil, fmt.Errorf("localConn is nil, nat entry not exist") - } - cond.L.Unlock() - } else { - if cond == nil { - return nil, fmt.Errorf("cond is nil, nat entry not exist") - } - defer func() { - natTable.DeleteLockForLocalConn(local, remote) - cond.Broadcast() - }() - conn, err := listenLocalConn(rAddr, lAddr, tunnel) - if err != nil { - log.Errorln("listenLocalConn failed with error: %s, packet loss (rAddr[%T]=%s lAddr[%T]=%s)", err.Error(), rAddr, remote, lAddr, local) - return nil, err - } - natTable.AddForLocalConn(local, remote, conn) - localConn = conn - } - } - return localConn, nil -} - -// this function listen at rAddr -// and send what received to program itself, then send to real remote -func listenLocalConn(rAddr, lAddr net.Addr, tunnel C.Tunnel) (*net.UDPConn, error) { - additions := []inbound.Addition{ - inbound.WithInName("DEFAULT-TPROXY"), - inbound.WithSpecialRules(""), - } - lc, err := dialUDP("udp", rAddr.(*net.UDPAddr).AddrPort(), lAddr.(*net.UDPAddr).AddrPort()) - if err != nil { - return nil, err - } - go func() { - log.Debugln("TProxy listenLocalConn rAddr=%s lAddr=%s", rAddr.String(), lAddr.String()) - for { - buf := pool.Get(pool.UDPBufferSize) - br, err := lc.Read(buf) - if err != nil { - if errors.Is(err, net.ErrClosed) { - log.Debugln("TProxy local conn listener exit.. rAddr=%s lAddr=%s", rAddr.String(), lAddr.String()) - pool.Put(buf) - return - } - } - // since following localPackets are pass through this socket which listen rAddr - // I choose current listener as packet's packet conn - handlePacketConn(lc, tunnel, buf[:br], lAddr.(*net.UDPAddr).AddrPort(), rAddr.(*net.UDPAddr).AddrPort(), additions...) - } - }() - return lc, nil -} diff --git a/listener/tproxy/setsockopt_linux.go b/listener/tproxy/setsockopt_linux.go deleted file mode 100644 index 9189f11529..0000000000 --- a/listener/tproxy/setsockopt_linux.go +++ /dev/null @@ -1,56 +0,0 @@ -//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) - } - - if err == nil { - _ = setDSCPsockopt(fd, isIPv6) - } - }) - - return err -} - -func setDSCPsockopt(fd uintptr, isIPv6 bool) (err error) { - if err == nil { - err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVTOS, 1) - } - - if err == nil && isIPv6 { - err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, syscall.IPV6_RECVTCLASS, 1) - } - - return -} diff --git a/listener/tproxy/setsockopt_other.go b/listener/tproxy/setsockopt_other.go deleted file mode 100644 index 9ba06f910a..0000000000 --- a/listener/tproxy/setsockopt_other.go +++ /dev/null @@ -1,12 +0,0 @@ -//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/tproxy.go b/listener/tproxy/tproxy.go deleted file mode 100644 index 6056047a41..0000000000 --- a/listener/tproxy/tproxy.go +++ /dev/null @@ -1,84 +0,0 @@ -package tproxy - -import ( - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/component/keepalive" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/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, tunnel C.Tunnel, additions ...inbound.Addition) { - target := socks5.ParseAddrToSocksAddr(conn.LocalAddr()) - keepalive.TCPKeepAlive(conn) - // TProxy's conn.LocalAddr() is target address, so we set from l.listener - additions = append([]inbound.Addition{inbound.WithInAddr(l.listener.Addr())}, additions...) - tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.TPROXY, additions...)) -} - -func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-TPROXY"), - inbound.WithSpecialRules(""), - } - } - 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, tunnel, additions...) - } - }() - - return rl, nil -} diff --git a/listener/tproxy/tproxy_iptables.go b/listener/tproxy/tproxy_iptables.go deleted file mode 100644 index bc26b125fd..0000000000 --- a/listener/tproxy/tproxy_iptables.go +++ /dev/null @@ -1,208 +0,0 @@ -package tproxy - -import ( - "errors" - "fmt" - "net" - "runtime" - - "github.com/metacubex/mihomo/common/cmd" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/log" -) - -var ( - dnsPort uint16 - tProxyPort uint16 - interfaceName string - DnsRedirect bool -) - -const ( - PROXY_FWMARK = "0x2d0" - PROXY_ROUTE_TABLE = "0x2d0" -) - -func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dnsredir bool, dport uint16) error { - if _, err := cmd.ExecCmd("iptables -V"); err != nil { - return fmt.Errorf("current operations system [%s] are not support iptables or command iptables does not exist", runtime.GOOS) - } - - if ifname == "" { - return errors.New("the 'interface-name' can not be empty") - } - - interfaceName = ifname - tProxyPort = tport - DnsRedirect = dnsredir - dnsPort = dport - - // add route - execCmd(fmt.Sprintf("ip -f inet rule add fwmark %s lookup %s", PROXY_FWMARK, PROXY_ROUTE_TABLE)) - execCmd(fmt.Sprintf("ip -f inet route add local default dev %s table %s", interfaceName, PROXY_ROUTE_TABLE)) - - // set FORWARD - if interfaceName != "lo" { - execCmd("sysctl -w net.ipv4.ip_forward=1") - execCmd(fmt.Sprintf("iptables -t filter -A FORWARD -o %s -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT", interfaceName)) - execCmd(fmt.Sprintf("iptables -t filter -A FORWARD -o %s -j ACCEPT", interfaceName)) - execCmd(fmt.Sprintf("iptables -t filter -A FORWARD -i %s ! -o %s -j ACCEPT", interfaceName, interfaceName)) - execCmd(fmt.Sprintf("iptables -t filter -A FORWARD -i %s -o %s -j ACCEPT", interfaceName, interfaceName)) - } - - // set mihomo divert - execCmd("iptables -t mangle -N mihomo_divert") - execCmd("iptables -t mangle -F mihomo_divert") - execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_divert -j MARK --set-mark %s", PROXY_FWMARK)) - execCmd("iptables -t mangle -A mihomo_divert -j ACCEPT") - - // set pre routing - execCmd("iptables -t mangle -N mihomo_prerouting") - execCmd("iptables -t mangle -F mihomo_prerouting") - execCmd("iptables -t mangle -A mihomo_prerouting -s 172.17.0.0/16 -j RETURN") - if DnsRedirect { - execCmd("iptables -t mangle -A mihomo_prerouting -p udp --dport 53 -j ACCEPT") - execCmd("iptables -t mangle -A mihomo_prerouting -p tcp --dport 53 -j ACCEPT") - } - execCmd("iptables -t mangle -A mihomo_prerouting -m addrtype --dst-type LOCAL -j RETURN") - addLocalnetworkToChain("mihomo_prerouting", bypass) - execCmd("iptables -t mangle -A mihomo_prerouting -p tcp -m socket -j mihomo_divert") - execCmd("iptables -t mangle -A mihomo_prerouting -p udp -m socket -j mihomo_divert") - execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_prerouting -p tcp -j TPROXY --on-port %d --tproxy-mark %s/%s", tProxyPort, PROXY_FWMARK, PROXY_FWMARK)) - execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_prerouting -p udp -j TPROXY --on-port %d --tproxy-mark %s/%s", tProxyPort, PROXY_FWMARK, PROXY_FWMARK)) - execCmd("iptables -t mangle -A PREROUTING -j mihomo_prerouting") - - if DnsRedirect { - execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) - } - - // set post routing - if interfaceName != "lo" { - execCmd(fmt.Sprintf("iptables -t nat -A POSTROUTING -o %s -m addrtype ! --src-type LOCAL -j MASQUERADE", interfaceName)) - } - - // set output - execCmd("iptables -t mangle -N mihomo_output") - execCmd("iptables -t mangle -F mihomo_output") - execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) - if DnsRedirect { - execCmd("iptables -t mangle -A mihomo_output -p udp -m multiport --dports 53,123,137 -j ACCEPT") - execCmd("iptables -t mangle -A mihomo_output -p tcp --dport 53 -j ACCEPT") - } - execCmd("iptables -t mangle -A mihomo_output -m addrtype --dst-type LOCAL -j RETURN") - execCmd("iptables -t mangle -A mihomo_output -m addrtype --dst-type BROADCAST -j RETURN") - addLocalnetworkToChain("mihomo_output", bypass) - execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_output -p tcp -j MARK --set-mark %s", PROXY_FWMARK)) - execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_output -p udp -j MARK --set-mark %s", PROXY_FWMARK)) - execCmd(fmt.Sprintf("iptables -t mangle -I OUTPUT -o %s -j mihomo_output", interfaceName)) - - // set dns output - if DnsRedirect { - execCmd("iptables -t nat -N mihomo_dns_output") - execCmd("iptables -t nat -F mihomo_dns_output") - execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) - execCmd("iptables -t nat -A mihomo_dns_output -s 172.17.0.0/16 -j RETURN") - execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p udp -j REDIRECT --to-ports %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p tcp -j REDIRECT --to-ports %d", dnsPort)) - execCmd("iptables -t nat -I OUTPUT -p tcp --dport 53 -j mihomo_dns_output") - execCmd("iptables -t nat -I OUTPUT -p udp --dport 53 -j mihomo_dns_output") - } - - return nil -} - -func CleanupTProxyIPTables() { - if runtime.GOOS != "linux" || interfaceName == "" || tProxyPort == 0 { - return - } - - log.Warnln("Cleanup tproxy linux iptables") - - dialer.DefaultRoutingMark.CompareAndSwap(2158, 0) - - if _, err := cmd.ExecCmd("iptables -t mangle -L mihomo_divert"); err != nil { - return - } - - // clean route - execCmd(fmt.Sprintf("ip -f inet rule del fwmark %s lookup %s", PROXY_FWMARK, PROXY_ROUTE_TABLE)) - execCmd(fmt.Sprintf("ip -f inet route del local default dev %s table %s", interfaceName, PROXY_ROUTE_TABLE)) - - // clean FORWARD - if interfaceName != "lo" { - execCmd(fmt.Sprintf("iptables -t filter -D FORWARD -i %s ! -o %s -j ACCEPT", interfaceName, interfaceName)) - execCmd(fmt.Sprintf("iptables -t filter -D FORWARD -i %s -o %s -j ACCEPT", interfaceName, interfaceName)) - execCmd(fmt.Sprintf("iptables -t filter -D FORWARD -o %s -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT", interfaceName)) - execCmd(fmt.Sprintf("iptables -t filter -D FORWARD -o %s -j ACCEPT", interfaceName)) - } - - // clean PREROUTING - if DnsRedirect { - execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) - } - execCmd("iptables -t mangle -D PREROUTING -j mihomo_prerouting") - - // clean POSTROUTING - if interfaceName != "lo" { - execCmd(fmt.Sprintf("iptables -t nat -D POSTROUTING -o %s -m addrtype ! --src-type LOCAL -j MASQUERADE", interfaceName)) - } - - // clean OUTPUT - execCmd(fmt.Sprintf("iptables -t mangle -D OUTPUT -o %s -j mihomo_output", interfaceName)) - if DnsRedirect { - execCmd("iptables -t nat -D OUTPUT -p tcp --dport 53 -j mihomo_dns_output") - execCmd("iptables -t nat -D OUTPUT -p udp --dport 53 -j mihomo_dns_output") - } - - // clean chain - execCmd("iptables -t mangle -F mihomo_prerouting") - execCmd("iptables -t mangle -X mihomo_prerouting") - execCmd("iptables -t mangle -F mihomo_divert") - execCmd("iptables -t mangle -X mihomo_divert") - execCmd("iptables -t mangle -F mihomo_output") - execCmd("iptables -t mangle -X mihomo_output") - if DnsRedirect { - execCmd("iptables -t nat -F mihomo_dns_output") - execCmd("iptables -t nat -X mihomo_dns_output") - } - interfaceName = "" - tProxyPort = 0 - dnsPort = 0 -} - -func addLocalnetworkToChain(chain string, bypass []string) { - for _, bp := range bypass { - _, _, err := net.ParseCIDR(bp) - if err != nil { - log.Warnln("[IPTABLES] %s", err) - continue - } - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d %s -j RETURN", chain, bp)) - } - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 0.0.0.0/8 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 10.0.0.0/8 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 100.64.0.0/10 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 127.0.0.0/8 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 169.254.0.0/16 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 172.16.0.0/12 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.0.0.0/24 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.0.2.0/24 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.88.99.0/24 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 192.168.0.0/16 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 198.51.100.0/24 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 203.0.113.0/24 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 224.0.0.0/4 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 240.0.0.0/4 -j RETURN", chain)) - execCmd(fmt.Sprintf("iptables -t mangle -A %s -d 255.255.255.255/32 -j RETURN", chain)) -} - -func execCmd(cmdStr string) { - log.Debugln("[IPTABLES] %s", cmdStr) - - _, err := cmd.ExecCmd(cmdStr) - if err != nil { - log.Warnln("[IPTABLES] exec cmd: %v", err) - } -} diff --git a/listener/tproxy/udp.go b/listener/tproxy/udp.go deleted file mode 100644 index 22b4d696e7..0000000000 --- a/listener/tproxy/udp.go +++ /dev/null @@ -1,106 +0,0 @@ -package tproxy - -import ( - "net" - "net/netip" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/pool" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/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, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-TPROXY"), - inbound.WithSpecialRules(""), - } - } - 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 { - pool.Put(buf) - continue - } - - dscp, _ := getDSCP(oob[:oobn]) - additions := append(additions, inbound.WithDSCP(dscp)) // don't change outside additions - - if rAddr.Addr().Is4() { - // try to unmap 4in6 address - lAddr = netip.AddrPortFrom(lAddr.Addr().Unmap(), lAddr.Port()) - } - handlePacketConn(l, tunnel, buf[:n], lAddr, rAddr, additions...) - } - }() - - return rl, nil -} - -func handlePacketConn(pc net.PacketConn, tunnel C.Tunnel, buf []byte, lAddr, rAddr netip.AddrPort, additions ...inbound.Addition) { - target := socks5.AddrFromStdAddrPort(rAddr) - pkt := &packet{ - pc: pc, - lAddr: lAddr, - buf: buf, - tunnel: tunnel, - } - tunnel.HandleUDPPacket(inbound.NewPacket(target, pkt, C.TPROXY, additions...)) -} diff --git a/listener/tproxy/udp_linux.go b/listener/tproxy/udp_linux.go deleted file mode 100644 index c96d4cc735..0000000000 --- a/listener/tproxy/udp_linux.go +++ /dev/null @@ -1,165 +0,0 @@ -//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. - var sa unix.Sockaddr - for i := range scms { - sa, err = unix.ParseOrigDstAddr(&scms[i]) - if err == nil { - break - } - } - - 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 -} - -func getDSCP(oob []byte) (uint8, error) { - scms, err := unix.ParseSocketControlMessage(oob) - if err != nil { - return 0, fmt.Errorf("parse control message: %w", err) - } - var dscp uint8 - for i := range scms { - dscp, err = parseDSCP(&scms[i]) - if err == nil { - break - } - } - - if err != nil { - return 0, fmt.Errorf("retrieve DSCP: %w", err) - } - return dscp, nil -} - -func parseDSCP(m *unix.SocketControlMessage) (uint8, error) { - switch { - case m.Header.Level == unix.SOL_IP && m.Header.Type == unix.IP_TOS: - dscp := uint8(m.Data[0] >> 2) - return dscp, nil - - case m.Header.Level == unix.SOL_IPV6 && m.Header.Type == unix.IPV6_TCLASS: - dscp := uint8(m.Data[0] >> 2) - return dscp, nil - - default: - return 0, nil - } -} diff --git a/listener/tproxy/udp_other.go b/listener/tproxy/udp_other.go deleted file mode 100644 index 2e0e0ae729..0000000000 --- a/listener/tproxy/udp_other.go +++ /dev/null @@ -1,21 +0,0 @@ -//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 getDSCP(oob []byte) (uint8, error) { - return 0, 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/trojan/packet.go b/listener/trojan/packet.go deleted file mode 100644 index 5111242ebd..0000000000 --- a/listener/trojan/packet.go +++ /dev/null @@ -1,43 +0,0 @@ -package trojan - -import ( - "errors" - "net" -) - -type packet struct { - pc net.PacketConn - rAddr net.Addr - payload []byte - put func() -} - -func (c *packet) Data() []byte { - return c.payload -} - -// WriteBack wirtes UDP packet with source(ip, port) = `addr` -func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { - if addr == nil { - err = errors.New("address is invalid") - return - } - return c.pc.WriteTo(b, addr) -} - -// LocalAddr returns the source IP/Port of UDP Packet -func (c *packet) LocalAddr() net.Addr { - return c.rAddr -} - -func (c *packet) Drop() { - if c.put != nil { - c.put() - c.put = nil - } - c.payload = nil -} - -func (c *packet) InAddr() net.Addr { - return c.pc.LocalAddr() -} diff --git a/listener/trojan/server.go b/listener/trojan/server.go deleted file mode 100644 index d3ca98d783..0000000000 --- a/listener/trojan/server.go +++ /dev/null @@ -1,283 +0,0 @@ -package trojan - -import ( - "crypto/tls" - "errors" - "io" - "net" - "net/http" - "strings" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/component/ca" - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/reality" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/transport/gun" - "github.com/metacubex/mihomo/transport/shadowsocks/core" - "github.com/metacubex/mihomo/transport/socks5" - "github.com/metacubex/mihomo/transport/trojan" - mihomoVMess "github.com/metacubex/mihomo/transport/vmess" - - "github.com/metacubex/smux" -) - -type Listener struct { - closed bool - config LC.TrojanServer - listeners []net.Listener - keys map[[trojan.KeyLength]byte]string - pickCipher core.Cipher - handler *sing.ListenerHandler -} - -func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-TROJAN"), - inbound.WithSpecialRules(""), - } - } - h, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.TROJAN, - Additions: additions, - MuxOption: config.MuxOption, - }) - if err != nil { - return nil, err - } - - keys := make(map[[trojan.KeyLength]byte]string) - for _, user := range config.Users { - keys[trojan.Key(user.Password)] = user.Username - } - - var pickCipher core.Cipher - if config.TrojanSSOption.Enabled { - if config.TrojanSSOption.Password == "" { - return nil, errors.New("empty password") - } - if config.TrojanSSOption.Method == "" { - config.TrojanSSOption.Method = "AES-128-GCM" - } - pickCipher, err = core.PickCipher(config.TrojanSSOption.Method, nil, config.TrojanSSOption.Password) - if err != nil { - return nil, err - } - } - sl = &Listener{false, config, nil, keys, pickCipher, h} - - tlsConfig := &tls.Config{} - var realityBuilder *reality.Builder - var httpHandler http.Handler - - if config.Certificate != "" && config.PrivateKey != "" { - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { - return nil, errors.New("certificate is unavailable in reality") - } - realityBuilder, err = config.RealityConfig.Build(tunnel) - if err != nil { - return nil, err - } - } - if config.WsPath != "" { - httpMux := http.NewServeMux() - httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { - conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - sl.HandleConn(conn, tunnel, additions...) - }) - httpHandler = httpMux - tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") - } - if config.GrpcServiceName != "" { - httpHandler = gun.NewServerHandler(gun.ServerOption{ - ServiceName: config.GrpcServiceName, - ConnHandler: func(conn net.Conn) { - sl.HandleConn(conn, tunnel, additions...) - }, - HttpHandler: httpHandler, - }) - tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...) // h2 must before http/1.1 - } - - for _, addr := range strings.Split(config.Listen, ",") { - addr := addr - - //TCP - l, err := inbound.Listen("tcp", addr) - if err != nil { - return nil, err - } - if realityBuilder != nil { - l = realityBuilder.NewListener(l) - } else if len(tlsConfig.Certificates) > 0 { - l = tls.NewListener(l, tlsConfig) - } else if !config.TrojanSSOption.Enabled { - return nil, errors.New("disallow using Trojan without both certificates/reality/ss config") - } - sl.listeners = append(sl.listeners, l) - - go func() { - if httpHandler != nil { - _ = http.Serve(l, httpHandler) - return - } - for { - c, err := l.Accept() - if err != nil { - if sl.closed { - break - } - continue - } - - go sl.HandleConn(c, tunnel, additions...) - } - }() - } - - return sl, nil -} - -func (l *Listener) Close() error { - l.closed = true - var retErr error - for _, lis := range l.listeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - return retErr -} - -func (l *Listener) Config() string { - return l.config.String() -} - -func (l *Listener) AddrList() (addrList []net.Addr) { - for _, lis := range l.listeners { - addrList = append(addrList, lis.Addr()) - } - return -} - -func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - defer conn.Close() - - if l.pickCipher != nil { - conn = l.pickCipher.StreamConn(conn) - } - - var key [trojan.KeyLength]byte - if _, err := io.ReadFull(conn, key[:]); err != nil { - //log.Warnln("read key error: %s", err.Error()) - return - } - - if user, ok := l.keys[key]; ok { - additions = append(additions, inbound.WithInUser(user)) - } else { - //log.Warnln("no such key") - return - } - - var crlf [2]byte - if _, err := io.ReadFull(conn, crlf[:]); err != nil { - //log.Warnln("read crlf error: %s", err.Error()) - return - } - - l.handleConn(false, conn, tunnel, additions...) -} - -func (l *Listener) handleConn(inMux bool, conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - if inMux { - defer conn.Close() - } - - command, err := socks5.ReadByte(conn) - if err != nil { - //log.Warnln("read command error: %s", err.Error()) - return - } - - switch command { - case trojan.CommandTCP, trojan.CommandUDP, trojan.CommandMux: - default: - //log.Warnln("unknown command: %d", command) - return - } - - target, err := socks5.ReadAddr0(conn) - if err != nil { - //log.Warnln("read target error: %s", err.Error()) - return - } - - if !inMux { - var crlf [2]byte - if _, err := io.ReadFull(conn, crlf[:]); err != nil { - //log.Warnln("read crlf error: %s", err.Error()) - return - } - } - - switch command { - case trojan.CommandTCP: - //tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.TROJAN, additions...)) - l.handler.HandleSocket(target, conn, additions...) - case trojan.CommandUDP: - pc := trojan.NewPacketConn(conn) - for { - data, put, remoteAddr, err := pc.WaitReadFrom() - if err != nil { - if put != nil { - put() - } - break - } - cPacket := &packet{ - pc: pc, - rAddr: remoteAddr, - payload: data, - put: put, - } - - tunnel.HandleUDPPacket(inbound.NewPacket(target, cPacket, C.TROJAN, additions...)) - } - case trojan.CommandMux: - if inMux { - //log.Warnln("invalid command: %d", command) - return - } - smuxConfig := smux.DefaultConfig() - smuxConfig.KeepAliveDisabled = true - session, err := smux.Server(conn, smuxConfig) - if err != nil { - //log.Warnln("smux server error: %s", err.Error()) - return - } - defer session.Close() - for { - stream, err := session.AcceptStream() - if err != nil { - return - } - go l.handleConn(true, stream, tunnel, additions...) - } - } -} diff --git a/listener/tuic/server.go b/listener/tuic/server.go deleted file mode 100644 index 7bc63a737b..0000000000 --- a/listener/tuic/server.go +++ /dev/null @@ -1,214 +0,0 @@ -package tuic - -import ( - "crypto/tls" - "net" - "strings" - "time" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/sockopt" - "github.com/metacubex/mihomo/component/ca" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/socks5" - "github.com/metacubex/mihomo/transport/tuic" - - "github.com/gofrs/uuid/v5" - "github.com/metacubex/quic-go" - "golang.org/x/exp/slices" -) - -const ServerMaxIncomingStreams = (1 << 32) - 1 - -type Listener struct { - closed bool - config LC.TuicServer - udpListeners []net.PacketConn - servers []*tuic.Server -} - -func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-TUIC"), - inbound.WithSpecialRules(""), - } - } - h, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.TUIC, - Additions: additions, - MuxOption: config.MuxOption, - }) - if err != nil { - return nil, err - } - - cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) - if err != nil { - return nil, err - } - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS13, - Certificates: []tls.Certificate{cert}, - } - if len(config.ALPN) > 0 { - tlsConfig.NextProtos = config.ALPN - } else { - tlsConfig.NextProtos = []string{"h3"} - } - - if config.MaxIdleTime == 0 { - config.MaxIdleTime = 15000 - } - if config.AuthenticationTimeout == 0 { - config.AuthenticationTimeout = 1000 - } - - quicConfig := &quic.Config{ - MaxIdleTimeout: time.Duration(config.MaxIdleTime) * time.Millisecond, - MaxIncomingStreams: ServerMaxIncomingStreams, - MaxIncomingUniStreams: ServerMaxIncomingStreams, - EnableDatagrams: true, - Allow0RTT: true, - DisablePathManager: true, // for port hopping - } - quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10 - quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow - quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10 - quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow - - packetOverHead := tuic.PacketOverHeadV4 - if len(config.Token) == 0 { - packetOverHead = tuic.PacketOverHeadV5 - } - - if config.CWND == 0 { - config.CWND = 32 - } - - if config.MaxUdpRelayPacketSize == 0 { - config.MaxUdpRelayPacketSize = 1500 - } - maxDatagramFrameSize := config.MaxUdpRelayPacketSize + packetOverHead - if maxDatagramFrameSize > 1400 { - maxDatagramFrameSize = 1400 - } - config.MaxUdpRelayPacketSize = maxDatagramFrameSize - packetOverHead - quicConfig.MaxDatagramFrameSize = int64(maxDatagramFrameSize) - - handleTcpFn := func(conn net.Conn, addr socks5.Addr, _additions ...inbound.Addition) error { - //newAdditions := additions - //if len(_additions) > 0 { - // newAdditions = slices.Clone(additions) - // newAdditions = append(newAdditions, _additions...) - //} - //conn, metadata := inbound.NewSocket(addr, conn, C.TUIC, newAdditions...) - //go tunnel.HandleTCPConn(conn, metadata) - go h.HandleSocket(addr, conn, _additions...) // h.HandleSocket will block, so open a new goroutine - return nil - } - handleUdpFn := func(addr socks5.Addr, packet C.UDPPacket, _additions ...inbound.Addition) error { - newAdditions := additions - if len(_additions) > 0 { - newAdditions = slices.Clone(additions) - newAdditions = append(newAdditions, _additions...) - } - tunnel.HandleUDPPacket(inbound.NewPacket(addr, packet, C.TUIC, newAdditions...)) - return nil - } - - option := &tuic.ServerOption{ - HandleTcpFn: handleTcpFn, - HandleUdpFn: handleUdpFn, - TlsConfig: tlsC.UConfig(tlsConfig), - QuicConfig: quicConfig, - CongestionController: config.CongestionController, - AuthenticationTimeout: time.Duration(config.AuthenticationTimeout) * time.Millisecond, - MaxUdpRelayPacketSize: config.MaxUdpRelayPacketSize, - CWND: config.CWND, - } - if len(config.Token) > 0 { - tokens := make([][32]byte, len(config.Token)) - for i, token := range config.Token { - tokens[i] = tuic.GenTKN(token) - } - option.Tokens = tokens - } - if len(config.Users) > 0 { - users := make(map[[16]byte]string) - for _uuid, password := range config.Users { - users[uuid.FromStringOrNil(_uuid)] = password - } - option.Users = users - } - - sl := &Listener{false, config, nil, nil} - - for _, addr := range strings.Split(config.Listen, ",") { - addr := addr - - ul, err := inbound.ListenPacket("udp", addr) - if err != nil { - return nil, err - } - - if err := sockopt.UDPReuseaddr(ul); err != nil { - log.Warnln("Failed to Reuse UDP Address: %s", err) - } - - sl.udpListeners = append(sl.udpListeners, ul) - - var server *tuic.Server - server, err = tuic.NewServer(option, ul) - if err != nil { - return nil, err - } - - sl.servers = append(sl.servers, server) - - go func() { - err := server.Serve() - if err != nil { - if sl.closed { - return - } - } - }() - } - - return sl, nil -} - -func (l *Listener) Close() error { - l.closed = true - var retErr error - for _, lis := range l.servers { - err := lis.Close() - if err != nil { - retErr = err - } - } - for _, lis := range l.udpListeners { - err := lis.Close() - if err != nil { - retErr = err - } - } - return retErr -} - -func (l *Listener) Config() LC.TuicServer { - return l.config -} - -func (l *Listener) AddrList() (addrList []net.Addr) { - for _, lis := range l.udpListeners { - addrList = append(addrList, lis.LocalAddr()) - } - return -} diff --git a/listener/tunnel/packet.go b/listener/tunnel/packet.go deleted file mode 100644 index 165004d6e4..0000000000 --- a/listener/tunnel/packet.go +++ /dev/null @@ -1,36 +0,0 @@ -package tunnel - -import ( - "net" - - "github.com/metacubex/mihomo/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) - c.payload = nil -} - -func (c *packet) InAddr() net.Addr { - return c.pc.LocalAddr() -} diff --git a/listener/tunnel/tcp.go b/listener/tunnel/tcp.go deleted file mode 100644 index 7c916a38f5..0000000000 --- a/listener/tunnel/tcp.go +++ /dev/null @@ -1,76 +0,0 @@ -package tunnel - -import ( - "fmt" - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/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, tunnel C.Tunnel, additions ...inbound.Addition) { - tunnel.HandleTCPConn(inbound.NewSocket(l.target, conn, C.TUNNEL, additions...)) -} - -func New(addr, target, proxy string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - l, err := inbound.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, - } - - if proxy != "" { - additions = append([]inbound.Addition{inbound.WithSpecialProxy(proxy)}, additions...) - } - - go func() { - for { - c, err := l.Accept() - if err != nil { - if rl.closed { - break - } - continue - } - go rl.handleTCP(c, tunnel, additions...) - } - }() - - return rl, nil -} diff --git a/listener/tunnel/udp.go b/listener/tunnel/udp.go deleted file mode 100644 index f7d980aba2..0000000000 --- a/listener/tunnel/udp.go +++ /dev/null @@ -1,85 +0,0 @@ -package tunnel - -import ( - "fmt" - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/pool" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/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, tunnel C.Tunnel, additions ...inbound.Addition) (*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, - } - - if proxy != "" { - additions = append([]inbound.Addition{inbound.WithSpecialProxy(proxy)}, additions...) - } - - 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, tunnel, buf[:n], remoteAddr, additions...) - } - }() - - return sl, nil -} - -func (l *PacketConn) handleUDP(pc net.PacketConn, tunnel C.Tunnel, buf []byte, addr net.Addr, additions ...inbound.Addition) { - cPacket := &packet{ - pc: pc, - rAddr: addr, - payload: buf, - } - - tunnel.HandleUDPPacket(inbound.NewPacket(l.target, cPacket, C.TUNNEL, additions...)) -} diff --git a/log/level.go b/log/level.go deleted file mode 100644 index a4c6ecbd41..0000000000 --- a/log/level.go +++ /dev/null @@ -1,92 +0,0 @@ -package log - -import ( - "encoding/json" - "errors" - "strings" -) - -// 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[strings.ToLower(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[strings.ToLower(tp)] - if !exist { - return errors.New("invalid mode") - } - *l = level - return nil -} - -// UnmarshalText unserialize LogLevel -func (l *LogLevel) UnmarshalText(data []byte) error { - level, exist := LogLevelMapping[strings.ToLower(string(data))] - if !exist { - return errors.New("invalid mode") - } - *l = level - return nil -} - -// MarshalYAML serialize LogLevel with yaml -func (l LogLevel) MarshalYAML() (any, error) { - return l.String(), nil -} - -// MarshalJSON serialize LogLevel with json -func (l LogLevel) MarshalJSON() ([]byte, error) { - return json.Marshal(l.String()) -} - -// MarshalText serialize LogLevel -func (l LogLevel) MarshalText() ([]byte, error) { - return []byte(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 deleted file mode 100644 index f1c68b4270..0000000000 --- a/log/log.go +++ /dev/null @@ -1,104 +0,0 @@ -package log - -import ( - "fmt" - "os" - - "github.com/metacubex/mihomo/common/observable" - - log "github.com/sirupsen/logrus" -) - -var ( - logCh = make(chan Event) - source = observable.NewObservable[Event](logCh) - level = INFO -) - -func init() { - log.SetOutput(os.Stdout) - log.SetLevel(log.DebugLevel) - log.SetFormatter(&log.TextFormatter{ - FullTimestamp: true, - TimestampFormat: "2006-01-02T15:04:05.000000000Z07:00", - EnvironmentOverrideColors: true, - }) -} - -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[Event] { - sub, _ := source.Subscribe() - return sub -} - -func UnSubscribe(sub observable.Subscription[Event]) { - 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) - } -} - -func newLog(logLevel LogLevel, format string, v ...any) Event { - return Event{ - LogLevel: logLevel, - Payload: fmt.Sprintf(format, v...), - } -} diff --git a/log/sing.go b/log/sing.go deleted file mode 100644 index 854b7b874a..0000000000 --- a/log/sing.go +++ /dev/null @@ -1,81 +0,0 @@ -package log - -import ( - "context" - "fmt" - - L "github.com/metacubex/sing/common/logger" -) - -type singLogger struct{} - -func (l singLogger) TraceContext(ctx context.Context, args ...any) { - Debugln(fmt.Sprint(args...)) -} - -func (l singLogger) DebugContext(ctx context.Context, args ...any) { - Debugln(fmt.Sprint(args...)) -} - -func (l singLogger) InfoContext(ctx context.Context, args ...any) { - Infoln(fmt.Sprint(args...)) -} - -func (l singLogger) WarnContext(ctx context.Context, args ...any) { - Warnln(fmt.Sprint(args...)) -} - -func (l singLogger) ErrorContext(ctx context.Context, args ...any) { - Errorln(fmt.Sprint(args...)) -} - -func (l singLogger) FatalContext(ctx context.Context, args ...any) { - Fatalln(fmt.Sprint(args...)) -} - -func (l singLogger) PanicContext(ctx context.Context, args ...any) { - Fatalln(fmt.Sprint(args...)) -} - -func (l singLogger) Trace(args ...any) { - Debugln(fmt.Sprint(args...)) -} - -func (l singLogger) Debug(args ...any) { - Debugln(fmt.Sprint(args...)) -} - -func (l singLogger) Info(args ...any) { - Infoln(fmt.Sprint(args...)) -} - -func (l singLogger) Warn(args ...any) { - Warnln(fmt.Sprint(args...)) -} - -func (l singLogger) Error(args ...any) { - Errorln(fmt.Sprint(args...)) -} - -func (l singLogger) Fatal(args ...any) { - Fatalln(fmt.Sprint(args...)) -} - -func (l singLogger) Panic(args ...any) { - Fatalln(fmt.Sprint(args...)) -} - -type singInfoToDebugLogger struct { - singLogger -} - -func (l singInfoToDebugLogger) InfoContext(ctx context.Context, args ...any) { - Debugln(fmt.Sprint(args...)) -} - -func (l singInfoToDebugLogger) Info(args ...any) { - Debugln(fmt.Sprint(args...)) -} - -var SingLogger L.ContextLogger = singLogger{} -var SingInfoToDebugLogger L.ContextLogger = singInfoToDebugLogger{} diff --git a/main.go b/main.go deleted file mode 100644 index 3bc3d74f73..0000000000 --- a/main.go +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import ( - "context" - "encoding/base64" - "flag" - "fmt" - "io" - "net" - "os" - "os/signal" - "path/filepath" - "runtime" - "strings" - "syscall" - - "github.com/metacubex/mihomo/component/generater" - "github.com/metacubex/mihomo/component/geodata" - "github.com/metacubex/mihomo/component/updater" - "github.com/metacubex/mihomo/config" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" - "github.com/metacubex/mihomo/hub" - "github.com/metacubex/mihomo/hub/executor" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/rules/provider" - - "go.uber.org/automaxprocs/maxprocs" -) - -var ( - version bool - testConfig bool - geodataMode bool - homeDir string - configFile string - configString string - configBytes []byte - externalUI string - externalController string - externalControllerUnix string - externalControllerPipe string - secret string -) - -func init() { - flag.StringVar(&homeDir, "d", os.Getenv("CLASH_HOME_DIR"), "set configuration directory") - flag.StringVar(&configFile, "f", os.Getenv("CLASH_CONFIG_FILE"), "specify configuration file") - flag.StringVar(&configString, "config", os.Getenv("CLASH_CONFIG_STRING"), "specify base64-encoded configuration string") - flag.StringVar(&externalUI, "ext-ui", os.Getenv("CLASH_OVERRIDE_EXTERNAL_UI_DIR"), "override external ui directory") - flag.StringVar(&externalController, "ext-ctl", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER"), "override external controller address") - flag.StringVar(&externalControllerUnix, "ext-ctl-unix", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_UNIX"), "override external controller unix address") - flag.StringVar(&externalControllerPipe, "ext-ctl-pipe", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_PIPE"), "override external controller pipe address") - flag.StringVar(&secret, "secret", os.Getenv("CLASH_OVERRIDE_SECRET"), "override secret for RESTful API") - flag.BoolVar(&geodataMode, "m", false, "set geodata mode") - flag.BoolVar(&version, "v", false, "show current version of mihomo") - flag.BoolVar(&testConfig, "t", false, "test configuration and exit") - flag.Parse() -} - -func main() { - // Defensive programming: panic when code mistakenly calls net.DefaultResolver - net.DefaultResolver.PreferGo = true - net.DefaultResolver.Dial = func(ctx context.Context, network, address string) (net.Conn, error) { - panic("should never be called") - } - - _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) - - if len(os.Args) > 1 && os.Args[1] == "convert-ruleset" { - provider.ConvertMain(os.Args[2:]) - return - } - - if len(os.Args) > 1 && os.Args[1] == "generate" { - generater.Main(os.Args[2:]) - return - } - - if version { - fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", - C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) - if tags := features.Tags(); len(tags) != 0 { - fmt.Printf("Use tags: %s\n", strings.Join(tags, ", ")) - } - - return - } - - if homeDir != "" { - if !filepath.IsAbs(homeDir) { - currentDir, _ := os.Getwd() - homeDir = filepath.Join(currentDir, homeDir) - } - C.SetHomeDir(homeDir) - } - - if geodataMode { - geodata.SetGeodataMode(true) - } - - if configString != "" { - var err error - configBytes, err = base64.StdEncoding.DecodeString(configString) - if err != nil { - log.Fatalln("Initial configuration error: %s", err.Error()) - return - } - } else if configFile == "-" { - var err error - configBytes, err = io.ReadAll(os.Stdin) - if err != nil { - log.Fatalln("Initial configuration error: %s", err.Error()) - return - } - } else { - if configFile != "" { - if !filepath.IsAbs(configFile) { - currentDir, _ := os.Getwd() - configFile = filepath.Join(currentDir, 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 len(configBytes) != 0 { - if _, err := executor.ParseWithBytes(configBytes); err != nil { - log.Errorln(err.Error()) - fmt.Println("configuration test failed") - os.Exit(1) - } - } else { - 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 externalUI != "" { - options = append(options, hub.WithExternalUI(externalUI)) - } - if externalController != "" { - options = append(options, hub.WithExternalController(externalController)) - } - if externalControllerUnix != "" { - options = append(options, hub.WithExternalControllerUnix(externalControllerUnix)) - } - if externalControllerPipe != "" { - options = append(options, hub.WithExternalControllerPipe(externalControllerPipe)) - } - if secret != "" { - options = append(options, hub.WithSecret(secret)) - } - - if err := hub.Parse(configBytes, options...); err != nil { - log.Fatalln("Parse config error: %s", err.Error()) - } - - if updater.GeoAutoUpdate() { - updater.RegisterGeoUpdater() - } - - defer executor.Shutdown() - - termSign := make(chan os.Signal, 1) - hupSign := make(chan os.Signal, 1) - signal.Notify(termSign, syscall.SIGINT, syscall.SIGTERM) - signal.Notify(hupSign, syscall.SIGHUP) - for { - select { - case <-termSign: - return - case <-hupSign: - if err := hub.Parse(configBytes, options...); err != nil { - log.Errorln("Parse config error: %s", err.Error()) - } - } - } -} diff --git a/mihomo/__init__.py b/mihomo/__init__.py new file mode 100644 index 0000000000..bd0c3d18b8 --- /dev/null +++ b/mihomo/__init__.py @@ -0,0 +1,4 @@ +from . import tools +from .client import * +from .errors import * +from .models import * diff --git a/mihomo/client.py b/mihomo/client.py new file mode 100644 index 0000000000..bd144c050e --- /dev/null +++ b/mihomo/client.py @@ -0,0 +1,156 @@ +import typing +from enum import Enum + +import aiohttp + +from . import tools +from .errors import HttpRequestError, InvalidParams, UserNotFound +from .models import StarrailInfoParsed +from .models.v1 import StarrailInfoParsedV1 + + +class Language(Enum): + CHT = "cht" + CHS = "cn" + DE = "de" + EN = "en" + ES = "es" + FR = "fr" + ID = "id" + JP = "jp" + KR = "kr" + PT = "pt" + RU = "ru" + TH = "th" + VI = "vi" + + +class MihomoAPI: + """ + Represents an client for Mihomo API. + + Args: + language (Language, optional): + The language to use for API responses.Defaults to Language.CHT. + + Attributes: + - BASE_URL (str): The base URL of the API. + - ASSET_URL (str): The base URL for the asset files. + + """ + + BASE_URL: typing.Final[str] = "https://api.mihomo.me/sr_info_parsed" + ASSET_URL: typing.Final[ + str + ] = "https://raw.githubusercontent.com/Mar-7th/StarRailRes/master" + + def __init__(self, language: Language = Language.CHT): + self.lang = language + + async def request( + self, + uid: int | str, + language: Language, + *, + params: dict[str, str] = {}, + ) -> typing.Any: + """ + Makes an HTTP request to the API. + + Args: + - uid (int | str): The user ID. + - language (Language): The language to use for the API response. + + Returns: + typing.Any: The response from the API. + + Raises: + HttpRequestError: If the HTTP request fails. + InvalidParams: If the API request contains invalid parameters. + UserNotFound: If the requested user is not found. + + """ + url = self.BASE_URL + "/" + str(uid) + params.update({"lang": language.value}) + + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params) as response: + match response.status: + case 200: + return await response.json(encoding="utf-8") + case 400: + try: + data = await response.json(encoding="utf-8") + except: + raise InvalidParams() + else: + if isinstance(data, dict) and ( + detail := data.get("detail") + ): + raise InvalidParams(detail) + raise InvalidParams() + case 404: + raise UserNotFound() + case _: + raise HttpRequestError(response.status, str(response.reason)) + + async def fetch_user( + self, + uid: int, + *, + replace_icon_name_with_url: bool = False, + ) -> StarrailInfoParsed: + """ + Fetches user data from the API. + + Args: + - uid (`int`): The user ID. + - replace_icon_name_with_url (`bool`): Whether to replace icon names with asset URLs. + + Returns: + StarrailInfoParsed: The parsed user data from mihomo API. + + """ + data = await self.request(uid, self.lang) + if replace_icon_name_with_url is True: + data = tools.replace_icon_name_with_url(data) + data = StarrailInfoParsed.parse_obj(data) + return data + + async def fetch_user_v1( + self, + uid: int, + *, + replace_icon_name_with_url: bool = False, + ) -> StarrailInfoParsedV1: + """ + Fetches user data from the API using version 1 format. + + Args: + - uid (`int`): The user ID. + - replace_icon_name_with_url (`bool`): Whether to replace icon names with asset URLs. + + Returns: + StarrailInfoParsedV1: The parsed user data from the Mihomo API (version 1). + + """ + data = await self.request(uid, self.lang, params={"version": "v1"}) + data = tools.remove_empty_dict(data) + if replace_icon_name_with_url is True: + data = tools.replace_icon_name_with_url(data) + data = StarrailInfoParsedV1.parse_obj(data) + data = tools.replace_trailblazer_name(data) + return data + + def get_icon_url(self, icon: str) -> str: + """ + Gets the asset url for the given icon. + + Args: + icon (str): The icon name. + + Returns: + str: The asset url for the icon. + + """ + return self.ASSET_URL + "/" + icon diff --git a/mihomo/errors.py b/mihomo/errors.py new file mode 100644 index 0000000000..5ecd78cab8 --- /dev/null +++ b/mihomo/errors.py @@ -0,0 +1,42 @@ +class BaseException(Exception): + """Base exception class.""" + + message: str = "" + + def __init__(self, message: str | None = None, *args: object) -> None: + if message is not None: + self.message = message + super().__init__(self.message, *args) + + +class HttpRequestError(BaseException): + """Exception raised when an HTTP request fails.""" + + status: int = 0 + reason: str = "" + + def __init__( + self, + status: int, + reason: str, + message: str | None = None, + *args: object, + ) -> None: + if not message: + message = f"[{status}] {reason}" + self.status = status + self.reason = reason + self.message = message + super().__init__(message, *args) + + +class UserNotFound(BaseException): + """Exception raised when a user is not found.""" + + message = "User not found." + + +class InvalidParams(BaseException): + """Exception raised when invalid parameters are provided.""" + + message: str = "Invalid parameters" diff --git a/mihomo/models/__init__.py b/mihomo/models/__init__.py new file mode 100644 index 0000000000..04eae11215 --- /dev/null +++ b/mihomo/models/__init__.py @@ -0,0 +1,6 @@ +from .base import * +from .character import * +from .combat import * +from .equipment import * +from .player import * +from .stat import * diff --git a/mihomo/models/base.py b/mihomo/models/base.py new file mode 100644 index 0000000000..55efd72a86 --- /dev/null +++ b/mihomo/models/base.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel, Field + +from .character import Character +from .player import Player + + +class StarrailInfoParsed(BaseModel): + """ + Mihomo parsed data + + Attributes: + - player (`Player`): The player's info. + - characters (list[`Character`]): The list of characters. + """ + + player: Player + """Player's basic info""" + characters: list[Character] + """The list of characters""" diff --git a/mihomo/models/character.py b/mihomo/models/character.py new file mode 100644 index 0000000000..0eb724add7 --- /dev/null +++ b/mihomo/models/character.py @@ -0,0 +1,92 @@ +from typing import Any + +from pydantic import BaseModel, Field, root_validator + +from .combat import Element, Path, Trace, TraceTreeNode +from .equipment import LightCone, Relic, RelicSet +from .stat import Attribute, Property + + +class Character(BaseModel): + """ + Represents a character. + + Attributes: + - Basic info: + - id (`str`): The character's ID. + - name (`str`): The character's name. + - rarity (`int`): The character's rarity. + - level (`int`): The character's current level. + - max_level (`int`): The maximum character level according to the current ascension phase. + - ascension (`int`): Ascension phase. + - eidolon (`int`): The character's eidolon rank. + - eidolon_icons (list[`str`]): The list of character's eiodolon icons. + - Image + - icon (`str`): The character avatar image + - preview (`str`): The character's preview image. + - portrait (`str`): The character's portrait image. + - Combat + - path (`Path`): The character's path. + - element (`Element`): The character's element. + - traces (list[`Trace`]): The list of character's skill traces. + - trace_tree (list[`TraceTreeNode]): The list of the character's skill traces. + - Equipment + - light_cone (`LightCone` | `None`): The character's light cone (weapon), or None if not applicable. + - relics (list[`Relic`] | `None`): The list of character's relics, or None if not applicable. + - relic_set (list[`RelicSet`] | `None`): The list of character's relic sets, or None if not applicable. + - stats (list[`Stat`]): The list of character's stats. + - Stats + - attributes (list[`Attribute`]): The list of character's attributes. + - additions (list[`Attribute`]): The list of character's additional attributes. + - properties (list[`Property`]): The list of character's properties. + """ + + id: str + """Character's ID""" + name: str + """Character's name""" + rarity: int + """Character's rarity""" + level: int + """Character's level""" + ascension: int = Field(..., alias="promotion") + """Ascension phase""" + eidolon: int = Field(..., alias="rank") + """Character's eidolon rank""" + eidolon_icons: list[str] = Field([], alias="rank_icons") + """The list of character's eiodolon icons""" + + icon: str + """Character avatar image""" + preview: str + """Character preview image""" + portrait: str + """Character portrait image""" + + path: Path + """Character's path""" + element: Element + """Character's element""" + traces: list[Trace] = Field(..., alias="skills") + """The list of character's skill traces""" + trace_tree: list[TraceTreeNode] = Field([], alias="skill_trees") + """The list of the character's skill traces""" + + light_cone: LightCone | None = None + """Character's light cone (weapon)""" + relics: list[Relic] = [] + """The list of character's relics""" + relic_sets: list[RelicSet] = [] + """The list of character's relic sets""" + + attributes: list[Attribute] + """The list of character's attributes""" + additions: list[Attribute] + """The list of character's additional attributes""" + properties: list[Property] + """The list of character's properties""" + + @property + def max_level(self) -> int: + """The maximum character level according to the current ascension phase""" + return 20 + 10 * self.ascension diff --git a/mihomo/models/combat.py b/mihomo/models/combat.py new file mode 100644 index 0000000000..d2e0f0b345 --- /dev/null +++ b/mihomo/models/combat.py @@ -0,0 +1,112 @@ +from pydantic import BaseModel + + +class Element(BaseModel): + """ + Represents an element. + + Attributes: + - id (`str`): The ID of the element. + - name (`str`): The name of the element. + - color (`str`): The color of the element. + - icon (`str`): The element icon. + """ + + id: str + """The ID of the element""" + name: str + """The name of the element""" + color: str + """The color of the element""" + icon: str + """The element icon""" + + +class Path(BaseModel): + """ + Paths are congregations of Imaginary energy, with which the ideals harmonize. + + Attributes: + - id (`str`): The ID of the path. + - name (`str`): The name of the path. + - icon (`str`): The path icon. + """ + + id: str + """The ID of the path""" + name: str + """The name of the path""" + icon: str + """The path icon""" + + +class Trace(BaseModel): + """ + Represents a character's skill trace. + + Attributes: + - id (`int`): The ID of the trace. + - name (`str`): The name of the trace. + - level (`int`): The current level of the trace. + - max_level (`int`): The maximum level of the trace. + - element (`Element` | None): The element of the trace, or None if not applicable. + - type (`str`): The type of the trace. + - type_text (`str`): The type text of the trace. + - effect (`str`): The effect of the trace. + - effect_text (`str`): The effect text of the trace. + - simple_desc (`str`): The simple description of the trace. + - desc (`str`): The detailed description of the trace. + - icon (`str`): The trace icon. + """ + + id: int + """The ID of the trace""" + name: str + """The name of the trace""" + level: int + """The current level of the trace""" + max_level: int + """The maximum level of the trace""" + element: Element | None = None + """The element of the trace""" + type: str + """The type of the trace""" + type_text: str + """The type text of the trace""" + effect: str + """The effect of the trace""" + effect_text: str + """The effect text of the trace""" + simple_desc: str + """The simple description of the trace""" + desc: str + """The detailed description of the trace""" + icon: str + """The trace icon""" + + +class TraceTreeNode(BaseModel): + """ + Represents a node in the trace skill tree of a character. + + Attributes: + - id (`int`): The ID of the trace. + - level (`int`): The level of the trace. + - max_level (`int`): The max level of the trace. + - icon (`str`): The icon of the trace. + - anchor (`str`): The position of the trace tree node. + - parent (`int` | `None`): The preceding node id of trace. + """ + + id: int + """The ID of the trace""" + level: int + """The level of the trace""" + max_level: int + """The max level of the trace""" + icon: str + """The icon of the trace""" + anchor: str + """The position of the trace tree node""" + parent: int | None = None + """The preceding node id of trace""" diff --git a/mihomo/models/equipment.py b/mihomo/models/equipment.py new file mode 100644 index 0000000000..feca166abe --- /dev/null +++ b/mihomo/models/equipment.py @@ -0,0 +1,118 @@ +from pydantic import BaseModel, Field + +from .combat import Path +from .stat import Attribute, MainAffix, Property, SubAffix + + +class LightCone(BaseModel): + """ + Represents a light cone (weapon). + + Attributes: + - id (`int`): The ID of the light cone. + - name (`str`): The name of the light cone. + - rarity (`int`): The rarity of the light cone. + - superimpose (`int`): The superimpose rank of the light cone. + - level (`int`): The current level of the light cone. + - max_level (`int`): The maximum light cone level according to the current ascension phase. + - ascension (`int`): The ascension phase of the light cone. + - icon (`str`): The light cone icon image. + - preview (`str`): The light cone preview image. + - portrait (`str`): The light cone portrait image. + - path (`Path`): The path of the light cone. + - attributes (list[`Attribute`]): The list of attributes of the light cone. + - properties (list[`Property`]): The list of properties of the light cone. + """ + + id: int + """The ID of the light cone""" + name: str + """The name of the light cone""" + rarity: int + """The rarity of the light cone""" + superimpose: int = Field(..., alias="rank") + """The superimpose rank of the light cone""" + level: int + """The level of the light cone""" + ascension: int = Field(..., alias="promotion") + """The ascension phase of the light cone""" + icon: str + """The light cone icon image""" + preview: str + """The light cone preview image""" + portrait: str + """The light cone portrait image""" + path: Path + """The path of the light cone""" + attributes: list[Attribute] + """The list of attributes of the light cone""" + properties: list[Property] + """The list of properties of the light cone""" + + @property + def max_level(self) -> int: + """The maximum light cone level according to the current ascension phase""" + return 20 + 10 * self.ascension + + +class Relic(BaseModel): + """ + Represents a relic. + + Attributes: + - id (`int`): The ID of the relic. + - name (`str`): The name of the relic. + - set_id (`int`): The ID of the relic set. + - set_name (`str`): The name of the relic set. + - rarity (`int`): The rarity of the relic. + - level (`int`): The level of the relic. + - main_property (`MainAffix`): The main affix of the relic. + - sub_property (list[`SubAffix`]): The list of sub-affixes of the relic. + - icon (`str`): The relic icon. + """ + + id: int + """The ID of the relic""" + name: str + """The name of the relic""" + set_id: int + """The ID of the relic set""" + set_name: str + """The name of the relic set""" + rarity: int + """The rarity of the relic""" + level: int + """The level of the relic""" + main_affix: MainAffix + """The main affix of the relic""" + sub_affixes: list[SubAffix] = Field([], alias="sub_affix") + """The list of sub-affixes of the relic""" + icon: str + """The relic icon""" + + +class RelicSet(BaseModel): + """ + Represents a set of relics. + + Attributes: + - id (`int`): The ID of the relic set. + - name (`str`): The name of the relic set. + - icon (`str`): The icon of the relic set. + - num (`int`): The number of relics in the set. + - desc (`str`): The description of the relic set. + - properties (list[`Property`]): The list of properties of the relic set. + """ + + id: int + """The ID of the relic set""" + name: str + """The name of the relic set""" + icon: str + """The icon of the relic set""" + num: int + """The number of relics in the set""" + desc: str + """The description of the relic set""" + properties: list[Property] + """The list of properties of the relic set""" diff --git a/mihomo/models/player.py b/mihomo/models/player.py new file mode 100644 index 0000000000..cf4022fd58 --- /dev/null +++ b/mihomo/models/player.py @@ -0,0 +1,97 @@ +from pydantic import BaseModel, Field, root_validator + + +class Avatar(BaseModel): + """Profile picture""" + + id: int + name: str + icon: str + + +class ForgottenHall(BaseModel): + """The progress of the Forgotten Hall + + Attributes: + - memory (`int`): The progress of the memory. + - memory_of_chaos_id (`int`): The ID of the memory of chaos, or None if not applicable. + - memory_of_chaos (`int`): The progress of the memory of chaos, or None if not applicable. + """ + + memory: int = Field(..., alias="level") + """The progress of the memory (level)""" + memory_of_chaos_id: int = Field(..., alias="chaos_id") + """The ID of the memory of chaos (chaos_id)""" + memory_of_chaos: int = Field(..., alias="chaos_level") + """The progress of the memory of chaos (chaos_level)""" + + +class Player(BaseModel): + """ + Player basic info + + Attributes: + - uid (`int`): The player's uid. + - name (`str`): The player's nickname. + - level (`int`): The player's Trailblaze level. + - world_level (`int`): The player's Equilibrium level. + - friend_count (`int`): The number of friends. + - avatar (`Avatar`): The player's profile picture. + - signature (`str`): The player's bio. + - is_display (`bool`): Is the player's profile display enabled. + + - forgotten_hall (`ForgottenHall` | None): The progress of the Forgotten Hall, or None if not applicable. + - simulated_universes (`int`): The number of simulated universes passed. + - light_cones (`int`): The number of light cones owned. + - characters (`int`): The number of characters owned. + - achievements (`int`): The number of achievements unlocked. + """ + + uid: int + """Player's uid""" + name: str = Field(..., alias="nickname") + """Player's nickname""" + level: int + """Trailblaze level""" + world_level: int + """Equilibrium level""" + friend_count: int + """Number of friends""" + avatar: Avatar + """Profile picture""" + signature: str + """Bio""" + is_display: bool + """Is the player's profile display enabled.""" + + forgotten_hall: ForgottenHall | None = Field(None, alias="memory_data") + """The progress of the Forgotten Hall (memory_data)""" + simulated_universes: int = Field(0, alias="universe_level") + """Number of simulated universes passed (universe_level)""" + light_cones: int = Field(0, alias="light_cone_count") + """Number of light cones owned""" + characters: int = Field(0, alias="avatar_count") + """Number of characters owned""" + achievements: int = Field(0, alias="achievement_count") + """Number of achievements unlocked""" + + @root_validator(pre=True) + def decompose_space_info(cls, data): + if isinstance(data, dict): + space_info = data.get("space_info") + if isinstance(space_info, dict): + data.update(space_info) + return data + + @root_validator(pre=True) + def transform_for_backward_compatibility(cls, data): + if isinstance(data, dict): + if "pass_area_progress" in data and "universe_level" not in data: + data["universe_level"] = data["pass_area_progress"] + if "challenge_data" in data and "memory_data" not in data: + c: dict[str, int] = data["challenge_data"] + data["memory_data"] = {} + data["memory_data"]["level"] = c.get("pre_maze_group_index") + data["memory_data"]["chaos_id"] = c.get("maze_group_id") + data["memory_data"]["chaos_level"] = c.get("maze_group_index") + return data diff --git a/mihomo/models/stat.py b/mihomo/models/stat.py new file mode 100644 index 0000000000..a458ee495c --- /dev/null +++ b/mihomo/models/stat.py @@ -0,0 +1,97 @@ +from pydantic import BaseModel, Field + + +class Attribute(BaseModel): + """ + Represents an attribute. + + Attributes: + - field (`str`): The field of the attribute. + - name (`str`): The name of the attribute. + - icon (`str`): The attribute icon image. + - value (`float`): The value of the attribute. + - displayed_value (`str`): The displayed value of the attribute. + - is_percent (`bool`): Indicates if the value is in percentage. + """ + + field: str + """The field of the attribute""" + name: str + """The name of the attribute""" + icon: str + """The attribute icon image""" + value: float + """The value of the attribute""" + displayed_value: str = Field(..., alias="display") + """The displayed value of the attribute""" + is_percent: bool = Field(..., alias="percent") + """Indicates if the value is in percentage""" + + +class Property(BaseModel): + """ + Represents a property. + + Attributes: + - type (`str`): The type of the property. + - field (`str`): The field of the property. + - name (`str`): The name of the property. + - icon (`str`): The property icon image. + - value (`float`): The value of the property. + - displayed_value (`str`): The displayed value of the property. + - is_percent (`bool`): Indicates if the value is in percentage. + """ + + type: str + """The type of the property""" + field: str + """The field of the property""" + name: str + """The name of the property""" + icon: str + """The property icon image""" + value: float + """The value of the property""" + displayed_value: str = Field(..., alias="display") + """The displayed value of the property""" + is_percent: bool = Field(..., alias="percent") + """Indicates if the value is in percentage""" + + +class MainAffix(Property): + """ + Represents a relic main affix. + + Attributes: + - type (`str`): The type of the affix. + - field (`str`): The field of the affix. + - name (`str`): The name of the affix. + - icon (`str`): The affix icon image. + - value (`float`): The value of the affix. + - displayed_value (`str`): The displayed value of the affix. + - is_percent (`bool`): Indicates if the value is in percentage. + """ + + ... + + +class SubAffix(MainAffix): + """ + Represents a relic sub-affix. + + Attributes: + - type (`str`): The type of the affix. + - field (`str`): The field of the affix. + - name (`str`): The name of the affix. + - icon (`str`): The affix icon image. + - value (`float`): The value of the affix. + - displayed_value (`str`): The displayed value of the affix. + - is_percent (`bool`): Indicates if the value is in percentage. + - count (`int`): The upgrade times of the affix. + - step (`int`): The additional value of the affix. + """ + + count: int + """The upgrade times of the affix""" + step: int + """The additional value of the affix""" diff --git a/mihomo/models/v1/__init__.py b/mihomo/models/v1/__init__.py new file mode 100644 index 0000000000..8cd02e9978 --- /dev/null +++ b/mihomo/models/v1/__init__.py @@ -0,0 +1,4 @@ +from .base import * +from .character import * +from .equipment import * +from .player import * diff --git a/mihomo/models/v1/base.py b/mihomo/models/v1/base.py new file mode 100644 index 0000000000..cb8a2e8e63 --- /dev/null +++ b/mihomo/models/v1/base.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel, Field + +from .character import Character +from .player import Player, PlayerSpaceInfo + + +class StarrailInfoParsedV1(BaseModel): + """ + Mihomo parsed data V1 + + Attributes: + - player (`Player`): The player's basic info. + - player_details (`PlayerSpaceInfo`): The player's details. + - characters (list[`Character`]): The list of characters. + """ + + player: Player + """Player's basic info""" + player_details: PlayerSpaceInfo = Field(..., alias="PlayerSpaceInfo") + """Player's details""" + characters: list[Character] + """The list of characters""" diff --git a/mihomo/models/v1/character.py b/mihomo/models/v1/character.py new file mode 100644 index 0000000000..c95323254d --- /dev/null +++ b/mihomo/models/v1/character.py @@ -0,0 +1,150 @@ +from typing import Any + +from pydantic import BaseModel, Field, root_validator + +from .equipment import LightCone, Relic, RelicSet + + +class EidolonIcon(BaseModel): + """ + Represents an Eidolon icon. + + Attributes: + - icon (`str`): The eidolon icon. + - unlock (`bool`): Indicates if the eidolon is unlocked. + """ + + icon: str + """The eidolon icon""" + unlock: bool + """Indicates if the eidolon is unlocked""" + + +class Trace(BaseModel): + """ + Represents a character's skill trace. + + Attributes: + - name (`str`): The name of the trace. + - level (`int`): The level of the trace. + - type (`str`): The type of the trace. + - icon (`str`): The trace icon. + """ + + name: str + """The name of the trace""" + level: int + """The level of the trace""" + type: str + """The type of the trace""" + icon: str + """The trace icon""" + + +class Stat(BaseModel): + """ + Represents a character's stat. + + Attributes: + - name (`str`): The name of the stat. + - base (`str`): The base value of the stat. + - addition (`str` | `None`): The additional value of the stat, or None if not applicable. + - icon (`str`): The stat icon. + """ + + name: str + """The name of the stat""" + base: str + """The base value of the stat""" + addition: str | None = None + """The additional value of the stat""" + icon: str + """The stat icon""" + + +class Character(BaseModel): + """ + Represents a character. + + Attributes: + - Basic info: + - id (`str`): The character's ID. + - name (`str`): The character's name. + - rarity (`int`): The character's rarity. + - level (`int`): The character's level. + - Eidolon + - eidolon (`int`): The character's eidolon rank. + - eidolon_icons (list[`EidolonIcon`]): The list of eidolon icons. + - Image + - icon (`str`): The character avatar image + - preview (`str`): The character's preview image. + - portrait (`str`): The character's portrait image. + - Combat type + - path (`str`): The character's path. + - path_icon (`str`): The character's path icon. + - element (`str`): The character's element. + - element_icon (`str`): The character's element icon. + - color (`str`): The character's element color. + - Equipment + - traces (list[`Trace`]): The list of character's skill traces. + - light_cone (`LightCone` | `None`): The character's light cone (weapon), or None if not applicable. + - relics (list[`Relic`] | `None`): The list of character's relics, or None if not applicable. + - relic_set (list[`RelicSet`] | `None`): The list of character's relic sets, or None if not applicable. + - stats (list[`Stat`]): The list of character's stats. + """ + + id: str + """Character's ID""" + name: str + """Character's name""" + rarity: int + """Character's rarity""" + level: int + """Character's level""" + + eidolon: int = Field(..., alias="rank") + """Character's eidolon rank""" + eidolon_icons: list[EidolonIcon] = Field(..., alias="rank_icons") + """The list of eidolon icons""" + + preview: str + """Character preview image""" + portrait: str + """Character portrait image""" + + path: str + """Character's path""" + path_icon: str + """Character's path icon""" + + element: str + """Character's element""" + element_icon: str + """Character's element icon""" + + color: str + """Character's element color""" + + traces: list[Trace] = Field(..., alias="skill") + """The list of character's skill traces""" + light_cone: LightCone | None = None + """Character's light cone (weapon)""" + relics: list[Relic] | None = Field(None, alias="relic") + """The list of character's relics""" + relic_set: list[RelicSet] | None = None + """The list of character's relic sets""" + stats: list[Stat] = Field(..., alias="property") + """The list of character's stats""" + + @root_validator(pre=True) + def dict_to_list(cls, data: dict[str, Any]): + # The keys of the original dict is not necessary, so remove them here. + if isinstance(data, dict) and data.get("relic") is not None: + if isinstance(data["relic"], dict): + data["relic"] = list(data["relic"].values()) + return data + + @property + def icon(self) -> str: + """Character avatar image""" + return f"icon/character/{self.id}.png" diff --git a/mihomo/models/v1/equipment.py b/mihomo/models/v1/equipment.py new file mode 100644 index 0000000000..f2da5099f4 --- /dev/null +++ b/mihomo/models/v1/equipment.py @@ -0,0 +1,71 @@ +from pydantic import BaseModel, Field + + +class LightCone(BaseModel): + """ + Represents a light cone (weapon). + + Attributes: + - name (`str`): The name of the light cone. + - rarity (`int`): The rarity of the light cone. + - superimpose (`int`): The superimpose rank of the light cone. + - level (`int`): The level of the light cone. + - icon (`str`): The light cone icon. + """ + + name: str + rarity: int + superimpose: int = Field(..., alias="rank") + level: int + icon: str + + +class RelicProperty(BaseModel): + """ + Represents a property of a relic. + + Attributes: + - name (`str`): The name of the relic property. + - value (`str`): The value of the relic property. + - icon (`str`): The property icon. + """ + + name: str + value: str + icon: str + + +class Relic(BaseModel): + """ + Represents a relic. + + Attributes: + - name (`str`): The name of the relic. + - rarity (`int`): The rarity of the relic. + - level (`int`): The level of the relic. + - main_property (`RelicProperty`): The main property of the relic. + - sub_property (list[`RelicProperty`]): The list of sub properties of the relic. + - icon (`str`): The relic icon. + """ + + name: str + rarity: int + level: int + main_property: RelicProperty + sub_property: list[RelicProperty] + icon: str + + +class RelicSet(BaseModel): + """ + Represents a set of relics. + + Attributes: + - name (`str`): The name of the relic set. + - icon (`str`): The relic set icon. + - desc (`int`): The description of the relic set. + """ + + name: str + icon: str + desc: int diff --git a/mihomo/models/v1/player.py b/mihomo/models/v1/player.py new file mode 100644 index 0000000000..1a140969d8 --- /dev/null +++ b/mihomo/models/v1/player.py @@ -0,0 +1,65 @@ +from pydantic import BaseModel, Field + + +class Player(BaseModel): + """ + Player basic info + + Attributes: + - uid (`str`): The player's uid. + - name (`str`): The player's nickname. + - level (`int`): The player's Trailblaze level. + - icon (`str`): The player's profile picture. + - signature (`str`): The player's bio. + """ + + uid: str + """Player's uid""" + name: str + """Player's nickname""" + level: int + """Trailblaze level""" + icon: str + """Profile picture""" + signature: str + """Bio""" + + +class ForgottenHall(BaseModel): + """The progress of the Forgotten Hall + + Attributes: + - memory (`int`): The progress of the memory. + - memory_of_chaos_id (`int` | `None`): The ID of the memory of chaos, or None if not applicable. + - memory_of_chaos (`int` | `None`): The progress of the memory of chaos, or None if not applicable. + """ + + memory: int | None = Field(None, alias="PreMazeGroupIndex") + """The progress of the memory""" + memory_of_chaos_id: int | None = Field(None, alias="MazeGroupIndex") + """The ID of the memory of chaos""" + memory_of_chaos: int | None = Field(None, alias="MazeGroupID") + """The progress of the memory of chaos""" + + +class PlayerSpaceInfo(BaseModel): + """Player details + + Attributes: + - forgotten_hall (`ForgottenHall` | None): The progress of the Forgotten Hall, or None if not applicable. + - simulated_universes (`int`): The number of simulated universes passed. + - light_cones (`int`): The number of light cones owned. + - characters (`int`): The number of characters owned. + - achievements (`int`): The number of achievements unlocked. + """ + + forgotten_hall: ForgottenHall | None = Field(None, alias="ChallengeData") + """The progress of the Forgotten Hall""" + simulated_universes: int = Field(0, alias="PassAreaProgress") + """Number of simulated universes passed""" + light_cones: int = Field(0, alias="LightConeCount") + """Number of light cones owned""" + characters: int = Field(0, alias="AvatarCount") + """Number of characters owned""" + achievements: int = Field(0, alias="AchievementCount") + """Number of achievements unlocked""" diff --git a/mihomo/tools.py b/mihomo/tools.py new file mode 100644 index 0000000000..1829496186 --- /dev/null +++ b/mihomo/tools.py @@ -0,0 +1,107 @@ +from typing import Final, TypeVar + +from .models import Character, StarrailInfoParsed +from .models.v1 import Character, StarrailInfoParsedV1 + +RawData = TypeVar("RawData") +ParsedData = TypeVar("ParsedData", StarrailInfoParsed, StarrailInfoParsedV1) + +ASSET_URL: Final[str] = "https://raw.githubusercontent.com/Mar-7th/StarRailRes/master" + + +def remove_empty_dict(data: RawData) -> RawData: + """ + Recursively removes empty dictionaries from the given raw data. + + Args: + - data (`RawData`): The input raw data. + + Returns: + - `RawData`: The data with empty dictionaries removed. + """ + if isinstance(data, dict): + for key in data.keys(): + data[key] = None if (data[key] == {}) else remove_empty_dict(data[key]) + elif isinstance(data, list): + for i in range(len(data)): + data[i] = remove_empty_dict(data[i]) + return data + + +def replace_icon_name_with_url(data: RawData) -> RawData: + """ + Replaces icon file names with asset URLs in the given raw data. + + Example: Replace "/icon/avatar/1201.png" with + "https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/avatar/1201.png" + + Args: + - data (`RawData`): The input raw data. + + Returns: + - `RawData`: The data with icon file names replaced by asset URLs. + """ + if isinstance(data, dict): + for key in data.keys(): + data[key] = replace_icon_name_with_url(data[key]) + elif isinstance(data, list): + for i in range(len(data)): + data[i] = replace_icon_name_with_url(data[i]) + elif isinstance(data, str): + if ".png" in data: + data = ASSET_URL + "/" + data + return data + + +def replace_trailblazer_name(data: StarrailInfoParsedV1) -> StarrailInfoParsedV1: + """ + Replaces the trailblazer name with the player's name. + + Args: + - data (`StarrailInfoParsed`): The input StarrailInfoParsed data. + + Returns: + - `StarrailInfoParsed`: The updated StarrailInfoParsed data. + """ + for i in range(len(data.characters)): + if data.characters[i].name == r"{NICKNAME}": + data.characters[i].name = data.player.name + return data + + +def remove_duplicate_character(data: ParsedData) -> ParsedData: + """ + Removes duplicate characters from the given StarrailInfoParsed data. + + Args: + - data (`ParsedData`): The input StarrailInfoParsed data. + + Returns: + - `ParsedData`: The updated StarrailInfoParsed data without duplicate characters. + """ + new_characters = [] + characters_ids: set[str] = set() + for character in data.characters: + if character.id not in characters_ids: + new_characters.append(character) + characters_ids.add(character.id) + data.characters = new_characters + return data + + +def merge_character_data(new_data: ParsedData, old_data: ParsedData) -> ParsedData: + """ + Append the old data characters to the list of new data characters. + The player's info from the old data will be omitted/discarded. + + Args: + - new_data (`ParsedData`): The new data to be merged. + - old_data (`ParsedData`): The old data to merge into. + + Returns: + - `ParsedData`: The merged new data. + """ + for character in old_data.characters: + new_data.characters.append(character) + new_data = remove_duplicate_character(new_data) + return new_data diff --git a/ntp/service.go b/ntp/service.go deleted file mode 100644 index 6fb7453e29..0000000000 --- a/ntp/service.go +++ /dev/null @@ -1,128 +0,0 @@ -package ntp - -import ( - "context" - "sync" - "time" - - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/proxydialer" - "github.com/metacubex/mihomo/log" - - M "github.com/metacubex/sing/common/metadata" - "github.com/metacubex/sing/common/ntp" -) - -var offset time.Duration -var service *Service - -type Service struct { - server M.Socksaddr - dialer proxydialer.SingDialer - ticker *time.Ticker - ctx context.Context - cancel context.CancelFunc - mu sync.Mutex - syncSystemTime bool - running bool -} - -func ReCreateNTPService(server string, interval time.Duration, dialerProxy string, syncSystemTime bool) { - if service != nil { - service.Stop() - } - ctx, cancel := context.WithCancel(context.Background()) - service = &Service{ - server: M.ParseSocksaddr(server), - dialer: proxydialer.NewByNameSingDialer(dialerProxy, dialer.NewDialer()), - ticker: time.NewTicker(interval * time.Minute), - ctx: ctx, - cancel: cancel, - syncSystemTime: syncSystemTime, - } - service.Start() -} - -func (srv *Service) Start() { - srv.mu.Lock() - defer srv.mu.Unlock() - log.Infoln("NTP service start, sync system time is %t", srv.syncSystemTime) - err := srv.update() - if err != nil { - log.Errorln("Initialize NTP time failed: %s", err) - return - } - service.running = true - go srv.loopUpdate() -} - -func (srv *Service) Stop() { - srv.mu.Lock() - defer srv.mu.Unlock() - if service.running { - srv.ticker.Stop() - srv.cancel() - service.running = false - } -} - -func (srv *Service) Running() bool { - if srv == nil { - return false - } - srv.mu.Lock() - defer srv.mu.Unlock() - return srv.running -} - -func (srv *Service) update() error { - var response *ntp.Response - var err error - for i := 0; i < 3; i++ { - if response, err = ntp.Exchange(context.Background(), srv.dialer, srv.server); err == nil { - break - } - if i == 2 { - return err - } - } - offset = response.ClockOffset - if offset > time.Duration(0) { - log.Infoln("System clock is ahead of NTP time by %s", offset) - } else if offset < time.Duration(0) { - log.Infoln("System clock is behind NTP time by %s", -offset) - } - if srv.syncSystemTime { - timeNow := response.Time - syncErr := setSystemTime(timeNow) - if syncErr == nil { - log.Infoln("Sync system time success: %s", timeNow.Local().Format(ntp.TimeLayout)) - } else { - log.Errorln("Write time to system: %s", syncErr) - srv.syncSystemTime = false - } - } - return nil -} - -func (srv *Service) loopUpdate() { - for { - select { - case <-srv.ctx.Done(): - return - case <-srv.ticker.C: - } - err := srv.update() - if err != nil { - log.Warnln("Sync time failed: %s", err) - } - } -} - -func Now() time.Time { - now := time.Now() - if service.Running() && offset.Abs() > 0 { - now = now.Add(offset) - } - return now -} diff --git a/ntp/time_stub.go b/ntp/time_stub.go deleted file mode 100644 index 120509836c..0000000000 --- a/ntp/time_stub.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !(windows || linux || darwin) - -package ntp - -import ( - "os" - "time" -) - -func setSystemTime(nowTime time.Time) error { - return os.ErrInvalid -} diff --git a/ntp/time_unix.go b/ntp/time_unix.go deleted file mode 100644 index 9e8194734f..0000000000 --- a/ntp/time_unix.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build linux || darwin - -package ntp - -import ( - "time" - - "golang.org/x/sys/unix" -) - -func setSystemTime(nowTime time.Time) error { - timeVal := unix.NsecToTimeval(nowTime.UnixNano()) - return unix.Settimeofday(&timeVal) -} diff --git a/ntp/time_windows.go b/ntp/time_windows.go deleted file mode 100644 index 8ef29b1b75..0000000000 --- a/ntp/time_windows.go +++ /dev/null @@ -1,32 +0,0 @@ -package ntp - -import ( - "time" - "unsafe" - - "golang.org/x/sys/windows" -) - -func setSystemTime(nowTime time.Time) error { - var systemTime windows.Systemtime - systemTime.Year = uint16(nowTime.Year()) - systemTime.Month = uint16(nowTime.Month()) - systemTime.Day = uint16(nowTime.Day()) - systemTime.Hour = uint16(nowTime.Hour()) - systemTime.Minute = uint16(nowTime.Minute()) - systemTime.Second = uint16(nowTime.Second()) - systemTime.Milliseconds = uint16(nowTime.UnixMilli() - nowTime.Unix()*1000) - - dllKernel32 := windows.NewLazySystemDLL("kernel32.dll") - proc := dllKernel32.NewProc("SetSystemTime") - - _, _, err := proc.Call( - uintptr(unsafe.Pointer(&systemTime)), - ) - - if err != nil && err.Error() != "The operation completed successfully." { - return err - } - - return nil -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..aa566eea15 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "mihomo" +version = "1.1.7" +authors = [ + { name="KT", email="xns77477@gmail.com" }, +] +description = "A simple Python Pydantic model for Honkai: Star Rail parsed data from the Mihomo API." +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +dependencies = [ + "aiohttp==3.*", + "pydantic==1.*", +] \ No newline at end of file diff --git a/rules/common/base.go b/rules/common/base.go deleted file mode 100644 index 1abbe72cf3..0000000000 --- a/rules/common/base.go +++ /dev/null @@ -1,44 +0,0 @@ -package common - -import ( - "errors" - - C "github.com/metacubex/mihomo/constant" - - "golang.org/x/exp/slices" -) - -var ( - errPayload = errors.New("payloadRule error") -) - -// params -var ( - NoResolve = "no-resolve" - Src = "src" -) - -type Base struct { -} - -func (b *Base) ShouldFindProcess() bool { - return false -} - -func (b *Base) ShouldResolveIP() bool { - return false -} - -func (b *Base) ProviderNames() []string { return nil } - -func ParseParams(params []string) (isSrc bool, noResolve bool) { - isSrc = slices.Contains(params, Src) - if isSrc { - noResolve = true - } else { - noResolve = slices.Contains(params, NoResolve) - } - return -} - -type ParseRuleFunc func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (C.Rule, error) diff --git a/rules/common/domain.go b/rules/common/domain.go deleted file mode 100644 index 306eb65fd8..0000000000 --- a/rules/common/domain.go +++ /dev/null @@ -1,41 +0,0 @@ -package common - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - "golang.org/x/net/idna" -) - -type Domain struct { - *Base - domain string - adapter string -} - -func (d *Domain) RuleType() C.RuleType { - return C.Domain -} - -func (d *Domain) Match(metadata *C.Metadata) (bool, string) { - return metadata.RuleHost() == d.domain, d.adapter -} - -func (d *Domain) Adapter() string { - return d.adapter -} - -func (d *Domain) Payload() string { - return d.domain -} - -func NewDomain(domain string, adapter string) *Domain { - punycode, _ := idna.ToASCII(strings.ToLower(domain)) - return &Domain{ - Base: &Base{}, - domain: punycode, - adapter: adapter, - } -} - -//var _ C.Rule = (*Domain)(nil) diff --git a/rules/common/domain_keyword.go b/rules/common/domain_keyword.go deleted file mode 100644 index 9d6f1c1559..0000000000 --- a/rules/common/domain_keyword.go +++ /dev/null @@ -1,42 +0,0 @@ -package common - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - "golang.org/x/net/idna" -) - -type DomainKeyword struct { - *Base - keyword string - adapter string -} - -func (dk *DomainKeyword) RuleType() C.RuleType { - return C.DomainKeyword -} - -func (dk *DomainKeyword) Match(metadata *C.Metadata) (bool, string) { - domain := metadata.RuleHost() - return strings.Contains(domain, dk.keyword), dk.adapter -} - -func (dk *DomainKeyword) Adapter() string { - return dk.adapter -} - -func (dk *DomainKeyword) Payload() string { - return dk.keyword -} - -func NewDomainKeyword(keyword string, adapter string) *DomainKeyword { - punycode, _ := idna.ToASCII(strings.ToLower(keyword)) - return &DomainKeyword{ - Base: &Base{}, - keyword: punycode, - adapter: adapter, - } -} - -//var _ C.Rule = (*DomainKeyword)(nil) diff --git a/rules/common/domain_regex.go b/rules/common/domain_regex.go deleted file mode 100644 index d214a772c1..0000000000 --- a/rules/common/domain_regex.go +++ /dev/null @@ -1,45 +0,0 @@ -package common - -import ( - C "github.com/metacubex/mihomo/constant" - - "github.com/dlclark/regexp2" -) - -type DomainRegex struct { - *Base - regex *regexp2.Regexp - adapter string -} - -func (dr *DomainRegex) RuleType() C.RuleType { - return C.DomainRegex -} - -func (dr *DomainRegex) Match(metadata *C.Metadata) (bool, string) { - domain := metadata.RuleHost() - match, _ := dr.regex.MatchString(domain) - return match, dr.adapter -} - -func (dr *DomainRegex) Adapter() string { - return dr.adapter -} - -func (dr *DomainRegex) Payload() string { - return dr.regex.String() -} - -func NewDomainRegex(regex string, adapter string) (*DomainRegex, error) { - r, err := regexp2.Compile(regex, regexp2.IgnoreCase) - if err != nil { - return nil, err - } - return &DomainRegex{ - Base: &Base{}, - regex: r, - adapter: adapter, - }, nil -} - -//var _ C.Rule = (*DomainRegex)(nil) diff --git a/rules/common/domain_suffix.go b/rules/common/domain_suffix.go deleted file mode 100644 index c5b87208eb..0000000000 --- a/rules/common/domain_suffix.go +++ /dev/null @@ -1,42 +0,0 @@ -package common - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - "golang.org/x/net/idna" -) - -type DomainSuffix struct { - *Base - suffix string - adapter string -} - -func (ds *DomainSuffix) RuleType() C.RuleType { - return C.DomainSuffix -} - -func (ds *DomainSuffix) Match(metadata *C.Metadata) (bool, string) { - domain := metadata.RuleHost() - return strings.HasSuffix(domain, "."+ds.suffix) || domain == ds.suffix, ds.adapter -} - -func (ds *DomainSuffix) Adapter() string { - return ds.adapter -} - -func (ds *DomainSuffix) Payload() string { - return ds.suffix -} - -func NewDomainSuffix(suffix string, adapter string) *DomainSuffix { - punycode, _ := idna.ToASCII(strings.ToLower(suffix)) - return &DomainSuffix{ - Base: &Base{}, - suffix: punycode, - adapter: adapter, - } -} - -//var _ C.Rule = (*DomainSuffix)(nil) diff --git a/rules/common/dscp.go b/rules/common/dscp.go deleted file mode 100644 index c839b20dfd..0000000000 --- a/rules/common/dscp.go +++ /dev/null @@ -1,49 +0,0 @@ -package common - -import ( - "fmt" - - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" -) - -type DSCP struct { - *Base - ranges utils.IntRanges[uint8] - payload string - adapter string -} - -func (d *DSCP) RuleType() C.RuleType { - return C.DSCP -} - -func (d *DSCP) Match(metadata *C.Metadata) (bool, string) { - return d.ranges.Check(metadata.DSCP), d.adapter -} - -func (d *DSCP) Adapter() string { - return d.adapter -} - -func (d *DSCP) Payload() string { - return d.payload -} - -func NewDSCP(dscp string, adapter string) (*DSCP, error) { - ranges, err := utils.NewUnsignedRanges[uint8](dscp) - if err != nil { - return nil, fmt.Errorf("parse DSCP rule fail: %w", err) - } - for _, r := range ranges { - if r.End() > 63 { - return nil, fmt.Errorf("DSCP couldn't be negative or exceed 63") - } - } - return &DSCP{ - Base: &Base{}, - payload: dscp, - ranges: ranges, - adapter: adapter, - }, nil -} diff --git a/rules/common/final.go b/rules/common/final.go deleted file mode 100644 index d3a415a06d..0000000000 --- a/rules/common/final.go +++ /dev/null @@ -1,35 +0,0 @@ -package common - -import ( - C "github.com/metacubex/mihomo/constant" -) - -type Match struct { - *Base - adapter string -} - -func (f *Match) RuleType() C.RuleType { - return C.MATCH -} - -func (f *Match) Match(metadata *C.Metadata) (bool, string) { - return true, f.adapter -} - -func (f *Match) Adapter() string { - return f.adapter -} - -func (f *Match) Payload() string { - return "" -} - -func NewMatch(adapter string) *Match { - return &Match{ - Base: &Base{}, - adapter: adapter, - } -} - -//var _ C.Rule = (*Match)(nil) diff --git a/rules/common/geoip.go b/rules/common/geoip.go deleted file mode 100644 index 61fae50443..0000000000 --- a/rules/common/geoip.go +++ /dev/null @@ -1,224 +0,0 @@ -package common - -import ( - "errors" - "fmt" - "net/netip" - "strings" - - "github.com/metacubex/mihomo/component/geodata" - "github.com/metacubex/mihomo/component/geodata/router" - "github.com/metacubex/mihomo/component/mmdb" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - "golang.org/x/exp/slices" -) - -type GEOIP struct { - *Base - country string - adapter string - noResolveIP bool - isSourceIP bool -} - -var _ C.Rule = (*GEOIP)(nil) - -func (g *GEOIP) RuleType() C.RuleType { - if g.isSourceIP { - return C.SrcGEOIP - } - return C.GEOIP -} - -func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) { - ip := metadata.DstIP - if g.isSourceIP { - ip = metadata.SrcIP - } - if !ip.IsValid() { - return false, "" - } - - if g.country == "lan" { - return g.isLan(ip), g.adapter - } - - if geodata.GeodataMode() { - if g.isSourceIP { - if slices.Contains(metadata.SrcGeoIP, g.country) { - return true, g.adapter - } - } else { - if slices.Contains(metadata.DstGeoIP, g.country) { - return true, g.adapter - } - } - matcher, err := g.getIPMatcher() - if err != nil { - return false, "" - } - match := matcher.Match(ip) - if match { - if g.isSourceIP { - metadata.SrcGeoIP = append(metadata.SrcGeoIP, g.country) - } else { - metadata.DstGeoIP = append(metadata.DstGeoIP, g.country) - } - } - return match, g.adapter - } - - if g.isSourceIP { - if metadata.SrcGeoIP != nil { - return slices.Contains(metadata.SrcGeoIP, g.country), g.adapter - } - } else { - if metadata.DstGeoIP != nil { - return slices.Contains(metadata.DstGeoIP, g.country), g.adapter - } - } - codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) - if g.isSourceIP { - metadata.SrcGeoIP = codes - } else { - metadata.DstGeoIP = codes - } - if slices.Contains(codes, g.country) { - return true, g.adapter - } - return false, "" -} - -// MatchIp implements C.IpMatcher -func (g *GEOIP) MatchIp(ip netip.Addr) bool { - if !ip.IsValid() { - return false - } - - if g.country == "lan" { - return g.isLan(ip) - } - - if geodata.GeodataMode() { - matcher, err := g.getIPMatcher() - if err != nil { - return false - } - return matcher.Match(ip) - } - - codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) - return slices.Contains(codes, g.country) -} - -// MatchIp implements C.IpMatcher -func (g dnsFallbackFilter) MatchIp(ip netip.Addr) bool { - if !ip.IsValid() { - return false - } - - if g.isLan(ip) { // compatible with original behavior - return false - } - - if geodata.GeodataMode() { - matcher, err := g.getIPMatcher() - if err != nil { - return false - } - return !matcher.Match(ip) - } - - codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) - return !slices.Contains(codes, g.country) -} - -type dnsFallbackFilter struct { - *GEOIP -} - -func (g *GEOIP) DnsFallbackFilter() C.IpMatcher { // for dns.fallback-filter.geoip - return dnsFallbackFilter{GEOIP: g} -} - -func (g *GEOIP) isLan(ip netip.Addr) bool { - return ip.IsPrivate() || - ip.IsUnspecified() || - ip.IsLoopback() || - ip.IsMulticast() || - ip.IsLinkLocalUnicast() || - resolver.IsFakeBroadcastIP(ip) -} - -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) GetCountry() string { - return g.country -} - -func (g *GEOIP) GetIPMatcher() (router.IPMatcher, error) { - if geodata.GeodataMode() { - return g.getIPMatcher() - } - return nil, errors.New("not geodata mode") -} - -func (g *GEOIP) getIPMatcher() (router.IPMatcher, error) { - geoIPMatcher, err := geodata.LoadGeoIPMatcher(g.country) - if err != nil { - return nil, fmt.Errorf("[GeoIP] %w", err) - } - return geoIPMatcher, nil - -} - -func (g *GEOIP) GetRecodeSize() int { - if matcher, err := g.GetIPMatcher(); err == nil { - return matcher.Count() - } - return 0 -} - -func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP, error) { - country = strings.ToLower(country) - - geoip := &GEOIP{ - Base: &Base{}, - country: country, - adapter: adapter, - noResolveIP: noResolveIP, - isSourceIP: isSrc, - } - - if country == "lan" { - return geoip, nil - } - - if err := geodata.InitGeoIP(); err != nil { - log.Errorln("can't initial GeoIP: %s", err) - return nil, err - } - - if geodata.GeodataMode() { - geoIPMatcher, err := geoip.getIPMatcher() // test load - if err != nil { - return nil, err - } - log.Infoln("Finished initial GeoIP rule %s => %s, records: %d", country, adapter, geoIPMatcher.Count()) - } - - return geoip, nil -} diff --git a/rules/common/geosite.go b/rules/common/geosite.go deleted file mode 100644 index 851bc8a43d..0000000000 --- a/rules/common/geosite.go +++ /dev/null @@ -1,86 +0,0 @@ -package common - -import ( - "fmt" - - "github.com/metacubex/mihomo/component/geodata" - _ "github.com/metacubex/mihomo/component/geodata/memconservative" - "github.com/metacubex/mihomo/component/geodata/router" - _ "github.com/metacubex/mihomo/component/geodata/standard" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type GEOSITE struct { - *Base - country string - adapter string - recodeSize int -} - -func (gs *GEOSITE) RuleType() C.RuleType { - return C.GEOSITE -} - -func (gs *GEOSITE) Match(metadata *C.Metadata) (bool, string) { - return gs.MatchDomain(metadata.RuleHost()), gs.adapter -} - -// MatchDomain implements C.DomainMatcher -func (gs *GEOSITE) MatchDomain(domain string) bool { - if len(domain) == 0 { - return false - } - matcher, err := gs.GetDomainMatcher() - if err != nil { - return false - } - return matcher.ApplyDomain(domain) -} - -func (gs *GEOSITE) Adapter() string { - return gs.adapter -} - -func (gs *GEOSITE) Payload() string { - return gs.country -} - -func (gs *GEOSITE) GetDomainMatcher() (router.DomainMatcher, error) { - matcher, err := geodata.LoadGeoSiteMatcher(gs.country) - if err != nil { - return nil, fmt.Errorf("load GeoSite data error, %w", err) - } - return matcher, nil -} - -func (gs *GEOSITE) GetRecodeSize() int { - if matcher, err := gs.GetDomainMatcher(); err == nil { - return matcher.Count() - } - return 0 -} - -func NewGEOSITE(country string, adapter string) (*GEOSITE, error) { - if err := geodata.InitGeoSite(); err != nil { - log.Errorln("can't initial GeoSite: %s", err) - return nil, err - } - - geoSite := &GEOSITE{ - Base: &Base{}, - country: country, - adapter: adapter, - } - - matcher, err := geoSite.GetDomainMatcher() // test load - if err != nil { - return nil, err - } - - log.Infoln("Finished initial GeoSite rule %s => %s, records: %d", country, adapter, matcher.Count()) - - return geoSite, nil -} - -var _ C.Rule = (*GEOSITE)(nil) diff --git a/rules/common/in_name.go b/rules/common/in_name.go deleted file mode 100644 index 9b14ef6a61..0000000000 --- a/rules/common/in_name.go +++ /dev/null @@ -1,49 +0,0 @@ -package common - -import ( - "fmt" - C "github.com/metacubex/mihomo/constant" - "strings" -) - -type InName struct { - *Base - names []string - adapter string - payload string -} - -func (u *InName) Match(metadata *C.Metadata) (bool, string) { - for _, name := range u.names { - if metadata.InName == name { - return true, u.adapter - } - } - return false, "" -} - -func (u *InName) RuleType() C.RuleType { - return C.InName -} - -func (u *InName) Adapter() string { - return u.adapter -} - -func (u *InName) Payload() string { - return u.payload -} - -func NewInName(iNames, adapter string) (*InName, error) { - names := strings.Split(iNames, "/") - if len(names) == 0 { - return nil, fmt.Errorf("in name couldn't be empty") - } - - return &InName{ - Base: &Base{}, - names: names, - adapter: adapter, - payload: iNames, - }, nil -} diff --git a/rules/common/in_type.go b/rules/common/in_type.go deleted file mode 100644 index fc73b2081f..0000000000 --- a/rules/common/in_type.go +++ /dev/null @@ -1,74 +0,0 @@ -package common - -import ( - "fmt" - C "github.com/metacubex/mihomo/constant" - "strings" -) - -type InType struct { - *Base - types []C.Type - adapter string - payload string -} - -func (u *InType) Match(metadata *C.Metadata) (bool, string) { - for _, tp := range u.types { - if metadata.Type == tp { - return true, u.adapter - } - } - return false, "" -} - -func (u *InType) RuleType() C.RuleType { - return C.InType -} - -func (u *InType) Adapter() string { - return u.adapter -} - -func (u *InType) Payload() string { - return u.payload -} - -func NewInType(iTypes, adapter string) (*InType, error) { - types := strings.Split(iTypes, "/") - if len(types) == 0 { - return nil, fmt.Errorf("in type couldn't be empty") - } - - tps, err := parseInTypes(types) - if err != nil { - return nil, err - } - - return &InType{ - Base: &Base{}, - types: tps, - adapter: adapter, - payload: strings.ToUpper(iTypes), - }, nil -} - -func parseInTypes(tps []string) (res []C.Type, err error) { - for _, tp := range tps { - utp := strings.ToUpper(tp) - var r *C.Type - if utp == "SOCKS" { - r, _ = C.ParseType("SOCKS4") - res = append(res, *r) - r, _ = C.ParseType("SOCKS5") - res = append(res, *r) - } else { - r, err = C.ParseType(utp) - if err != nil { - return - } - res = append(res, *r) - } - } - return -} diff --git a/rules/common/in_user.go b/rules/common/in_user.go deleted file mode 100644 index ebe881af81..0000000000 --- a/rules/common/in_user.go +++ /dev/null @@ -1,49 +0,0 @@ -package common - -import ( - "fmt" - C "github.com/metacubex/mihomo/constant" - "strings" -) - -type InUser struct { - *Base - users []string - adapter string - payload string -} - -func (u *InUser) Match(metadata *C.Metadata) (bool, string) { - for _, user := range u.users { - if metadata.InUser == user { - return true, u.adapter - } - } - return false, "" -} - -func (u *InUser) RuleType() C.RuleType { - return C.InUser -} - -func (u *InUser) Adapter() string { - return u.adapter -} - -func (u *InUser) Payload() string { - return u.payload -} - -func NewInUser(iUsers, adapter string) (*InUser, error) { - users := strings.Split(iUsers, "/") - if len(users) == 0 { - return nil, fmt.Errorf("in user couldn't be empty") - } - - return &InUser{ - Base: &Base{}, - users: users, - adapter: adapter, - payload: iUsers, - }, nil -} diff --git a/rules/common/ipasn.go b/rules/common/ipasn.go deleted file mode 100644 index 7d554103f3..0000000000 --- a/rules/common/ipasn.go +++ /dev/null @@ -1,73 +0,0 @@ -package common - -import ( - "github.com/metacubex/mihomo/component/geodata" - "github.com/metacubex/mihomo/component/mmdb" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type ASN struct { - *Base - asn string - adapter string - noResolveIP bool - isSourceIP bool -} - -func (a *ASN) Match(metadata *C.Metadata) (bool, string) { - ip := metadata.DstIP - if a.isSourceIP { - ip = metadata.SrcIP - } - if !ip.IsValid() { - return false, "" - } - - asn, aso := mmdb.ASNInstance().LookupASN(ip.AsSlice()) - if a.isSourceIP { - metadata.SrcIPASN = asn + " " + aso - } else { - metadata.DstIPASN = asn + " " + aso - } - - return a.asn == asn, a.adapter -} - -func (a *ASN) RuleType() C.RuleType { - if a.isSourceIP { - return C.SrcIPASN - } - return C.IPASN -} - -func (a *ASN) Adapter() string { - return a.adapter -} - -func (a *ASN) Payload() string { - return a.asn -} - -func (a *ASN) ShouldResolveIP() bool { - return !a.noResolveIP -} - -func (a *ASN) GetASN() string { - return a.asn -} - -func NewIPASN(asn string, adapter string, isSrc, noResolveIP bool) (*ASN, error) { - if err := geodata.InitASN(); err != nil { - log.Errorln("can't initial ASN: %s", err) - return nil, err - } - - return &ASN{ - Base: &Base{}, - asn: asn, - adapter: adapter, - noResolveIP: noResolveIP, - isSourceIP: isSrc, - }, nil -} diff --git a/rules/common/ipcidr.go b/rules/common/ipcidr.go deleted file mode 100644 index 9c159502b4..0000000000 --- a/rules/common/ipcidr.go +++ /dev/null @@ -1,77 +0,0 @@ -package common - -import ( - "net/netip" - - C "github.com/metacubex/mihomo/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 - } -} - -type IPCIDR struct { - *Base - ipnet netip.Prefix - 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, string) { - ip := metadata.DstIP - if i.isSourceIP { - ip = metadata.SrcIP - } - return ip.IsValid() && i.ipnet.Contains(ip.WithZone("")), i.adapter -} - -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 NewIPCIDR(s string, adapter string, opts ...IPCIDROption) (*IPCIDR, error) { - ipnet, err := netip.ParsePrefix(s) - if err != nil { - return nil, errPayload - } - - ipcidr := &IPCIDR{ - Base: &Base{}, - ipnet: ipnet, - adapter: adapter, - } - - for _, o := range opts { - o(ipcidr) - } - - return ipcidr, nil -} - -//var _ C.Rule = (*IPCIDR)(nil) diff --git a/rules/common/ipsuffix.go b/rules/common/ipsuffix.go deleted file mode 100644 index 3251faf806..0000000000 --- a/rules/common/ipsuffix.go +++ /dev/null @@ -1,79 +0,0 @@ -package common - -import ( - C "github.com/metacubex/mihomo/constant" - "net/netip" -) - -type IPSuffix struct { - *Base - ipBytes []byte - bits int - payload string - adapter string - isSourceIP bool - noResolveIP bool -} - -func (is *IPSuffix) RuleType() C.RuleType { - if is.isSourceIP { - return C.SrcIPSuffix - } - return C.IPSuffix -} - -func (is *IPSuffix) Match(metadata *C.Metadata) (bool, string) { - ip := metadata.DstIP - if is.isSourceIP { - ip = metadata.SrcIP - } - - mIPBytes := ip.AsSlice() - if len(is.ipBytes) != len(mIPBytes) { - return false, "" - } - - size := len(mIPBytes) - bits := is.bits - - for i := bits / 8; i > 0; i-- { - if is.ipBytes[size-i] != mIPBytes[size-i] { - return false, "" - } - } - - if (is.ipBytes[size-bits/8-1] << (8 - bits%8)) != (mIPBytes[size-bits/8-1] << (8 - bits%8)) { - return false, "" - } - - return true, is.adapter -} - -func (is *IPSuffix) Adapter() string { - return is.adapter -} - -func (is *IPSuffix) Payload() string { - return is.payload -} - -func (is *IPSuffix) ShouldResolveIP() bool { - return !is.noResolveIP -} - -func NewIPSuffix(payload, adapter string, isSrc, noResolveIP bool) (*IPSuffix, error) { - ipnet, err := netip.ParsePrefix(payload) - if err != nil { - return nil, errPayload - } - - return &IPSuffix{ - Base: &Base{}, - payload: payload, - ipBytes: ipnet.Addr().AsSlice(), - bits: ipnet.Bits(), - adapter: adapter, - isSourceIP: isSrc, - noResolveIP: noResolveIP, - }, nil -} diff --git a/rules/common/network_type.go b/rules/common/network_type.go deleted file mode 100644 index 83a332d891..0000000000 --- a/rules/common/network_type.go +++ /dev/null @@ -1,47 +0,0 @@ -package common - -import ( - "fmt" - C "github.com/metacubex/mihomo/constant" - "strings" -) - -type NetworkType struct { - *Base - network C.NetWork - adapter string -} - -func NewNetworkType(network, adapter string) (*NetworkType, error) { - ntType := NetworkType{ - Base: &Base{}, - } - - ntType.adapter = adapter - switch strings.ToUpper(network) { - case "TCP": - ntType.network = C.TCP - case "UDP": - ntType.network = C.UDP - default: - return nil, fmt.Errorf("unsupported network type, only TCP/UDP") - } - - return &ntType, nil -} - -func (n *NetworkType) RuleType() C.RuleType { - return C.Network -} - -func (n *NetworkType) Match(metadata *C.Metadata) (bool, string) { - return n.network == metadata.NetWork, n.adapter -} - -func (n *NetworkType) Adapter() string { - return n.adapter -} - -func (n *NetworkType) Payload() string { - return n.network.String() -} diff --git a/rules/common/port.go b/rules/common/port.go deleted file mode 100644 index d3f6e1b467..0000000000 --- a/rules/common/port.go +++ /dev/null @@ -1,60 +0,0 @@ -package common - -import ( - "fmt" - - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" -) - -type Port struct { - *Base - adapter string - port string - ruleType C.RuleType - portRanges utils.IntRanges[uint16] -} - -func (p *Port) RuleType() C.RuleType { - return p.ruleType -} - -func (p *Port) Match(metadata *C.Metadata) (bool, string) { - targetPort := metadata.DstPort - switch p.ruleType { - case C.InPort: - targetPort = metadata.InPort - case C.SrcPort: - targetPort = metadata.SrcPort - } - return p.portRanges.Check(targetPort), p.adapter -} - -func (p *Port) Adapter() string { - return p.adapter -} - -func (p *Port) Payload() string { - return p.port -} - -func NewPort(port string, adapter string, ruleType C.RuleType) (*Port, error) { - portRanges, err := utils.NewUnsignedRanges[uint16](port) - if err != nil { - return nil, fmt.Errorf("%w, %w", errPayload, err) - } - - if len(portRanges) == 0 { - return nil, errPayload - } - - return &Port{ - Base: &Base{}, - adapter: adapter, - port: port, - ruleType: ruleType, - portRanges: portRanges, - }, nil -} - -var _ C.Rule = (*Port)(nil) diff --git a/rules/common/process.go b/rules/common/process.go deleted file mode 100644 index 8932e94660..0000000000 --- a/rules/common/process.go +++ /dev/null @@ -1,77 +0,0 @@ -package common - -import ( - "strings" - - C "github.com/metacubex/mihomo/constant" - - "github.com/dlclark/regexp2" -) - -type Process struct { - *Base - adapter string - process string - nameOnly bool - regexp *regexp2.Regexp -} - -func (ps *Process) RuleType() C.RuleType { - if ps.nameOnly { - if ps.regexp != nil { - return C.ProcessNameRegex - } - return C.ProcessName - } - - if ps.regexp != nil { - return C.ProcessPathRegex - } - return C.ProcessPath -} - -func (ps *Process) Match(metadata *C.Metadata) (bool, string) { - if ps.nameOnly { - if ps.regexp != nil { - match, _ := ps.regexp.MatchString(metadata.Process) - return match, ps.adapter - } - return strings.EqualFold(metadata.Process, ps.process), ps.adapter - } - - if ps.regexp != nil { - match, _ := ps.regexp.MatchString(metadata.ProcessPath) - return match, ps.adapter - } - return strings.EqualFold(metadata.ProcessPath, ps.process), ps.adapter -} - -func (ps *Process) Adapter() string { - return ps.adapter -} - -func (ps *Process) Payload() string { - return ps.process -} - -func (ps *Process) ShouldFindProcess() bool { - return true -} - -func NewProcess(process string, adapter string, nameOnly bool, regex bool) (*Process, error) { - var r *regexp2.Regexp - var err error - if regex { - r, err = regexp2.Compile(process, regexp2.IgnoreCase) - if err != nil { - return nil, err - } - } - return &Process{ - Base: &Base{}, - adapter: adapter, - process: process, - nameOnly: nameOnly, - regexp: r, - }, nil -} diff --git a/rules/common/uid.go b/rules/common/uid.go deleted file mode 100644 index c80632b038..0000000000 --- a/rules/common/uid.go +++ /dev/null @@ -1,64 +0,0 @@ -package common - -import ( - "fmt" - "runtime" - - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type Uid struct { - *Base - uids utils.IntRanges[uint32] - oUid string - adapter string -} - -func NewUid(oUid, adapter string) (*Uid, error) { - if !(runtime.GOOS == "linux" || runtime.GOOS == "android") { - return nil, fmt.Errorf("uid rule not support this platform") - } - - uidRange, err := utils.NewUnsignedRanges[uint32](oUid) - if err != nil { - return nil, fmt.Errorf("%w, %w", errPayload, err) - } - - if len(uidRange) == 0 { - return nil, errPayload - } - return &Uid{ - Base: &Base{}, - adapter: adapter, - oUid: oUid, - uids: uidRange, - }, nil -} - -func (u *Uid) RuleType() C.RuleType { - return C.Uid -} - -func (u *Uid) Match(metadata *C.Metadata) (bool, string) { - if metadata.Uid != 0 { - if u.uids.Check(metadata.Uid) { - return true, u.adapter - } - } - log.Warnln("[UID] could not get uid from %s", metadata.String()) - return false, "" -} - -func (u *Uid) Adapter() string { - return u.adapter -} - -func (u *Uid) Payload() string { - return u.oUid -} - -func (u *Uid) ShouldFindProcess() bool { - return true -} diff --git a/rules/logic/logic.go b/rules/logic/logic.go deleted file mode 100644 index f7b5a987ef..0000000000 --- a/rules/logic/logic.go +++ /dev/null @@ -1,306 +0,0 @@ -package logic - -import ( - "fmt" - "regexp" - "strings" - "sync" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/rules/common" - - list "github.com/bahlo/generic-list-go" -) - -type Logic struct { - *common.Base - payload string - adapter string - ruleType C.RuleType - rules []C.Rule - subRules map[string][]C.Rule - - payloadOnce sync.Once -} - -func NewSubRule(payload, adapter string, subRules map[string][]C.Rule, parseRule common.ParseRuleFunc) (*Logic, error) { - logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.SubRules, subRules: subRules} - err := logic.parsePayload(fmt.Sprintf("(%s)", payload), parseRule) - if err != nil { - return nil, err - } - - if len(logic.rules) != 1 { - return nil, fmt.Errorf("Sub-Rule rule must contain one rule") - } - return logic, nil -} - -func NewNOT(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) { - logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.NOT} - err := logic.parsePayload(payload, parseRule) - if err != nil { - return nil, err - } - - if len(logic.rules) != 1 { - return nil, fmt.Errorf("not rule must contain one rule") - } - return logic, nil -} - -func NewOR(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) { - logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.OR} - err := logic.parsePayload(payload, parseRule) - if err != nil { - return nil, err - } - return logic, nil -} - -func NewAND(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) { - logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.AND} - err := logic.parsePayload(payload, parseRule) - if err != nil { - return nil, err - } - return logic, nil -} - -type Range struct { - start int - end int - index int -} - -func (r Range) containRange(preStart, preEnd int) bool { - return preStart < r.start && preEnd > r.end -} - -func (logic *Logic) payloadToRule(subPayload string, parseRule common.ParseRuleFunc) (C.Rule, error) { - splitStr := strings.SplitN(subPayload, ",", 2) - if len(splitStr) < 2 { - return nil, fmt.Errorf("[%s] format is error", subPayload) - } - - tp := splitStr[0] - payload := splitStr[1] - switch tp { - case "MATCH", "SUB-RULE": - return nil, fmt.Errorf("unsupported rule type [%s] on logic rule", tp) - case "NOT", "OR", "AND": - return parseRule(tp, payload, "", nil, nil) - } - param := strings.Split(payload, ",") - return parseRule(tp, param[0], "", param[1:], nil) -} - -func (logic *Logic) format(payload string) ([]Range, error) { - stack := list.New[Range]() - num := 0 - subRanges := make([]Range, 0) - for i, c := range payload { - if c == '(' { - sr := Range{ - start: i, - index: num, - } - - num++ - stack.PushBack(sr) - } else if c == ')' { - if stack.Len() == 0 { - return nil, fmt.Errorf("missing '('") - } - - sr := stack.Back() - stack.Remove(sr) - sr.Value.end = i - subRanges = append(subRanges, sr.Value) - } - } - - if stack.Len() != 0 { - return nil, fmt.Errorf("format error is missing )") - } - - sortResult := make([]Range, len(subRanges)) - for _, sr := range subRanges { - sortResult[sr.index] = sr - } - - return sortResult, nil -} - -func (logic *Logic) findSubRuleRange(payload string, ruleRanges []Range) []Range { - payloadLen := len(payload) - subRuleRange := make([]Range, 0) - for _, rr := range ruleRanges { - if rr.start == 0 && rr.end == payloadLen-1 { - // 最大范围跳过 - continue - } - - containInSub := false - for _, r := range subRuleRange { - if rr.containRange(r.start, r.end) { - // The subRuleRange contains a range of rr, which is the next level node of the tree - containInSub = true - break - } - } - - if !containInSub { - subRuleRange = append(subRuleRange, rr) - } - } - - return subRuleRange -} - -func (logic *Logic) parsePayload(payload string, parseRule common.ParseRuleFunc) error { - regex, err := regexp.Compile("\\(.*\\)") - if err != nil { - return err - } - - if regex.MatchString(payload) { - subAllRanges, err := logic.format(payload) - if err != nil { - return err - } - rules := make([]C.Rule, 0, len(subAllRanges)) - - subRanges := logic.findSubRuleRange(payload, subAllRanges) - for _, subRange := range subRanges { - subPayload := payload[subRange.start+1 : subRange.end] - - rule, err := logic.payloadToRule(subPayload, parseRule) - if err != nil { - return err - } - - rules = append(rules, rule) - } - - logic.rules = rules - - return nil - } - - return fmt.Errorf("payload format error") -} - -func (logic *Logic) RuleType() C.RuleType { - return logic.ruleType -} - -func matchSubRules(metadata *C.Metadata, name string, subRules map[string][]C.Rule) (bool, string) { - for _, rule := range subRules[name] { - if m, a := rule.Match(metadata); m { - if rule.RuleType() == C.SubRules { - return matchSubRules(metadata, rule.Adapter(), subRules) - } else { - return m, a - } - } - } - return false, "" -} - -func (logic *Logic) Match(metadata *C.Metadata) (bool, string) { - switch logic.ruleType { - case C.SubRules: - if m, _ := logic.rules[0].Match(metadata); m { - return matchSubRules(metadata, logic.adapter, logic.subRules) - } - return false, "" - case C.NOT: - if m, _ := logic.rules[0].Match(metadata); !m { - return true, logic.adapter - } - return false, "" - case C.OR: - for _, rule := range logic.rules { - if m, _ := rule.Match(metadata); m { - return true, logic.adapter - } - } - return false, "" - case C.AND: - for _, rule := range logic.rules { - if m, _ := rule.Match(metadata); !m { - return false, logic.adapter - } - } - return true, logic.adapter - default: - return false, "" - } -} - -func (logic *Logic) Adapter() string { - return logic.adapter -} - -func (logic *Logic) Payload() string { - logic.payloadOnce.Do(func() { // a little bit expensive, so only computed once - switch logic.ruleType { - case C.NOT: - logic.payload = fmt.Sprintf("(!(%s,%s))", logic.rules[0].RuleType(), logic.rules[0].Payload()) - case C.OR: - payloads := make([]string, 0, len(logic.rules)) - for _, rule := range logic.rules { - payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) - } - logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " || ")) - case C.AND: - payloads := make([]string, 0, len(logic.rules)) - for _, rule := range logic.rules { - payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) - } - logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " && ")) - default: - } - }) - return logic.payload -} - -func (logic *Logic) ShouldResolveIP() bool { - if logic.ruleType == C.SubRules { - for _, rule := range logic.subRules[logic.adapter] { - if rule.ShouldResolveIP() { - return true - } - } - } - for _, rule := range logic.rules { - if rule.ShouldResolveIP() { - return true - } - } - return false -} - -func (logic *Logic) ShouldFindProcess() bool { - if logic.ruleType == C.SubRules { - for _, rule := range logic.subRules[logic.adapter] { - if rule.ShouldFindProcess() { - return true - } - } - } - for _, rule := range logic.rules { - if rule.ShouldFindProcess() { - return true - } - } - return false -} - -func (logic *Logic) ProviderNames() (names []string) { - for _, rule := range logic.rules { - names = append(names, rule.ProviderNames()...) - } - return -} diff --git a/rules/logic_test/logic_test.go b/rules/logic_test/logic_test.go deleted file mode 100644 index e88c857878..0000000000 --- a/rules/logic_test/logic_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package logic_test - -import ( - // https://github.com/golang/go/wiki/CodeReviewComments#import-dot - . "github.com/metacubex/mihomo/rules/logic" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/rules" - "github.com/stretchr/testify/assert" - "testing" -) - -var ParseRule = rules.ParseRule - -func TestAND(t *testing.T) { - and, err := NewAND("((DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) - assert.Equal(t, nil, err) - assert.Equal(t, "DIRECT", and.Adapter()) - assert.Equal(t, false, and.ShouldResolveIP()) - m, _ := and.Match(&C.Metadata{ - Host: "baidu.com", - NetWork: C.TCP, - DstPort: 20000, - }) - assert.Equal(t, true, m) - - and, err = NewAND("(DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) - assert.NotEqual(t, nil, err) - - and, err = NewAND("((AND,(DOMAIN,baidu.com),(NETWORK,TCP)),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) - assert.Equal(t, nil, err) -} - -func TestNOT(t *testing.T) { - not, err := NewNOT("((DST-PORT,6000-6500))", "REJECT", ParseRule) - assert.Equal(t, nil, err) - m, _ := not.Match(&C.Metadata{ - DstPort: 6100, - }) - assert.Equal(t, false, m) - - _, err = NewNOT("((DST-PORT,5600-6666),(DOMAIN,baidu.com))", "DIRECT", ParseRule) - assert.NotEqual(t, nil, err) - - _, err = NewNOT("(())", "DIRECT", ParseRule) - assert.NotEqual(t, nil, err) -} - -func TestOR(t *testing.T) { - or, err := NewOR("((DOMAIN,baidu.com),(NETWORK,TCP),(DST-PORT,10001-65535))", "DIRECT", ParseRule) - assert.Equal(t, nil, err) - m, _ := or.Match(&C.Metadata{ - NetWork: C.TCP, - }) - assert.Equal(t, true, m) - assert.Equal(t, false, or.ShouldResolveIP()) -} diff --git a/rules/parser.go b/rules/parser.go deleted file mode 100644 index f4e945c915..0000000000 --- a/rules/parser.go +++ /dev/null @@ -1,97 +0,0 @@ -package rules - -import ( - "fmt" - - C "github.com/metacubex/mihomo/constant" - RC "github.com/metacubex/mihomo/rules/common" - "github.com/metacubex/mihomo/rules/logic" - RP "github.com/metacubex/mihomo/rules/provider" -) - -func ParseRule(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error) { - switch tp { - case "DOMAIN": - parsed = RC.NewDomain(payload, target) - case "DOMAIN-SUFFIX": - parsed = RC.NewDomainSuffix(payload, target) - case "DOMAIN-KEYWORD": - parsed = RC.NewDomainKeyword(payload, target) - case "DOMAIN-REGEX": - parsed, parseErr = RC.NewDomainRegex(payload, target) - case "GEOSITE": - parsed, parseErr = RC.NewGEOSITE(payload, target) - case "GEOIP": - isSrc, noResolve := RC.ParseParams(params) - parsed, parseErr = RC.NewGEOIP(payload, target, isSrc, noResolve) - case "SRC-GEOIP": - parsed, parseErr = RC.NewGEOIP(payload, target, true, true) - case "IP-ASN": - isSrc, noResolve := RC.ParseParams(params) - parsed, parseErr = RC.NewIPASN(payload, target, isSrc, noResolve) - case "SRC-IP-ASN": - parsed, parseErr = RC.NewIPASN(payload, target, true, true) - case "IP-CIDR", "IP-CIDR6": - isSrc, noResolve := RC.ParseParams(params) - parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(isSrc), RC.WithIPCIDRNoResolve(noResolve)) - case "SRC-IP-CIDR": - parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(true), RC.WithIPCIDRNoResolve(true)) - case "IP-SUFFIX": - isSrc, noResolve := RC.ParseParams(params) - parsed, parseErr = RC.NewIPSuffix(payload, target, isSrc, noResolve) - case "SRC-IP-SUFFIX": - parsed, parseErr = RC.NewIPSuffix(payload, target, true, true) - case "SRC-PORT": - parsed, parseErr = RC.NewPort(payload, target, C.SrcPort) - case "DST-PORT": - parsed, parseErr = RC.NewPort(payload, target, C.DstPort) - case "IN-PORT": - parsed, parseErr = RC.NewPort(payload, target, C.InPort) - case "DSCP": - parsed, parseErr = RC.NewDSCP(payload, target) - case "PROCESS-NAME": - parsed, parseErr = RC.NewProcess(payload, target, true, false) - case "PROCESS-PATH": - parsed, parseErr = RC.NewProcess(payload, target, false, false) - case "PROCESS-NAME-REGEX": - parsed, parseErr = RC.NewProcess(payload, target, true, true) - case "PROCESS-PATH-REGEX": - parsed, parseErr = RC.NewProcess(payload, target, false, true) - case "NETWORK": - parsed, parseErr = RC.NewNetworkType(payload, target) - case "UID": - parsed, parseErr = RC.NewUid(payload, target) - case "IN-TYPE": - parsed, parseErr = RC.NewInType(payload, target) - case "IN-USER": - parsed, parseErr = RC.NewInUser(payload, target) - case "IN-NAME": - parsed, parseErr = RC.NewInName(payload, target) - case "SUB-RULE": - parsed, parseErr = logic.NewSubRule(payload, target, subRules, ParseRule) - case "AND": - parsed, parseErr = logic.NewAND(payload, target, ParseRule) - case "OR": - parsed, parseErr = logic.NewOR(payload, target, ParseRule) - case "NOT": - parsed, parseErr = logic.NewNOT(payload, target, ParseRule) - case "RULE-SET": - isSrc, noResolve := RC.ParseParams(params) - parsed, parseErr = RP.NewRuleSet(payload, target, isSrc, noResolve) - case "MATCH": - parsed = RC.NewMatch(target) - parseErr = nil - case "": - parseErr = fmt.Errorf("missing subsequent parameters: %s", payload) - default: - parseErr = fmt.Errorf("unsupported rule type: %s", tp) - } - - if parseErr != nil { - return nil, parseErr - } - - return -} - -var _ RC.ParseRuleFunc = ParseRule diff --git a/rules/provider/classical_strategy.go b/rules/provider/classical_strategy.go deleted file mode 100644 index 95a772e845..0000000000 --- a/rules/provider/classical_strategy.go +++ /dev/null @@ -1,104 +0,0 @@ -package provider - -import ( - "fmt" - "strings" - - C "github.com/metacubex/mihomo/constant" - P "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/log" -) - -type classicalStrategy struct { - rules []C.Rule - count int - shouldResolveIP bool - shouldFindProcess bool - parse func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) -} - -func (c *classicalStrategy) Behavior() P.RuleBehavior { - return P.Classical -} - -func (c *classicalStrategy) Match(metadata *C.Metadata) bool { - for _, rule := range c.rules { - if m, _ := rule.Match(metadata); m { - return true - } - } - - return false -} - -func (c *classicalStrategy) Count() int { - return c.count -} - -func (c *classicalStrategy) ShouldResolveIP() bool { - return c.shouldResolveIP -} - -func (c *classicalStrategy) ShouldFindProcess() bool { - return c.shouldFindProcess -} - -func (c *classicalStrategy) Reset() { - c.rules = nil - c.count = 0 - c.shouldFindProcess = false - c.shouldResolveIP = false -} - -func (c *classicalStrategy) Insert(rule string) { - ruleType, rule, params := ruleParse(rule) - - if ruleType == "PROCESS-NAME" { - c.shouldFindProcess = true - } - - r, err := c.parse(ruleType, rule, "", params) - if err != nil { - log.Warnln("parse classical rule error: %s", err.Error()) - } else { - if r.ShouldResolveIP() { - c.shouldResolveIP = true - } - if r.ShouldFindProcess() { - c.shouldFindProcess = true - } - - c.rules = append(c.rules, r) - c.count++ - } -} - -func (c *classicalStrategy) FinishInsert() {} - -func ruleParse(ruleRaw string) (string, string, []string) { - item := strings.Split(ruleRaw, ",") - if len(item) == 1 { - return "", item[0], nil - } else if len(item) == 2 { - return item[0], item[1], nil - } else if len(item) > 2 { - if item[0] == "NOT" || item[0] == "OR" || item[0] == "AND" || item[0] == "SUB-RULE" || item[0] == "DOMAIN-REGEX" || item[0] == "PROCESS-NAME-REGEX" || item[0] == "PROCESS-PATH-REGEX" { - return item[0], strings.Join(item[1:], ","), nil - } else { - return item[0], item[1], item[2:] - } - } - - return "", "", nil -} - -func NewClassicalStrategy(parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) *classicalStrategy { - return &classicalStrategy{rules: []C.Rule{}, parse: func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) { - switch tp { - case "MATCH", "RULE-SET", "SUB-RULE": - return nil, fmt.Errorf("unsupported rule type on classical rule-set: %s", tp) - default: - return parse(tp, payload, target, params, nil) - } - }} -} diff --git a/rules/provider/domain_strategy.go b/rules/provider/domain_strategy.go deleted file mode 100644 index b893f038b2..0000000000 --- a/rules/provider/domain_strategy.go +++ /dev/null @@ -1,107 +0,0 @@ -package provider - -import ( - "errors" - "io" - "strings" - - "github.com/metacubex/mihomo/component/trie" - C "github.com/metacubex/mihomo/constant" - P "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/log" - - "golang.org/x/exp/slices" -) - -type domainStrategy struct { - count int - domainTrie *trie.DomainTrie[struct{}] - domainSet *trie.DomainSet -} - -func (d *domainStrategy) Behavior() P.RuleBehavior { - return P.Domain -} - -func (d *domainStrategy) ShouldFindProcess() bool { - return false -} - -func (d *domainStrategy) Match(metadata *C.Metadata) bool { - return d.domainSet != nil && d.domainSet.Has(metadata.RuleHost()) -} - -func (d *domainStrategy) Count() int { - return d.count -} - -func (d *domainStrategy) ShouldResolveIP() bool { - return false -} - -func (d *domainStrategy) Reset() { - d.domainTrie = trie.New[struct{}]() - d.domainSet = nil - d.count = 0 -} - -func (d *domainStrategy) Insert(rule string) { - if strings.ContainsRune(rule, '/') { - log.Warnln("invalid domain:[%s]", rule) - return - } - err := d.domainTrie.Insert(rule, struct{}{}) - if err != nil { - log.Warnln("invalid domain:[%s]", rule) - } else { - d.count++ - } -} - -func (d *domainStrategy) FinishInsert() { - d.domainSet = d.domainTrie.NewDomainSet() - d.domainTrie = nil -} - -func (d *domainStrategy) FromMrs(r io.Reader, count int) error { - domainSet, err := trie.ReadDomainSetBin(r) - if err != nil { - return err - } - d.count = count - d.domainSet = domainSet - return nil -} - -func (d *domainStrategy) WriteMrs(w io.Writer) error { - if d.domainSet == nil { - return errors.New("nil domainSet") - } - return d.domainSet.WriteBin(w) -} - -func (d *domainStrategy) DumpMrs(f func(key string) bool) { - if d.domainSet != nil { - var keys []string - d.domainSet.Foreach(func(key string) bool { - keys = append(keys, key) - return true - }) - slices.Sort(keys) - - for _, key := range keys { - if _, ok := slices.BinarySearch(keys, "+."+key); ok { - continue // ignore the rules added by trie internal processing - } - if !f(key) { - return - } - } - } -} - -var _ mrsRuleStrategy = (*domainStrategy)(nil) - -func NewDomainStrategy() *domainStrategy { - return &domainStrategy{} -} diff --git a/rules/provider/ipcidr_strategy.go b/rules/provider/ipcidr_strategy.go deleted file mode 100644 index 9efffed96b..0000000000 --- a/rules/provider/ipcidr_strategy.go +++ /dev/null @@ -1,100 +0,0 @@ -package provider - -import ( - "errors" - "io" - "net/netip" - - "github.com/metacubex/mihomo/component/cidr" - C "github.com/metacubex/mihomo/constant" - P "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/log" - - "go4.org/netipx" -) - -type ipcidrStrategy struct { - count int - shouldResolveIP bool - cidrSet *cidr.IpCidrSet - //trie *trie.IpCidrTrie -} - -func (i *ipcidrStrategy) Behavior() P.RuleBehavior { - return P.IPCIDR -} - -func (i *ipcidrStrategy) ShouldFindProcess() bool { - return false -} - -func (i *ipcidrStrategy) Match(metadata *C.Metadata) bool { - // return i.trie != nil && i.trie.IsContain(metadata.DstIP.AsSlice()) - return i.cidrSet != nil && i.cidrSet.IsContain(metadata.DstIP) -} - -func (i *ipcidrStrategy) Count() int { - return i.count -} - -func (i *ipcidrStrategy) ShouldResolveIP() bool { - return i.shouldResolveIP -} - -func (i *ipcidrStrategy) Reset() { - // i.trie = trie.NewIpCidrTrie() - i.cidrSet = cidr.NewIpCidrSet() - i.count = 0 - i.shouldResolveIP = false -} - -func (i *ipcidrStrategy) Insert(rule string) { - //err := i.trie.AddIpCidrForString(rule) - err := i.cidrSet.AddIpCidrForString(rule) - if err != nil { - log.Warnln("invalid Ipcidr:[%s]", rule) - } else { - i.shouldResolveIP = true - i.count++ - } -} - -func (i *ipcidrStrategy) FinishInsert() { - i.cidrSet.Merge() -} - -func (i *ipcidrStrategy) FromMrs(r io.Reader, count int) error { - cidrSet, err := cidr.ReadIpCidrSet(r) - if err != nil { - return err - } - i.count = count - i.cidrSet = cidrSet - if i.count > 0 { - i.shouldResolveIP = true - } - return nil -} - -func (i *ipcidrStrategy) WriteMrs(w io.Writer) error { - if i.cidrSet == nil { - return errors.New("nil cidrSet") - } - return i.cidrSet.WriteBin(w) -} - -func (i *ipcidrStrategy) DumpMrs(f func(key string) bool) { - if i.cidrSet != nil { - i.cidrSet.Foreach(func(prefix netip.Prefix) bool { - return f(prefix.String()) - }) - } -} - -func (i *ipcidrStrategy) ToIpCidr() *netipx.IPSet { - return i.cidrSet.ToIPSet() -} - -func NewIPCidrStrategy() *ipcidrStrategy { - return &ipcidrStrategy{} -} diff --git a/rules/provider/mrs_converter.go b/rules/provider/mrs_converter.go deleted file mode 100644 index dbbe51cb29..0000000000 --- a/rules/provider/mrs_converter.go +++ /dev/null @@ -1,120 +0,0 @@ -package provider - -import ( - "encoding/binary" - "errors" - "fmt" - "io" - "os" - - P "github.com/metacubex/mihomo/constant/provider" - - "github.com/klauspost/compress/zstd" -) - -func ConvertToMrs(buf []byte, behavior P.RuleBehavior, format P.RuleFormat, w io.Writer) (err error) { - strategy := newStrategy(behavior, nil) - strategy, err = rulesParse(buf, strategy, format) - if err != nil { - return err - } - if strategy.Count() == 0 { - return errors.New("empty rule") - } - if _strategy, ok := strategy.(mrsRuleStrategy); ok { - if format == P.MrsRule { // export to TextRule - _strategy.DumpMrs(func(key string) bool { - _, err = fmt.Fprintln(w, key) - if err != nil { - return false - } - return true - }) - return nil - } - - var encoder *zstd.Encoder - encoder, err = zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) - if err != nil { - return err - } - defer func() { - zstdErr := encoder.Close() - if err == nil { - err = zstdErr - } - }() - - // header - _, err = encoder.Write(MrsMagicBytes[:]) - if err != nil { - return err - } - - // behavior - _behavior := []byte{behavior.Byte()} - _, err = encoder.Write(_behavior[:]) - if err != nil { - return err - } - - // count - count := int64(_strategy.Count()) - err = binary.Write(encoder, binary.BigEndian, count) - if err != nil { - return err - } - - // extra (reserved for future using) - var extra []byte - err = binary.Write(encoder, binary.BigEndian, int64(len(extra))) - if err != nil { - return err - } - _, err = encoder.Write(extra) - if err != nil { - return err - } - - return _strategy.WriteMrs(encoder) - } else { - return ErrInvalidFormat - } -} - -func ConvertMain(args []string) { - if len(args) > 3 { - behavior, err := P.ParseBehavior(args[0]) - if err != nil { - panic(err) - } - format, err := P.ParseRuleFormat(args[1]) - if err != nil { - panic(err) - } - source := args[2] - target := args[3] - - sourceFile, err := os.ReadFile(source) - if err != nil { - panic(err) - } - - targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - panic(err) - } - - err = ConvertToMrs(sourceFile, behavior, format, targetFile) - if err != nil { - panic(err) - } - - err = targetFile.Close() - if err != nil { - panic(err) - } - } else { - panic("Usage: convert-ruleset ") - } -} diff --git a/rules/provider/mrs_reader.go b/rules/provider/mrs_reader.go deleted file mode 100644 index 66f62127c8..0000000000 --- a/rules/provider/mrs_reader.go +++ /dev/null @@ -1,72 +0,0 @@ -package provider - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - - "github.com/klauspost/compress/zstd" -) - -var MrsMagicBytes = [4]byte{'M', 'R', 'S', 1} // MRSv1 - -func rulesMrsParse(buf []byte, strategy ruleStrategy) (ruleStrategy, error) { - if _strategy, ok := strategy.(mrsRuleStrategy); ok { - reader, err := zstd.NewReader(bytes.NewReader(buf)) - if err != nil { - return nil, err - } - defer reader.Close() - - // header - var header [4]byte - _, err = io.ReadFull(reader, header[:]) - if err != nil { - return nil, err - } - if header != MrsMagicBytes { - return nil, fmt.Errorf("invalid MrsMagic bytes") - } - - // behavior - var _behavior [1]byte - _, err = io.ReadFull(reader, _behavior[:]) - if err != nil { - return nil, err - } - if _behavior[0] != strategy.Behavior().Byte() { - return nil, fmt.Errorf("invalid behavior") - } - - // count - var count int64 - err = binary.Read(reader, binary.BigEndian, &count) - if err != nil { - return nil, err - } - - // extra (reserved for future using) - var length int64 - err = binary.Read(reader, binary.BigEndian, &length) - if err != nil { - return nil, err - } - if length < 0 { - return nil, errors.New("length is invalid") - } - if length > 0 { - extra := make([]byte, length) - _, err = io.ReadFull(reader, extra) - if err != nil { - return nil, err - } - } - - err = _strategy.FromMrs(reader, int(count)) - return strategy, err - } else { - return nil, ErrInvalidFormat - } -} diff --git a/rules/provider/parse.go b/rules/provider/parse.go deleted file mode 100644 index 5fa98dc7c1..0000000000 --- a/rules/provider/parse.go +++ /dev/null @@ -1,64 +0,0 @@ -package provider - -import ( - "fmt" - "time" - - "github.com/metacubex/mihomo/common/structure" - "github.com/metacubex/mihomo/component/resource" - C "github.com/metacubex/mihomo/constant" - P "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/rules/common" -) - -type ruleProviderSchema struct { - Type string `provider:"type"` - Behavior string `provider:"behavior"` - Path string `provider:"path,omitempty"` - URL string `provider:"url,omitempty"` - Proxy string `provider:"proxy,omitempty"` - Format string `provider:"format,omitempty"` - Interval int `provider:"interval,omitempty"` - SizeLimit int64 `provider:"size-limit,omitempty"` - Payload []string `provider:"payload,omitempty"` -} - -func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRuleFunc) (P.RuleProvider, error) { - schema := &ruleProviderSchema{} - decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) - if err := decoder.Decode(mapping, schema); err != nil { - return nil, err - } - behavior, err := P.ParseBehavior(schema.Behavior) - if err != nil { - return nil, err - } - format, err := P.ParseRuleFormat(schema.Format) - if err != nil { - return nil, err - } - - var vehicle P.Vehicle - switch schema.Type { - case "file": - path := C.Path.Resolve(schema.Path) - vehicle = resource.NewFileVehicle(path) - case "http": - path := C.Path.GetPathByHash("rules", schema.URL) - if schema.Path != "" { - path = C.Path.Resolve(schema.Path) - if !C.Path.IsSafePath(path) { - return nil, C.Path.ErrNotSafePath(path) - } - } - vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, nil, resource.DefaultHttpTimeout, schema.SizeLimit) - case "inline": - return NewInlineProvider(name, behavior, schema.Payload, parse), nil - default: - return nil, fmt.Errorf("unsupported vehicle type: %s", schema.Type) - } - - interval := time.Duration(uint(schema.Interval)) * time.Second - - return NewRuleSetProvider(name, behavior, format, interval, vehicle, schema.Payload, parse), nil -} diff --git a/rules/provider/provider.go b/rules/provider/provider.go deleted file mode 100644 index 23a5781f04..0000000000 --- a/rules/provider/provider.go +++ /dev/null @@ -1,351 +0,0 @@ -package provider - -import ( - "bytes" - "encoding/json" - "errors" - "io" - "runtime" - "strings" - "time" - - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/component/resource" - C "github.com/metacubex/mihomo/constant" - P "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/rules/common" - - "gopkg.in/yaml.v3" -) - -var tunnel P.Tunnel - -func SetTunnel(t P.Tunnel) { - tunnel = t -} - -type RulePayload struct { - /** - key: Domain or IP Cidr - value: Rule type or is empty - */ - Payload []string `yaml:"payload"` - Rules []string `yaml:"rules"` -} - -type providerForApi struct { - Behavior string `json:"behavior"` - Format string `json:"format"` - Name string `json:"name"` - RuleCount int `json:"ruleCount"` - Type string `json:"type"` - VehicleType string `json:"vehicleType"` - UpdatedAt time.Time `json:"updatedAt"` - Payload []string `json:"payload,omitempty"` -} - -type ruleStrategy interface { - Behavior() P.RuleBehavior - Match(metadata *C.Metadata) bool - Count() int - ShouldResolveIP() bool - ShouldFindProcess() bool - Reset() - Insert(rule string) - FinishInsert() -} - -type mrsRuleStrategy interface { - ruleStrategy - FromMrs(r io.Reader, count int) error - WriteMrs(w io.Writer) error - DumpMrs(f func(key string) bool) -} - -type baseProvider struct { - behavior P.RuleBehavior - strategy ruleStrategy -} - -func (bp *baseProvider) Type() P.ProviderType { - return P.Rule -} - -func (bp *baseProvider) Behavior() P.RuleBehavior { - return bp.behavior -} - -func (bp *baseProvider) Count() int { - return bp.strategy.Count() -} - -func (bp *baseProvider) Match(metadata *C.Metadata) bool { - return bp.strategy != nil && bp.strategy.Match(metadata) -} - -func (bp *baseProvider) ShouldResolveIP() bool { - return bp.strategy.ShouldResolveIP() -} - -func (bp *baseProvider) ShouldFindProcess() bool { - return bp.strategy.ShouldFindProcess() -} - -func (bp *baseProvider) Strategy() any { - return bp.strategy -} - -type ruleSetProvider struct { - baseProvider - *resource.Fetcher[ruleStrategy] - format P.RuleFormat -} - -type RuleSetProvider struct { - *ruleSetProvider -} - -func (rp *ruleSetProvider) Initial() error { - _, err := rp.Fetcher.Initial() - return err -} - -func (rp *ruleSetProvider) Update() error { - _, _, err := rp.Fetcher.Update() - return err -} - -func (rp *ruleSetProvider) MarshalJSON() ([]byte, error) { - return json.Marshal( - providerForApi{ - Behavior: rp.behavior.String(), - Format: rp.format.String(), - Name: rp.Fetcher.Name(), - RuleCount: rp.strategy.Count(), - Type: rp.Type().String(), - UpdatedAt: rp.UpdatedAt(), - VehicleType: rp.VehicleType().String(), - }) -} - -func (rp *RuleSetProvider) Close() error { - runtime.SetFinalizer(rp, nil) - return rp.ruleSetProvider.Close() -} - -func NewRuleSetProvider(name string, behavior P.RuleBehavior, format P.RuleFormat, interval time.Duration, vehicle P.Vehicle, payload []string, parse common.ParseRuleFunc) P.RuleProvider { - rp := &ruleSetProvider{ - baseProvider: baseProvider{ - behavior: behavior, - }, - format: format, - } - - onUpdate := func(strategy ruleStrategy) { - rp.strategy = strategy - tunnel.RuleUpdateCallback().Emit(rp) - } - - rp.strategy = newStrategy(behavior, parse) - if len(payload) > 0 { // using as fallback rules - rp.strategy = rulesParseInline(payload, rp.strategy) - } - rp.Fetcher = resource.NewFetcher(name, interval, vehicle, func(bytes []byte) (ruleStrategy, error) { - return rulesParse(bytes, newStrategy(behavior, parse), format) - }, onUpdate) - - wrapper := &RuleSetProvider{ - rp, - } - - runtime.SetFinalizer(wrapper, (*RuleSetProvider).Close) - return wrapper -} - -func newStrategy(behavior P.RuleBehavior, parse common.ParseRuleFunc) ruleStrategy { - switch behavior { - case P.Domain: - strategy := NewDomainStrategy() - return strategy - case P.IPCIDR: - strategy := NewIPCidrStrategy() - return strategy - case P.Classical: - strategy := NewClassicalStrategy(parse) - return strategy - default: - return nil - } -} - -var ( - ErrNoPayload = errors.New("file must have a `payload` field") - ErrInvalidFormat = errors.New("invalid format") -) - -func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStrategy, error) { - strategy.Reset() - if format == P.MrsRule { - return rulesMrsParse(buf, strategy) - } - - schema := &RulePayload{} - - firstLineBuffer := pool.GetBuffer() - defer pool.PutBuffer(firstLineBuffer) - firstLineLength := 0 - - s := 0 // search start index - for s < len(buf) { - // search buffer for a new line. - line := buf[s:] - if i := bytes.IndexByte(line, '\n'); i >= 0 { - i += s - line = buf[s : i+1] - s = i + 1 - } else { - s = len(buf) // stop loop in next step - if firstLineLength == 0 && format == P.YamlRule { // no head or only one line body - return nil, ErrNoPayload - } - } - var str string - switch format { - case P.TextRule: - str = string(line) - str = strings.TrimSpace(str) - if len(str) == 0 { - continue - } - if str[0] == '#' { // comment - continue - } - if strings.HasPrefix(str, "//") { // comment in Premium core - continue - } - case P.YamlRule: - trimLine := bytes.TrimSpace(line) - if len(trimLine) == 0 { - continue - } - if trimLine[0] == '#' { // comment - continue - } - firstLineBuffer.Write(line) - if firstLineLength == 0 { // find payload head - firstLineLength = firstLineBuffer.Len() - firstLineBuffer.WriteString(" - ''") // a test line - - err := yaml.Unmarshal(firstLineBuffer.Bytes(), schema) - firstLineBuffer.Truncate(firstLineLength) - if err == nil && (len(schema.Rules) > 0 || len(schema.Payload) > 0) { // found - continue - } - - // not found or err!=nil - firstLineBuffer.Truncate(0) - firstLineLength = 0 - continue - } - - // parse payload body - err := yaml.Unmarshal(firstLineBuffer.Bytes(), schema) - firstLineBuffer.Truncate(firstLineLength) - if err != nil { - continue - } - - if len(schema.Rules) > 0 { - str = schema.Rules[0] - } - if len(schema.Payload) > 0 { - str = schema.Payload[0] - } - default: - return nil, ErrInvalidFormat - } - - if str == "" { - continue - } - - strategy.Insert(str) - } - - strategy.FinishInsert() - - return strategy, nil -} - -func rulesParseInline(rs []string, strategy ruleStrategy) ruleStrategy { - strategy.Reset() - for _, r := range rs { - if r != "" { - strategy.Insert(r) - } - } - strategy.FinishInsert() - return strategy -} - -type InlineProvider struct { - *inlineProvider -} - -type inlineProvider struct { - baseProvider - name string - updateAt time.Time - payload []string -} - -func (i *inlineProvider) Name() string { - return i.name -} - -func (i *inlineProvider) Initial() error { - return nil -} - -func (i *inlineProvider) Update() error { - // make api update happy - i.updateAt = time.Now() - return nil -} - -func (i *inlineProvider) VehicleType() P.VehicleType { - return P.Inline -} - -func (i *inlineProvider) MarshalJSON() ([]byte, error) { - return json.Marshal( - providerForApi{ - Behavior: i.behavior.String(), - Name: i.Name(), - RuleCount: i.strategy.Count(), - Type: i.Type().String(), - VehicleType: i.VehicleType().String(), - UpdatedAt: i.updateAt, - Payload: i.payload, - }) -} - -func NewInlineProvider(name string, behavior P.RuleBehavior, payload []string, parse common.ParseRuleFunc) P.RuleProvider { - ip := &inlineProvider{ - baseProvider: baseProvider{ - behavior: behavior, - strategy: newStrategy(behavior, parse), - }, - payload: payload, - name: name, - updateAt: time.Now(), - } - ip.strategy = rulesParseInline(payload, ip.strategy) - - wrapper := &InlineProvider{ - ip, - } - - //runtime.SetFinalizer(wrapper, (*InlineProvider).Close) - return wrapper -} diff --git a/rules/provider/rule_set.go b/rules/provider/rule_set.go deleted file mode 100644 index 2ad0bd3d49..0000000000 --- a/rules/provider/rule_set.go +++ /dev/null @@ -1,93 +0,0 @@ -package provider - -import ( - "net/netip" - - C "github.com/metacubex/mihomo/constant" - P "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/rules/common" -) - -type RuleSet struct { - *common.Base - ruleProviderName string - adapter string - isSrc bool - noResolveIP bool - shouldFindProcess bool -} - -func (rs *RuleSet) ShouldFindProcess() bool { - if rs.shouldFindProcess { - return true - } - if provider, ok := rs.getProvider(); ok { - return provider.ShouldFindProcess() - } - return false -} - -func (rs *RuleSet) RuleType() C.RuleType { - return C.RuleSet -} - -func (rs *RuleSet) Match(metadata *C.Metadata) (bool, string) { - if provider, ok := rs.getProvider(); ok { - if rs.isSrc { - metadata.SwapSrcDst() - defer metadata.SwapSrcDst() - } - return provider.Match(metadata), rs.adapter - } - return false, "" -} - -// MatchDomain implements C.DomainMatcher -func (rs *RuleSet) MatchDomain(domain string) bool { - ok, _ := rs.Match(&C.Metadata{Host: domain}) - return ok -} - -// MatchIp implements C.IpMatcher -func (rs *RuleSet) MatchIp(ip netip.Addr) bool { - ok, _ := rs.Match(&C.Metadata{DstIP: ip}) - return ok -} - -func (rs *RuleSet) Adapter() string { - return rs.adapter -} - -func (rs *RuleSet) Payload() string { - return rs.ruleProviderName -} - -func (rs *RuleSet) ShouldResolveIP() bool { - if rs.noResolveIP { - return false - } - if provider, ok := rs.getProvider(); ok { - return provider.ShouldResolveIP() - } - return false -} - -func (rs *RuleSet) ProviderNames() []string { - return []string{rs.ruleProviderName} -} - -func (rs *RuleSet) getProvider() (P.RuleProvider, bool) { - pp, ok := tunnel.RuleProviders()[rs.ruleProviderName] - return pp, ok -} - -func NewRuleSet(ruleProviderName string, adapter string, isSrc bool, noResolveIP bool) (*RuleSet, error) { - rs := &RuleSet{ - Base: &common.Base{}, - ruleProviderName: ruleProviderName, - adapter: adapter, - isSrc: isSrc, - noResolveIP: noResolveIP, - } - return rs, nil -} diff --git a/test/.golangci.yaml b/test/.golangci.yaml deleted file mode 100644 index e1fbbf76e4..0000000000 --- a/test/.golangci.yaml +++ /dev/null @@ -1,16 +0,0 @@ -linters: - disable-all: true - enable: - - gofumpt - - govet - - gci - - staticcheck - -linters-settings: - gci: - sections: - - standard - - prefix(github.com/metacubex/mihomo) - - default - staticcheck: - go: '1.19' diff --git a/test/Makefile b/test/Makefile deleted file mode 100644 index d238be0361..0000000000 --- a/test/Makefile +++ /dev/null @@ -1,9 +0,0 @@ -lint: - GOOS=darwin golangci-lint run ./... - GOOS=linux golangci-lint run ./... - -test: - go test -p 1 -v ./... - -benchmark: - go test -benchmem -run=^$$ -bench . diff --git a/test/README.md b/test/README.md deleted file mode 100644 index e9fa563011..0000000000 --- a/test/README.md +++ /dev/null @@ -1,59 +0,0 @@ -## Mihomo 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 mihomo -> (change chunkSize to measure the maximum throughput of mihomo on your machine) - -``` -$ make benchmark -``` diff --git a/test/clash_test.go b/test/clash_test.go deleted file mode 100644 index 90ac9d2286..0000000000 --- a/test/clash_test.go +++ /dev/null @@ -1,678 +0,0 @@ -package main - -import ( - "context" - "crypto/md5" - "crypto/rand" - "errors" - "fmt" - "io" - "net" - "net/netip" - "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/metacubex/mihomo/adapter/outbound" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/hub/executor" - "github.com/metacubex/mihomo/transport/socks5" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - ImageShadowsocks = "mritd/shadowsocks:latest" - ImageShadowsocksRust = "ghcr.io/shadowsocks/ssserver-rust:latest" - ImageVmess = "v2fly/v2fly-core:v4.45.2" - ImageVmessLatest = "sagernet/v2fly-core:latest" - ImageVless = "teddysun/xray:latest" - ImageTrojan = "trojangfw/trojan:latest" - ImageTrojanGo = "p4gefau1t/trojan-go:latest" - ImageSnell = "ghcr.io/icpz/snell-server:latest" - ImageXray = "teddysun/xray:latest" - ImageHysteria = "tobyxdd/hysteria:latest" -) - -var ( - waitTime = time.Second - localIP = netip.MustParseAddr("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, - ImageVless, - ImageTrojan, - ImageTrojanGo, - ImageSnell, - ImageXray, - ImageHysteria, - } - - 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) { - pingCh, pongCh, test := newPingPongPair() - go func() { - l, err := Listen("tcp", ":10001") - require.NoError(t, err) - defer l.Close() - - c, err := l.Accept() - require.NoError(t, err) - - buf := make([]byte, 4) - _, err = io.ReadFull(c, buf) - require.NoError(t, err) - - pingCh <- buf - _, err = c.Write([]byte("pong")) - require.NoError(t, err) - }() - - go func() { - c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer c.Close() - - _, err = socks5.ClientHandshake(c, socks5.ParseAddr("127.0.0.1:10001"), socks5.CmdConnect, nil) - require.NoError(t, err) - - _, err = c.Write([]byte("ping")) - require.NoError(t, err) - - buf := make([]byte, 4) - _, err = io.ReadFull(c, buf) - require.NoError(t, err) - - pongCh <- buf - }() - - test(t) -} - -func testPingPongWithConn(t *testing.T, cc func() 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 - } - }() - - c := cc() - defer c.Close() - - go func() { - if _, err := c.Write([]byte("ping")); err != nil { - return - } - - buf := make([]byte, 4) - if _, err := io.ReadFull(c, buf); err != nil { - t.Error(err) - 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.AsSlice(), 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, cc func() 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, - } - }() - - c := cc() - defer c.Close() - - 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.AsSlice(), 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{} - go func() { - for i := 0; i < times; i++ { - buf := make([]byte, chunkSize) - if _, err := rand.Read(buf[1:]); err != nil { - t.Log(err.Error()) - return - } - buf[0] = byte(i) - - hash := md5.Sum(buf) - mux.Lock() - hashMap[i] = hash[:] - mux.Unlock() - - if _, err := pc.WriteTo(buf, addr); err != nil { - t.Log(err.Error()) - return - } - } - }() - - 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) { - assert.NoError(t, testPingPongWithConn(t, func() net.Conn { - conn, err := proxy.DialContext(context.Background(), &C.Metadata{ - Host: localIP.String(), - DstPort: 10001, - }) - require.NoError(t, err) - return conn - })) - - assert.NoError(t, testLargeDataWithConn(t, func() net.Conn { - conn, err := proxy.DialContext(context.Background(), &C.Metadata{ - Host: localIP.String(), - DstPort: 10001, - }) - require.NoError(t, err) - return 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 TestMihomo_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(localIP.String(), "10000"))) - 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 deleted file mode 100644 index dbe9a3db3e..0000000000 --- a/test/config/example.org-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------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 deleted file mode 100644 index 9b99259a3a..0000000000 --- a/test/config/example.org.pem +++ /dev/null @@ -1,25 +0,0 @@ ------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/hysteria.json b/test/config/hysteria.json deleted file mode 100644 index 928d96ae15..0000000000 --- a/test/config/hysteria.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "listen": ":10002", - "cert": "/home/ubuntu/my.crt", - "key": "/home/ubuntu/my.key", - "obfs": "fuck me till the daylight", - "up_mbps": 100, - "down_mbps": 100 -} \ No newline at end of file diff --git a/test/config/snell-http.conf b/test/config/snell-http.conf deleted file mode 100644 index f5130dc10c..0000000000 --- a/test/config/snell-http.conf +++ /dev/null @@ -1,4 +0,0 @@ -[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 deleted file mode 100644 index bf8b51396b..0000000000 --- a/test/config/snell-tls.conf +++ /dev/null @@ -1,4 +0,0 @@ -[snell-server] -listen = 0.0.0.0:10002 -psk = password -obfs = tls diff --git a/test/config/snell.conf b/test/config/snell.conf deleted file mode 100644 index 40266391b1..0000000000 --- a/test/config/snell.conf +++ /dev/null @@ -1,3 +0,0 @@ -[snell-server] -listen = 0.0.0.0:10002 -psk = password diff --git a/test/config/trojan-grpc.json b/test/config/trojan-grpc.json deleted file mode 100644 index eb0dcc9903..0000000000 --- a/test/config/trojan-grpc.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "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 deleted file mode 100644 index efc0acbd06..0000000000 --- a/test/config/trojan-ws.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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-xtls.json b/test/config/trojan-xtls.json deleted file mode 100644 index c3a72eeedc..0000000000 --- a/test/config/trojan-xtls.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "inbounds": [ - { - "port": 10002, - "listen": "0.0.0.0", - "protocol": "trojan", - "settings": { - "clients": [ - { - "password": "example", - "email": "xtls@example.com", - "flow": "xtls-rprx-direct", - "level": 0 - } - ] - }, - "streamSettings": { - "network": "tcp", - "security": "xtls", - "xtlsSettings": { - "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/trojan.json b/test/config/trojan.json deleted file mode 100644 index 18e33a9801..0000000000 --- a/test/config/trojan.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "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/vless-tls.json b/test/config/vless-tls.json deleted file mode 100644 index ea20ec4159..0000000000 --- a/test/config/vless-tls.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "inbounds": [ - { - "port": 10002, - "listen": "0.0.0.0", - "protocol": "vless", - "settings": { - "clients": [ - { - "id": "b831381d-6324-4d53-ad4f-8cda48b30811", - "level": 0, - "email": "love@example.com" - } - ], - "decryption": "none" - }, - "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/vless-ws.json b/test/config/vless-ws.json deleted file mode 100644 index 8a59764437..0000000000 --- a/test/config/vless-ws.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "inbounds": [ - { - "port": 10002, - "listen": "0.0.0.0", - "protocol": "vless", - "settings": { - "clients": [ - { - "id": "b831381d-6324-4d53-ad4f-8cda48b30811", - "level": 0, - "email": "ws@example.com" - } - ], - "decryption": "none" - }, - "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/vless-xtls.json b/test/config/vless-xtls.json deleted file mode 100644 index 1d352c3fc6..0000000000 --- a/test/config/vless-xtls.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "inbounds": [ - { - "port": 10002, - "listen": "0.0.0.0", - "protocol": "vless", - "settings": { - "clients": [ - { - "id": "b831381d-6324-4d53-ad4f-8cda48b30811", - "email": "xtls@example.com", - "flow": "xtls-rprx-direct", - "level": 0 - } - ], - "decryption": "none" - }, - "streamSettings": { - "network": "tcp", - "security": "xtls", - "xtlsSettings": { - "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-grpc.json b/test/config/vmess-grpc.json deleted file mode 100644 index 178e0685fb..0000000000 --- a/test/config/vmess-grpc.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "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 deleted file mode 100644 index 90550c35fd..0000000000 --- a/test/config/vmess-http.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "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 deleted file mode 100644 index c6916a1b5a..0000000000 --- a/test/config/vmess-http2.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "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 deleted file mode 100644 index 17e87d662a..0000000000 --- a/test/config/vmess-tls.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "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 deleted file mode 100644 index c22909bff3..0000000000 --- a/test/config/vmess-ws-0rtt.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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.json b/test/config/vmess-ws-tls.json deleted file mode 100644 index 14278f3d8e..0000000000 --- a/test/config/vmess-ws-tls.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "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 deleted file mode 100644 index 2bcb604dda..0000000000 --- a/test/config/vmess-ws.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "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 deleted file mode 100644 index 1a8f935557..0000000000 --- a/test/config/vmess.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "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/config/xray-shadowsocks.json b/test/config/xray-shadowsocks.json deleted file mode 100644 index 1df376dc59..0000000000 --- a/test/config/xray-shadowsocks.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "inbounds": [ - { - "port": 10002, - "listen": "0.0.0.0", - "protocol": "shadowsocks", - "settings": { - "network": "tcp,udp", - "clients": [ - { - "method": "aes-128-gcm", - "level": 0, - "password": "FzcLbKs2dY9mhL" - } - ] - } - } - ], - "outbounds": [ - { - "protocol": "freedom" - } - ], - "log": { - "loglevel": "debug" - } -} \ No newline at end of file diff --git a/test/dns_test.go b/test/dns_test.go deleted file mode 100644 index f45ffbe07d..0000000000 --- a/test/dns_test.go +++ /dev/null @@ -1,100 +0,0 @@ -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 TestMihomo_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 TestMihomo_DNSHostAndFakeIP(t *testing.T) { - basic := ` -log-level: silent -hosts: - foo.mihomo.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.4"}, - {"bar.org", "198.18.0.5"}, - {"foo.org", "198.18.0.4"}, - {"foo.mihomo.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 deleted file mode 100644 index caaab216ba..0000000000 --- a/test/docker_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "context" - "os" - - "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 - } - - response, err := c.ContainerAttach(context.Background(), container.ID, types.ContainerAttachOptions{ - Stdout: true, - Stderr: true, - Logs: true, - }) - if err != nil { - return "", err - } - - go func() { - response.Reader.WriteTo(os.Stderr) - }() - - 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 deleted file mode 100644 index f364c2c89d..0000000000 --- a/test/go.mod +++ /dev/null @@ -1,122 +0,0 @@ -module mihomo-test - -go 1.20 - -require ( - github.com/docker/docker v20.10.21+incompatible - github.com/docker/go-connections v0.4.0 - github.com/metacubex/mihomo v0.0.0 - github.com/miekg/dns v1.1.57 - github.com/stretchr/testify v1.8.4 - golang.org/x/net v0.18.0 -) - -replace github.com/metacubex/mihomo => ../ - -require ( - github.com/3andne/restls-client-go v0.1.6 // indirect - github.com/Microsoft/go-winio v0.6.0 // indirect - github.com/RyuaNerin/go-krypto v1.0.2 // indirect - github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect - github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect - github.com/andybalholm/brotli v1.0.5 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/coreos/go-iptables v0.7.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/distribution/reference v0.5.0 // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect - github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect - github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect - github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/gaukas/godicttls v0.0.4 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.3.1 // indirect - github.com/gofrs/uuid/v5 v5.0.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/btree v1.1.2 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect - github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c // indirect - github.com/josharian/native v1.1.0 // indirect - github.com/jpillora/backoff v1.0.0 // indirect - github.com/klauspost/compress v1.16.7 // indirect - github.com/klauspost/cpuid/v2 v2.2.6 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mdlayher/netlink v1.7.2 // indirect - github.com/mdlayher/socket v0.4.1 // indirect - github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect - github.com/metacubex/gvisor v0.0.0-20231001104248-0f672c3fb8d8 // indirect - github.com/metacubex/quic-go v0.40.1-0.20231130135418-0c1b47cf9394 // indirect - github.com/metacubex/sing-quic v0.0.0-20231130141855-0022295e524b // indirect - github.com/metacubex/sing-shadowsocks v0.2.5 // indirect - github.com/metacubex/sing-shadowsocks2 v0.1.4 // indirect - github.com/metacubex/sing-tun v0.1.15-0.20231103033938-170591e8d5bd // indirect - github.com/metacubex/sing-vmess v0.1.9-0.20230921005247-a0488d7dac74 // indirect - github.com/metacubex/sing-wireguard v0.0.0-20231001110902-321836559170 // indirect - github.com/moby/term v0.5.0 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/mroth/weightedrand/v2 v2.1.0 // indirect - github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // indirect - github.com/openacid/low v0.1.21 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect - github.com/oschwald/maxminddb-golang v1.12.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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/puzpuzpuz/xsync/v3 v3.0.2 // indirect - github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-20 v0.4.1 // indirect - github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect - github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect - github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect - github.com/sagernet/sing v0.2.18-0.20231108041402-4fbbd193203c // indirect - github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 // indirect - github.com/sagernet/sing-shadowtls v0.1.4 // indirect - github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect - github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 // indirect - github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect - github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f // indirect - github.com/samber/lo v1.38.1 // indirect - github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect - github.com/shirou/gopsutil/v3 v3.23.10 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect - github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect - github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect - github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect - github.com/zhangyunhao116/fastrand v0.3.0 // indirect - gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect - go.uber.org/mock v0.3.0 // indirect - go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.15.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.2.1 // indirect -) diff --git a/test/go.sum b/test/go.sum deleted file mode 100644 index 05a23ce937..0000000000 --- a/test/go.sum +++ /dev/null @@ -1,314 +0,0 @@ -github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08= -github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= -github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= -github.com/RyuaNerin/go-krypto v1.0.2 h1:9KiZrrBs+tDrQ66dNy4nrX6SzntKtSKdm0wKHhdB4WM= -github.com/RyuaNerin/go-krypto v1.0.2/go.mod h1:17LzMeJCgzGTkPH3TmfzRnEJ/yA7ErhTPp9sxIqONtA= -github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= -github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= -github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -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/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -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.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog= -github.com/docker/docker v20.10.21+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.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= -github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= -github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= -github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= -github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= -github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY= -github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= -github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= -github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= -github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= -github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU= -github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c h1:PgxFEySCI41sH0mB7/2XswdXbUykQsRUGod8Rn+NubM= -github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -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/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -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/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= -github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= -github.com/metacubex/gvisor v0.0.0-20231001104248-0f672c3fb8d8 h1:npBvaPAT145UY8682AzpUMWpdIxJti/WPLjy7gCiYYs= -github.com/metacubex/gvisor v0.0.0-20231001104248-0f672c3fb8d8/go.mod h1:ZR6Gas7P1GcADCVBc1uOrA0bLQqDDyp70+63fD/BE2c= -github.com/metacubex/quic-go v0.40.1-0.20231130135418-0c1b47cf9394 h1:dIT+KB2hknBCrwVAXPeY9tpzzkOZP5m40yqUteRT6/Y= -github.com/metacubex/quic-go v0.40.1-0.20231130135418-0c1b47cf9394/go.mod h1:F/t8VnA47xoia8ABlNA4InkZjssvFJ5p6E6jKdbkgAs= -github.com/metacubex/sing-quic v0.0.0-20231130141855-0022295e524b h1:7XXoEePvxfkQN9b2wB8UXU3uzb9uL8syEFF7A9VAKKQ= -github.com/metacubex/sing-quic v0.0.0-20231130141855-0022295e524b/go.mod h1:Gu5/zqZDd5G1AUtoV2yjAPWOEy7zwbU2DBUjdxJh0Kw= -github.com/metacubex/sing-shadowsocks v0.2.5 h1:O2RRSHlKGEpAVG/OHJQxyHqDy8uvvdCW/oW2TDBOIhc= -github.com/metacubex/sing-shadowsocks v0.2.5/go.mod h1:Xz2uW9BEYGEoA8B4XEpoxt7ERHClFCwsMAvWaruoyMo= -github.com/metacubex/sing-shadowsocks2 v0.1.4 h1:OOCf8lgsVcpTOJUeaFAMzyKVebaQOBnKirDdUdBoKIE= -github.com/metacubex/sing-shadowsocks2 v0.1.4/go.mod h1:Qz028sLfdY3qxGRm9FDI+IM2Ae3ty2wR7HIzD/56h/k= -github.com/metacubex/sing-tun v0.1.15-0.20231103033938-170591e8d5bd h1:k0+92eARqyTAovGhg2AxdsMWHjUsdiGCnR5NuXF3CQY= -github.com/metacubex/sing-tun v0.1.15-0.20231103033938-170591e8d5bd/go.mod h1:Q7zmpJ+qOvMMXyUoYlxGQuWkqALUpXzFSSqO+KLPyzA= -github.com/metacubex/sing-vmess v0.1.9-0.20230921005247-a0488d7dac74 h1:FtupiyFkaVjFvRa7B/uDtRWg5BNsoyPC9MTev3sDasY= -github.com/metacubex/sing-vmess v0.1.9-0.20230921005247-a0488d7dac74/go.mod h1:8EWBZpc+qNvf5gmvjAtMHK1/DpcWqzfcBL842K00BsM= -github.com/metacubex/sing-wireguard v0.0.0-20231001110902-321836559170 h1:DBGA0hmrP4pVIwLiXUONdphjcppED+plmVaKf1oqkwk= -github.com/metacubex/sing-wireguard v0.0.0-20231001110902-321836559170/go.mod h1:/VbJfbdLnANE+SKXyMk/96sTRrD4GdFLh5mkegqqFcY= -github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= -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/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= -github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= -github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= -github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= -github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= -github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= -github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= -github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I= -github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do= -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/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= -github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= -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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= -github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= -github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= -github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= -github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= -github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= -github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= -github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= -github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= -github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= -github.com/sagernet/sing v0.2.18-0.20231108041402-4fbbd193203c h1:uask61Pxc3nGqsOSjqnBKrwfODWRoEa80lXm04LNk0E= -github.com/sagernet/sing v0.2.18-0.20231108041402-4fbbd193203c/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= -github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= -github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07/go.mod h1:u/MZf32xPG8jEKe3t+xUV67EBnKtDtCaPhsJQOQGUYU= -github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= -github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= -github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as= -github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37/go.mod h1:3skNSftZDJWTGVtVaM2jfbce8qHnmH/AGDRe62iNOg0= -github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 h1:Px+hN4Vzgx+iCGVnWH5A8eR7JhNnIV3rGQmBxA7cw6Q= -github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6/go.mod h1:zovq6vTvEM6ECiqE3Eeb9rpIylPpamPcmrJ9tv0Bt0M= -github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4= -github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= -github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho= -github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk= -github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= -github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= -github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= -github.com/shirou/gopsutil/v3 v3.23.10 h1:/N42opWlYzegYaVkWejXWJpbzKv2JDy3mrgGzKsh9hM= -github.com/shirou/gopsutil/v3 v3.23.10/go.mod h1:JIE26kpucQi+innVlAUnIEOSBhBUkirr5b44yr55+WE= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= -github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= -github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= -github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU= -github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo= -github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8= -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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -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/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -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.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig= -github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc= -gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= -gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= -go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= -go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ= -go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -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.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -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.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -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-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/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-20201204225414-ed752295db88/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-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -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-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -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.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= -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= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -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/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= -lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= -lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/test/hysteria_test.go b/test/hysteria_test.go deleted file mode 100644 index e783d9c2f3..0000000000 --- a/test/hysteria_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "fmt" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/metacubex/mihomo/adapter/outbound" - C "github.com/metacubex/mihomo/constant" - "github.com/stretchr/testify/assert" -) - -func TestMihomo_Hysteria(t *testing.T) { - cfg := &container.Config{ - Image: ImageHysteria, - ExposedPorts: defaultExposedPorts, - Cmd: []string{"server"}, - } - hostCfg := &container.HostConfig{ - PortBindings: defaultPortBindings, - Binds: []string{ - fmt.Sprintf("%s:/config.json", C.Path.Resolve("hysteria.json")), - fmt.Sprintf("%s:/home/ubuntu/my.crt", C.Path.Resolve("example.org.pem")), - fmt.Sprintf("%s:/home/ubuntu/my.key", C.Path.Resolve("example.org-key.pem")), - }, - } - - id, err := startContainer(cfg, hostCfg, "hysteria") - if err != nil { - assert.FailNow(t, err.Error()) - } - - t.Cleanup(func() { - cleanContainer(id) - }) - - proxy, err := outbound.NewHysteria(outbound.HysteriaOption{ - Name: "hysteria", - Server: localIP.String(), - Port: 10002, - Obfs: "fuck me till the daylight", - Up: "100", - Down: "100", - SkipCertVerify: true, - }) - if err != nil { - assert.FailNow(t, err.Error()) - } - - time.Sleep(waitTime) - testSuit(t, proxy) -} diff --git a/test/snell_test.go b/test/snell_test.go deleted file mode 100644 index 311ca7b776..0000000000 --- a/test/snell_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package main - -import ( - "fmt" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/metacubex/mihomo/adapter/outbound" - C "github.com/metacubex/mihomo/constant" - "github.com/stretchr/testify/require" -) - -func TestMihomo_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 TestMihomo_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 TestMihomo_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 TestMihomo_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 deleted file mode 100644 index 866fe3a845..0000000000 --- a/test/ss_test.go +++ /dev/null @@ -1,315 +0,0 @@ -package main - -import ( - "crypto/rand" - "encoding/base64" - "fmt" - "net" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/metacubex/mihomo/adapter/outbound" - C "github.com/metacubex/mihomo/constant" - "github.com/stretchr/testify/require" -) - -func TestMihomo_Shadowsocks(t *testing.T) { - for _, method := range []string{ - "aes-128-ctr", - "aes-192-ctr", - "aes-256-ctr", - "aes-128-cfb", - "aes-192-cfb", - "aes-256-cfb", - "rc4-md5", - "chacha20-ietf", - "aes-128-gcm", - "aes-256-gcm", - "chacha20-ietf-poly1305", - "xchacha20-ietf-poly1305", - } { - t.Run(method, func(t *testing.T) { - testMihomo_Shadowsocks(t, method, "FzcLbKs2dY9mhL") - }) - } - for _, method := range []string{ - "aes-128-gcm", - "aes-256-gcm", - "chacha20-ietf-poly1305", - } { - t.Run(method, func(t *testing.T) { - testMihomo_ShadowsocksRust(t, method, "FzcLbKs2dY9mhL") - }) - } -} - -func TestMihomo_Shadowsocks2022(t *testing.T) { - for _, method := range []string{ - "2022-blake3-aes-128-gcm", - } { - t.Run(method, func(t *testing.T) { - testMihomo_ShadowsocksRust(t, method, mkKey(16)) - }) - } - for _, method := range []string{ - "2022-blake3-aes-256-gcm", - "2022-blake3-chacha20-poly1305", - } { - t.Run(method, func(t *testing.T) { - testMihomo_ShadowsocksRust(t, method, mkKey(32)) - }) - } -} - -func mkKey(bits int) string { - k := make([]byte, bits) - rand.Read(k) - return base64.StdEncoding.EncodeToString(k) -} - -func testMihomo_Shadowsocks(t *testing.T, method string, password string) { - cfg := &container.Config{ - Image: ImageShadowsocks, - Env: []string{ - "SS_MODULE=ss-server", - "SS_CONFIG=-s 0.0.0.0 -u -p 10002 -m " + method + " -k " + password, - }, - 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: password, - Cipher: method, - UDP: true, - }) - require.NoError(t, err) - - time.Sleep(waitTime) - testSuit(t, proxy) -} - -func testMihomo_ShadowsocksRust(t *testing.T, method string, password string) { - cfg := &container.Config{ - Image: ImageShadowsocksRust, - Entrypoint: []string{"ssserver"}, - Cmd: []string{"-s", "0.0.0.0:10002", "-m", method, "-k", password, "-U", "-v"}, - ExposedPorts: defaultExposedPorts, - } - hostCfg := &container.HostConfig{ - PortBindings: defaultPortBindings, - } - - id, err := startContainer(cfg, hostCfg, "ss-rust") - require.NoError(t, err) - - t.Cleanup(func() { - cleanContainer(id) - }) - - proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{ - Name: "ss", - Server: localIP.String(), - Port: 10002, - Password: password, - Cipher: method, - UDP: true, - }) - require.NoError(t, err) - - time.Sleep(waitTime) - testSuit(t, proxy) -} - -func TestMihomo_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 TestMihomo_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 TestMihomo_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) -} - -func TestMihomo_ShadowsocksUoT(t *testing.T) { - configPath := C.Path.Resolve("xray-shadowsocks.json") - - cfg := &container.Config{ - Image: ImageVless, - ExposedPorts: defaultExposedPorts, - } - hostCfg := &container.HostConfig{ - PortBindings: defaultPortBindings, - Binds: []string{fmt.Sprintf("%s:/etc/xray/config.json", configPath)}, - } - - id, err := startContainer(cfg, hostCfg, "xray-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: "aes-128-gcm", - UDP: true, - UDPOverTCP: true, - }) - require.NoError(t, err) - - time.Sleep(waitTime) - testSuit(t, proxy) -} diff --git a/test/trojan_test.go b/test/trojan_test.go deleted file mode 100644 index c6b1fea09d..0000000000 --- a/test/trojan_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package main - -import ( - "fmt" - "net" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/metacubex/mihomo/adapter/outbound" - C "github.com/metacubex/mihomo/constant" - "github.com/stretchr/testify/require" -) - -func TestMihomo_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 TestMihomo_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 TestMihomo_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 TestMihomo_TrojanXTLS(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-xtls.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-xtls") - if err != nil { - require.NoError(t, err) - } - defer 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: "tcp", - Flow: "xtls-rprx-direct", - FlowShow: true, - }) - if err != nil { - 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 deleted file mode 100644 index 3d0fce886b..0000000000 --- a/test/util.go +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 6518a807eb..0000000000 --- a/test/util_darwin_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "fmt" - "net" - "net/netip" - "syscall" - - "golang.org/x/net/route" -) - -func defaultRouteIP() (netip.Addr, error) { - idx, err := defaultRouteInterfaceIndex() - if err != nil { - return netip.Addr{}, err - } - iface, err := net.InterfaceByIndex(idx) - if err != nil { - return netip.Addr{}, err - } - addrs, err := iface.Addrs() - if err != nil { - return netip.Addr{}, err - } - for _, addr := range addrs { - ip := addr.(*net.IPNet).IP - if ip.To4() != nil { - a, _ := netip.AddrFromSlice(ip) - return a, nil - } - } - - return netip.Addr{}, err -} - -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 deleted file mode 100644 index fc4a68af06..0000000000 --- a/test/util_other_test.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !darwin - -package main - -import ( - "errors" - "net/netip" -) - -func defaultRouteIP() (netip.Addr, error) { - return netip.Addr{}, errors.New("not supported") -} diff --git a/test/vless_test.go b/test/vless_test.go deleted file mode 100644 index d0e6f071a7..0000000000 --- a/test/vless_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package main - -import ( - "fmt" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/metacubex/mihomo/adapter/outbound" - C "github.com/metacubex/mihomo/constant" - "github.com/stretchr/testify/assert" -) - -// TODO: fix udp test -func TestMihomo_VlessTLS(t *testing.T) { - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - hostCfg := &container.HostConfig{ - PortBindings: defaultPortBindings, - Binds: []string{ - fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-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, "vless-tls") - if err != nil { - assert.FailNow(t, err.Error()) - } - defer cleanContainer(id) - - proxy, err := outbound.NewVless(outbound.VlessOption{ - Name: "vless", - Server: localIP.String(), - Port: 10002, - UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", - TLS: true, - SkipCertVerify: true, - ServerName: "example.org", - UDP: true, - }) - if err != nil { - assert.FailNow(t, err.Error()) - } - - time.Sleep(waitTime) - testSuit(t, proxy) -} - -// TODO: fix udp test -func TestMihomo_VlessXTLS(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("vless-xtls.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, "vless-xtls") - if err != nil { - assert.FailNow(t, err.Error()) - } - defer cleanContainer(id) - - proxy, err := outbound.NewVless(outbound.VlessOption{ - Name: "vless", - Server: localIP.String(), - Port: 10002, - UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", - TLS: true, - SkipCertVerify: true, - ServerName: "example.org", - UDP: true, - Flow: "xtls-rprx-direct", - }) - if err != nil { - assert.FailNow(t, err.Error()) - } - - time.Sleep(waitTime) - testSuit(t, proxy) -} - -// TODO: fix udp test -func TestMihomo_VlessWS(t *testing.T) { - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - hostCfg := &container.HostConfig{ - PortBindings: defaultPortBindings, - Binds: []string{ - fmt.Sprintf("%s:/etc/v2ray/config.json", C.Path.Resolve("vless-ws.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, "vless-ws") - if err != nil { - assert.FailNow(t, err.Error()) - } - defer cleanContainer(id) - - proxy, err := outbound.NewVless(outbound.VlessOption{ - Name: "vless", - Server: localIP.String(), - Port: 10002, - UUID: "b831381d-6324-4d53-ad4f-8cda48b30811", - TLS: true, - SkipCertVerify: true, - ServerName: "example.org", - Network: "ws", - UDP: true, - }) - if err != nil { - assert.FailNow(t, err.Error()) - } - - time.Sleep(waitTime) - testSuit(t, proxy) -} diff --git a/test/vmess_test.go b/test/vmess_test.go deleted file mode 100644 index 80c3d4d842..0000000000 --- a/test/vmess_test.go +++ /dev/null @@ -1,462 +0,0 @@ -package main - -import ( - "fmt" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/metacubex/mihomo/adapter/outbound" - C "github.com/metacubex/mihomo/constant" - "github.com/stretchr/testify/require" -) - -func TestMihomo_Vmess(t *testing.T) { - configPath := C.Path.Resolve("vmess.json") - - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - 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 TestMihomo_VmessAuthenticatedLength(t *testing.T) { - configPath := C.Path.Resolve("vmess.json") - - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - 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, - AuthenticatedLength: true, - }) - require.NoError(t, err) - - time.Sleep(waitTime) - testSuit(t, proxy) -} - -func TestMihomo_VmessPacketAddr(t *testing.T) { - configPath := C.Path.Resolve("vmess.json") - - cfg := &container.Config{ - Image: ImageVmessLatest, - ExposedPorts: defaultExposedPorts, - } - 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, - PacketAddr: true, - }) - require.NoError(t, err) - - time.Sleep(waitTime) - testSuit(t, proxy) -} - -func TestMihomo_VmessTLS(t *testing.T) { - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - 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 TestMihomo_VmessHTTP2(t *testing.T) { - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - 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 TestMihomo_VmessHTTP(t *testing.T) { - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - 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 TestMihomo_VmessWebsocket(t *testing.T) { - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - 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 TestMihomo_VmessWebsocketTLS(t *testing.T) { - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - 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") - 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 TestMihomo_VmessGrpc(t *testing.T) { - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - 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 TestMihomo_VmessWebsocket0RTT(t *testing.T) { - cfg := &container.Config{ - Image: ImageVmess, - ExposedPorts: defaultExposedPorts, - } - 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 TestMihomo_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, - } - 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/anytls/client.go b/transport/anytls/client.go deleted file mode 100644 index 44633ae345..0000000000 --- a/transport/anytls/client.go +++ /dev/null @@ -1,101 +0,0 @@ -package anytls - -import ( - "context" - "crypto/sha256" - "encoding/binary" - "net" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/buf" - "github.com/metacubex/mihomo/transport/anytls/padding" - "github.com/metacubex/mihomo/transport/anytls/session" - "github.com/metacubex/mihomo/transport/vmess" - - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" -) - -type ClientConfig struct { - Password string - IdleSessionCheckInterval time.Duration - IdleSessionTimeout time.Duration - MinIdleSession int - Server M.Socksaddr - Dialer N.Dialer - TLSConfig *vmess.TLSConfig -} - -type Client struct { - passwordSha256 []byte - tlsConfig *vmess.TLSConfig - dialer N.Dialer - server M.Socksaddr - sessionClient *session.Client - padding atomic.TypedValue[*padding.PaddingFactory] -} - -func NewClient(ctx context.Context, config ClientConfig) *Client { - pw := sha256.Sum256([]byte(config.Password)) - c := &Client{ - passwordSha256: pw[:], - tlsConfig: config.TLSConfig, - dialer: config.Dialer, - server: config.Server, - } - // Initialize the padding state of this client - padding.UpdatePaddingScheme(padding.DefaultPaddingScheme, &c.padding) - c.sessionClient = session.NewClient(ctx, c.CreateOutboundTLSConnection, &c.padding, config.IdleSessionCheckInterval, config.IdleSessionTimeout, config.MinIdleSession) - return c -} - -func (c *Client) CreateProxy(ctx context.Context, destination M.Socksaddr) (net.Conn, error) { - conn, err := c.sessionClient.CreateStream(ctx) - if err != nil { - return nil, err - } - err = M.SocksaddrSerializer.WriteAddrPort(conn, destination) - if err != nil { - conn.Close() - return nil, err - } - return conn, nil -} - -func (c *Client) CreateOutboundTLSConnection(ctx context.Context) (net.Conn, error) { - conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) - if err != nil { - return nil, err - } - - b := buf.NewPacket() - defer b.Release() - - b.Write(c.passwordSha256) - var paddingLen int - if pad := c.padding.Load().GenerateRecordPayloadSizes(0); len(pad) > 0 { - paddingLen = pad[0] - } - binary.BigEndian.PutUint16(b.Extend(2), uint16(paddingLen)) - if paddingLen > 0 { - b.WriteZeroN(paddingLen) - } - - tlsConn, err := vmess.StreamTLSConn(ctx, conn, c.tlsConfig) - if err != nil { - conn.Close() - return nil, err - } - - _, err = b.WriteTo(tlsConn) - if err != nil { - tlsConn.Close() - return nil, err - } - return tlsConn, nil -} - -func (h *Client) Close() error { - return h.sessionClient.Close() -} diff --git a/transport/anytls/padding/padding.go b/transport/anytls/padding/padding.go deleted file mode 100644 index 498feb05c0..0000000000 --- a/transport/anytls/padding/padding.go +++ /dev/null @@ -1,92 +0,0 @@ -package padding - -import ( - "crypto/md5" - "crypto/rand" - "fmt" - "math/big" - "strconv" - "strings" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/transport/anytls/util" -) - -const CheckMark = -1 - -var DefaultPaddingScheme = []byte(`stop=8 -0=30-30 -1=100-400 -2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000 -3=9-9,500-1000 -4=500-1000 -5=500-1000 -6=500-1000 -7=500-1000`) - -type PaddingFactory struct { - scheme util.StringMap - RawScheme []byte - Stop uint32 - Md5 string -} - -func UpdatePaddingScheme(rawScheme []byte, to *atomic.TypedValue[*PaddingFactory]) bool { - if p := NewPaddingFactory(rawScheme); p != nil { - to.Store(p) - return true - } - return false -} - -func NewPaddingFactory(rawScheme []byte) *PaddingFactory { - p := &PaddingFactory{ - RawScheme: rawScheme, - Md5: fmt.Sprintf("%x", md5.Sum(rawScheme)), - } - scheme := util.StringMapFromBytes(rawScheme) - if len(scheme) == 0 { - return nil - } - if stop, err := strconv.Atoi(scheme["stop"]); err == nil { - p.Stop = uint32(stop) - } else { - return nil - } - p.scheme = scheme - return p -} - -func (p *PaddingFactory) GenerateRecordPayloadSizes(pkt uint32) (pktSizes []int) { - if s, ok := p.scheme[strconv.Itoa(int(pkt))]; ok { - sRanges := strings.Split(s, ",") - for _, sRange := range sRanges { - sRangeMinMax := strings.Split(sRange, "-") - if len(sRangeMinMax) == 2 { - _min, err := strconv.ParseInt(sRangeMinMax[0], 10, 64) - if err != nil { - continue - } - _max, err := strconv.ParseInt(sRangeMinMax[1], 10, 64) - if err != nil { - continue - } - if _min > _max { - _min, _max = _max, _min - } - if _min <= 0 || _max <= 0 { - continue - } - if _min == _max { - pktSizes = append(pktSizes, int(_min)) - } else { - i, _ := rand.Int(rand.Reader, big.NewInt(_max-_min)) - pktSizes = append(pktSizes, int(i.Int64()+_min)) - } - } else if sRange == "c" { - pktSizes = append(pktSizes, CheckMark) - } - } - } - return -} diff --git a/transport/anytls/pipe/deadline.go b/transport/anytls/pipe/deadline.go deleted file mode 100644 index 29c4ec0ab7..0000000000 --- a/transport/anytls/pipe/deadline.go +++ /dev/null @@ -1,74 +0,0 @@ -package pipe - -import ( - "sync" - "time" -) - -// PipeDeadline is an abstraction for handling timeouts. -type PipeDeadline struct { - mu sync.Mutex // Guards timer and cancel - timer *time.Timer - cancel chan struct{} // Must be non-nil -} - -func MakePipeDeadline() PipeDeadline { - return PipeDeadline{cancel: make(chan struct{})} -} - -// Set sets the point in time when the deadline will time out. -// A timeout event is signaled by closing the channel returned by waiter. -// Once a timeout has occurred, the deadline can be refreshed by specifying a -// t value in the future. -// -// A zero value for t prevents timeout. -func (d *PipeDeadline) Set(t time.Time) { - d.mu.Lock() - defer d.mu.Unlock() - - if d.timer != nil && !d.timer.Stop() { - <-d.cancel // Wait for the timer callback to finish and close cancel - } - d.timer = nil - - // Time is zero, then there is no deadline. - closed := isClosedChan(d.cancel) - if t.IsZero() { - if closed { - d.cancel = make(chan struct{}) - } - return - } - - // Time in the future, setup a timer to cancel in the future. - if dur := time.Until(t); dur > 0 { - if closed { - d.cancel = make(chan struct{}) - } - d.timer = time.AfterFunc(dur, func() { - close(d.cancel) - }) - return - } - - // Time in the past, so close immediately. - if !closed { - close(d.cancel) - } -} - -// Wait returns a channel that is closed when the deadline is exceeded. -func (d *PipeDeadline) Wait() chan struct{} { - d.mu.Lock() - defer d.mu.Unlock() - return d.cancel -} - -func isClosedChan(c <-chan struct{}) bool { - select { - case <-c: - return true - default: - return false - } -} diff --git a/transport/anytls/pipe/io_pipe.go b/transport/anytls/pipe/io_pipe.go deleted file mode 100644 index 5d0fd252ec..0000000000 --- a/transport/anytls/pipe/io_pipe.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Pipe adapter to connect code expecting an io.Reader -// with code expecting an io.Writer. - -package pipe - -import ( - "io" - "os" - "sync" - "time" -) - -// onceError is an object that will only store an error once. -type onceError struct { - sync.Mutex // guards following - err error -} - -func (a *onceError) Store(err error) { - a.Lock() - defer a.Unlock() - if a.err != nil { - return - } - a.err = err -} -func (a *onceError) Load() error { - a.Lock() - defer a.Unlock() - return a.err -} - -// A pipe is the shared pipe structure underlying PipeReader and PipeWriter. -type pipe struct { - wrMu sync.Mutex // Serializes Write operations - wrCh chan []byte - rdCh chan int - - once sync.Once // Protects closing done - done chan struct{} - rerr onceError - werr onceError - - readDeadline PipeDeadline - writeDeadline PipeDeadline -} - -func (p *pipe) read(b []byte) (n int, err error) { - select { - case <-p.done: - return 0, p.readCloseError() - case <-p.readDeadline.Wait(): - return 0, os.ErrDeadlineExceeded - default: - } - - select { - case bw := <-p.wrCh: - nr := copy(b, bw) - p.rdCh <- nr - return nr, nil - case <-p.done: - return 0, p.readCloseError() - case <-p.readDeadline.Wait(): - return 0, os.ErrDeadlineExceeded - } -} - -func (p *pipe) closeRead(err error) error { - if err == nil { - err = io.ErrClosedPipe - } - p.rerr.Store(err) - p.once.Do(func() { close(p.done) }) - return nil -} - -func (p *pipe) write(b []byte) (n int, err error) { - select { - case <-p.done: - return 0, p.writeCloseError() - case <-p.writeDeadline.Wait(): - return 0, os.ErrDeadlineExceeded - default: - p.wrMu.Lock() - defer p.wrMu.Unlock() - } - - for once := true; once || len(b) > 0; once = false { - select { - case p.wrCh <- b: - nw := <-p.rdCh - b = b[nw:] - n += nw - case <-p.done: - return n, p.writeCloseError() - case <-p.writeDeadline.Wait(): - return n, os.ErrDeadlineExceeded - } - } - return n, nil -} - -func (p *pipe) closeWrite(err error) error { - if err == nil { - err = io.EOF - } - p.werr.Store(err) - p.once.Do(func() { close(p.done) }) - return nil -} - -// readCloseError is considered internal to the pipe type. -func (p *pipe) readCloseError() error { - rerr := p.rerr.Load() - if werr := p.werr.Load(); rerr == nil && werr != nil { - return werr - } - return io.ErrClosedPipe -} - -// writeCloseError is considered internal to the pipe type. -func (p *pipe) writeCloseError() error { - werr := p.werr.Load() - if rerr := p.rerr.Load(); werr == nil && rerr != nil { - return rerr - } - return io.ErrClosedPipe -} - -// A PipeReader is the read half of a pipe. -type PipeReader struct{ pipe } - -// Read implements the standard Read interface: -// it reads data from the pipe, blocking until a writer -// arrives or the write end is closed. -// If the write end is closed with an error, that error is -// returned as err; otherwise err is EOF. -func (r *PipeReader) Read(data []byte) (n int, err error) { - return r.pipe.read(data) -} - -// Close closes the reader; subsequent writes to the -// write half of the pipe will return the error [ErrClosedPipe]. -func (r *PipeReader) Close() error { - return r.CloseWithError(nil) -} - -// CloseWithError closes the reader; subsequent writes -// to the write half of the pipe will return the error err. -// -// CloseWithError never overwrites the previous error if it exists -// and always returns nil. -func (r *PipeReader) CloseWithError(err error) error { - return r.pipe.closeRead(err) -} - -// A PipeWriter is the write half of a pipe. -type PipeWriter struct{ r PipeReader } - -// Write implements the standard Write interface: -// it writes data to the pipe, blocking until one or more readers -// have consumed all the data or the read end is closed. -// If the read end is closed with an error, that err is -// returned as err; otherwise err is [ErrClosedPipe]. -func (w *PipeWriter) Write(data []byte) (n int, err error) { - return w.r.pipe.write(data) -} - -// Close closes the writer; subsequent reads from the -// read half of the pipe will return no bytes and EOF. -func (w *PipeWriter) Close() error { - return w.CloseWithError(nil) -} - -// CloseWithError closes the writer; subsequent reads from the -// read half of the pipe will return no bytes and the error err, -// or EOF if err is nil. -// -// CloseWithError never overwrites the previous error if it exists -// and always returns nil. -func (w *PipeWriter) CloseWithError(err error) error { - return w.r.pipe.closeWrite(err) -} - -// Pipe creates a synchronous in-memory pipe. -// It can be used to connect code expecting an [io.Reader] -// with code expecting an [io.Writer]. -// -// Reads and Writes on the pipe are matched one to one -// except when multiple Reads are needed to consume a single Write. -// That is, each Write to the [PipeWriter] blocks until it has satisfied -// one or more Reads from the [PipeReader] that fully consume -// the written data. -// The data is copied directly from the Write to the corresponding -// Read (or Reads); there is no internal buffering. -// -// It is safe to call Read and Write in parallel with each other or with Close. -// Parallel calls to Read and parallel calls to Write are also safe: -// the individual calls will be gated sequentially. -// -// Added SetReadDeadline and SetWriteDeadline methods based on `io.Pipe`. -func Pipe() (*PipeReader, *PipeWriter) { - pw := &PipeWriter{r: PipeReader{pipe: pipe{ - wrCh: make(chan []byte), - rdCh: make(chan int), - done: make(chan struct{}), - readDeadline: MakePipeDeadline(), - writeDeadline: MakePipeDeadline(), - }}} - return &pw.r, pw -} - -func (p *PipeReader) SetReadDeadline(t time.Time) error { - if isClosedChan(p.done) { - return io.ErrClosedPipe - } - p.readDeadline.Set(t) - return nil -} - -func (p *PipeWriter) SetWriteDeadline(t time.Time) error { - if isClosedChan(p.r.done) { - return io.ErrClosedPipe - } - p.r.writeDeadline.Set(t) - return nil -} diff --git a/transport/anytls/session/client.go b/transport/anytls/session/client.go deleted file mode 100644 index 50fd7b42ef..0000000000 --- a/transport/anytls/session/client.go +++ /dev/null @@ -1,201 +0,0 @@ -package session - -import ( - "context" - "fmt" - "io" - "math" - "net" - "sync" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/transport/anytls/padding" - "github.com/metacubex/mihomo/transport/anytls/skiplist" - "github.com/metacubex/mihomo/transport/anytls/util" -) - -type Client struct { - die context.Context - dieCancel context.CancelFunc - - dialOut util.DialOutFunc - - sessionCounter atomic.Uint64 - - idleSession *skiplist.SkipList[uint64, *Session] - idleSessionLock sync.Mutex - - sessions map[uint64]*Session - sessionsLock sync.Mutex - - padding *atomic.TypedValue[*padding.PaddingFactory] - - idleSessionTimeout time.Duration - minIdleSession int -} - -func NewClient(ctx context.Context, dialOut util.DialOutFunc, _padding *atomic.TypedValue[*padding.PaddingFactory], idleSessionCheckInterval, idleSessionTimeout time.Duration, minIdleSession int) *Client { - c := &Client{ - sessions: make(map[uint64]*Session), - dialOut: dialOut, - padding: _padding, - idleSessionTimeout: idleSessionTimeout, - minIdleSession: minIdleSession, - } - if idleSessionCheckInterval <= time.Second*5 { - idleSessionCheckInterval = time.Second * 30 - } - if c.idleSessionTimeout <= time.Second*5 { - c.idleSessionTimeout = time.Second * 30 - } - c.die, c.dieCancel = context.WithCancel(ctx) - c.idleSession = skiplist.NewSkipList[uint64, *Session]() - util.StartRoutine(c.die, idleSessionCheckInterval, c.idleCleanup) - return c -} - -func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) { - select { - case <-c.die.Done(): - return nil, io.ErrClosedPipe - default: - } - - var session *Session - var stream *Stream - var err error - - for i := 0; i < 3; i++ { - session, err = c.findSession(ctx) - if session == nil { - return nil, fmt.Errorf("failed to create session: %w", err) - } - stream, err = session.OpenStream() - if err != nil { - _ = session.Close() - continue - } - break - } - if session == nil || stream == nil { - return nil, fmt.Errorf("too many closed session: %w", err) - } - - stream.dieHook = func() { - if !session.IsClosed() { - select { - case <-c.die.Done(): - // Now client has been closed - go session.Close() - default: - c.idleSessionLock.Lock() - session.idleSince = time.Now() - c.idleSession.Insert(math.MaxUint64-session.seq, session) - c.idleSessionLock.Unlock() - } - } - } - - return stream, nil -} - -func (c *Client) findSession(ctx context.Context) (*Session, error) { - var idle *Session - - c.idleSessionLock.Lock() - if !c.idleSession.IsEmpty() { - it := c.idleSession.Iterate() - idle = it.Value() - c.idleSession.Remove(it.Key()) - } - c.idleSessionLock.Unlock() - - if idle == nil { - s, err := c.createSession(ctx) - return s, err - } - return idle, nil -} - -func (c *Client) createSession(ctx context.Context) (*Session, error) { - underlying, err := c.dialOut(ctx) - if err != nil { - return nil, err - } - - session := NewClientSession(underlying, c.padding) - session.seq = c.sessionCounter.Add(1) - session.dieHook = func() { - //logrus.Debugln("session died", session) - c.idleSessionLock.Lock() - c.idleSession.Remove(math.MaxUint64 - session.seq) - c.idleSessionLock.Unlock() - - c.sessionsLock.Lock() - delete(c.sessions, session.seq) - c.sessionsLock.Unlock() - } - - c.sessionsLock.Lock() - c.sessions[session.seq] = session - c.sessionsLock.Unlock() - - session.Run() - return session, nil -} - -func (c *Client) Close() error { - c.dieCancel() - - c.sessionsLock.Lock() - sessionToClose := make([]*Session, 0, len(c.sessions)) - for _, session := range c.sessions { - sessionToClose = append(sessionToClose, session) - } - c.sessions = make(map[uint64]*Session) - c.sessionsLock.Unlock() - - for _, session := range sessionToClose { - session.Close() - } - - return nil -} - -func (c *Client) idleCleanup() { - c.idleCleanupExpTime(time.Now().Add(-c.idleSessionTimeout)) -} - -func (c *Client) idleCleanupExpTime(expTime time.Time) { - sessionToRemove := make([]*Session, 0, c.idleSession.Len()) - - c.idleSessionLock.Lock() - it := c.idleSession.Iterate() - - activeCount := 0 - for it.IsNotEnd() { - session := it.Value() - key := it.Key() - it.MoveToNext() - - if !session.idleSince.Before(expTime) { - activeCount++ - continue - } - - if activeCount < c.minIdleSession { - session.idleSince = time.Now() - activeCount++ - continue - } - - sessionToRemove = append(sessionToRemove, session) - c.idleSession.Remove(key) - } - c.idleSessionLock.Unlock() - - for _, session := range sessionToRemove { - session.Close() - } -} diff --git a/transport/anytls/session/frame.go b/transport/anytls/session/frame.go deleted file mode 100644 index 8f6283213d..0000000000 --- a/transport/anytls/session/frame.go +++ /dev/null @@ -1,49 +0,0 @@ -package session - -import ( - "encoding/binary" -) - -const ( // cmds - cmdWaste = 0 // Paddings - cmdSYN = 1 // stream open - cmdPSH = 2 // data push - cmdFIN = 3 // stream close, a.k.a EOF mark - cmdSettings = 4 // Settings (Client send to Server) - cmdAlert = 5 // Alert - cmdUpdatePaddingScheme = 6 // update padding scheme - // Since version 2 - cmdSYNACK = 7 // Server reports to the client that the stream has been opened - cmdHeartRequest = 8 // Keep alive command - cmdHeartResponse = 9 // Keep alive command - cmdServerSettings = 10 // Settings (Server send to client) -) - -const ( - headerOverHeadSize = 1 + 4 + 2 -) - -// frame defines a packet from or to be multiplexed into a single connection -type frame struct { - cmd byte // 1 - sid uint32 // 4 - data []byte // 2 + len(data) -} - -func newFrame(cmd byte, sid uint32) frame { - return frame{cmd: cmd, sid: sid} -} - -type rawHeader [headerOverHeadSize]byte - -func (h rawHeader) Cmd() byte { - return h[0] -} - -func (h rawHeader) StreamID() uint32 { - return binary.BigEndian.Uint32(h[1:]) -} - -func (h rawHeader) Length() uint16 { - return binary.BigEndian.Uint16(h[5:]) -} diff --git a/transport/anytls/session/session.go b/transport/anytls/session/session.go deleted file mode 100644 index c80639ce3b..0000000000 --- a/transport/anytls/session/session.go +++ /dev/null @@ -1,465 +0,0 @@ -package session - -import ( - "crypto/md5" - "encoding/binary" - "fmt" - "io" - "net" - "runtime/debug" - "strconv" - "sync" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/buf" - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/anytls/padding" - "github.com/metacubex/mihomo/transport/anytls/util" -) - -type Session struct { - conn net.Conn - connLock sync.Mutex - - streams map[uint32]*Stream - streamId atomic.Uint32 - streamLock sync.RWMutex - - dieOnce sync.Once - die chan struct{} - dieHook func() - - synDone func() - synDoneLock sync.Mutex - - // pool - seq uint64 - idleSince time.Time - padding *atomic.TypedValue[*padding.PaddingFactory] - - peerVersion byte - - // client - isClient bool - sendPadding bool - buffering bool - buffer []byte - pktCounter atomic.Uint32 - - // server - onNewStream func(stream *Stream) -} - -func NewClientSession(conn net.Conn, _padding *atomic.TypedValue[*padding.PaddingFactory]) *Session { - s := &Session{ - conn: conn, - isClient: true, - sendPadding: true, - padding: _padding, - } - s.die = make(chan struct{}) - s.streams = make(map[uint32]*Stream) - return s -} - -func NewServerSession(conn net.Conn, onNewStream func(stream *Stream), _padding *atomic.TypedValue[*padding.PaddingFactory]) *Session { - s := &Session{ - conn: conn, - onNewStream: onNewStream, - padding: _padding, - } - s.die = make(chan struct{}) - s.streams = make(map[uint32]*Stream) - return s -} - -func (s *Session) Run() { - if !s.isClient { - s.recvLoop() - return - } - - settings := util.StringMap{ - "v": "2", - "client": "mihomo/" + constant.Version, - "padding-md5": s.padding.Load().Md5, - } - f := newFrame(cmdSettings, 0) - f.data = settings.ToBytes() - s.buffering = true - s.writeFrame(f) - - go s.recvLoop() -} - -// IsClosed does a safe check to see if we have shutdown -func (s *Session) IsClosed() bool { - select { - case <-s.die: - return true - default: - return false - } -} - -// Close is used to close the session and all streams. -func (s *Session) Close() error { - var once bool - s.dieOnce.Do(func() { - close(s.die) - once = true - }) - if once { - if s.dieHook != nil { - s.dieHook() - s.dieHook = nil - } - s.streamLock.Lock() - for _, stream := range s.streams { - stream.Close() - } - s.streams = make(map[uint32]*Stream) - s.streamLock.Unlock() - return s.conn.Close() - } else { - return io.ErrClosedPipe - } -} - -// OpenStream is used to create a new stream for CLIENT -func (s *Session) OpenStream() (*Stream, error) { - if s.IsClosed() { - return nil, io.ErrClosedPipe - } - - sid := s.streamId.Add(1) - stream := newStream(sid, s) - - //logrus.Debugln("stream open", sid, s.streams) - - if sid >= 2 && s.peerVersion >= 2 { - s.synDoneLock.Lock() - if s.synDone != nil { - s.synDone() - } - s.synDone = util.NewDeadlineWatcher(time.Second*3, func() { - s.Close() - }) - s.synDoneLock.Unlock() - } - - if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil { - return nil, err - } - - s.buffering = false // proxy Write it's SocksAddr to flush the buffer - - s.streamLock.Lock() - defer s.streamLock.Unlock() - select { - case <-s.die: - return nil, io.ErrClosedPipe - default: - s.streams[sid] = stream - return stream, nil - } -} - -func (s *Session) recvLoop() error { - defer func() { - if r := recover(); r != nil { - log.Errorln("[BUG] %v %s", r, string(debug.Stack())) - } - }() - defer s.Close() - - var receivedSettingsFromClient bool - var hdr rawHeader - - for { - if s.IsClosed() { - return io.ErrClosedPipe - } - // read header first - if _, err := io.ReadFull(s.conn, hdr[:]); err == nil { - sid := hdr.StreamID() - switch hdr.Cmd() { - case cmdPSH: - if hdr.Length() > 0 { - buffer := pool.Get(int(hdr.Length())) - if _, err := io.ReadFull(s.conn, buffer); err == nil { - s.streamLock.RLock() - stream, ok := s.streams[sid] - s.streamLock.RUnlock() - if ok { - stream.pipeW.Write(buffer) - } - pool.Put(buffer) - } else { - pool.Put(buffer) - return err - } - } - case cmdSYN: // should be server only - if !s.isClient && !receivedSettingsFromClient { - f := newFrame(cmdAlert, 0) - f.data = []byte("client did not send its settings") - s.writeFrame(f) - return nil - } - s.streamLock.Lock() - if _, ok := s.streams[sid]; !ok { - stream := newStream(sid, s) - s.streams[sid] = stream - go func() { - if s.onNewStream != nil { - s.onNewStream(stream) - } else { - stream.Close() - } - }() - } - s.streamLock.Unlock() - case cmdSYNACK: // should be client only - s.synDoneLock.Lock() - if s.synDone != nil { - s.synDone() - s.synDone = nil - } - s.synDoneLock.Unlock() - if hdr.Length() > 0 { - buffer := pool.Get(int(hdr.Length())) - if _, err := io.ReadFull(s.conn, buffer); err != nil { - pool.Put(buffer) - return err - } - // report error - s.streamLock.RLock() - stream, ok := s.streams[sid] - s.streamLock.RUnlock() - if ok { - stream.CloseWithError(fmt.Errorf("remote: %s", string(buffer))) - } - pool.Put(buffer) - } - case cmdFIN: - s.streamLock.RLock() - stream, ok := s.streams[sid] - s.streamLock.RUnlock() - if ok { - stream.Close() - } - //logrus.Debugln("stream fin", sid, s.streams) - case cmdWaste: - if hdr.Length() > 0 { - buffer := pool.Get(int(hdr.Length())) - if _, err := io.ReadFull(s.conn, buffer); err != nil { - pool.Put(buffer) - return err - } - pool.Put(buffer) - } - case cmdSettings: - if hdr.Length() > 0 { - buffer := pool.Get(int(hdr.Length())) - if _, err := io.ReadFull(s.conn, buffer); err != nil { - pool.Put(buffer) - return err - } - if !s.isClient { - receivedSettingsFromClient = true - m := util.StringMapFromBytes(buffer) - paddingF := s.padding.Load() - if m["padding-md5"] != paddingF.Md5 { - // logrus.Debugln("remote md5 is", m["padding-md5"]) - f := newFrame(cmdUpdatePaddingScheme, 0) - f.data = paddingF.RawScheme - _, err = s.writeFrame(f) - if err != nil { - pool.Put(buffer) - return err - } - } - // check client's version - if v, err := strconv.Atoi(m["v"]); err == nil && v >= 2 { - s.peerVersion = byte(v) - // send cmdServerSettings - f := newFrame(cmdServerSettings, 0) - f.data = util.StringMap{ - "v": "2", - }.ToBytes() - _, err = s.writeFrame(f) - if err != nil { - pool.Put(buffer) - return err - } - } - } - pool.Put(buffer) - } - case cmdAlert: - if hdr.Length() > 0 { - buffer := pool.Get(int(hdr.Length())) - if _, err := io.ReadFull(s.conn, buffer); err != nil { - pool.Put(buffer) - return err - } - if s.isClient { - log.Errorln("[Alert from server] %s", string(buffer)) - } - pool.Put(buffer) - return nil - } - case cmdUpdatePaddingScheme: - if hdr.Length() > 0 { - // `rawScheme` Do not use buffer to prevent subsequent misuse - rawScheme := make([]byte, int(hdr.Length())) - if _, err := io.ReadFull(s.conn, rawScheme); err != nil { - return err - } - if s.isClient { - if padding.UpdatePaddingScheme(rawScheme, s.padding) { - log.Debugln("[Update padding succeed] %x\n", md5.Sum(rawScheme)) - } else { - log.Warnln("[Update padding failed] %x\n", md5.Sum(rawScheme)) - } - } - } - case cmdHeartRequest: - if _, err := s.writeFrame(newFrame(cmdHeartResponse, sid)); err != nil { - return err - } - case cmdHeartResponse: - // Active keepalive checking is not implemented yet - break - case cmdServerSettings: - if hdr.Length() > 0 { - buffer := pool.Get(int(hdr.Length())) - if _, err := io.ReadFull(s.conn, buffer); err != nil { - pool.Put(buffer) - return err - } - if s.isClient { - // check server's version - m := util.StringMapFromBytes(buffer) - if v, err := strconv.Atoi(m["v"]); err == nil { - s.peerVersion = byte(v) - } - } - pool.Put(buffer) - } - default: - // I don't know what command it is (can't have data) - } - } else { - return err - } - } -} - -func (s *Session) streamClosed(sid uint32) error { - if s.IsClosed() { - return io.ErrClosedPipe - } - _, err := s.writeFrame(newFrame(cmdFIN, sid)) - s.streamLock.Lock() - delete(s.streams, sid) - s.streamLock.Unlock() - return err -} - -func (s *Session) writeFrame(frame frame) (int, error) { - dataLen := len(frame.data) - - buffer := buf.NewSize(dataLen + headerOverHeadSize) - buffer.WriteByte(frame.cmd) - binary.BigEndian.PutUint32(buffer.Extend(4), frame.sid) - binary.BigEndian.PutUint16(buffer.Extend(2), uint16(dataLen)) - buffer.Write(frame.data) - _, err := s.writeConn(buffer.Bytes()) - buffer.Release() - if err != nil { - return 0, err - } - - return dataLen, nil -} - -func (s *Session) writeConn(b []byte) (n int, err error) { - s.connLock.Lock() - defer s.connLock.Unlock() - - if s.buffering { - s.buffer = append(s.buffer, b...) - return len(b), nil - } else if len(s.buffer) > 0 { - b = append(s.buffer, b...) - s.buffer = nil - } - - // calulate & send padding - if s.sendPadding { - pkt := s.pktCounter.Add(1) - paddingF := s.padding.Load() - if pkt < paddingF.Stop { - pktSizes := paddingF.GenerateRecordPayloadSizes(pkt) - for _, l := range pktSizes { - remainPayloadLen := len(b) - if l == padding.CheckMark { - if remainPayloadLen == 0 { - break - } else { - continue - } - } - if remainPayloadLen > l { // this packet is all payload - _, err = s.conn.Write(b[:l]) - if err != nil { - return 0, err - } - n += l - b = b[l:] - } else if remainPayloadLen > 0 { // this packet contains padding and the last part of payload - paddingLen := l - remainPayloadLen - headerOverHeadSize - if paddingLen > 0 { - padding := make([]byte, headerOverHeadSize+paddingLen) - padding[0] = cmdWaste - binary.BigEndian.PutUint32(padding[1:5], 0) - binary.BigEndian.PutUint16(padding[5:7], uint16(paddingLen)) - b = append(b, padding...) - } - _, err = s.conn.Write(b) - if err != nil { - return 0, err - } - n += remainPayloadLen - b = nil - } else { // this packet is all padding - padding := make([]byte, headerOverHeadSize+l) - padding[0] = cmdWaste - binary.BigEndian.PutUint32(padding[1:5], 0) - binary.BigEndian.PutUint16(padding[5:7], uint16(l)) - _, err = s.conn.Write(padding) - if err != nil { - return 0, err - } - b = nil - } - } - // maybe still remain payload to write - if len(b) == 0 { - return - } else { - n2, err := s.conn.Write(b) - return n + n2, err - } - } else { - s.sendPadding = false - } - } - - return s.conn.Write(b) -} diff --git a/transport/anytls/session/stream.go b/transport/anytls/session/stream.go deleted file mode 100644 index f7e8de673f..0000000000 --- a/transport/anytls/session/stream.go +++ /dev/null @@ -1,150 +0,0 @@ -package session - -import ( - "io" - "net" - "os" - "sync" - "time" - - "github.com/metacubex/mihomo/transport/anytls/pipe" -) - -// Stream implements net.Conn -type Stream struct { - id uint32 - - sess *Session - - pipeR *pipe.PipeReader - pipeW *pipe.PipeWriter - writeDeadline pipe.PipeDeadline - - dieOnce sync.Once - dieHook func() - dieErr error - - reportOnce sync.Once -} - -// newStream initiates a Stream struct -func newStream(id uint32, sess *Session) *Stream { - s := new(Stream) - s.id = id - s.sess = sess - s.pipeR, s.pipeW = pipe.Pipe() - s.writeDeadline = pipe.MakePipeDeadline() - return s -} - -// Read implements net.Conn -func (s *Stream) Read(b []byte) (n int, err error) { - n, err = s.pipeR.Read(b) - if n == 0 && s.dieErr != nil { - err = s.dieErr - } - return -} - -// Write implements net.Conn -func (s *Stream) Write(b []byte) (n int, err error) { - select { - case <-s.writeDeadline.Wait(): - return 0, os.ErrDeadlineExceeded - default: - } - f := newFrame(cmdPSH, s.id) - f.data = b - n, err = s.sess.writeFrame(f) - return -} - -// Close implements net.Conn -func (s *Stream) Close() error { - return s.CloseWithError(io.ErrClosedPipe) -} - -func (s *Stream) CloseWithError(err error) error { - // if err != io.ErrClosedPipe { - // logrus.Debugln(err) - // } - var once bool - s.dieOnce.Do(func() { - s.dieErr = err - s.pipeR.Close() - once = true - }) - if once { - if s.dieHook != nil { - s.dieHook() - s.dieHook = nil - } - return s.sess.streamClosed(s.id) - } else { - return s.dieErr - } -} - -func (s *Stream) SetReadDeadline(t time.Time) error { - return s.pipeR.SetReadDeadline(t) -} - -func (s *Stream) SetWriteDeadline(t time.Time) error { - s.writeDeadline.Set(t) - return nil -} - -func (s *Stream) SetDeadline(t time.Time) error { - s.SetWriteDeadline(t) - return s.SetReadDeadline(t) -} - -// LocalAddr satisfies net.Conn interface -func (s *Stream) LocalAddr() net.Addr { - if ts, ok := s.sess.conn.(interface { - LocalAddr() net.Addr - }); ok { - return ts.LocalAddr() - } - return nil -} - -// RemoteAddr satisfies net.Conn interface -func (s *Stream) RemoteAddr() net.Addr { - if ts, ok := s.sess.conn.(interface { - RemoteAddr() net.Addr - }); ok { - return ts.RemoteAddr() - } - return nil -} - -// HandshakeFailure should be called when Server fail to create outbound proxy -func (s *Stream) HandshakeFailure(err error) error { - var once bool - s.reportOnce.Do(func() { - once = true - }) - if once && err != nil && s.sess.peerVersion >= 2 { - f := newFrame(cmdSYNACK, s.id) - f.data = []byte(err.Error()) - if _, err := s.sess.writeFrame(f); err != nil { - return err - } - } - return nil -} - -// HandshakeSuccess should be called when Server success to create outbound proxy -func (s *Stream) HandshakeSuccess() error { - var once bool - s.reportOnce.Do(func() { - once = true - }) - if once && s.sess.peerVersion >= 2 { - if _, err := s.sess.writeFrame(newFrame(cmdSYNACK, s.id)); err != nil { - return err - } - } - return nil -} diff --git a/transport/anytls/skiplist/contianer.go b/transport/anytls/skiplist/contianer.go deleted file mode 100644 index ceda042193..0000000000 --- a/transport/anytls/skiplist/contianer.go +++ /dev/null @@ -1,46 +0,0 @@ -package skiplist - -// Container is a holder object that stores a collection of other objects. -type Container interface { - IsEmpty() bool // IsEmpty checks if the container has no elements. - Len() int // Len returns the number of elements in the container. - Clear() // Clear erases all elements from the container. After this call, Len() returns zero. -} - -// Map is a associative container that contains key-value pairs with unique keys. -type Map[K any, V any] interface { - Container - Has(K) bool // Checks whether the container contains element with specific key. - Find(K) *V // Finds element with specific key. - Insert(K, V) // Inserts a key-value pair in to the container or replace existing value. - Remove(K) bool // Remove element with specific key. - ForEach(func(K, V)) // Iterate the container. - ForEachIf(func(K, V) bool) // Iterate the container, stops when the callback returns false. - ForEachMutable(func(K, *V)) // Iterate the container, *V is mutable. - ForEachMutableIf(func(K, *V) bool) // Iterate the container, *V is mutable, stops when the callback returns false. -} - -// Set is a containers that store unique elements. -type Set[K any] interface { - Container - Has(K) bool // Checks whether the container contains element with specific key. - Insert(K) // Inserts a key-value pair in to the container or replace existing value. - InsertN(...K) // Inserts multiple key-value pairs in to the container or replace existing value. - Remove(K) bool // Remove element with specific key. - RemoveN(...K) // Remove multiple elements with specific keys. - ForEach(func(K)) // Iterate the container. - ForEachIf(func(K) bool) // Iterate the container, stops when the callback returns false. -} - -// Iterator is the interface for container's iterator. -type Iterator[T any] interface { - IsNotEnd() bool // Whether it is point to the end of the range. - MoveToNext() // Let it point to the next element. - Value() T // Return the value of current element. -} - -// MapIterator is the interface for map's iterator. -type MapIterator[K any, V any] interface { - Iterator[V] - Key() K // The key of the element -} diff --git a/transport/anytls/skiplist/skiplist.go b/transport/anytls/skiplist/skiplist.go deleted file mode 100644 index f1ce402a95..0000000000 --- a/transport/anytls/skiplist/skiplist.go +++ /dev/null @@ -1,457 +0,0 @@ -package skiplist - -// This implementation is based on https://github.com/liyue201/gostl/tree/master/ds/skiplist -// (many thanks), added many optimizations, such as: -// -// - adaptive level -// - lesser search for prevs when key already exists. -// - reduce memory allocations -// - richer interface. -// -// etc. - -import ( - "math/bits" - "math/rand" - "time" -) - -const ( - skipListMaxLevel = 40 -) - -// SkipList is a probabilistic data structure that seem likely to supplant balanced trees as the -// implementation method of choice for many applications. Skip list algorithms have the same -// asymptotic expected time bounds as balanced trees and are simpler, faster and use less space. -// -// See https://en.wikipedia.org/wiki/Skip_list for more details. -type SkipList[K any, V any] struct { - level int // Current level, may increase dynamically during insertion - len int // Total elements numner in the skiplist. - head skipListNode[K, V] // head.next[level] is the head of each level. - // This cache is used to save the previous nodes when modifying the skip list to avoid - // allocating memory each time it is called. - prevsCache []*skipListNode[K, V] - rander *rand.Rand - impl skipListImpl[K, V] -} - -// NewSkipList creates a new SkipList for Ordered key type. -func NewSkipList[K Ordered, V any]() *SkipList[K, V] { - sl := skipListOrdered[K, V]{} - sl.init() - sl.impl = (skipListImpl[K, V])(&sl) - return &sl.SkipList -} - -// NewSkipListFromMap creates a new SkipList from a map. -func NewSkipListFromMap[K Ordered, V any](m map[K]V) *SkipList[K, V] { - sl := NewSkipList[K, V]() - for k, v := range m { - sl.Insert(k, v) - } - return sl -} - -// NewSkipListFunc creates a new SkipList with specified compare function keyCmp. -func NewSkipListFunc[K any, V any](keyCmp CompareFn[K]) *SkipList[K, V] { - sl := skipListFunc[K, V]{} - sl.init() - sl.keyCmp = keyCmp - sl.impl = skipListImpl[K, V](&sl) - return &sl.SkipList -} - -// IsEmpty implements the Container interface. -func (sl *SkipList[K, V]) IsEmpty() bool { - return sl.len == 0 -} - -// Len implements the Container interface. -func (sl *SkipList[K, V]) Len() int { - return sl.len -} - -// Clear implements the Container interface. -func (sl *SkipList[K, V]) Clear() { - for i := range sl.head.next { - sl.head.next[i] = nil - } - sl.level = 1 - sl.len = 0 -} - -// Iterate return an iterator to the skiplist. -func (sl *SkipList[K, V]) Iterate() MapIterator[K, V] { - return &skipListIterator[K, V]{sl.head.next[0], nil} -} - -// Insert inserts a key-value pair into the skiplist. -// If the key is already in the skip list, it's value will be updated. -func (sl *SkipList[K, V]) Insert(key K, value V) { - node, prevs := sl.impl.findInsertPoint(key) - - if node != nil { - // Already exist, update the value - node.value = value - return - } - - level := sl.randomLevel() - node = newSkipListNode(level, key, value) - - minLevel := level - if sl.level < level { - minLevel = sl.level - } - for i := 0; i < minLevel; i++ { - node.next[i] = prevs[i].next[i] - prevs[i].next[i] = node - } - - if level > sl.level { - for i := sl.level; i < level; i++ { - sl.head.next[i] = node - } - sl.level = level - } - - sl.len++ -} - -// Find returns the value associated with the passed key if the key is in the skiplist, otherwise -// returns nil. -func (sl *SkipList[K, V]) Find(key K) *V { - node := sl.impl.findNode(key) - if node != nil { - return &node.value - } - return nil -} - -// Has implement the Map interface. -func (sl *SkipList[K, V]) Has(key K) bool { - return sl.impl.findNode(key) != nil -} - -// LowerBound returns an iterator to the first element in the skiplist that -// does not satisfy element < value (i.e. greater or equal to), -// or a end itetator if no such element is found. -func (sl *SkipList[K, V]) LowerBound(key K) MapIterator[K, V] { - return &skipListIterator[K, V]{sl.impl.lowerBound(key), nil} -} - -// UpperBound returns an iterator to the first element in the skiplist that -// does not satisfy value < element (i.e. strictly greater), -// or a end itetator if no such element is found. -func (sl *SkipList[K, V]) UpperBound(key K) MapIterator[K, V] { - return &skipListIterator[K, V]{sl.impl.upperBound(key), nil} -} - -// FindRange returns an iterator in range [first, last) (last is not includeed). -func (sl *SkipList[K, V]) FindRange(first, last K) MapIterator[K, V] { - return &skipListIterator[K, V]{sl.impl.lowerBound(first), sl.impl.upperBound(last)} -} - -// Remove removes the key-value pair associated with the passed key and returns true if the key is -// in the skiplist, otherwise returns false. -func (sl *SkipList[K, V]) Remove(key K) bool { - node, prevs := sl.impl.findRemovePoint(key) - if node == nil { - return false - } - for i, v := range node.next { - prevs[i].next[i] = v - } - for sl.level > 1 && sl.head.next[sl.level-1] == nil { - sl.level-- - } - sl.len-- - return true -} - -// ForEach implements the Map interface. -func (sl *SkipList[K, V]) ForEach(op func(K, V)) { - for e := sl.head.next[0]; e != nil; e = e.next[0] { - op(e.key, e.value) - } -} - -// ForEachMutable implements the Map interface. -func (sl *SkipList[K, V]) ForEachMutable(op func(K, *V)) { - for e := sl.head.next[0]; e != nil; e = e.next[0] { - op(e.key, &e.value) - } -} - -// ForEachIf implements the Map interface. -func (sl *SkipList[K, V]) ForEachIf(op func(K, V) bool) { - for e := sl.head.next[0]; e != nil; e = e.next[0] { - if !op(e.key, e.value) { - return - } - } -} - -// ForEachMutableIf implements the Map interface. -func (sl *SkipList[K, V]) ForEachMutableIf(op func(K, *V) bool) { - for e := sl.head.next[0]; e != nil; e = e.next[0] { - if !op(e.key, &e.value) { - return - } - } -} - -/// SkipList implementation part. - -type skipListNode[K any, V any] struct { - key K - value V - next []*skipListNode[K, V] -} - -//go:generate bash ./skiplist_newnode_generate.sh skipListMaxLevel skiplist_newnode.go -// func newSkipListNode[K Ordered, V any](level int, key K, value V) *skipListNode[K, V] - -type skipListIterator[K any, V any] struct { - node, end *skipListNode[K, V] -} - -func (it *skipListIterator[K, V]) IsNotEnd() bool { - return it.node != it.end -} - -func (it *skipListIterator[K, V]) MoveToNext() { - it.node = it.node.next[0] -} - -func (it *skipListIterator[K, V]) Key() K { - return it.node.key -} - -func (it *skipListIterator[K, V]) Value() V { - return it.node.value -} - -// skipListImpl is an interface to provide different implementation for Ordered key or CompareFn. -// -// We can use CompareFn to cumpare Ordered keys, but a separated implementation is much faster. -// We don't make the whole skip list an interface, in order to share the type independented method. -// And because these methods are called directly without going through the interface, they are also -// much faster. -type skipListImpl[K any, V any] interface { - findNode(key K) *skipListNode[K, V] - lowerBound(key K) *skipListNode[K, V] - upperBound(key K) *skipListNode[K, V] - findInsertPoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) - findRemovePoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) -} - -func (sl *SkipList[K, V]) init() { - sl.level = 1 - // #nosec G404 -- This is not a security condition - sl.rander = rand.New(rand.NewSource(time.Now().Unix())) - sl.prevsCache = make([]*skipListNode[K, V], skipListMaxLevel) - sl.head.next = make([]*skipListNode[K, V], skipListMaxLevel) -} - -func (sl *SkipList[K, V]) randomLevel() int { - total := uint64(1)< 3 && 1<<(level-3) > sl.len { - level-- - } - - return level -} - -/// skipListOrdered part - -// skipListOrdered is the skip list implementation for Ordered types. -type skipListOrdered[K Ordered, V any] struct { - SkipList[K, V] -} - -func (sl *skipListOrdered[K, V]) findNode(key K) *skipListNode[K, V] { - return sl.doFindNode(key, true) -} - -func (sl *skipListOrdered[K, V]) doFindNode(key K, eq bool) *skipListNode[K, V] { - // This function execute the job of findNode if eq is true, otherwise lowBound. - // Passing the control variable eq is ugly but it's faster than testing node - // again outside the function in findNode. - prev := &sl.head - for i := sl.level - 1; i >= 0; i-- { - for cur := prev.next[i]; cur != nil; cur = cur.next[i] { - if cur.key == key { - return cur - } - if cur.key > key { - // All other node in this level must be greater than the key, - // search the next level. - break - } - prev = cur - } - } - if eq { - return nil - } - return prev.next[0] -} - -func (sl *skipListOrdered[K, V]) lowerBound(key K) *skipListNode[K, V] { - return sl.doFindNode(key, false) -} - -func (sl *skipListOrdered[K, V]) upperBound(key K) *skipListNode[K, V] { - node := sl.lowerBound(key) - if node != nil && node.key == key { - return node.next[0] - } - return node -} - -// findInsertPoint returns (*node, nil) to the existed node if the key exists, -// or (nil, []*node) to the previous nodes if the key doesn't exist -func (sl *skipListOrdered[K, V]) findInsertPoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) { - prevs := sl.prevsCache[0:sl.level] - prev := &sl.head - for i := sl.level - 1; i >= 0; i-- { - for next := prev.next[i]; next != nil; next = next.next[i] { - if next.key == key { - // The key is already existed, prevs are useless because no new node insertion. - // stop searching. - return next, nil - } - if next.key > key { - // All other node in this level must be greater than the key, - // search the next level. - break - } - prev = next - } - prevs[i] = prev - } - return nil, prevs -} - -// findRemovePoint finds the node which match the key and it's previous nodes. -func (sl *skipListOrdered[K, V]) findRemovePoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) { - prevs := sl.findPrevNodes(key) - node := prevs[0].next[0] - if node == nil || node.key != key { - return nil, nil - } - return node, prevs -} - -func (sl *skipListOrdered[K, V]) findPrevNodes(key K) []*skipListNode[K, V] { - prevs := sl.prevsCache[0:sl.level] - prev := &sl.head - for i := sl.level - 1; i >= 0; i-- { - for next := prev.next[i]; next != nil; next = next.next[i] { - if next.key >= key { - break - } - prev = next - } - prevs[i] = prev - } - return prevs -} - -/// skipListFunc part - -// skipListFunc is the skip list implementation which compare keys with func. -type skipListFunc[K any, V any] struct { - SkipList[K, V] - keyCmp CompareFn[K] -} - -func (sl *skipListFunc[K, V]) findNode(key K) *skipListNode[K, V] { - node := sl.lowerBound(key) - if node != nil && sl.keyCmp(node.key, key) == 0 { - return node - } - return nil -} - -func (sl *skipListFunc[K, V]) lowerBound(key K) *skipListNode[K, V] { - var prev = &sl.head - for i := sl.level - 1; i >= 0; i-- { - cur := prev.next[i] - for ; cur != nil; cur = cur.next[i] { - cmpRet := sl.keyCmp(cur.key, key) - if cmpRet == 0 { - return cur - } - if cmpRet > 0 { - break - } - prev = cur - } - } - return prev.next[0] -} - -func (sl *skipListFunc[K, V]) upperBound(key K) *skipListNode[K, V] { - node := sl.lowerBound(key) - if node != nil && sl.keyCmp(node.key, key) == 0 { - return node.next[0] - } - return node -} - -// findInsertPoint returns (*node, nil) to the existed node if the key exists, -// or (nil, []*node) to the previous nodes if the key doesn't exist -func (sl *skipListFunc[K, V]) findInsertPoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) { - prevs := sl.prevsCache[0:sl.level] - prev := &sl.head - for i := sl.level - 1; i >= 0; i-- { - for cur := prev.next[i]; cur != nil; cur = cur.next[i] { - r := sl.keyCmp(cur.key, key) - if r == 0 { - // The key is already existed, prevs are useless because no new node insertion. - // stop searching. - return cur, nil - } - if r > 0 { - // All other node in this level must be greater than the key, - // search the next level. - break - } - prev = cur - } - prevs[i] = prev - } - return nil, prevs -} - -// findRemovePoint finds the node which match the key and it's previous nodes. -func (sl *skipListFunc[K, V]) findRemovePoint(key K) (*skipListNode[K, V], []*skipListNode[K, V]) { - prevs := sl.findPrevNodes(key) - node := prevs[0].next[0] - if node == nil || sl.keyCmp(node.key, key) != 0 { - return nil, nil - } - return node, prevs -} - -func (sl *skipListFunc[K, V]) findPrevNodes(key K) []*skipListNode[K, V] { - prevs := sl.prevsCache[0:sl.level] - prev := &sl.head - for i := sl.level - 1; i >= 0; i-- { - for next := prev.next[i]; next != nil; next = next.next[i] { - if sl.keyCmp(next.key, key) >= 0 { - break - } - prev = next - } - prevs[i] = prev - } - return prevs -} diff --git a/transport/anytls/skiplist/skiplist_newnode.go b/transport/anytls/skiplist/skiplist_newnode.go deleted file mode 100644 index 4e8a6d8839..0000000000 --- a/transport/anytls/skiplist/skiplist_newnode.go +++ /dev/null @@ -1,297 +0,0 @@ -// AUTO GENERATED CODE, DON'T EDIT!!! -// EDIT skiplist_newnode_generate.sh accordingly. - -package skiplist - -// newSkipListNode creates a new node initialized with specified key, value and next slice. -func newSkipListNode[K any, V any](level int, key K, value V) *skipListNode[K, V] { - // For nodes with each levels, point their next slice to the nexts array allocated together, - // which can reduce 1 memory allocation and improve performance. - // - // The generics of the golang doesn't support non-type parameters like in C++, - // so we have to generate it manually. - switch level { - case 1: - n := struct { - head skipListNode[K, V] - nexts [1]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 2: - n := struct { - head skipListNode[K, V] - nexts [2]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 3: - n := struct { - head skipListNode[K, V] - nexts [3]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 4: - n := struct { - head skipListNode[K, V] - nexts [4]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 5: - n := struct { - head skipListNode[K, V] - nexts [5]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 6: - n := struct { - head skipListNode[K, V] - nexts [6]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 7: - n := struct { - head skipListNode[K, V] - nexts [7]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 8: - n := struct { - head skipListNode[K, V] - nexts [8]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 9: - n := struct { - head skipListNode[K, V] - nexts [9]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 10: - n := struct { - head skipListNode[K, V] - nexts [10]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 11: - n := struct { - head skipListNode[K, V] - nexts [11]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 12: - n := struct { - head skipListNode[K, V] - nexts [12]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 13: - n := struct { - head skipListNode[K, V] - nexts [13]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 14: - n := struct { - head skipListNode[K, V] - nexts [14]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 15: - n := struct { - head skipListNode[K, V] - nexts [15]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 16: - n := struct { - head skipListNode[K, V] - nexts [16]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 17: - n := struct { - head skipListNode[K, V] - nexts [17]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 18: - n := struct { - head skipListNode[K, V] - nexts [18]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 19: - n := struct { - head skipListNode[K, V] - nexts [19]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 20: - n := struct { - head skipListNode[K, V] - nexts [20]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 21: - n := struct { - head skipListNode[K, V] - nexts [21]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 22: - n := struct { - head skipListNode[K, V] - nexts [22]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 23: - n := struct { - head skipListNode[K, V] - nexts [23]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 24: - n := struct { - head skipListNode[K, V] - nexts [24]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 25: - n := struct { - head skipListNode[K, V] - nexts [25]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 26: - n := struct { - head skipListNode[K, V] - nexts [26]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 27: - n := struct { - head skipListNode[K, V] - nexts [27]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 28: - n := struct { - head skipListNode[K, V] - nexts [28]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 29: - n := struct { - head skipListNode[K, V] - nexts [29]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 30: - n := struct { - head skipListNode[K, V] - nexts [30]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 31: - n := struct { - head skipListNode[K, V] - nexts [31]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 32: - n := struct { - head skipListNode[K, V] - nexts [32]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 33: - n := struct { - head skipListNode[K, V] - nexts [33]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 34: - n := struct { - head skipListNode[K, V] - nexts [34]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 35: - n := struct { - head skipListNode[K, V] - nexts [35]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 36: - n := struct { - head skipListNode[K, V] - nexts [36]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 37: - n := struct { - head skipListNode[K, V] - nexts [37]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 38: - n := struct { - head skipListNode[K, V] - nexts [38]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 39: - n := struct { - head skipListNode[K, V] - nexts [39]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - case 40: - n := struct { - head skipListNode[K, V] - nexts [40]*skipListNode[K, V] - }{head: skipListNode[K, V]{key, value, nil}} - n.head.next = n.nexts[:] - return &n.head - } - - panic("should not reach here") -} diff --git a/transport/anytls/skiplist/types.go b/transport/anytls/skiplist/types.go deleted file mode 100644 index c534f46092..0000000000 --- a/transport/anytls/skiplist/types.go +++ /dev/null @@ -1,75 +0,0 @@ -package skiplist - -// Signed is a constraint that permits any signed integer type. -// If future releases of Go add new predeclared signed integer types, -// this constraint will be modified to include them. -type Signed interface { - ~int | ~int8 | ~int16 | ~int32 | ~int64 -} - -// Unsigned is a constraint that permits any unsigned integer type. -// If future releases of Go add new predeclared unsigned integer types, -// this constraint will be modified to include them. -type Unsigned interface { - ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr -} - -// Integer is a constraint that permits any integer type. -// If future releases of Go add new predeclared integer types, -// this constraint will be modified to include them. -type Integer interface { - Signed | Unsigned -} - -// Float is a constraint that permits any floating-point type. -// If future releases of Go add new predeclared floating-point types, -// this constraint will be modified to include them. -type Float interface { - ~float32 | ~float64 -} - -// Ordered is a constraint that permits any ordered type: any type -// that supports the operators < <= >= >. -// If future releases of Go add new ordered types, -// this constraint will be modified to include them. -type Ordered interface { - Integer | Float | ~string -} - -// Numeric is a constraint that permits any numeric type. -type Numeric interface { - Integer | Float -} - -// LessFn is a function that returns whether 'a' is less than 'b'. -type LessFn[T any] func(a, b T) bool - -// CompareFn is a 3 way compare function that -// returns 1 if a > b, -// returns 0 if a == b, -// returns -1 if a < b. -type CompareFn[T any] func(a, b T) int - -// HashFn is a function that returns the hash of 't'. -type HashFn[T any] func(t T) uint64 - -// Equals wraps the '==' operator for comparable types. -func Equals[T comparable](a, b T) bool { - return a == b -} - -// Less wraps the '<' operator for ordered types. -func Less[T Ordered](a, b T) bool { - return a < b -} - -// OrderedCompare provide default CompareFn for ordered types. -func OrderedCompare[T Ordered](a, b T) int { - if a < b { - return -1 - } - if a > b { - return 1 - } - return 0 -} diff --git a/transport/anytls/util/deadline.go b/transport/anytls/util/deadline.go deleted file mode 100644 index 8167bf9555..0000000000 --- a/transport/anytls/util/deadline.go +++ /dev/null @@ -1,25 +0,0 @@ -package util - -import ( - "sync" - "time" -) - -func NewDeadlineWatcher(ddl time.Duration, timeOut func()) (done func()) { - t := time.NewTimer(ddl) - closeCh := make(chan struct{}) - go func() { - defer t.Stop() - select { - case <-closeCh: - case <-t.C: - timeOut() - } - }() - var once sync.Once - return func() { - once.Do(func() { - close(closeCh) - }) - } -} diff --git a/transport/anytls/util/routine.go b/transport/anytls/util/routine.go deleted file mode 100644 index 4fdfbdd414..0000000000 --- a/transport/anytls/util/routine.go +++ /dev/null @@ -1,28 +0,0 @@ -package util - -import ( - "context" - "runtime/debug" - "time" - - "github.com/metacubex/mihomo/log" -) - -func StartRoutine(ctx context.Context, d time.Duration, f func()) { - go func() { - defer func() { - if r := recover(); r != nil { - log.Errorln("[BUG] %v %s", r, string(debug.Stack())) - } - }() - for { - time.Sleep(d) - f() - select { - case <-ctx.Done(): - return - default: - } - } - }() -} diff --git a/transport/anytls/util/string_map.go b/transport/anytls/util/string_map.go deleted file mode 100644 index 27fb3581f1..0000000000 --- a/transport/anytls/util/string_map.go +++ /dev/null @@ -1,27 +0,0 @@ -package util - -import ( - "strings" -) - -type StringMap map[string]string - -func (s StringMap) ToBytes() []byte { - var lines []string - for k, v := range s { - lines = append(lines, k+"="+v) - } - return []byte(strings.Join(lines, "\n")) -} - -func StringMapFromBytes(b []byte) StringMap { - var m = make(StringMap) - var lines = strings.Split(string(b), "\n") - for _, line := range lines { - v := strings.SplitN(line, "=", 2) - if len(v) == 2 { - m[v[0]] = v[1] - } - } - return m -} diff --git a/transport/anytls/util/type.go b/transport/anytls/util/type.go deleted file mode 100644 index 4f28d4890e..0000000000 --- a/transport/anytls/util/type.go +++ /dev/null @@ -1,8 +0,0 @@ -package util - -import ( - "context" - "net" -) - -type DialOutFunc func(ctx context.Context) (net.Conn, error) diff --git a/transport/gost-plugin/websocket.go b/transport/gost-plugin/websocket.go deleted file mode 100644 index 23d06b9462..0000000000 --- a/transport/gost-plugin/websocket.go +++ /dev/null @@ -1,102 +0,0 @@ -package gost - -import ( - "context" - "crypto/tls" - "net" - "net/http" - - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/transport/vmess" - smux "github.com/metacubex/smux" -) - -// Option is options of gost websocket -type Option struct { - Host string - Port string - Path string - Headers map[string]string - TLS bool - SkipCertVerify bool - Fingerprint string - Mux bool -} - -// muxConn is a wrapper around smux.Stream that also closes the session when closed -type muxConn struct { - net.Conn - session *smux.Session -} - -func (m *muxConn) Close() error { - streamErr := m.Conn.Close() - sessionErr := m.session.Close() - - // Return stream error if there is one, otherwise return session error - if streamErr != nil { - return streamErr - } - return sessionErr -} - -// NewGostWebsocket return a gost websocket -func NewGostWebsocket(ctx context.Context, 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 - tlsConfig := &tls.Config{ - ServerName: option.Host, - InsecureSkipVerify: option.SkipCertVerify, - NextProtos: []string{"http/1.1"}, - } - var err error - config.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) - if err != nil { - return nil, err - } - - if host := config.Headers.Get("Host"); host != "" { - config.TLSConfig.ServerName = host - } - } - - var err error - conn, err = vmess.StreamWebsocketConn(ctx, conn, config) - if err != nil { - return nil, err - } - - if option.Mux { - config := smux.DefaultConfig() - config.KeepAliveDisabled = true - - session, err := smux.Client(conn, config) - if err != nil { - return nil, err - } - - stream, err := session.OpenStream() - if err != nil { - session.Close() - return nil, err - } - - return &muxConn{ - Conn: stream, - session: session, - }, nil - } - return conn, nil -} diff --git a/transport/gun/gun.go b/transport/gun/gun.go deleted file mode 100644 index 13d4046d75..0000000000 --- a/transport/gun/gun.go +++ /dev/null @@ -1,362 +0,0 @@ -// 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/http/httptrace" - "net/url" - "sync" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/buf" - "github.com/metacubex/mihomo/common/pool" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - - "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(ctx context.Context, network, addr string) (net.Conn, error) - -type Conn struct { - initFn func() (io.ReadCloser, netAddr, error) - writer io.Writer - closer io.Closer - netAddr - - reader io.ReadCloser - once sync.Once - closed atomic.Bool - err error - remain int - br *bufio.Reader - // deadlines - deadline *time.Timer -} - -type Config struct { - ServiceName string - Host string - ClientFingerprint string -} - -func (g *Conn) initReader() { - reader, addr, err := g.initFn() - if err != nil { - g.err = err - if closer, ok := g.writer.(io.Closer); ok { - closer.Close() - } - return - } - g.netAddr = addr - - if !g.closed.Load() { - g.reader = reader - g.br = bufio.NewReader(reader) - } else { - reader.Close() - } -} - -func (g *Conn) Init() error { - g.once.Do(g.initReader) - return g.err -} - -func (g *Conn) Read(b []byte) (n int, err error) { - if err = g.Init(); err != nil { - return - } - - 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.reader == 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))) - var grpcHeader [5]byte - grpcPayloadLen := uint32(varuintSize + 1 + len(b)) - binary.BigEndian.PutUint32(grpcHeader[1:5], grpcPayloadLen) - - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - buf.Write(grpcHeader[:]) - buf.Write(protobufHeader[:varuintSize+1]) - buf.Write(b) - - _, err = g.writer.Write(buf.Bytes()) - if err == io.ErrClosedPipe && g.err != nil { - err = g.err - } - - if flusher, ok := g.writer.(http.Flusher); ok { - flusher.Flush() - } - - return len(b), err -} - -func (g *Conn) WriteBuffer(buffer *buf.Buffer) error { - defer buffer.Release() - dataLen := buffer.Len() - varLen := UVarintLen(uint64(dataLen)) - header := buffer.ExtendHeader(6 + varLen) - _ = header[6] // bounds check hint to compiler - header[0] = 0x00 - binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+dataLen)) - header[5] = 0x0A - binary.PutUvarint(header[6:], uint64(dataLen)) - _, err := g.writer.Write(buffer.Bytes()) - - if err == io.ErrClosedPipe && g.err != nil { - err = g.err - } - - if flusher, ok := g.writer.(http.Flusher); ok { - flusher.Flush() - } - - return err -} - -func (g *Conn) FrontHeadroom() int { - return 6 + binary.MaxVarintLen64 -} - -func (g *Conn) Close() error { - g.closed.Store(true) - var errorArr []error - - if reader := g.reader; reader != nil { - if err := reader.Close(); err != nil { - errorArr = append(errorArr, err) - } - } - - if closer, ok := g.writer.(io.Closer); ok { - if err := closer.Close(); err != nil { - errorArr = append(errorArr, err) - } - } - - if closer := g.closer; closer != nil { - if err := closer.Close(); err != nil { - errorArr = append(errorArr, err) - } - } - - return errors.Join(errorArr...) -} - -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, clientFingerprint string, realityConfig *tlsC.RealityConfig) *TransportWrap { - dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { - ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) - defer cancel() - pconn, err := dialFn(ctx, network, addr) - if err != nil { - return nil, err - } - - if tlsConfig == nil { - return pconn, nil - } - - if clientFingerprint, ok := tlsC.GetFingerprint(clientFingerprint); ok { - if realityConfig == nil { - tlsConn := tlsC.UClient(pconn, tlsC.UConfig(cfg), clientFingerprint) - if err := tlsConn.HandshakeContext(ctx); err != nil { - pconn.Close() - return nil, err - } - state := tlsConn.ConnectionState() - if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { - tlsConn.Close() - return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) - } - return tlsConn, nil - } else { - realityConn, err := tlsC.GetRealityConn(ctx, pconn, clientFingerprint, cfg, realityConfig) - if err != nil { - pconn.Close() - return nil, err - } - //state := realityConn.(*utls.UConn).ConnectionState() - //if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { - // realityConn.Close() - // return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) - //} - return realityConn, nil - } - } - if realityConfig != nil { - return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint") - } - - conn := tls.Client(pconn, cfg) - if err := conn.HandshakeContext(ctx); err != nil { - pconn.Close() - return nil, err - } - state := conn.ConnectionState() - if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { - conn.Close() - return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) - } - return conn, nil - } - - transport := &http2.Transport{ - DialTLSContext: dialFunc, - TLSClientConfig: tlsConfig, - AllowHTTP: false, - DisableCompression: true, - PingTimeout: 0, - } - - ctx, cancel := context.WithCancel(context.Background()) - wrap := &TransportWrap{ - Transport: transport, - ctx: ctx, - cancel: cancel, - } - return wrap -} - -func StreamGunWithTransport(transport *TransportWrap, 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, - } - request = request.WithContext(transport.ctx) - - conn := &Conn{ - initFn: func() (io.ReadCloser, netAddr, error) { - nAddr := netAddr{} - trace := &httptrace.ClientTrace{ - GotConn: func(connInfo httptrace.GotConnInfo) { - nAddr.localAddr = connInfo.Conn.LocalAddr() - nAddr.remoteAddr = connInfo.Conn.RemoteAddr() - }, - } - request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace)) - response, err := transport.RoundTrip(request) - if err != nil { - return nil, nAddr, err - } - return response.Body, nAddr, nil - }, - writer: writer, - } - - go conn.Init() - return conn, nil -} - -func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config, realityConfig *tlsC.RealityConfig) (net.Conn, error) { - dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { - return conn, nil - } - - transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, realityConfig) - c, err := StreamGunWithTransport(transport, cfg) - if err != nil { - return nil, err - } - if c, ok := c.(*Conn); ok { // The incoming net.Conn should be closed synchronously with the generated gun.Conn - c.closer = conn - } - return c, nil -} diff --git a/transport/gun/server.go b/transport/gun/server.go deleted file mode 100644 index 953c487816..0000000000 --- a/transport/gun/server.go +++ /dev/null @@ -1,118 +0,0 @@ -package gun - -import ( - "io" - "net" - "net/http" - "strings" - "sync" - "time" - - "github.com/metacubex/mihomo/common/buf" - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" -) - -const idleTimeout = 30 * time.Second - -type ServerOption struct { - ServiceName string - ConnHandler func(conn net.Conn) - HttpHandler http.Handler -} - -func NewServerHandler(options ServerOption) http.Handler { - path := "/" + options.ServiceName + "/Tun" - connHandler := options.ConnHandler - httpHandler := options.HttpHandler - if httpHandler == nil { - httpHandler = http.NewServeMux() - } - // using h2c.NewHandler to ensure we can work in plain http2 - // and some tls conn is not *tls.Conn (like *reality.Conn) - return h2c.NewHandler(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - if request.URL.Path == path && - request.Method == http.MethodPost && - strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc") { - - writer.Header().Set("Content-Type", "application/grpc") - writer.Header().Set("TE", "trailers") - writer.WriteHeader(http.StatusOK) - - conn := &Conn{ - initFn: func() (io.ReadCloser, netAddr, error) { - nAddr := netAddr{} - if request.RemoteAddr != "" { - metadata := C.Metadata{} - if err := metadata.SetRemoteAddress(request.RemoteAddr); err == nil { - nAddr.remoteAddr = net.TCPAddrFromAddrPort(metadata.AddrPort()) - } - } - if addr, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { - nAddr.localAddr = addr - } - return request.Body, nAddr, nil - }, - writer: writer, - } - _ = conn.Init() - - wrapper := &h2ConnWrapper{ - // gun.Conn can't correct handle ReadDeadline - // so call N.NewDeadlineConn to add a safe wrapper - ExtendedConn: N.NewDeadlineConn(conn), - } - connHandler(wrapper) - wrapper.CloseWrapper() - - return - } - - httpHandler.ServeHTTP(writer, request) - }), &http2.Server{ - IdleTimeout: idleTimeout, - }) -} - -// h2ConnWrapper used to avoid "panic: Write called after Handler finished" for gun.Conn -type h2ConnWrapper struct { - N.ExtendedConn - access sync.Mutex - closed bool -} - -func (w *h2ConnWrapper) Write(p []byte) (n int, err error) { - w.access.Lock() - defer w.access.Unlock() - if w.closed { - return 0, net.ErrClosed - } - return w.ExtendedConn.Write(p) -} - -func (w *h2ConnWrapper) WriteBuffer(buffer *buf.Buffer) error { - w.access.Lock() - defer w.access.Unlock() - if w.closed { - return net.ErrClosed - } - return w.ExtendedConn.WriteBuffer(buffer) -} - -func (w *h2ConnWrapper) CloseWrapper() { - w.access.Lock() - defer w.access.Unlock() - w.closed = true -} - -func (w *h2ConnWrapper) Close() error { - w.CloseWrapper() - return w.ExtendedConn.Close() -} - -func (w *h2ConnWrapper) Upstream() any { - return w.ExtendedConn -} diff --git a/transport/gun/transport.go b/transport/gun/transport.go deleted file mode 100644 index 9c5c437560..0000000000 --- a/transport/gun/transport.go +++ /dev/null @@ -1,37 +0,0 @@ -package gun - -import ( - "context" - "net" - "sync" - - "golang.org/x/net/http2" -) - -type TransportWrap struct { - *http2.Transport - ctx context.Context - cancel context.CancelFunc - closeOnce sync.Once -} - -func (tw *TransportWrap) Close() error { - tw.closeOnce.Do(func() { - tw.cancel() - closeTransport(tw.Transport) - }) - return nil -} - -type netAddr struct { - remoteAddr net.Addr - localAddr net.Addr -} - -func (addr netAddr) RemoteAddr() net.Addr { - return addr.remoteAddr -} - -func (addr netAddr) LocalAddr() net.Addr { - return addr.localAddr -} diff --git a/transport/gun/transport_close.go b/transport/gun/transport_close.go deleted file mode 100644 index 2b30ed7cdd..0000000000 --- a/transport/gun/transport_close.go +++ /dev/null @@ -1,64 +0,0 @@ -package gun - -import ( - "net" - "net/http" - "sync" - "time" - "unsafe" - - "golang.org/x/net/http2" -) - -type clientConnPool struct { - t *http2.Transport - - mu sync.Mutex - conns map[string][]*http2.ClientConn // key is host:port - dialing map[string]unsafe.Pointer // currently in-flight dials - keys map[*http2.ClientConn][]string - addConnCalls map[string]unsafe.Pointer // in-flight addConnIfNeeded calls -} - -type clientConn struct { - t *http.Transport - tconn net.Conn // usually *tls.Conn, except specialized impls -} - -type efaceWords struct { - typ unsafe.Pointer - data unsafe.Pointer -} - -type tlsConn interface { - net.Conn - NetConn() net.Conn -} - -func closeClientConn(cc *http2.ClientConn) { // like forceCloseConn() in http2.ClientConn but also apply for tls-like conn - if conn, ok := (*clientConn)(unsafe.Pointer(cc)).tconn.(tlsConn); ok { - t := time.AfterFunc(time.Second, func() { - _ = conn.NetConn().Close() - }) - defer t.Stop() - } - _ = cc.Close() -} - -func closeTransport(tr *http2.Transport) { - connPool := transportConnPool(tr) - p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) - p.mu.Lock() - defer p.mu.Unlock() - for _, vv := range p.conns { - for _, cc := range vv { - closeClientConn(cc) - } - } - // cleanup - p.conns = make(map[string][]*http2.ClientConn) - p.keys = make(map[*http2.ClientConn][]string) -} - -//go:linkname transportConnPool golang.org/x/net/http2.(*Transport).connPool -func transportConnPool(t *http2.Transport) http2.ClientConnPool diff --git a/transport/gun/utils.go b/transport/gun/utils.go deleted file mode 100644 index e4a663152d..0000000000 --- a/transport/gun/utils.go +++ /dev/null @@ -1,26 +0,0 @@ -package gun - -func UVarintLen(x uint64) int { - switch { - case x < 1<<(7*1): - return 1 - case x < 1<<(7*2): - return 2 - case x < 1<<(7*3): - return 3 - case x < 1<<(7*4): - return 4 - case x < 1<<(7*5): - return 5 - case x < 1<<(7*6): - return 6 - case x < 1<<(7*7): - return 7 - case x < 1<<(7*8): - return 8 - case x < 1<<(7*9): - return 9 - default: - return 10 - } -} diff --git a/transport/hysteria/congestion/brutal.go b/transport/hysteria/congestion/brutal.go deleted file mode 100644 index 601949dec2..0000000000 --- a/transport/hysteria/congestion/brutal.go +++ /dev/null @@ -1,144 +0,0 @@ -package congestion - -import ( - "github.com/metacubex/quic-go/congestion" - "time" -) - -const ( - initMaxDatagramSize = 1252 - - pktInfoSlotCount = 5 // slot index is based on seconds, so this is basically how many seconds we sample - minSampleCount = 50 - minAckRate = 0.8 -) - -var _ congestion.CongestionControlEx = &BrutalSender{} - -type BrutalSender struct { - rttStats congestion.RTTStatsProvider - bps congestion.ByteCount - maxDatagramSize congestion.ByteCount - pacer *pacer - - pktInfoSlots [pktInfoSlotCount]pktInfo - ackRate float64 -} - -type pktInfo struct { - Timestamp int64 - AckCount uint64 - LossCount uint64 -} - -func NewBrutalSender(bps congestion.ByteCount) *BrutalSender { - bs := &BrutalSender{ - bps: bps, - maxDatagramSize: initMaxDatagramSize, - ackRate: 1, - } - bs.pacer = newPacer(func() congestion.ByteCount { - return congestion.ByteCount(float64(bs.bps) / bs.ackRate) - }) - return bs -} - -func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) { - b.rttStats = rttStats -} - -func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time { - return b.pacer.TimeUntilSend() -} - -func (b *BrutalSender) HasPacingBudget(now time.Time) bool { - return b.pacer.Budget(now) >= b.maxDatagramSize -} - -func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool { - return bytesInFlight < b.GetCongestionWindow() -} - -func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount { - rtt := maxDuration(b.rttStats.LatestRTT(), b.rttStats.SmoothedRTT()) - if rtt <= 0 { - return 10240 - } - return congestion.ByteCount(float64(b.bps) * rtt.Seconds() * 1.5 / b.ackRate) -} - -func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount, - packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool) { - b.pacer.SentPacket(sentTime, bytes) -} - -func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, - priorInFlight congestion.ByteCount, eventTime time.Time) { - // Stub -} - -func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount, - priorInFlight congestion.ByteCount) { - // Stub -} - -func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { - currentTimestamp := eventTime.Unix() - slot := currentTimestamp % pktInfoSlotCount - if b.pktInfoSlots[slot].Timestamp == currentTimestamp { - b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets)) - b.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets)) - } else { - // uninitialized slot or too old, reset - b.pktInfoSlots[slot].Timestamp = currentTimestamp - b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets)) - b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets)) - } - b.updateAckRate(currentTimestamp) -} - -func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) { - b.maxDatagramSize = size - b.pacer.SetMaxDatagramSize(size) -} - -func (b *BrutalSender) updateAckRate(currentTimestamp int64) { - minTimestamp := currentTimestamp - pktInfoSlotCount - var ackCount, lossCount uint64 - for _, info := range b.pktInfoSlots { - if info.Timestamp < minTimestamp { - continue - } - ackCount += info.AckCount - lossCount += info.LossCount - } - if ackCount+lossCount < minSampleCount { - b.ackRate = 1 - return - } - rate := float64(ackCount) / float64(ackCount+lossCount) - if rate < minAckRate { - b.ackRate = minAckRate - return - } - b.ackRate = rate -} - -func (b *BrutalSender) InSlowStart() bool { - return false -} - -func (b *BrutalSender) InRecovery() bool { - return false -} - -func (b *BrutalSender) MaybeExitSlowStart() {} - -func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {} - -func maxDuration(a, b time.Duration) time.Duration { - if a > b { - return a - } - return b -} diff --git a/transport/hysteria/congestion/pacer.go b/transport/hysteria/congestion/pacer.go deleted file mode 100644 index 2dff53008d..0000000000 --- a/transport/hysteria/congestion/pacer.go +++ /dev/null @@ -1,85 +0,0 @@ -package congestion - -import ( - "github.com/metacubex/quic-go/congestion" - "math" - "time" -) - -const ( - maxBurstPackets = 10 - minPacingDelay = time.Millisecond -) - -// The pacer implements a token bucket pacing algorithm. -type pacer struct { - budgetAtLastSent congestion.ByteCount - maxDatagramSize congestion.ByteCount - lastSentTime time.Time - getBandwidth func() congestion.ByteCount // in bytes/s -} - -func newPacer(getBandwidth func() congestion.ByteCount) *pacer { - p := &pacer{ - budgetAtLastSent: maxBurstPackets * initMaxDatagramSize, - maxDatagramSize: initMaxDatagramSize, - getBandwidth: getBandwidth, - } - return p -} - -func (p *pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) { - budget := p.Budget(sendTime) - if size > budget { - p.budgetAtLastSent = 0 - } else { - p.budgetAtLastSent = budget - size - } - p.lastSentTime = sendTime -} - -func (p *pacer) Budget(now time.Time) congestion.ByteCount { - if p.lastSentTime.IsZero() { - return p.maxBurstSize() - } - budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 - return minByteCount(p.maxBurstSize(), budget) -} - -func (p *pacer) maxBurstSize() congestion.ByteCount { - return maxByteCount( - congestion.ByteCount((minPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9, - maxBurstPackets*p.maxDatagramSize, - ) -} - -// TimeUntilSend returns when the next packet should be sent. -// It returns the zero value of time.Time if a packet can be sent immediately. -func (p *pacer) TimeUntilSend() time.Time { - if p.budgetAtLastSent >= p.maxDatagramSize { - return time.Time{} - } - return p.lastSentTime.Add(maxDuration( - minPacingDelay, - time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/ - float64(p.getBandwidth())))*time.Nanosecond, - )) -} - -func (p *pacer) SetMaxDatagramSize(s congestion.ByteCount) { - p.maxDatagramSize = s -} - -func maxByteCount(a, b congestion.ByteCount) congestion.ByteCount { - if a < b { - return b - } - return a -} - -func minByteCount(a, b congestion.ByteCount) congestion.ByteCount { - if a < b { - return a - } - return b -} diff --git a/transport/hysteria/conns/faketcp/LICENSE b/transport/hysteria/conns/faketcp/LICENSE deleted file mode 100644 index 79fbecb0ad..0000000000 --- a/transport/hysteria/conns/faketcp/LICENSE +++ /dev/null @@ -1 +0,0 @@ -Grabbed from https://github.com/xtaci/tcpraw with modifications \ No newline at end of file diff --git a/transport/hysteria/conns/faketcp/obfs.go b/transport/hysteria/conns/faketcp/obfs.go deleted file mode 100644 index cf58e56962..0000000000 --- a/transport/hysteria/conns/faketcp/obfs.go +++ /dev/null @@ -1,93 +0,0 @@ -package faketcp - -import ( - "github.com/metacubex/mihomo/transport/hysteria/obfs" - "net" - "sync" - "syscall" - "time" -) - -const udpBufferSize = 65535 - -type ObfsFakeTCPConn struct { - orig *TCPConn - obfs obfs.Obfuscator - readBuf []byte - readMutex sync.Mutex - writeBuf []byte - writeMutex sync.Mutex -} - -func NewObfsFakeTCPConn(orig *TCPConn, obfs obfs.Obfuscator) *ObfsFakeTCPConn { - return &ObfsFakeTCPConn{ - orig: orig, - obfs: obfs, - readBuf: make([]byte, udpBufferSize), - writeBuf: make([]byte, udpBufferSize), - } -} - -func (c *ObfsFakeTCPConn) ReadFrom(p []byte) (int, net.Addr, error) { - for { - c.readMutex.Lock() - n, addr, err := c.orig.ReadFrom(c.readBuf) - if n <= 0 { - c.readMutex.Unlock() - return 0, addr, err - } - newN := c.obfs.Deobfuscate(c.readBuf[:n], p) - c.readMutex.Unlock() - if newN > 0 { - // Valid packet - return newN, addr, err - } else if err != nil { - // Not valid and orig.ReadFrom had some error - return 0, addr, err - } - } -} - -func (c *ObfsFakeTCPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.writeMutex.Lock() - bn := c.obfs.Obfuscate(p, c.writeBuf) - _, err = c.orig.WriteTo(c.writeBuf[:bn], addr) - c.writeMutex.Unlock() - if err != nil { - return 0, err - } else { - return len(p), nil - } -} - -func (c *ObfsFakeTCPConn) Close() error { - return c.orig.Close() -} - -func (c *ObfsFakeTCPConn) LocalAddr() net.Addr { - return c.orig.LocalAddr() -} - -func (c *ObfsFakeTCPConn) SetDeadline(t time.Time) error { - return c.orig.SetDeadline(t) -} - -func (c *ObfsFakeTCPConn) SetReadDeadline(t time.Time) error { - return c.orig.SetReadDeadline(t) -} - -func (c *ObfsFakeTCPConn) SetWriteDeadline(t time.Time) error { - return c.orig.SetWriteDeadline(t) -} - -func (c *ObfsFakeTCPConn) SetReadBuffer(bytes int) error { - return c.orig.SetReadBuffer(bytes) -} - -func (c *ObfsFakeTCPConn) SetWriteBuffer(bytes int) error { - return c.orig.SetWriteBuffer(bytes) -} - -func (c *ObfsFakeTCPConn) SyscallConn() (syscall.RawConn, error) { - return c.orig.SyscallConn() -} diff --git a/transport/hysteria/conns/faketcp/tcp_linux.go b/transport/hysteria/conns/faketcp/tcp_linux.go deleted file mode 100644 index fb59cf985b..0000000000 --- a/transport/hysteria/conns/faketcp/tcp_linux.go +++ /dev/null @@ -1,640 +0,0 @@ -//go:build linux && !no_fake_tcp -// +build linux,!no_fake_tcp - -package faketcp - -import ( - "crypto/rand" - "encoding/binary" - "errors" - "fmt" - "io" - "io/ioutil" - "net" - "sync" - "sync/atomic" - "syscall" - "time" - - "github.com/coreos/go-iptables/iptables" - "github.com/metacubex/gopacket" - "github.com/metacubex/gopacket/layers" - - "github.com/metacubex/mihomo/component/dialer" -) - -var ( - errOpNotImplemented = errors.New("operation not implemented") - errTimeout = errors.New("timeout") - expire = time.Minute -) - -// a message from NIC -type message struct { - bts []byte - addr net.Addr -} - -// a tcp flow information of a connection pair -type tcpFlow struct { - conn *net.TCPConn // the related system TCP connection of this flow - handle *net.IPConn // the handle to send packets - seq uint32 // TCP sequence number - ack uint32 // TCP acknowledge number - networkLayer gopacket.SerializableLayer // network layer header for tx - ts time.Time // last packet incoming time - buf gopacket.SerializeBuffer // a buffer for write - tcpHeader layers.TCP -} - -// TCPConn defines a TCP-packet oriented connection -type TCPConn struct { - die chan struct{} - dieOnce sync.Once - - // the main golang sockets - tcpconn *net.TCPConn // from net.Dial - listener *net.TCPListener // from net.Listen - - // handles - handles []*net.IPConn - - // packets captured from all related NICs will be delivered to this channel - chMessage chan message - - // all TCP flows - flowTable map[string]*tcpFlow - flowsLock sync.Mutex - - // iptables - iptables *iptables.IPTables - iprule []string - - ip6tables *iptables.IPTables - ip6rule []string - - // deadlines - readDeadline atomic.Value - writeDeadline atomic.Value - - // serialization - opts gopacket.SerializeOptions -} - -// lockflow locks the flow table and apply function `f` to the entry, and create one if not exist -func (conn *TCPConn) lockflow(addr net.Addr, f func(e *tcpFlow)) { - key := addr.String() - conn.flowsLock.Lock() - e := conn.flowTable[key] - if e == nil { // entry first visit - e = new(tcpFlow) - e.ts = time.Now() - e.buf = gopacket.NewSerializeBuffer() - } - f(e) - conn.flowTable[key] = e - conn.flowsLock.Unlock() -} - -// clean expired flows -func (conn *TCPConn) cleaner() { - ticker := time.NewTicker(time.Minute) - select { - case <-conn.die: - return - case <-ticker.C: - conn.flowsLock.Lock() - for k, v := range conn.flowTable { - if time.Now().Sub(v.ts) > expire { - if v.conn != nil { - setTTL(v.conn, 64) - v.conn.Close() - } - delete(conn.flowTable, k) - } - } - conn.flowsLock.Unlock() - } -} - -// captureFlow capture every inbound packets based on rules of BPF -func (conn *TCPConn) captureFlow(handle *net.IPConn, port int) { - buf := make([]byte, 2048) - opt := gopacket.DecodeOptions{NoCopy: true, Lazy: true} - for { - n, addr, err := handle.ReadFromIP(buf) - if err != nil { - return - } - - // try decoding TCP frame from buf[:n] - packet := gopacket.NewPacket(buf[:n], layers.LayerTypeTCP, opt) - transport := packet.TransportLayer() - tcp, ok := transport.(*layers.TCP) - if !ok { - continue - } - - // port filtering - if int(tcp.DstPort) != port { - continue - } - - // address building - var src net.TCPAddr - src.IP = addr.IP - src.Port = int(tcp.SrcPort) - - var orphan bool - // flow maintaince - conn.lockflow(&src, func(e *tcpFlow) { - if e.conn == nil { // make sure it's related to net.TCPConn - orphan = true // mark as orphan if it's not related net.TCPConn - } - - // to keep track of TCP header related to this source - e.ts = time.Now() - if tcp.ACK { - e.seq = tcp.Ack - } - if tcp.SYN { - e.ack = tcp.Seq + 1 - } - if tcp.PSH { - if e.ack == tcp.Seq { - e.ack = tcp.Seq + uint32(len(tcp.Payload)) - } - } - e.handle = handle - }) - - // push data if it's not orphan - if !orphan && tcp.PSH { - payload := make([]byte, len(tcp.Payload)) - copy(payload, tcp.Payload) - select { - case conn.chMessage <- message{payload, &src}: - case <-conn.die: - return - } - } - } -} - -// ReadFrom implements the PacketConn ReadFrom method. -func (conn *TCPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - var timer *time.Timer - var deadline <-chan time.Time - if d, ok := conn.readDeadline.Load().(time.Time); ok && !d.IsZero() { - timer = time.NewTimer(time.Until(d)) - defer timer.Stop() - deadline = timer.C - } - - select { - case <-deadline: - return 0, nil, errTimeout - case <-conn.die: - return 0, nil, io.EOF - case packet := <-conn.chMessage: - n = copy(p, packet.bts) - return n, packet.addr, nil - } -} - -// WriteTo implements the PacketConn WriteTo method. -func (conn *TCPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - var deadline <-chan time.Time - if d, ok := conn.writeDeadline.Load().(time.Time); ok && !d.IsZero() { - timer := time.NewTimer(time.Until(d)) - defer timer.Stop() - deadline = timer.C - } - - select { - case <-deadline: - return 0, errTimeout - case <-conn.die: - return 0, io.EOF - default: - raddr, err := net.ResolveTCPAddr("tcp", addr.String()) - if err != nil { - return 0, err - } - - var lport int - if conn.tcpconn != nil { - lport = conn.tcpconn.LocalAddr().(*net.TCPAddr).Port - } else { - lport = conn.listener.Addr().(*net.TCPAddr).Port - } - - conn.lockflow(addr, func(e *tcpFlow) { - // if the flow doesn't have handle , assume this packet has lost, without notification - if e.handle == nil { - n = len(p) - return - } - - // build tcp header with local and remote port - e.tcpHeader.SrcPort = layers.TCPPort(lport) - e.tcpHeader.DstPort = layers.TCPPort(raddr.Port) - binary.Read(rand.Reader, binary.LittleEndian, &e.tcpHeader.Window) - e.tcpHeader.Window |= 0x8000 // make sure it's larger than 32768 - e.tcpHeader.Ack = e.ack - e.tcpHeader.Seq = e.seq - e.tcpHeader.PSH = true - e.tcpHeader.ACK = true - - // build IP header with src & dst ip for TCP checksum - if raddr.IP.To4() != nil { - ip := &layers.IPv4{ - Protocol: layers.IPProtocolTCP, - SrcIP: e.handle.LocalAddr().(*net.IPAddr).IP.To4(), - DstIP: raddr.IP.To4(), - } - e.tcpHeader.SetNetworkLayerForChecksum(ip) - } else { - ip := &layers.IPv6{ - NextHeader: layers.IPProtocolTCP, - SrcIP: e.handle.LocalAddr().(*net.IPAddr).IP.To16(), - DstIP: raddr.IP.To16(), - } - e.tcpHeader.SetNetworkLayerForChecksum(ip) - } - - e.buf.Clear() - gopacket.SerializeLayers(e.buf, conn.opts, &e.tcpHeader, gopacket.Payload(p)) - if conn.tcpconn != nil { - _, err = e.handle.Write(e.buf.Bytes()) - } else { - _, err = e.handle.WriteToIP(e.buf.Bytes(), &net.IPAddr{IP: raddr.IP}) - } - // increase seq in flow - e.seq += uint32(len(p)) - n = len(p) - }) - } - return -} - -// Close closes the connection. -func (conn *TCPConn) Close() error { - var err error - conn.dieOnce.Do(func() { - // signal closing - close(conn.die) - - // close all established tcp connections - if conn.tcpconn != nil { // client - setTTL(conn.tcpconn, 64) - err = conn.tcpconn.Close() - } else if conn.listener != nil { - err = conn.listener.Close() // server - conn.flowsLock.Lock() - for k, v := range conn.flowTable { - if v.conn != nil { - setTTL(v.conn, 64) - v.conn.Close() - } - delete(conn.flowTable, k) - } - conn.flowsLock.Unlock() - } - - // close handles - for k := range conn.handles { - conn.handles[k].Close() - } - - // delete iptable - if conn.iptables != nil { - conn.iptables.Delete("filter", "OUTPUT", conn.iprule...) - } - if conn.ip6tables != nil { - conn.ip6tables.Delete("filter", "OUTPUT", conn.ip6rule...) - } - }) - return err -} - -// LocalAddr returns the local network address. -func (conn *TCPConn) LocalAddr() net.Addr { - if conn.tcpconn != nil { - return conn.tcpconn.LocalAddr() - } else if conn.listener != nil { - return conn.listener.Addr() - } - return nil -} - -// SetDeadline implements the Conn SetDeadline method. -func (conn *TCPConn) SetDeadline(t time.Time) error { - if err := conn.SetReadDeadline(t); err != nil { - return err - } - if err := conn.SetWriteDeadline(t); err != nil { - return err - } - return nil -} - -// SetReadDeadline implements the Conn SetReadDeadline method. -func (conn *TCPConn) SetReadDeadline(t time.Time) error { - conn.readDeadline.Store(t) - return nil -} - -// SetWriteDeadline implements the Conn SetWriteDeadline method. -func (conn *TCPConn) SetWriteDeadline(t time.Time) error { - conn.writeDeadline.Store(t) - return nil -} - -// SetDSCP sets the 6bit DSCP field in IPv4 header, or 8bit Traffic Class in IPv6 header. -func (conn *TCPConn) SetDSCP(dscp int) error { - for k := range conn.handles { - if err := setDSCP(conn.handles[k], dscp); err != nil { - return err - } - } - return nil -} - -// SetReadBuffer sets the size of the operating system's receive buffer associated with the connection. -func (conn *TCPConn) SetReadBuffer(bytes int) error { - var err error - for k := range conn.handles { - if err := conn.handles[k].SetReadBuffer(bytes); err != nil { - return err - } - } - return err -} - -// SetWriteBuffer sets the size of the operating system's transmit buffer associated with the connection. -func (conn *TCPConn) SetWriteBuffer(bytes int) error { - var err error - for k := range conn.handles { - if err := conn.handles[k].SetWriteBuffer(bytes); err != nil { - return err - } - } - return err -} - -func (conn *TCPConn) SyscallConn() (syscall.RawConn, error) { - if len(conn.handles) == 0 { - return nil, errors.New("no handles") - // How is it possible? - } - return conn.handles[0].SyscallConn() -} - -// Dial connects to the remote TCP port, -// and returns a single packet-oriented connection -func Dial(network, address string) (*TCPConn, error) { - // init gopacket.layers - layers.Init() - // remote address resolve - raddr, err := net.ResolveTCPAddr(network, address) - if err != nil { - return nil, err - } - - var lTcpAddr *net.TCPAddr - var lIpAddr *net.IPAddr - rAddrPort := raddr.AddrPort() - ifaceName := dialer.DefaultInterface.Load() - if ifaceName == "" { - if finder := dialer.DefaultInterfaceFinder.Load(); finder != nil { - ifaceName = finder.FindInterfaceName(rAddrPort.Addr()) - } - } - if len(ifaceName) > 0 { - addr, err := dialer.LookupLocalAddrFromIfaceName(ifaceName, network, rAddrPort.Addr(), int(rAddrPort.Port())) - if err != nil { - return nil, err - } - lTcpAddr = addr.(*net.TCPAddr) - lIpAddr = &net.IPAddr{IP: lTcpAddr.IP} - } - - // AF_INET - handle, err := net.DialIP("ip:tcp", lIpAddr, &net.IPAddr{IP: raddr.IP}) - if err != nil { - return nil, err - } - - // create an established tcp connection - // will hack this tcp connection for packet transmission - tcpconn, err := net.DialTCP(network, lTcpAddr, raddr) - if err != nil { - return nil, err - } - - // fields - conn := new(TCPConn) - conn.die = make(chan struct{}) - conn.flowTable = make(map[string]*tcpFlow) - conn.tcpconn = tcpconn - conn.chMessage = make(chan message) - conn.lockflow(tcpconn.RemoteAddr(), func(e *tcpFlow) { e.conn = tcpconn }) - conn.handles = append(conn.handles, handle) - conn.opts = gopacket.SerializeOptions{ - FixLengths: true, - ComputeChecksums: true, - } - go conn.captureFlow(handle, tcpconn.LocalAddr().(*net.TCPAddr).Port) - go conn.cleaner() - - // iptables - err = setTTL(tcpconn, 1) - if err != nil { - return nil, err - } - - if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4); err == nil { - rule := []string{"-m", "ttl", "--ttl-eq", "1", "-p", "tcp", "-d", raddr.IP.String(), "--dport", fmt.Sprint(raddr.Port), "-j", "DROP"} - if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { - if !exists { - if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { - conn.iprule = rule - conn.iptables = ipt - } - } - } - } - if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6); err == nil { - rule := []string{"-m", "hl", "--hl-eq", "1", "-p", "tcp", "-d", raddr.IP.String(), "--dport", fmt.Sprint(raddr.Port), "-j", "DROP"} - if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { - if !exists { - if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { - conn.ip6rule = rule - conn.ip6tables = ipt - } - } - } - } - - // discard everything - go io.Copy(ioutil.Discard, tcpconn) - - return conn, nil -} - -// Listen acts like net.ListenTCP, -// and returns a single packet-oriented connection -func Listen(network, address string) (*TCPConn, error) { - // init gopacket.layers - layers.Init() - // fields - conn := new(TCPConn) - conn.flowTable = make(map[string]*tcpFlow) - conn.die = make(chan struct{}) - conn.chMessage = make(chan message) - conn.opts = gopacket.SerializeOptions{ - FixLengths: true, - ComputeChecksums: true, - } - - // resolve address - laddr, err := net.ResolveTCPAddr(network, address) - if err != nil { - return nil, err - } - - // AF_INET - ifaces, err := net.Interfaces() - if err != nil { - return nil, err - } - - if laddr.IP == nil || laddr.IP.IsUnspecified() { // if address is not specified, capture on all ifaces - var lasterr error - for _, iface := range ifaces { - if addrs, err := iface.Addrs(); err == nil { - for _, addr := range addrs { - if ipaddr, ok := addr.(*net.IPNet); ok { - if handle, err := net.ListenIP("ip:tcp", &net.IPAddr{IP: ipaddr.IP}); err == nil { - conn.handles = append(conn.handles, handle) - go conn.captureFlow(handle, laddr.Port) - } else { - lasterr = err - } - } - } - } - } - if len(conn.handles) == 0 { - return nil, lasterr - } - } else { - if handle, err := net.ListenIP("ip:tcp", &net.IPAddr{IP: laddr.IP}); err == nil { - conn.handles = append(conn.handles, handle) - go conn.captureFlow(handle, laddr.Port) - } else { - return nil, err - } - } - - // start listening - l, err := net.ListenTCP(network, laddr) - if err != nil { - return nil, err - } - - conn.listener = l - - // start cleaner - go conn.cleaner() - - // iptables drop packets marked with TTL = 1 - // TODO: what if iptables is not available, the next hop will send back ICMP Time Exceeded, - // is this still an acceptable behavior? - if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4); err == nil { - rule := []string{"-m", "ttl", "--ttl-eq", "1", "-p", "tcp", "--sport", fmt.Sprint(laddr.Port), "-j", "DROP"} - if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { - if !exists { - if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { - conn.iprule = rule - conn.iptables = ipt - } - } - } - } - if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6); err == nil { - rule := []string{"-m", "hl", "--hl-eq", "1", "-p", "tcp", "--sport", fmt.Sprint(laddr.Port), "-j", "DROP"} - if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { - if !exists { - if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { - conn.ip6rule = rule - conn.ip6tables = ipt - } - } - } - } - - // discard everything in original connection - go func() { - for { - tcpconn, err := l.AcceptTCP() - if err != nil { - return - } - - // if we cannot set TTL = 1, the only thing reasonable is panic - if err := setTTL(tcpconn, 1); err != nil { - panic(err) - } - - // record net.Conn - conn.lockflow(tcpconn.RemoteAddr(), func(e *tcpFlow) { e.conn = tcpconn }) - - // discard everything - go io.Copy(ioutil.Discard, tcpconn) - } - }() - - return conn, nil -} - -// setTTL sets the Time-To-Live field on a given connection -func setTTL(c *net.TCPConn, ttl int) error { - raw, err := c.SyscallConn() - if err != nil { - return err - } - addr := c.LocalAddr().(*net.TCPAddr) - - if addr.IP.To4() == nil { - raw.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_UNICAST_HOPS, ttl) - }) - } else { - raw.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TTL, ttl) - }) - } - return err -} - -// setDSCP sets the 6bit DSCP field in IPv4 header, or 8bit Traffic Class in IPv6 header. -func setDSCP(c *net.IPConn, dscp int) error { - raw, err := c.SyscallConn() - if err != nil { - return err - } - addr := c.LocalAddr().(*net.IPAddr) - - if addr.IP.To4() == nil { - raw.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_TCLASS, dscp) - }) - } else { - raw.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TOS, dscp<<2) - }) - } - return err -} diff --git a/transport/hysteria/conns/faketcp/tcp_stub.go b/transport/hysteria/conns/faketcp/tcp_stub.go deleted file mode 100644 index 9f9ff97dfa..0000000000 --- a/transport/hysteria/conns/faketcp/tcp_stub.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build !linux || no_fake_tcp -// +build !linux no_fake_tcp - -package faketcp - -import ( - "errors" - "net" -) - -type TCPConn struct{ *net.UDPConn } - -// Dial connects to the remote TCP port, -// and returns a single packet-oriented connection -func Dial(network, address string) (*TCPConn, error) { - return nil, errors.New("faketcp is not supported on this platform") -} - -func Listen(network, address string) (*TCPConn, error) { - return nil, errors.New("faketcp is not supported on this platform") -} diff --git a/transport/hysteria/conns/faketcp/tcp_test.go b/transport/hysteria/conns/faketcp/tcp_test.go deleted file mode 100644 index 03f73afb8c..0000000000 --- a/transport/hysteria/conns/faketcp/tcp_test.go +++ /dev/null @@ -1,192 +0,0 @@ -//go:build linux -// +build linux - -package faketcp - -import ( - _ "net/http/pprof" -) - -//const testPortStream = "127.0.0.1:3456" -//const testPortPacket = "127.0.0.1:3457" - -const testPortStream = "127.0.0.1:3456" -const portServerPacket = "[::]:3457" -const portRemotePacket = "127.0.0.1:3457" - -//func init() { -// startTCPServer() -// startTCPRawServer() -// go func() { -// log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) -// }() -//} -// -//func startTCPServer() net.Listener { -// l, err := net.Listen("tcp", testPortStream) -// if err != nil { -// log.Panicln(err) -// } -// -// go func() { -// defer l.Close() -// for { -// conn, err := l.Accept() -// if err != nil { -// log.Println(err) -// return -// } -// -// go handleRequest(conn) -// } -// }() -// return l -//} -// -//func startTCPRawServer() *TCPConn { -// conn, err := Listen("tcp", portServerPacket) -// if err != nil { -// log.Panicln(err) -// } -// err = conn.SetReadBuffer(1024 * 1024) -// if err != nil { -// log.Println(err) -// } -// err = conn.SetWriteBuffer(1024 * 1024) -// if err != nil { -// log.Println(err) -// } -// -// go func() { -// defer conn.Close() -// buf := make([]byte, 1024) -// for { -// n, addr, err := conn.ReadFrom(buf) -// if err != nil { -// log.Println("server readfrom:", err) -// return -// } -// //echo -// n, err = conn.WriteTo(buf[:n], addr) -// if err != nil { -// log.Println("server writeTo:", err) -// return -// } -// } -// }() -// return conn -//} -// -//func handleRequest(conn net.Conn) { -// defer conn.Close() -// -// for { -// buf := make([]byte, 1024) -// size, err := conn.Read(buf) -// if err != nil { -// log.Println("handleRequest:", err) -// return -// } -// data := buf[:size] -// conn.Write(data) -// } -//} -// -//func TestDialTCPStream(t *testing.T) { -// conn, err := Dial("tcp", testPortStream) -// if err != nil { -// t.Fatal(err) -// } -// defer conn.Close() -// -// addr, err := net.ResolveTCPAddr("tcp", testPortStream) -// if err != nil { -// t.Fatal(err) -// } -// -// n, err := conn.WriteTo([]byte("abc"), addr) -// if err != nil { -// t.Fatal(n, err) -// } -// -// buf := make([]byte, 1024) -// if n, addr, err := conn.ReadFrom(buf); err != nil { -// t.Fatal(n, addr, err) -// } else { -// log.Println(string(buf[:n]), "from:", addr) -// } -//} -// -//func TestDialToTCPPacket(t *testing.T) { -// conn, err := Dial("tcp", portRemotePacket) -// if err != nil { -// t.Fatal(err) -// } -// defer conn.Close() -// -// addr, err := net.ResolveTCPAddr("tcp", portRemotePacket) -// if err != nil { -// t.Fatal(err) -// } -// -// n, err := conn.WriteTo([]byte("abc"), addr) -// if err != nil { -// t.Fatal(n, err) -// } -// log.Println("written") -// -// buf := make([]byte, 1024) -// log.Println("readfrom buf") -// if n, addr, err := conn.ReadFrom(buf); err != nil { -// log.Println(err) -// t.Fatal(n, addr, err) -// } else { -// log.Println(string(buf[:n]), "from:", addr) -// } -// -// log.Println("complete") -//} -// -//func TestSettings(t *testing.T) { -// conn, err := Dial("tcp", portRemotePacket) -// if err != nil { -// t.Fatal(err) -// } -// defer conn.Close() -// if err := conn.SetDSCP(46); err != nil { -// log.Fatal("SetDSCP:", err) -// } -// if err := conn.SetReadBuffer(4096); err != nil { -// log.Fatal("SetReaderBuffer:", err) -// } -// if err := conn.SetWriteBuffer(4096); err != nil { -// log.Fatal("SetWriteBuffer:", err) -// } -//} -// -//func BenchmarkEcho(b *testing.B) { -// conn, err := Dial("tcp", portRemotePacket) -// if err != nil { -// b.Fatal(err) -// } -// defer conn.Close() -// -// addr, err := net.ResolveTCPAddr("tcp", portRemotePacket) -// if err != nil { -// b.Fatal(err) -// } -// -// buf := make([]byte, 1024) -// b.ReportAllocs() -// b.SetBytes(int64(len(buf))) -// for i := 0; i < b.N; i++ { -// n, err := conn.WriteTo(buf, addr) -// if err != nil { -// b.Fatal(n, err) -// } -// -// if n, addr, err := conn.ReadFrom(buf); err != nil { -// b.Fatal(n, addr, err) -// } -// } -//} diff --git a/transport/hysteria/conns/udp/hop.go b/transport/hysteria/conns/udp/hop.go deleted file mode 100644 index 0a88874921..0000000000 --- a/transport/hysteria/conns/udp/hop.go +++ /dev/null @@ -1,365 +0,0 @@ -package udp - -import ( - "errors" - "net" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "github.com/metacubex/mihomo/transport/hysteria/obfs" - "github.com/metacubex/mihomo/transport/hysteria/utils" - - "github.com/metacubex/randv2" -) - -const ( - packetQueueSize = 1024 -) - -// ObfsUDPHopClientPacketConn is the UDP port-hopping packet connection for client side. -// It hops to a different local & server port every once in a while. -type ObfsUDPHopClientPacketConn struct { - serverAddr net.Addr // Combined udpHopAddr - serverAddrs []net.Addr - hopInterval time.Duration - - obfs obfs.Obfuscator - - connMutex sync.RWMutex - prevConn net.PacketConn - currentConn net.PacketConn - addrIndex int - - readBufferSize int - writeBufferSize int - - recvQueue chan *udpPacket - closeChan chan struct{} - closed bool - - bufPool sync.Pool -} - -type udpHopAddr string - -func (a *udpHopAddr) Network() string { - return "udp-hop" -} - -func (a *udpHopAddr) String() string { - return string(*a) -} - -type udpPacket struct { - buf []byte - n int - addr net.Addr -} - -func NewObfsUDPHopClientPacketConn(server string, serverPorts string, hopInterval time.Duration, obfs obfs.Obfuscator, dialer utils.PacketDialer) (net.PacketConn, error) { - ports, err := parsePorts(serverPorts) - if err != nil { - return nil, err - } - // Resolve the server IP address, then attach the ports to UDP addresses - rAddr, err := dialer.RemoteAddr(server) - if err != nil { - return nil, err - } - ip, _, err := net.SplitHostPort(rAddr.String()) - if err != nil { - return nil, err - } - serverAddrs := make([]net.Addr, len(ports)) - for i, port := range ports { - serverAddrs[i] = &net.UDPAddr{ - IP: net.ParseIP(ip), - Port: int(port), - } - } - hopAddr := udpHopAddr(server) - conn := &ObfsUDPHopClientPacketConn{ - serverAddr: &hopAddr, - serverAddrs: serverAddrs, - hopInterval: hopInterval, - obfs: obfs, - addrIndex: randv2.IntN(len(serverAddrs)), - recvQueue: make(chan *udpPacket, packetQueueSize), - closeChan: make(chan struct{}), - bufPool: sync.Pool{ - New: func() interface{} { - return make([]byte, udpBufferSize) - }, - }, - } - curConn, err := dialer.ListenPacket(rAddr) - if err != nil { - return nil, err - } - if obfs != nil { - conn.currentConn = NewObfsUDPConn(curConn, obfs) - } else { - conn.currentConn = curConn - } - go conn.recvRoutine(conn.currentConn) - go conn.hopRoutine(dialer, rAddr) - if _, ok := conn.currentConn.(syscall.Conn); ok { - return &ObfsUDPHopClientPacketConnWithSyscall{conn}, nil - } - return conn, nil -} - -func (c *ObfsUDPHopClientPacketConn) recvRoutine(conn net.PacketConn) { - for { - buf := c.bufPool.Get().([]byte) - n, addr, err := conn.ReadFrom(buf) - if err != nil { - return - } - select { - case c.recvQueue <- &udpPacket{buf, n, addr}: - default: - // Drop the packet if the queue is full - c.bufPool.Put(buf) - } - } -} - -func (c *ObfsUDPHopClientPacketConn) hopRoutine(dialer utils.PacketDialer, rAddr net.Addr) { - ticker := time.NewTicker(c.hopInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - c.hop(dialer, rAddr) - case <-c.closeChan: - return - } - } -} - -func (c *ObfsUDPHopClientPacketConn) hop(dialer utils.PacketDialer, rAddr net.Addr) { - c.connMutex.Lock() - defer c.connMutex.Unlock() - if c.closed { - return - } - newConn, err := dialer.ListenPacket(rAddr) - if err != nil { - // Skip this hop if failed to listen - return - } - // Close prevConn, - // prevConn <- currentConn - // currentConn <- newConn - // update addrIndex - // - // We need to keep receiving packets from the previous connection, - // because otherwise there will be packet loss due to the time gap - // between we hop to a new port and the server acknowledges this change. - if c.prevConn != nil { - _ = c.prevConn.Close() // recvRoutine will exit on error - } - c.prevConn = c.currentConn - if c.obfs != nil { - c.currentConn = NewObfsUDPConn(newConn, c.obfs) - } else { - c.currentConn = newConn - } - // Set buffer sizes if previously set - if c.readBufferSize > 0 { - _ = trySetPacketConnReadBuffer(c.currentConn, c.readBufferSize) - } - if c.writeBufferSize > 0 { - _ = trySetPacketConnWriteBuffer(c.currentConn, c.writeBufferSize) - } - go c.recvRoutine(c.currentConn) - c.addrIndex = randv2.IntN(len(c.serverAddrs)) -} - -func (c *ObfsUDPHopClientPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { - for { - select { - case p := <-c.recvQueue: - /* - // Check if the packet is from one of the server addresses - for _, addr := range c.serverAddrs { - if addr.String() == p.addr.String() { - // Copy the packet to the buffer - n := copy(b, p.buf[:p.n]) - c.bufPool.Put(p.buf) - return n, c.serverAddr, nil - } - } - // Drop the packet, continue - c.bufPool.Put(p.buf) - */ - // The above code was causing performance issues when the range is large, - // so we skip the check for now. Should probably still check by using a map - // or something in the future. - n := copy(b, p.buf[:p.n]) - c.bufPool.Put(p.buf) - return n, c.serverAddr, nil - case <-c.closeChan: - return 0, nil, net.ErrClosed - } - // Ignore packets from other addresses - } -} - -func (c *ObfsUDPHopClientPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { - c.connMutex.RLock() - defer c.connMutex.RUnlock() - if c.closed { - return 0, net.ErrClosed - } - /* - // Check if the address is the server address - if addr.String() != c.serverAddr.String() { - return 0, net.ErrWriteToConnected - } - */ - // Skip the check for now, always write to the server - return c.currentConn.WriteTo(b, c.serverAddrs[c.addrIndex]) -} - -func (c *ObfsUDPHopClientPacketConn) Close() error { - c.connMutex.Lock() - defer c.connMutex.Unlock() - if c.closed { - return nil - } - // Close prevConn and currentConn - // Close closeChan to unblock ReadFrom & hopRoutine - // Set closed flag to true to prevent double close - if c.prevConn != nil { - _ = c.prevConn.Close() - } - err := c.currentConn.Close() - close(c.closeChan) - c.closed = true - c.serverAddrs = nil // For GC - return err -} - -func (c *ObfsUDPHopClientPacketConn) LocalAddr() net.Addr { - c.connMutex.RLock() - defer c.connMutex.RUnlock() - return c.currentConn.LocalAddr() -} - -func (c *ObfsUDPHopClientPacketConn) SetReadDeadline(t time.Time) error { - // Not supported - return nil -} - -func (c *ObfsUDPHopClientPacketConn) SetWriteDeadline(t time.Time) error { - // Not supported - return nil -} - -func (c *ObfsUDPHopClientPacketConn) SetDeadline(t time.Time) error { - err := c.SetReadDeadline(t) - if err != nil { - return err - } - return c.SetWriteDeadline(t) -} - -func (c *ObfsUDPHopClientPacketConn) SetReadBuffer(bytes int) error { - c.connMutex.Lock() - defer c.connMutex.Unlock() - c.readBufferSize = bytes - if c.prevConn != nil { - _ = trySetPacketConnReadBuffer(c.prevConn, bytes) - } - return trySetPacketConnReadBuffer(c.currentConn, bytes) -} - -func (c *ObfsUDPHopClientPacketConn) SetWriteBuffer(bytes int) error { - c.connMutex.Lock() - defer c.connMutex.Unlock() - c.writeBufferSize = bytes - if c.prevConn != nil { - _ = trySetPacketConnWriteBuffer(c.prevConn, bytes) - } - return trySetPacketConnWriteBuffer(c.currentConn, bytes) -} - -func trySetPacketConnReadBuffer(pc net.PacketConn, bytes int) error { - sc, ok := pc.(interface { - SetReadBuffer(bytes int) error - }) - if ok { - return sc.SetReadBuffer(bytes) - } - return nil -} - -func trySetPacketConnWriteBuffer(pc net.PacketConn, bytes int) error { - sc, ok := pc.(interface { - SetWriteBuffer(bytes int) error - }) - if ok { - return sc.SetWriteBuffer(bytes) - } - return nil -} - -type ObfsUDPHopClientPacketConnWithSyscall struct { - *ObfsUDPHopClientPacketConn -} - -func (c *ObfsUDPHopClientPacketConnWithSyscall) SyscallConn() (syscall.RawConn, error) { - c.connMutex.RLock() - defer c.connMutex.RUnlock() - sc, ok := c.currentConn.(syscall.Conn) - if !ok { - return nil, errors.New("not supported") - } - return sc.SyscallConn() -} - -// parsePorts parses the multi-port server address and returns the host and ports. -// Supports both comma-separated single ports and dash-separated port ranges. -// Format: "host:port1,port2-port3,port4" -func parsePorts(serverPorts string) (ports []uint16, err error) { - portStrs := strings.Split(serverPorts, ",") - for _, portStr := range portStrs { - if strings.Contains(portStr, "-") { - // Port range - portRange := strings.Split(portStr, "-") - if len(portRange) != 2 { - return nil, net.InvalidAddrError("invalid port range") - } - start, err := strconv.ParseUint(portRange[0], 10, 16) - if err != nil { - return nil, net.InvalidAddrError("invalid port range") - } - end, err := strconv.ParseUint(portRange[1], 10, 16) - if err != nil { - return nil, net.InvalidAddrError("invalid port range") - } - if start > end { - start, end = end, start - } - for i := start; i <= end; i++ { - ports = append(ports, uint16(i)) - } - } else { - // Single port - port, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - return nil, net.InvalidAddrError("invalid port") - } - ports = append(ports, uint16(port)) - } - } - if len(ports) == 0 { - return nil, net.InvalidAddrError("invalid port") - } - return ports, nil -} diff --git a/transport/hysteria/conns/udp/obfs.go b/transport/hysteria/conns/udp/obfs.go deleted file mode 100644 index a5c6c06ca9..0000000000 --- a/transport/hysteria/conns/udp/obfs.go +++ /dev/null @@ -1,80 +0,0 @@ -package udp - -import ( - "github.com/metacubex/mihomo/transport/hysteria/obfs" - "net" - "sync" - "time" -) - -const udpBufferSize = 65535 - -type ObfsUDPConn struct { - orig net.PacketConn - obfs obfs.Obfuscator - readBuf []byte - readMutex sync.Mutex - writeBuf []byte - writeMutex sync.Mutex -} - -func NewObfsUDPConn(orig net.PacketConn, obfs obfs.Obfuscator) *ObfsUDPConn { - return &ObfsUDPConn{ - orig: orig, - obfs: obfs, - readBuf: make([]byte, udpBufferSize), - writeBuf: make([]byte, udpBufferSize), - } -} - -func (c *ObfsUDPConn) ReadFrom(p []byte) (int, net.Addr, error) { - for { - c.readMutex.Lock() - n, addr, err := c.orig.ReadFrom(c.readBuf) - if n <= 0 { - c.readMutex.Unlock() - return 0, addr, err - } - newN := c.obfs.Deobfuscate(c.readBuf[:n], p) - c.readMutex.Unlock() - if newN > 0 { - // Valid packet - return newN, addr, err - } else if err != nil { - // Not valid and orig.ReadFrom had some error - return 0, addr, err - } - } -} - -func (c *ObfsUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.writeMutex.Lock() - bn := c.obfs.Obfuscate(p, c.writeBuf) - _, err = c.orig.WriteTo(c.writeBuf[:bn], addr) - c.writeMutex.Unlock() - if err != nil { - return 0, err - } else { - return len(p), nil - } -} - -func (c *ObfsUDPConn) Close() error { - return c.orig.Close() -} - -func (c *ObfsUDPConn) LocalAddr() net.Addr { - return c.orig.LocalAddr() -} - -func (c *ObfsUDPConn) SetDeadline(t time.Time) error { - return c.orig.SetDeadline(t) -} - -func (c *ObfsUDPConn) SetReadDeadline(t time.Time) error { - return c.orig.SetReadDeadline(t) -} - -func (c *ObfsUDPConn) SetWriteDeadline(t time.Time) error { - return c.orig.SetWriteDeadline(t) -} diff --git a/transport/hysteria/conns/wechat/obfs.go b/transport/hysteria/conns/wechat/obfs.go deleted file mode 100644 index c5ec47ee91..0000000000 --- a/transport/hysteria/conns/wechat/obfs.go +++ /dev/null @@ -1,99 +0,0 @@ -package wechat - -import ( - "encoding/binary" - "net" - "sync" - "time" - - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/hysteria/obfs" - - "github.com/metacubex/randv2" -) - -const udpBufferSize = 65535 - -type ObfsWeChatUDPConn struct { - orig net.PacketConn - obfs obfs.Obfuscator - readBuf []byte - readMutex sync.Mutex - writeBuf []byte - writeMutex sync.Mutex - sn uint32 -} - -func NewObfsWeChatUDPConn(orig net.PacketConn, obfs obfs.Obfuscator) *ObfsWeChatUDPConn { - log.Infoln("new wechat") - return &ObfsWeChatUDPConn{ - orig: orig, - obfs: obfs, - readBuf: make([]byte, udpBufferSize), - writeBuf: make([]byte, udpBufferSize), - sn: randv2.Uint32() & 0xFFFF, - } -} - -func (c *ObfsWeChatUDPConn) ReadFrom(p []byte) (int, net.Addr, error) { - for { - c.readMutex.Lock() - n, addr, err := c.orig.ReadFrom(c.readBuf) - if n <= 13 { - c.readMutex.Unlock() - return 0, addr, err - } - newN := c.obfs.Deobfuscate(c.readBuf[13:n], p) - c.readMutex.Unlock() - if newN > 0 { - // Valid packet - return newN, addr, err - } else if err != nil { - // Not valid and orig.ReadFrom had some error - return 0, addr, err - } - } -} - -func (c *ObfsWeChatUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.writeMutex.Lock() - c.writeBuf[0] = 0xa1 - c.writeBuf[1] = 0x08 - binary.BigEndian.PutUint32(c.writeBuf[2:], c.sn) - c.sn++ - c.writeBuf[6] = 0x00 - c.writeBuf[7] = 0x10 - c.writeBuf[8] = 0x11 - c.writeBuf[9] = 0x18 - c.writeBuf[10] = 0x30 - c.writeBuf[11] = 0x22 - c.writeBuf[12] = 0x30 - bn := c.obfs.Obfuscate(p, c.writeBuf[13:]) - _, err = c.orig.WriteTo(c.writeBuf[:13+bn], addr) - c.writeMutex.Unlock() - if err != nil { - return 0, err - } else { - return len(p), nil - } -} - -func (c *ObfsWeChatUDPConn) Close() error { - return c.orig.Close() -} - -func (c *ObfsWeChatUDPConn) LocalAddr() net.Addr { - return c.orig.LocalAddr() -} - -func (c *ObfsWeChatUDPConn) SetDeadline(t time.Time) error { - return c.orig.SetDeadline(t) -} - -func (c *ObfsWeChatUDPConn) SetReadDeadline(t time.Time) error { - return c.orig.SetReadDeadline(t) -} - -func (c *ObfsWeChatUDPConn) SetWriteDeadline(t time.Time) error { - return c.orig.SetWriteDeadline(t) -} diff --git a/transport/hysteria/core/client.go b/transport/hysteria/core/client.go deleted file mode 100644 index 03acb943ee..0000000000 --- a/transport/hysteria/core/client.go +++ /dev/null @@ -1,450 +0,0 @@ -package core - -import ( - "bytes" - "context" - "errors" - "fmt" - "net" - "strconv" - "sync" - "time" - - tlsC "github.com/metacubex/mihomo/component/tls" - "github.com/metacubex/mihomo/transport/hysteria/obfs" - "github.com/metacubex/mihomo/transport/hysteria/pmtud_fix" - "github.com/metacubex/mihomo/transport/hysteria/transport" - "github.com/metacubex/mihomo/transport/hysteria/utils" - - "github.com/lunixbochs/struc" - "github.com/metacubex/quic-go" - "github.com/metacubex/quic-go/congestion" - "github.com/metacubex/randv2" -) - -var ( - ErrClosed = errors.New("closed") -) - -type CongestionFactory func(refBPS uint64) congestion.CongestionControl - -type Client struct { - transport *transport.ClientTransport - serverAddr string - serverPorts string - protocol string - sendBPS, recvBPS uint64 - auth []byte - congestionFactory CongestionFactory - obfuscator obfs.Obfuscator - - tlsConfig *tlsC.Config - quicConfig *quic.Config - - quicSession quic.Connection - reconnectMutex sync.Mutex - closed bool - - udpSessionMutex sync.RWMutex - udpSessionMap map[uint32]chan *udpMessage - udpDefragger defragger - hopInterval time.Duration - fastOpen bool -} - -func NewClient(serverAddr string, serverPorts string, protocol string, auth []byte, tlsConfig *tlsC.Config, quicConfig *quic.Config, - transport *transport.ClientTransport, sendBPS uint64, recvBPS uint64, congestionFactory CongestionFactory, - obfuscator obfs.Obfuscator, hopInterval time.Duration, fastOpen bool) (*Client, error) { - quicConfig.DisablePathMTUDiscovery = quicConfig.DisablePathMTUDiscovery || pmtud_fix.DisablePathMTUDiscovery - c := &Client{ - transport: transport, - serverAddr: serverAddr, - serverPorts: serverPorts, - protocol: protocol, - sendBPS: sendBPS, - recvBPS: recvBPS, - auth: auth, - congestionFactory: congestionFactory, - obfuscator: obfuscator, - tlsConfig: tlsConfig, - quicConfig: quicConfig, - hopInterval: hopInterval, - fastOpen: fastOpen, - } - return c, nil -} - -func (c *Client) connectToServer(dialer utils.PacketDialer) error { - qs, err := c.transport.QUICDial(c.protocol, c.serverAddr, c.serverPorts, c.tlsConfig, c.quicConfig, c.obfuscator, c.hopInterval, dialer) - if err != nil { - return err - } - // Control stream - ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout) - stream, err := qs.OpenStreamSync(ctx) - ctxCancel() - if err != nil { - _ = qs.CloseWithError(closeErrorCodeProtocol, "protocol error") - return err - } - ok, msg, err := c.handleControlStream(qs, stream) - if err != nil { - _ = qs.CloseWithError(closeErrorCodeProtocol, "protocol error") - return err - } - if !ok { - _ = qs.CloseWithError(closeErrorCodeAuth, "auth error") - return fmt.Errorf("auth error: %s", msg) - } - // All good - c.udpSessionMap = make(map[uint32]chan *udpMessage) - go c.handleMessage(qs) - c.quicSession = qs - return nil -} - -func (c *Client) handleControlStream(qs quic.Connection, stream quic.Stream) (bool, string, error) { - // Send protocol version - _, err := stream.Write([]byte{protocolVersion}) - if err != nil { - return false, "", err - } - // Send client hello - err = struc.Pack(stream, &clientHello{ - Rate: transmissionRate{ - SendBPS: c.sendBPS, - RecvBPS: c.recvBPS, - }, - Auth: c.auth, - }) - if err != nil { - return false, "", err - } - // Receive server hello - var sh serverHello - err = struc.Unpack(stream, &sh) - if err != nil { - return false, "", err - } - // Set the congestion accordingly - if sh.OK && c.congestionFactory != nil { - qs.SetCongestionControl(c.congestionFactory(sh.Rate.RecvBPS)) - } - return sh.OK, sh.Message, nil -} - -func (c *Client) handleMessage(qs quic.Connection) { - for { - msg, err := qs.ReceiveDatagram(context.Background()) - if err != nil { - break - } - var udpMsg udpMessage - err = struc.Unpack(bytes.NewBuffer(msg), &udpMsg) - if err != nil { - continue - } - dfMsg := c.udpDefragger.Feed(udpMsg) - if dfMsg == nil { - continue - } - c.udpSessionMutex.RLock() - ch, ok := c.udpSessionMap[dfMsg.SessionID] - if ok { - select { - case ch <- dfMsg: - // OK - default: - // Silently drop the message when the channel is full - } - } - c.udpSessionMutex.RUnlock() - } -} - -func (c *Client) openStreamWithReconnect(dialer utils.PacketDialer) (quic.Connection, quic.Stream, error) { - c.reconnectMutex.Lock() - defer c.reconnectMutex.Unlock() - if c.closed { - return nil, nil, ErrClosed - } - if c.quicSession == nil { - if err := c.connectToServer(dialer); err != nil { - // Still error, oops - return nil, nil, err - } - } - stream, err := c.quicSession.OpenStream() - if err == nil { - // All good - return c.quicSession, &wrappedQUICStream{stream}, nil - } - // Something is wrong - if nErr, ok := err.(net.Error); ok && nErr.Temporary() { - // Temporary error, just return - return nil, nil, err - } - // Permanent error, need to reconnect - if err := c.connectToServer(dialer); err != nil { - // Still error, oops - return nil, nil, err - } - // We are not going to try again even if it still fails the second time - stream, err = c.quicSession.OpenStream() - return c.quicSession, &wrappedQUICStream{stream}, err -} - -func (c *Client) DialTCP(host string, port uint16, dialer utils.PacketDialer) (net.Conn, error) { - session, stream, err := c.openStreamWithReconnect(dialer) - if err != nil { - return nil, err - } - // Send request - err = struc.Pack(stream, &clientRequest{ - UDP: false, - Host: host, - Port: port, - }) - if err != nil { - _ = stream.Close() - return nil, err - } - // If fast open is enabled, we return the stream immediately - // and defer the response handling to the first Read() call - if !c.fastOpen { - // Read response - var sr serverResponse - err = struc.Unpack(stream, &sr) - if err != nil { - _ = stream.Close() - return nil, err - } - if !sr.OK { - _ = stream.Close() - return nil, fmt.Errorf("connection rejected: %s", sr.Message) - } - } - - return &quicConn{ - Orig: stream, - PseudoLocalAddr: session.LocalAddr(), - PseudoRemoteAddr: session.RemoteAddr(), - Established: !c.fastOpen, - }, nil -} - -func (c *Client) DialUDP(dialer utils.PacketDialer) (UDPConn, error) { - session, stream, err := c.openStreamWithReconnect(dialer) - if err != nil { - return nil, err - } - // Send request - err = struc.Pack(stream, &clientRequest{ - UDP: true, - }) - if err != nil { - _ = stream.Close() - return nil, err - } - // Read response - var sr serverResponse - err = struc.Unpack(stream, &sr) - if err != nil { - _ = stream.Close() - return nil, err - } - if !sr.OK { - _ = stream.Close() - return nil, fmt.Errorf("connection rejected: %s", sr.Message) - } - - // Create a session in the map - c.udpSessionMutex.Lock() - nCh := make(chan *udpMessage, 1024) - // Store the current session map for CloseFunc below - // to ensures that we are adding and removing sessions on the same map, - // as reconnecting will reassign the map - sessionMap := c.udpSessionMap - sessionMap[sr.UDPSessionID] = nCh - c.udpSessionMutex.Unlock() - - pktConn := &quicPktConn{ - Session: session, - Stream: stream, - CloseFunc: func() { - c.udpSessionMutex.Lock() - if ch, ok := sessionMap[sr.UDPSessionID]; ok { - close(ch) - delete(sessionMap, sr.UDPSessionID) - } - c.udpSessionMutex.Unlock() - }, - UDPSessionID: sr.UDPSessionID, - MsgCh: nCh, - } - go pktConn.Hold() - return pktConn, nil -} - -func (c *Client) Close() error { - c.reconnectMutex.Lock() - defer c.reconnectMutex.Unlock() - var err error - if c.quicSession != nil { - err = c.quicSession.CloseWithError(closeErrorCodeGeneric, "") - } - c.closed = true - return err -} - -type quicConn struct { - Orig quic.Stream - PseudoLocalAddr net.Addr - PseudoRemoteAddr net.Addr - Established bool -} - -func (w *quicConn) Read(b []byte) (n int, err error) { - if !w.Established { - var sr serverResponse - err := struc.Unpack(w.Orig, &sr) - if err != nil { - _ = w.Close() - return 0, err - } - if !sr.OK { - _ = w.Close() - return 0, fmt.Errorf("connection rejected: %s", sr.Message) - } - w.Established = true - } - return w.Orig.Read(b) -} - -func (w *quicConn) Write(b []byte) (n int, err error) { - return w.Orig.Write(b) -} - -func (w *quicConn) Close() error { - return w.Orig.Close() -} - -func (w *quicConn) LocalAddr() net.Addr { - return w.PseudoLocalAddr -} - -func (w *quicConn) RemoteAddr() net.Addr { - return w.PseudoRemoteAddr -} - -func (w *quicConn) SetDeadline(t time.Time) error { - return w.Orig.SetDeadline(t) -} - -func (w *quicConn) SetReadDeadline(t time.Time) error { - return w.Orig.SetReadDeadline(t) -} - -func (w *quicConn) SetWriteDeadline(t time.Time) error { - return w.Orig.SetWriteDeadline(t) -} - -type UDPConn interface { - ReadFrom() ([]byte, string, error) - WriteTo([]byte, string) error - Close() error - LocalAddr() net.Addr - SetDeadline(t time.Time) error - SetReadDeadline(t time.Time) error - SetWriteDeadline(t time.Time) error -} - -type quicPktConn struct { - Session quic.Connection - Stream quic.Stream - CloseFunc func() - UDPSessionID uint32 - MsgCh <-chan *udpMessage -} - -func (c *quicPktConn) Hold() { - // Hold the stream until it's closed - buf := make([]byte, 1024) - for { - _, err := c.Stream.Read(buf) - if err != nil { - break - } - } - _ = c.Close() -} - -func (c *quicPktConn) ReadFrom() ([]byte, string, error) { - msg := <-c.MsgCh - if msg == nil { - // Closed - return nil, "", ErrClosed - } - return msg.Data, net.JoinHostPort(msg.Host, strconv.Itoa(int(msg.Port))), nil -} - -func (c *quicPktConn) WriteTo(p []byte, addr string) error { - host, port, err := utils.SplitHostPort(addr) - if err != nil { - return err - } - msg := udpMessage{ - SessionID: c.UDPSessionID, - Host: host, - Port: port, - FragCount: 1, - Data: p, - } - // try no frag first - var msgBuf bytes.Buffer - _ = struc.Pack(&msgBuf, &msg) - err = c.Session.SendDatagram(msgBuf.Bytes()) - if err != nil { - var errSize *quic.DatagramTooLargeError - if errors.As(err, &errSize) { - // need to frag - msg.MsgID = uint16(randv2.IntN(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 - fragMsgs := fragUDPMessage(msg, int(errSize.MaxDatagramPayloadSize)) - for _, fragMsg := range fragMsgs { - msgBuf.Reset() - _ = struc.Pack(&msgBuf, &fragMsg) - err = c.Session.SendDatagram(msgBuf.Bytes()) - if err != nil { - return err - } - } - return nil - } else { - // some other error - return err - } - } else { - return nil - } -} - -func (c *quicPktConn) Close() error { - c.CloseFunc() - return c.Stream.Close() -} - -func (c *quicPktConn) LocalAddr() net.Addr { - return c.Session.LocalAddr() -} - -func (c *quicPktConn) SetDeadline(t time.Time) error { - return c.Stream.SetDeadline(t) -} - -func (c *quicPktConn) SetReadDeadline(t time.Time) error { - return c.Stream.SetReadDeadline(t) -} - -func (c *quicPktConn) SetWriteDeadline(t time.Time) error { - return c.Stream.SetWriteDeadline(t) -} diff --git a/transport/hysteria/core/frag.go b/transport/hysteria/core/frag.go deleted file mode 100644 index 7a38774768..0000000000 --- a/transport/hysteria/core/frag.go +++ /dev/null @@ -1,67 +0,0 @@ -package core - -func fragUDPMessage(m udpMessage, maxSize int) []udpMessage { - if m.Size() <= maxSize { - return []udpMessage{m} - } - fullPayload := m.Data - maxPayloadSize := maxSize - m.HeaderSize() - off := 0 - fragID := uint8(0) - fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up - var frags []udpMessage - for off < len(fullPayload) { - payloadSize := len(fullPayload) - off - if payloadSize > maxPayloadSize { - payloadSize = maxPayloadSize - } - frag := m - frag.FragID = fragID - frag.FragCount = fragCount - frag.DataLen = uint16(payloadSize) - frag.Data = fullPayload[off : off+payloadSize] - frags = append(frags, frag) - off += payloadSize - fragID++ - } - return frags -} - -type defragger struct { - msgID uint16 - frags []*udpMessage - count uint8 -} - -func (d *defragger) Feed(m udpMessage) *udpMessage { - if m.FragCount <= 1 { - return &m - } - if m.FragID >= m.FragCount { - // wtf is this? - return nil - } - if m.MsgID != d.msgID { - // new message, clear previous state - d.msgID = m.MsgID - d.frags = make([]*udpMessage, m.FragCount) - d.count = 1 - d.frags[m.FragID] = &m - } else if d.frags[m.FragID] == nil { - d.frags[m.FragID] = &m - d.count++ - if int(d.count) == len(d.frags) { - // all fragments received, assemble - var data []byte - for _, frag := range d.frags { - data = append(data, frag.Data...) - } - m.DataLen = uint16(len(data)) - m.Data = data - m.FragID = 0 - m.FragCount = 1 - return &m - } - } - return nil -} diff --git a/transport/hysteria/core/frag_test.go b/transport/hysteria/core/frag_test.go deleted file mode 100644 index f2f246259e..0000000000 --- a/transport/hysteria/core/frag_test.go +++ /dev/null @@ -1,346 +0,0 @@ -package core - -import ( - "reflect" - "testing" -) - -func Test_fragUDPMessage(t *testing.T) { - type args struct { - m udpMessage - maxSize int - } - tests := []struct { - name string - args args - want []udpMessage - }{ - { - "no frag", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - 100, - }, - []udpMessage{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - }, - }, - { - "2 frags", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - 22, - }, - []udpMessage{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 2, - DataLen: 4, - Data: []byte("hell"), - }, - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 1, - FragCount: 2, - DataLen: 1, - Data: []byte("o"), - }, - }, - }, - { - "4 frags", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 20, - Data: []byte("wow wow wow lol lmao"), - }, - 23, - }, - []udpMessage{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 4, - DataLen: 5, - Data: []byte("wow w"), - }, - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 1, - FragCount: 4, - DataLen: 5, - Data: []byte("ow wo"), - }, - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 2, - FragCount: 4, - DataLen: 5, - Data: []byte("w lol"), - }, - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 3, - FragCount: 4, - DataLen: 5, - Data: []byte(" lmao"), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := fragUDPMessage(tt.args.m, tt.args.maxSize); !reflect.DeepEqual(got, tt.want) { - t.Errorf("fragUDPMessage() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_defragger_Feed(t *testing.T) { - d := &defragger{} - type args struct { - m udpMessage - } - tests := []struct { - name string - args args - want *udpMessage - }{ - { - "no frag", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - }, - &udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - }, - { - "frag 1 - 1/3", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 666, - FragID: 0, - FragCount: 3, - DataLen: 5, - Data: []byte("hello"), - }, - }, - nil, - }, - { - "frag 1 - 2/3", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 666, - FragID: 1, - FragCount: 3, - DataLen: 8, - Data: []byte(" shitty "), - }, - }, - nil, - }, - { - "frag 1 - 3/3", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 666, - FragID: 2, - FragCount: 3, - DataLen: 7, - Data: []byte("world!!"), - }, - }, - &udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 666, - FragID: 0, - FragCount: 1, - DataLen: 20, - Data: []byte("hello shitty world!!"), - }, - }, - { - "frag 2 - 1/2", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 777, - FragID: 0, - FragCount: 2, - DataLen: 5, - Data: []byte("hello"), - }, - }, - nil, - }, - { - "frag 3 - 2/2", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 778, - FragID: 1, - FragCount: 2, - DataLen: 5, - Data: []byte(" moto"), - }, - }, - nil, - }, - { - "frag 2 - 2/2", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 777, - FragID: 1, - FragCount: 2, - DataLen: 5, - Data: []byte(" moto"), - }, - }, - nil, - }, - { - "frag 2 - 1/2 re", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 777, - FragID: 0, - FragCount: 2, - DataLen: 5, - Data: []byte("hello"), - }, - }, - &udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 777, - FragID: 0, - FragCount: 1, - DataLen: 10, - Data: []byte("hello moto"), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := d.Feed(tt.args.m); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Feed() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/transport/hysteria/core/protocol.go b/transport/hysteria/core/protocol.go deleted file mode 100644 index 7fd64d7cdc..0000000000 --- a/transport/hysteria/core/protocol.go +++ /dev/null @@ -1,76 +0,0 @@ -package core - -import ( - "time" -) - -const ( - protocolVersion = uint8(3) - protocolVersionV2 = uint8(2) - protocolTimeout = 10 * time.Second - - closeErrorCodeGeneric = 0 - closeErrorCodeProtocol = 1 - closeErrorCodeAuth = 2 -) - -type transmissionRate struct { - SendBPS uint64 - RecvBPS uint64 -} - -type clientHello struct { - Rate transmissionRate - AuthLen uint16 `struc:"sizeof=Auth"` - Auth []byte -} - -type serverHello struct { - OK bool - Rate transmissionRate - MessageLen uint16 `struc:"sizeof=Message"` - Message string -} - -type clientRequest struct { - UDP bool - HostLen uint16 `struc:"sizeof=Host"` - Host string - Port uint16 -} - -type serverResponse struct { - OK bool - UDPSessionID uint32 - MessageLen uint16 `struc:"sizeof=Message"` - Message string -} - -type udpMessage struct { - SessionID uint32 - HostLen uint16 `struc:"sizeof=Host"` - Host string - Port uint16 - MsgID uint16 // doesn't matter when not fragmented, but must not be 0 when fragmented - FragID uint8 // doesn't matter when not fragmented, starts at 0 when fragmented - FragCount uint8 // must be 1 when not fragmented - DataLen uint16 `struc:"sizeof=Data"` - Data []byte -} - -func (m udpMessage) HeaderSize() int { - return 4 + 2 + len(m.Host) + 2 + 2 + 1 + 1 + 2 -} - -func (m udpMessage) Size() int { - return m.HeaderSize() + len(m.Data) -} - -type udpMessageV2 struct { - SessionID uint32 - HostLen uint16 `struc:"sizeof=Host"` - Host string - Port uint16 - DataLen uint16 `struc:"sizeof=Data"` - Data []byte -} diff --git a/transport/hysteria/core/stream.go b/transport/hysteria/core/stream.go deleted file mode 100644 index 627b478943..0000000000 --- a/transport/hysteria/core/stream.go +++ /dev/null @@ -1,54 +0,0 @@ -package core - -import ( - "context" - "github.com/metacubex/quic-go" - "time" -) - -// Handle stream close properly -// Ref: https://github.com/libp2p/go-libp2p-quic-transport/blob/master/stream.go -type wrappedQUICStream struct { - Stream quic.Stream -} - -func (s *wrappedQUICStream) StreamID() quic.StreamID { - return s.Stream.StreamID() -} - -func (s *wrappedQUICStream) Read(p []byte) (n int, err error) { - return s.Stream.Read(p) -} - -func (s *wrappedQUICStream) CancelRead(code quic.StreamErrorCode) { - s.Stream.CancelRead(code) -} - -func (s *wrappedQUICStream) SetReadDeadline(t time.Time) error { - return s.Stream.SetReadDeadline(t) -} - -func (s *wrappedQUICStream) Write(p []byte) (n int, err error) { - return s.Stream.Write(p) -} - -func (s *wrappedQUICStream) Close() error { - s.Stream.CancelRead(0) - return s.Stream.Close() -} - -func (s *wrappedQUICStream) CancelWrite(code quic.StreamErrorCode) { - s.Stream.CancelWrite(code) -} - -func (s *wrappedQUICStream) Context() context.Context { - return s.Stream.Context() -} - -func (s *wrappedQUICStream) SetWriteDeadline(t time.Time) error { - return s.Stream.SetWriteDeadline(t) -} - -func (s *wrappedQUICStream) SetDeadline(t time.Time) error { - return s.Stream.SetDeadline(t) -} diff --git a/transport/hysteria/obfs/dummy.go b/transport/hysteria/obfs/dummy.go deleted file mode 100644 index 3edff1017c..0000000000 --- a/transport/hysteria/obfs/dummy.go +++ /dev/null @@ -1,18 +0,0 @@ -package obfs - -type DummyObfuscator struct{} - -func NewDummyObfuscator() *DummyObfuscator { - return &DummyObfuscator{} -} - -func (x *DummyObfuscator) Deobfuscate(in []byte, out []byte) int { - if len(out) < len(in) { - return 0 - } - return copy(out, in) -} - -func (x *DummyObfuscator) Obfuscate(in []byte, out []byte) int { - return copy(out, in) -} diff --git a/transport/hysteria/obfs/obfs.go b/transport/hysteria/obfs/obfs.go deleted file mode 100644 index cb108a38b4..0000000000 --- a/transport/hysteria/obfs/obfs.go +++ /dev/null @@ -1,6 +0,0 @@ -package obfs - -type Obfuscator interface { - Deobfuscate(in []byte, out []byte) int - Obfuscate(in []byte, out []byte) int -} diff --git a/transport/hysteria/obfs/xplus.go b/transport/hysteria/obfs/xplus.go deleted file mode 100644 index 4d1e3f2944..0000000000 --- a/transport/hysteria/obfs/xplus.go +++ /dev/null @@ -1,44 +0,0 @@ -package obfs - -import ( - "crypto/rand" - "crypto/sha256" -) - -// [salt][obfuscated payload] - -const saltLen = 16 - -type XPlusObfuscator struct { - Key []byte -} - -func NewXPlusObfuscator(key []byte) *XPlusObfuscator { - return &XPlusObfuscator{ - Key: key, - } -} - -func (x *XPlusObfuscator) Deobfuscate(in []byte, out []byte) int { - pLen := len(in) - saltLen - if pLen <= 0 || len(out) < pLen { - // Invalid - return 0 - } - key := sha256.Sum256(append(x.Key, in[:saltLen]...)) - // Deobfuscate the payload - for i, c := range in[saltLen:] { - out[i] = c ^ key[i%sha256.Size] - } - return pLen -} - -func (x *XPlusObfuscator) Obfuscate(in []byte, out []byte) int { - _, _ = rand.Read(out[:saltLen]) // salt - // Obfuscate the payload - key := sha256.Sum256(append(x.Key, out[:saltLen]...)) - for i, c := range in { - out[i+saltLen] = c ^ key[i%sha256.Size] - } - return len(in) + saltLen -} diff --git a/transport/hysteria/obfs/xplus_test.go b/transport/hysteria/obfs/xplus_test.go deleted file mode 100644 index c1cf6295bb..0000000000 --- a/transport/hysteria/obfs/xplus_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package obfs - -import ( - "bytes" - "testing" -) - -func TestXPlusObfuscator(t *testing.T) { - x := NewXPlusObfuscator([]byte("Vaundy")) - tests := []struct { - name string - p []byte - }{ - {name: "1", p: []byte("HelloWorld")}, - {name: "2", p: []byte("Regret is just a horrible attempt at time travel that ends with you feeling like crap")}, - {name: "3", p: []byte("To be, or not to be, that is the question:\nWhether 'tis nobler in the mind to suffer\n" + - "The slings and arrows of outrageous fortune,\nOr to take arms against a sea of troubles\n" + - "And by opposing end them. To die—to sleep,\nNo more; and by a sleep to say we end")}, - {name: "empty", p: []byte("")}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buf := make([]byte, 10240) - n := x.Obfuscate(tt.p, buf) - n2 := x.Deobfuscate(buf[:n], buf[n:]) - if !bytes.Equal(tt.p, buf[n:n+n2]) { - t.Errorf("Inconsistent deobfuscate result: got %v, want %v", buf[n:n+n2], tt.p) - } - }) - } -} diff --git a/transport/hysteria/pmtud_fix/avail.go b/transport/hysteria/pmtud_fix/avail.go deleted file mode 100644 index af248f5c92..0000000000 --- a/transport/hysteria/pmtud_fix/avail.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build linux || windows || darwin - -package pmtud_fix - -const ( - DisablePathMTUDiscovery = false -) diff --git a/transport/hysteria/pmtud_fix/unavail.go b/transport/hysteria/pmtud_fix/unavail.go deleted file mode 100644 index 35b849d2e5..0000000000 --- a/transport/hysteria/pmtud_fix/unavail.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !linux && !windows && !darwin - -package pmtud_fix - -const ( - DisablePathMTUDiscovery = true -) diff --git a/transport/hysteria/transport/client.go b/transport/hysteria/transport/client.go deleted file mode 100644 index 7fcc08e126..0000000000 --- a/transport/hysteria/transport/client.go +++ /dev/null @@ -1,85 +0,0 @@ -package transport - -import ( - "fmt" - "net" - "time" - - tlsC "github.com/metacubex/mihomo/component/tls" - "github.com/metacubex/mihomo/transport/hysteria/conns/faketcp" - "github.com/metacubex/mihomo/transport/hysteria/conns/udp" - "github.com/metacubex/mihomo/transport/hysteria/conns/wechat" - obfsPkg "github.com/metacubex/mihomo/transport/hysteria/obfs" - "github.com/metacubex/mihomo/transport/hysteria/utils" - - "github.com/metacubex/quic-go" -) - -type ClientTransport struct{} - -func (ct *ClientTransport) quicPacketConn(proto string, rAddr net.Addr, serverPorts string, obfs obfsPkg.Obfuscator, hopInterval time.Duration, dialer utils.PacketDialer) (net.PacketConn, error) { - server := rAddr.String() - if len(proto) == 0 || proto == "udp" { - conn, err := dialer.ListenPacket(rAddr) - if err != nil { - return nil, err - } - if obfs != nil { - if serverPorts != "" { - return udp.NewObfsUDPHopClientPacketConn(server, serverPorts, hopInterval, obfs, dialer) - } - oc := udp.NewObfsUDPConn(conn, obfs) - return oc, nil - } else { - if serverPorts != "" { - return udp.NewObfsUDPHopClientPacketConn(server, serverPorts, hopInterval, nil, dialer) - } - return conn, nil - } - } else if proto == "wechat-video" { - conn, err := dialer.ListenPacket(rAddr) - if err != nil { - return nil, err - } - if obfs == nil { - obfs = obfsPkg.NewDummyObfuscator() - } - return wechat.NewObfsWeChatUDPConn(conn, obfs), nil - } else if proto == "faketcp" { - var conn *faketcp.TCPConn - conn, err := faketcp.Dial("tcp", server) - if err != nil { - return nil, err - } - if obfs != nil { - oc := faketcp.NewObfsFakeTCPConn(conn, obfs) - return oc, nil - } else { - return conn, nil - } - } else { - return nil, fmt.Errorf("unsupported protocol: %s", proto) - } -} - -func (ct *ClientTransport) QUICDial(proto string, server string, serverPorts string, tlsConfig *tlsC.Config, quicConfig *quic.Config, obfs obfsPkg.Obfuscator, hopInterval time.Duration, dialer utils.PacketDialer) (quic.Connection, error) { - serverUDPAddr, err := dialer.RemoteAddr(server) - if err != nil { - return nil, err - } - - pktConn, err := ct.quicPacketConn(proto, serverUDPAddr, serverPorts, obfs, hopInterval, dialer) - if err != nil { - return nil, err - } - - transport := quic.Transport{Conn: pktConn} - transport.SetCreatedConn(true) // auto close conn - transport.SetSingleUse(true) // auto close transport - qs, err := transport.Dial(dialer.Context(), serverUDPAddr, tlsConfig, quicConfig) - if err != nil { - _ = pktConn.Close() - return nil, err - } - return qs, nil -} diff --git a/transport/hysteria/utils/misc.go b/transport/hysteria/utils/misc.go deleted file mode 100644 index 670d737c75..0000000000 --- a/transport/hysteria/utils/misc.go +++ /dev/null @@ -1,49 +0,0 @@ -package utils - -import ( - "context" - "net" - "strconv" -) - -func SplitHostPort(hostport string) (string, uint16, error) { - host, port, err := net.SplitHostPort(hostport) - if err != nil { - return "", 0, err - } - portUint, err := strconv.ParseUint(port, 10, 16) - if err != nil { - return "", 0, err - } - return host, uint16(portUint), err -} - -func ParseIPZone(s string) (net.IP, string) { - s, zone := splitHostZone(s) - return net.ParseIP(s), zone -} - -func splitHostZone(s string) (host, zone string) { - if i := last(s, '%'); i > 0 { - host, zone = s[:i], s[i+1:] - } else { - host = s - } - return -} - -func last(s string, b byte) int { - i := len(s) - for i--; i >= 0; i-- { - if s[i] == b { - break - } - } - return i -} - -type PacketDialer interface { - ListenPacket(rAddr net.Addr) (net.PacketConn, error) - Context() context.Context - RemoteAddr(host string) (net.Addr, error) -} diff --git a/transport/restls/restls.go b/transport/restls/restls.go deleted file mode 100644 index 95ac698907..0000000000 --- a/transport/restls/restls.go +++ /dev/null @@ -1,43 +0,0 @@ -package restls - -import ( - "context" - "net" - - tls "github.com/3andne/restls-client-go" -) - -const ( - Mode string = "restls" -) - -type Restls struct { - *tls.UConn -} - -func (r *Restls) Upstream() any { - return r.UConn.NetConn() -} - -type Config = tls.Config - -var NewRestlsConfig = tls.NewRestlsConfig - -// NewRestls return a Restls Connection -func NewRestls(ctx context.Context, conn net.Conn, config *Config) (net.Conn, error) { - clientHellowID := tls.HelloChrome_Auto - if config != nil { - clientIDPtr := config.ClientID.Load() - if clientIDPtr != nil { - clientHellowID = *clientIDPtr - } - } - restls := &Restls{ - UConn: tls.UClient(conn, config, clientHellowID), - } - if err := restls.HandshakeContext(ctx); err != nil { - return nil, err - } - - return restls, nil -} diff --git a/transport/shadowsocks/README.md b/transport/shadowsocks/README.md deleted file mode 100644 index c9fc815c8c..0000000000 --- a/transport/shadowsocks/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## 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 deleted file mode 100644 index 7cb12c45c3..0000000000 --- a/transport/shadowsocks/core/cipher.go +++ /dev/null @@ -1,186 +0,0 @@ -package core - -import ( - "crypto/md5" - "errors" - "net" - "sort" - "strings" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" - "github.com/metacubex/mihomo/transport/shadowsocks/shadowstream" -) - -type Cipher interface { - StreamConnCipher - PacketConnCipher -} - -type StreamConnCipher interface { - StreamConn(net.Conn) net.Conn -} - -type PacketConnCipher interface { - PacketConn(N.EnhancePacketConn) N.EnhancePacketConn -} - -// 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" - aeadChacha8Poly1305 = "AEAD_CHACHA8_POLY1305" - aeadXChacha8Poly1305 = "AEAD_XCHACHA8_POLY1305" - aeadAes128Ccm = "AEAD_AES_128_CCM" - aeadAes192Ccm = "AEAD_AES_192_CCM" - aeadAes256Ccm = "AEAD_AES_256_CCM" -) - -// 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}, - aeadChacha8Poly1305: {32, shadowaead.Chacha8Poly1305}, - aeadXChacha8Poly1305: {32, shadowaead.XChacha8Poly1305}, - aeadAes128Ccm: {16, shadowaead.AESCCM}, - aeadAes192Ccm: {24, shadowaead.AESCCM}, - aeadAes256Ccm: {32, shadowaead.AESCCM}, -} - -// 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": {32, shadowstream.ChaCha20}, - "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 - case "CHACHA8-IETF-POLY1305": - name = aeadChacha8Poly1305 - case "XCHACHA8-IETF-POLY1305": - name = aeadXChacha8Poly1305 - case "AES-128-CCM": - name = aeadAes128Ccm - case "AES-192-CCM": - name = aeadAes192Ccm - case "AES-256-CCM": - name = aeadAes256Ccm - } - - 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 N.EnhancePacketConn) N.EnhancePacketConn { - 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 N.EnhancePacketConn) N.EnhancePacketConn { - 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 N.EnhancePacketConn) N.EnhancePacketConn { 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 deleted file mode 100644 index 7981d5b15a..0000000000 --- a/transport/shadowsocks/shadowaead/cipher.go +++ /dev/null @@ -1,133 +0,0 @@ -package shadowaead - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/sha1" - "io" - "strconv" - - "github.com/metacubex/chacha" - "gitlab.com/go-extension/aes-ccm" - "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 -} - -func aesCCM(key []byte) (cipher.AEAD, error) { - blk, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - return ccm.NewCCM(blk) -} - -// AESCCM 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 AESCCM(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: aesCCM}, 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 -} - -// Chacha8Poly1305 creates a new Cipher with a pre-shared key. len(psk) -// must be 32. -func Chacha8Poly1305(psk []byte) (Cipher, error) { - if len(psk) != chacha.KeySize { - return nil, KeySizeError(chacha.KeySize) - } - return &metaCipher{psk: psk, makeAEAD: chacha.NewChaCha8IETFPoly1305}, nil -} - -// XChacha8Poly1305 creates a new Cipher with a pre-shared key. len(psk) -// must be 32. -func XChacha8Poly1305(psk []byte) (Cipher, error) { - if len(psk) != chacha.KeySize { - return nil, KeySizeError(chacha.KeySize) - } - return &metaCipher{psk: psk, makeAEAD: chacha.NewXChaCha20IETFPoly1305}, nil -} diff --git a/transport/shadowsocks/shadowaead/packet.go b/transport/shadowsocks/shadowaead/packet.go deleted file mode 100644 index f9d21ec70b..0000000000 --- a/transport/shadowsocks/shadowaead/packet.go +++ /dev/null @@ -1,113 +0,0 @@ -package shadowaead - -import ( - "crypto/rand" - "errors" - "io" - "net" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/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 { - N.EnhancePacketConn - Cipher -} - -const maxPacketSize = 64 * 1024 - -// NewPacketConn wraps an N.EnhancePacketConn with cipher -func NewPacketConn(c N.EnhancePacketConn, ciph Cipher) *PacketConn { - return &PacketConn{EnhancePacketConn: 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.EnhancePacketConn.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.EnhancePacketConn.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 -} - -func (c *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - data, put, addr, err = c.EnhancePacketConn.WaitReadFrom() - if err != nil { - return - } - data, err = Unpack(data[c.Cipher.SaltSize():], data, c) - if err != nil { - if put != nil { - put() - } - data = nil - put = nil - return - } - return -} diff --git a/transport/shadowsocks/shadowaead/stream.go b/transport/shadowsocks/shadowaead/stream.go deleted file mode 100644 index de0993b27a..0000000000 --- a/transport/shadowsocks/shadowaead/stream.go +++ /dev/null @@ -1,285 +0,0 @@ -package shadowaead - -import ( - "crypto/cipher" - "crypto/rand" - "errors" - "io" - "net" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index dd39d03b0f..0000000000 --- a/transport/shadowsocks/shadowstream/cipher.go +++ /dev/null @@ -1,116 +0,0 @@ -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/old_chacha20.go b/transport/shadowsocks/shadowstream/old_chacha20.go deleted file mode 100644 index eb61232e30..0000000000 --- a/transport/shadowsocks/shadowstream/old_chacha20.go +++ /dev/null @@ -1,23 +0,0 @@ -package shadowstream - -import ( - "crypto/cipher" - - "github.com/metacubex/chacha" -) - -type chacha20key []byte - -func (k chacha20key) IVSize() int { - return chacha.NonceSize -} -func (k chacha20key) Encrypter(iv []byte) cipher.Stream { - c, _ := chacha.NewChaCha20(iv, k) - return c -} -func (k chacha20key) Decrypter(iv []byte) cipher.Stream { - return k.Encrypter(iv) -} -func ChaCha20(key []byte) (Cipher, error) { - return chacha20key(key), nil -} diff --git a/transport/shadowsocks/shadowstream/packet.go b/transport/shadowsocks/shadowstream/packet.go deleted file mode 100644 index f8cd7e616f..0000000000 --- a/transport/shadowsocks/shadowstream/packet.go +++ /dev/null @@ -1,109 +0,0 @@ -package shadowstream - -import ( - "crypto/rand" - "errors" - "io" - "net" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/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 -} - -// UnpackInplace decrypts pkt using stream cipher s. -// Returns a slice of pkt containing decrypted plaintext. -// Note: The data in the input dst will be changed -func UnpackInplace(pkt []byte, s Cipher) ([]byte, error) { - if len(pkt) < s.IVSize() { - return nil, ErrShortPacket - } - iv, dst := pkt[:s.IVSize()], pkt[s.IVSize():] - s.Decrypter(iv).XORKeyStream(dst, dst) - return dst, 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 { - N.EnhancePacketConn - Cipher -} - -// NewPacketConn wraps an N.EnhancePacketConn with stream cipher encryption/decryption. -func NewPacketConn(c N.EnhancePacketConn, ciph Cipher) *PacketConn { - return &PacketConn{EnhancePacketConn: 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.EnhancePacketConn.WriteTo(buf, addr) - return len(b), err -} - -func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { - n, addr, err := c.EnhancePacketConn.ReadFrom(b) - if err != nil { - return n, addr, err - } - bb, err := UnpackInplace(b[:n], c.Cipher) - if err != nil { - return n, addr, err - } - copy(b, bb) - return len(bb), addr, err -} - -func (c *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - data, put, addr, err = c.EnhancePacketConn.WaitReadFrom() - if err != nil { - return - } - data, err = UnpackInplace(data, c.Cipher) - if err != nil { - if put != nil { - put() - } - data = nil - put = nil - return - } - return -} diff --git a/transport/shadowsocks/shadowstream/stream.go b/transport/shadowsocks/shadowstream/stream.go deleted file mode 100644 index 6c4b0f6da7..0000000000 --- a/transport/shadowsocks/shadowstream/stream.go +++ /dev/null @@ -1,197 +0,0 @@ -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/shadowtls/shadowtls.go b/transport/shadowtls/shadowtls.go deleted file mode 100644 index a0a3d7fb7e..0000000000 --- a/transport/shadowtls/shadowtls.go +++ /dev/null @@ -1,151 +0,0 @@ -package shadowtls - -import ( - "context" - "crypto/hmac" - "crypto/sha1" - "crypto/tls" - "encoding/binary" - "fmt" - "hash" - "io" - "net" - - "github.com/metacubex/mihomo/common/pool" - C "github.com/metacubex/mihomo/constant" -) - -const ( - chunkSize = 1 << 13 - Mode string = "shadow-tls" - hashLen int = 8 - tlsHeaderLen int = 5 -) - -var ( - DefaultALPN = []string{"h2", "http/1.1"} -) - -// ShadowTLS is shadow-tls implementation -type ShadowTLS struct { - net.Conn - password []byte - remain int - firstRequest bool - tlsConfig *tls.Config -} - -type HashedConn struct { - net.Conn - hasher hash.Hash -} - -func newHashedStream(conn net.Conn, password []byte) HashedConn { - return HashedConn{ - Conn: conn, - hasher: hmac.New(sha1.New, password), - } -} - -func (h HashedConn) Read(b []byte) (n int, err error) { - n, err = h.Conn.Read(b) - h.hasher.Write(b[:n]) - return -} - -func (s *ShadowTLS) read(b []byte) (int, error) { - var buf [tlsHeaderLen]byte - _, err := io.ReadFull(s.Conn, buf[:]) - if err != nil { - return 0, fmt.Errorf("shadowtls read failed %w", err) - } - if buf[0] != 0x17 || buf[1] != 0x3 || buf[2] != 0x3 { - return 0, fmt.Errorf("invalid shadowtls header %v", buf) - } - length := int(binary.BigEndian.Uint16(buf[3:])) - - if length > len(b) { - n, err := s.Conn.Read(b) - if err != nil { - return n, err - } - s.remain = length - n - return n, nil - } - - return io.ReadFull(s.Conn, b[:length]) -} - -func (s *ShadowTLS) Read(b []byte) (int, error) { - if s.remain > 0 { - length := s.remain - if length > len(b) { - length = len(b) - } - - n, err := io.ReadFull(s.Conn, b[:length]) - if err != nil { - return n, fmt.Errorf("shadowtls Read failed with %w", err) - } - s.remain -= n - return n, nil - } - - return s.read(b) -} - -func (s *ShadowTLS) 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 := s.write(b[i:end]) - if err != nil { - return n, fmt.Errorf("shadowtls Write failed with %w, i=%d, end=%d, n=%d", err, i, end, n) - } - } - return length, nil -} - -func (s *ShadowTLS) write(b []byte) (int, error) { - var hashVal []byte - if s.firstRequest { - hashedConn := newHashedStream(s.Conn, s.password) - tlsConn := tls.Client(hashedConn, s.tlsConfig) - // fix tls handshake not timeout - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() - if err := tlsConn.HandshakeContext(ctx); err != nil { - return 0, fmt.Errorf("tls connect failed with %w", err) - } - hashVal = hashedConn.hasher.Sum(nil)[:hashLen] - s.firstRequest = false - } - - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - buf.Write([]byte{0x17, 0x03, 0x03}) - binary.Write(buf, binary.BigEndian, uint16(len(b)+len(hashVal))) - buf.Write(hashVal) - buf.Write(b) - _, err := s.Conn.Write(buf.Bytes()) - if err != nil { - // return 0 because errors occur here make the - // whole situation irrecoverable - return 0, err - } - return len(b), nil -} - -// NewShadowTLS return a ShadowTLS -func NewShadowTLS(conn net.Conn, password string, tlsConfig *tls.Config) net.Conn { - return &ShadowTLS{ - Conn: conn, - password: []byte(password), - firstRequest: true, - tlsConfig: tlsConfig, - } -} diff --git a/transport/simple-obfs/http.go b/transport/simple-obfs/http.go deleted file mode 100644 index 9c3f8e0029..0000000000 --- a/transport/simple-obfs/http.go +++ /dev/null @@ -1,99 +0,0 @@ -package obfs - -import ( - "bytes" - "crypto/rand" - "encoding/base64" - "fmt" - "io" - "net" - "net/http" - - "github.com/metacubex/mihomo/common/pool" - - "github.com/metacubex/randv2" -) - -// 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, err := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:])) - if err != nil { - return 0, err - } - req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", randv2.Int()%54, randv2.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 deleted file mode 100644 index a0cbc35038..0000000000 --- a/transport/simple-obfs/tls.go +++ /dev/null @@ -1,201 +0,0 @@ -package obfs - -import ( - "bytes" - "crypto/rand" - "encoding/binary" - "io" - "net" - "time" - - "github.com/metacubex/mihomo/common/pool" -) - -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.GetBuffer() - defer pool.PutBuffer(buf) - buf.Write([]byte{0x17, 0x03, 0x03}) - binary.Write(buf, binary.BigEndian, uint16(len(b))) - buf.Write(b) - _, err := to.Conn.Write(buf.Bytes()) - if err != nil { - // return 0 because errors occur here make the - // whole situation irrecoverable - return 0, err - } - return len(b), nil -} - -// 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 { - random := make([]byte, 28) - sessionID := make([]byte, 32) - rand.Read(random) - rand.Read(sessionID) - - buf := &bytes.Buffer{} - - // handshake, TLS 1.0 version, length - buf.WriteByte(22) - buf.Write([]byte{0x03, 0x01}) - length := uint16(212 + len(data) + len(server)) - buf.WriteByte(byte(length >> 8)) - buf.WriteByte(byte(length & 0xff)) - - // clientHello, length, TLS 1.2 version - buf.WriteByte(1) - buf.WriteByte(0) - binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server))) - buf.Write([]byte{0x03, 0x03}) - - // random with timestamp, sid len, sid - binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) - buf.Write(random) - buf.WriteByte(32) - buf.Write(sessionID) - - // cipher suites - buf.Write([]byte{0x00, 0x38}) - buf.Write([]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.Write([]byte{0x01, 0x00}) - - // extension length - binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server))) - - // session ticket - buf.Write([]byte{0x00, 0x23}) - binary.Write(buf, binary.BigEndian, uint16(len(data))) - buf.Write(data) - - // server name - buf.Write([]byte{0x00, 0x00}) - binary.Write(buf, binary.BigEndian, uint16(len(server)+5)) - binary.Write(buf, binary.BigEndian, uint16(len(server)+3)) - buf.WriteByte(0) - binary.Write(buf, binary.BigEndian, uint16(len(server))) - buf.Write([]byte(server)) - - // ec_point - buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02}) - - // groups - buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18}) - - // signature - buf.Write([]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.Write([]byte{0x00, 0x16, 0x00, 0x00}) - - // extended master secret - buf.Write([]byte{0x00, 0x17, 0x00, 0x00}) - - return buf.Bytes() -} diff --git a/transport/sing-shadowtls/shadowtls.go b/transport/sing-shadowtls/shadowtls.go deleted file mode 100644 index 904bcd6311..0000000000 --- a/transport/sing-shadowtls/shadowtls.go +++ /dev/null @@ -1,86 +0,0 @@ -package sing_shadowtls - -import ( - "context" - "crypto/tls" - "net" - - "github.com/metacubex/mihomo/component/ca" - tlsC "github.com/metacubex/mihomo/component/tls" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/sing-shadowtls" - "golang.org/x/exp/slices" -) - -const ( - Mode string = "shadow-tls" -) - -var ( - DefaultALPN = []string{"h2", "http/1.1"} - WsALPN = []string{"http/1.1"} -) - -type ShadowTLSOption struct { - Password string - Host string - Fingerprint string - ClientFingerprint string - SkipCertVerify bool - Version int - ALPN []string -} - -func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) (net.Conn, error) { - tlsConfig := &tls.Config{ - NextProtos: option.ALPN, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: option.SkipCertVerify, - ServerName: option.Host, - } - if option.Version == 1 { - tlsConfig.MaxVersion = tls.VersionTLS12 // ShadowTLS v1 only support TLS 1.2 - } - - var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) - if err != nil { - return nil, err - } - - tlsHandshake := uTLSHandshakeFunc(tlsConfig, option.ClientFingerprint) - client, err := shadowtls.NewClient(shadowtls.ClientConfig{ - Version: option.Version, - Password: option.Password, - TLSHandshake: tlsHandshake, - Logger: log.SingLogger, - }) - if err != nil { - return nil, err - } - return client.DialContextConn(ctx, conn) -} - -func uTLSHandshakeFunc(config *tls.Config, clientFingerprint string) shadowtls.TLSHandshakeFunc { - return func(ctx context.Context, conn net.Conn, sessionIDGenerator shadowtls.TLSSessionIDGeneratorFunc) error { - tlsConfig := tlsC.UConfig(config) - tlsConfig.SessionIDGenerator = sessionIDGenerator - if config.MaxVersion == tls.VersionTLS12 { // for ShadowTLS v1 - tlsConn := tlsC.Client(conn, tlsConfig) - return tlsConn.HandshakeContext(ctx) - } - if clientFingerprint, ok := tlsC.GetFingerprint(clientFingerprint); ok { - tlsConn := tlsC.UClient(conn, tlsConfig, clientFingerprint) - if slices.Equal(tlsConfig.NextProtos, WsALPN) { - err := tlsC.BuildWebsocketHandshakeState(tlsConn) - if err != nil { - return err - } - } - return tlsConn.HandshakeContext(ctx) - } - tlsConn := tlsC.Client(conn, tlsConfig) - return tlsConn.HandshakeContext(ctx) - } -} diff --git a/transport/snell/cipher.go b/transport/snell/cipher.go deleted file mode 100644 index e18ce5100f..0000000000 --- a/transport/snell/cipher.go +++ /dev/null @@ -1,56 +0,0 @@ -package snell - -import ( - "crypto/aes" - "crypto/cipher" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index cc097df782..0000000000 --- a/transport/snell/pool.go +++ /dev/null @@ -1,84 +0,0 @@ -package snell - -import ( - "context" - "net" - "time" - - "github.com/metacubex/mihomo/component/pool" - "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" -) - -type Pool struct { - pool *pool.Pool[*Snell] -} - -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, p}, nil -} - -func (p *Pool) Put(conn *Snell) { - 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 { - // mihomo 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[*Snell]( - func(ctx context.Context) (*Snell, error) { - return factory(ctx) - }, - pool.WithAge[*Snell](15000), - pool.WithSize[*Snell](10), - pool.WithEvict[*Snell](func(item *Snell) { - _ = item.Close() - }), - ) - - return &Pool{pool: p} -} diff --git a/transport/snell/snell.go b/transport/snell/snell.go deleted file mode 100644 index fe3e4ee005..0000000000 --- a/transport/snell/snell.go +++ /dev/null @@ -1,280 +0,0 @@ -package snell - -import ( - "encoding/binary" - "errors" - "fmt" - "io" - "net" - "sync" - - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" - "github.com/metacubex/mihomo/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.GetBuffer() - defer pool.PutBuffer(buf) - buf.WriteByte(Version) - if version == Version2 { - buf.WriteByte(CommandConnectV2) - } else { - buf.WriteByte(CommandConnect) - } - - // clientID length & id - buf.WriteByte(0) - - // host & port - buf.WriteByte(uint8(len(host))) - buf.WriteString(host) - binary.Write(buf, binary.BigEndian, 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.GetBuffer() - defer pool.PutBuffer(buf) - - // compose snell UDP address format (refer: icpz/snell-server-reversed) - // a brand new wheel to replace socks5 address format, well done Yachen - buf.WriteByte(CommondUDPForward) - switch socks5Addr[0] { - case socks5.AtypDomainName: - hostLen := socks5Addr[1] - buf.Write(socks5Addr[1 : 1+1+hostLen+2]) - case socks5.AtypIPv4: - buf.Write([]byte{0x00, 0x04}) - buf.Write(socks5Addr[1 : 1+net.IPv4len+2]) - case socks5.AtypIPv6: - buf.Write([]byte{0x00, 0x06}) - buf.Write(socks5Addr[1 : 1+net.IPv6len+2]) - } - - buf.Write(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 deleted file mode 100644 index 50708eda26..0000000000 --- a/transport/socks4/socks4.go +++ /dev/null @@ -1,196 +0,0 @@ -package socks4 - -import ( - "bytes" - "encoding/binary" - "errors" - "io" - "net" - "net/netip" - "strconv" - - "github.com/metacubex/mihomo/component/auth" -) - -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") -) - -var subnet = netip.PrefixFrom(netip.IPv4Unspecified(), 24) - -func ServerHandshake(rw io.ReadWriter, authenticator auth.Authenticator) (addr string, command Command, user string, err error) { - var req [8]byte - if _, err = io.ReadFull(rw, req[:]); err != nil { - return - } - - if req[0] != Version { - err = errVersionMismatched - return - } - - if command = req[1]; command != CmdConnect { - err = errCommandNotSupported - return - } - - var ( - dstIP = netip.AddrFrom4(*(*[4]byte)(req[4:8])) // [4]byte - dstPort = req[2:4] // [2]byte - ) - - var ( - host string - port string - code uint8 - userID []byte - ) - if userID, err = readUntilNull(rw); err != nil { - return - } - user = string(userID) - - if isReservedIP(dstIP) { - var target []byte - if target, err = readUntilNull(rw); err != nil { - return - } - host = string(target) - } - - port = strconv.Itoa(int(binary.BigEndian.Uint16(dstPort))) - if host != "" { - addr = net.JoinHostPort(host, port) - } else { - addr = net.JoinHostPort(dstIP.String(), port) - } - - // SOCKS4 only support USERID auth. - if authenticator == nil || authenticator.Verify(user, "") { - code = RequestGranted - } else { - code = RequestIdentdMismatched - err = ErrRequestIdentdMismatched - } - - var reply [8]byte - reply[0] = 0x00 // reply code - reply[1] = code // result code - copy(reply[4:8], dstIP.AsSlice()) - copy(reply[2:4], dstPort) - - _, wErr := rw.Write(reply[:]) - 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 - } - - dstIP, err := netip.ParseAddr(host) - if err != nil /* HOST */ { - dstIP = netip.AddrFrom4([4]byte{0, 0, 0, 1}) - } else if dstIP.Is6() /* IPv6 */ { - return errIPv6NotSupported - } - - req := &bytes.Buffer{} - req.WriteByte(Version) - req.WriteByte(command) - _ = binary.Write(req, binary.BigEndian, uint16(port)) - req.Write(dstIP.AsSlice()) - req.WriteString(userID) - req.WriteByte(0) /* NULL */ - - if isReservedIP(dstIP) /* SOCKS4A */ { - req.WriteString(host) - req.WriteByte(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 { - return !ip.IsUnspecified() && subnet.Contains(ip) -} - -func readUntilNull(r io.Reader) ([]byte, error) { - buf := &bytes.Buffer{} - var data [1]byte - - for { - if _, err := r.Read(data[:]); err != nil { - return nil, err - } - if data[0] == 0 { - return buf.Bytes(), nil - } - buf.WriteByte(data[0]) - } -} diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go deleted file mode 100644 index 5f699bb03e..0000000000 --- a/transport/socks5/socks5.go +++ /dev/null @@ -1,498 +0,0 @@ -package socks5 - -import ( - "bytes" - "encoding/binary" - "errors" - "io" - "net" - "net/netip" - "strconv" - - "github.com/metacubex/mihomo/component/auth" -) - -// 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, user string, 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(string(user), string(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 := ParseAddrToSocksAddr(rw.LocalAddr()) - 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 := &bytes.Buffer{} - authMsg.WriteByte(1) - authMsg.WriteByte(uint8(len(user.Username))) - authMsg.WriteString(user.Username) - authMsg.WriteByte(uint8(len(user.Password))) - authMsg.WriteString(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 -} - -func ReadAddr0(r io.Reader) (Addr, error) { - aType, err := ReadByte(r) // read 1st byte for address type - if err != nil { - return nil, err - } - - switch aType { - case AtypDomainName: - var domainLength byte - domainLength, err = ReadByte(r) // read 2nd byte for domain length - if err != nil { - return nil, err - } - b := make([]byte, 1+1+uint16(domainLength)+2) - _, err = io.ReadFull(r, b[2:]) - b[0] = aType - b[1] = domainLength - return b, err - case AtypIPv4: - var b [1 + net.IPv4len + 2]byte - _, err = io.ReadFull(r, b[1:]) - b[0] = aType - return b[:], err - case AtypIPv6: - var b [1 + net.IPv6len + 2]byte - _, err = io.ReadFull(r, b[1:]) - b[0] = aType - return b[:], err - } - - return nil, ErrAddressNotSupported -} - -func ReadByte(reader io.Reader) (byte, error) { - if br, isBr := reader.(io.ByteReader); isBr { - return br.ReadByte() - } - var b [1]byte - if _, err := io.ReadFull(reader, b[:]); err != nil { - return 0, err - } - return b[0], nil -} - -// 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 { - var addr Addr - host, port, err := net.SplitHostPort(s) - if err != nil { - return nil - } - if ip := net.ParseIP(host); ip != nil { - if ip4 := ip.To4(); ip4 != nil { - addr = make([]byte, 1+net.IPv4len+2) - addr[0] = AtypIPv4 - copy(addr[1:], ip4) - } else { - addr = make([]byte, 1+net.IPv6len+2) - addr[0] = AtypIPv6 - copy(addr[1:], ip) - } - } else { - if len(host) > 255 { - return nil - } - addr = make([]byte, 1+1+len(host)+2) - addr[0] = AtypDomainName - addr[1] = byte(len(host)) - copy(addr[2:], host) - } - - portnum, err := strconv.ParseUint(port, 10, 16) - if err != nil { - return nil - } - - addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum) - - return addr -} - -// 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 - switch addr := addr.(type) { - case *net.UDPAddr: - hostip = addr.IP - port = addr.Port - case *net.TCPAddr: - hostip = addr.IP - port = addr.Port - case nil: - return nil - } - - // fallback parse - if hostip == nil { - return ParseAddr(addr.String()) - } - - var parsed Addr - if ip4 := hostip.To4(); ip4.DefaultMask() != nil { - parsed = make([]byte, 1+net.IPv4len+2) - parsed[0] = AtypIPv4 - copy(parsed[1:], ip4) - binary.BigEndian.PutUint16(parsed[1+net.IPv4len:], uint16(port)) - - } else { - parsed = make([]byte, 1+net.IPv6len+2) - parsed[0] = AtypIPv6 - copy(parsed[1:], hostip) - binary.BigEndian.PutUint16(parsed[1+net.IPv6len:], uint16(port)) - } - return 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) { - if len(packet) < 5 { - err = errors.New("insufficient length of packet") - return - } - - // packet[0] and packet[1] are reserved - if !bytes.Equal(packet[:2], []byte{0, 0}) { - err = errors.New("reserved fields should be zero") - return - } - - if packet[2] != 0 /* fragments */ { - err = errors.New("discarding fragmented payload") - return - } - - addr = SplitAddr(packet[3:]) - if addr == nil { - err = errors.New("failed to read UDP header") - } - - payload = packet[3+len(addr):] - return -} - -func EncodeUDPPacket(addr Addr, payload []byte) (packet []byte, err error) { - if addr == nil { - err = errors.New("address is invalid") - return - } - packet = bytes.Join([][]byte{{0, 0, 0}, addr, payload}, []byte{}) - return -} diff --git a/transport/ssr/obfs/base.go b/transport/ssr/obfs/base.go deleted file mode 100644 index 7fd1b84cf7..0000000000 --- a/transport/ssr/obfs/base.go +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 4be6cbe8b1..0000000000 --- a/transport/ssr/obfs/http_post.go +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index d59b490d46..0000000000 --- a/transport/ssr/obfs/http_simple.go +++ /dev/null @@ -1,406 +0,0 @@ -package obfs - -import ( - "bytes" - "encoding/hex" - "io" - "net" - "strconv" - "strings" - - "github.com/metacubex/mihomo/common/pool" - - "github.com/metacubex/randv2" -) - -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 + randv2.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[randv2.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[randv2.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[randv2.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 deleted file mode 100644 index c56acc8adb..0000000000 --- a/transport/ssr/obfs/obfs.go +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index eb998a47b4..0000000000 --- a/transport/ssr/obfs/plain.go +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index a5ad2dabc9..0000000000 --- a/transport/ssr/obfs/random_head.go +++ /dev/null @@ -1,73 +0,0 @@ -package obfs - -import ( - "crypto/rand" - "encoding/binary" - "hash/crc32" - "net" - - "github.com/metacubex/mihomo/common/pool" - - "github.com/metacubex/randv2" -) - -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 := randv2.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 deleted file mode 100644 index d5955edc34..0000000000 --- a/transport/ssr/obfs/tls1.2_ticket_auth.go +++ /dev/null @@ -1,228 +0,0 @@ -package obfs - -import ( - "bytes" - "crypto/hmac" - "crypto/rand" - "encoding/binary" - "net" - "strings" - "time" - - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/transport/ssr/tools" - - "github.com/metacubex/randv2" -) - -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 := randv2.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 * (randv2.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[randv2.IntN(len(hosts))] - return host -} diff --git a/transport/ssr/protocol/auth_aes128_md5.go b/transport/ssr/protocol/auth_aes128_md5.go deleted file mode 100644 index c6ae415e45..0000000000 --- a/transport/ssr/protocol/auth_aes128_md5.go +++ /dev/null @@ -1,18 +0,0 @@ -package protocol - -import "github.com/metacubex/mihomo/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 deleted file mode 100644 index cfd555102c..0000000000 --- a/transport/ssr/protocol/auth_aes128_sha1.go +++ /dev/null @@ -1,283 +0,0 @@ -package protocol - -import ( - "bytes" - "crypto/rand" - "encoding/binary" - "math" - "net" - "strconv" - "strings" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/ssr/tools" - - "github.com/metacubex/randv2" -) - -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 N.EnhancePacketConn) N.EnhancePacketConn { - p := &authAES128{ - Base: a.Base, - authAES128Function: a.authAES128Function, - userData: a.userData, - } - return &PacketConn{EnhancePacketConn: 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 := randv2.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 randv2.IntN(32) - } - if dataLength > 900 { - return randv2.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(randv2.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 randv2.IntN(512) - } - return randv2.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 deleted file mode 100644 index 396172ef8f..0000000000 --- a/transport/ssr/protocol/auth_chain_a.go +++ /dev/null @@ -1,320 +0,0 @@ -package protocol - -import ( - "bytes" - "crypto/cipher" - "crypto/rand" - "crypto/rc4" - "encoding/base64" - "encoding/binary" - "errors" - "net" - "strconv" - "strings" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/shadowsocks/core" - "github.com/metacubex/mihomo/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 N.EnhancePacketConn) N.EnhancePacketConn { - p := &authChainA{ - Base: a.Base, - salt: a.salt, - userData: a.userData, - } - return &PacketConn{EnhancePacketConn: 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 - // Temporary workaround for https://github.com/metacubex/mihomo/issues/1352 - if dataLength < 0 || randDataLength < 0 || length < 0 { - return errors.New("ssr crashing blocked") - } - - 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) - } - // Temporary workaround for https://github.com/metacubex/mihomo/issues/1352 - if pos < 0 || pos+dataLength < 0 || dataLength < 0 { - return errors.New("ssr crashing blocked") - } - - 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 deleted file mode 100644 index 223613a952..0000000000 --- a/transport/ssr/protocol/auth_chain_b.go +++ /dev/null @@ -1,97 +0,0 @@ -package protocol - -import ( - "net" - "sort" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index 1c616c3fc2..0000000000 --- a/transport/ssr/protocol/auth_sha1_v4.go +++ /dev/null @@ -1,184 +0,0 @@ -package protocol - -import ( - "bytes" - "encoding/binary" - "hash/adler32" - "hash/crc32" - "net" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/transport/ssr/tools" - - "github.com/metacubex/randv2" -) - -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 N.EnhancePacketConn) N.EnhancePacketConn { - 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 randv2.IntN(256) - } - return randv2.IntN(512) -} diff --git a/transport/ssr/protocol/base.go b/transport/ssr/protocol/base.go deleted file mode 100644 index 798701777e..0000000000 --- a/transport/ssr/protocol/base.go +++ /dev/null @@ -1,79 +0,0 @@ -package protocol - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "encoding/binary" - "sync" - "time" - - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/shadowsocks/core" - - "github.com/metacubex/randv2" -) - -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 = randv2.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 deleted file mode 100644 index 4d8b630a75..0000000000 --- a/transport/ssr/protocol/origin.go +++ /dev/null @@ -1,35 +0,0 @@ -package protocol - -import ( - "bytes" - "net" - - N "github.com/metacubex/mihomo/common/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 N.EnhancePacketConn) N.EnhancePacketConn { 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 deleted file mode 100644 index 86d573f37f..0000000000 --- a/transport/ssr/protocol/packet.go +++ /dev/null @@ -1,54 +0,0 @@ -package protocol - -import ( - "net" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" -) - -type PacketConn struct { - N.EnhancePacketConn - 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.EnhancePacketConn.WriteTo(buf.Bytes(), addr) - return len(b), err -} - -func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { - n, addr, err := c.EnhancePacketConn.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 -} - -func (c *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - data, put, addr, err = c.EnhancePacketConn.WaitReadFrom() - if err != nil { - return - } - data, err = c.DecodePacket(data) - if err != nil { - if put != nil { - put() - } - data = nil - put = nil - return - } - return -} diff --git a/transport/ssr/protocol/protocol.go b/transport/ssr/protocol/protocol.go deleted file mode 100644 index ad6bf6ba77..0000000000 --- a/transport/ssr/protocol/protocol.go +++ /dev/null @@ -1,79 +0,0 @@ -package protocol - -import ( - "bytes" - "errors" - "fmt" - "net" - - N "github.com/metacubex/mihomo/common/net" - - "github.com/metacubex/randv2" -) - -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(N.EnhancePacketConn) N.EnhancePacketConn - 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) + randv2.IntN(32) - if bLength < dataLength { - return bLength - } - return dataLength -} diff --git a/transport/ssr/protocol/stream.go b/transport/ssr/protocol/stream.go deleted file mode 100644 index 436859c3db..0000000000 --- a/transport/ssr/protocol/stream.go +++ /dev/null @@ -1,50 +0,0 @@ -package protocol - -import ( - "bytes" - "net" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index ac15c97db4..0000000000 --- a/transport/ssr/tools/bufPool.go +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index b2a41561e9..0000000000 --- a/transport/ssr/tools/crypto.go +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index c76011e47a..0000000000 --- a/transport/ssr/tools/random.go +++ /dev/null @@ -1,57 +0,0 @@ -package tools - -import ( - "encoding/binary" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index 93819130fd..0000000000 --- a/transport/trojan/trojan.go +++ /dev/null @@ -1,229 +0,0 @@ -package trojan - -import ( - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "errors" - "io" - "net" - "sync" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/transport/socks5" -) - -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 - -const ( - CommandTCP byte = 1 - CommandUDP byte = 3 - CommandMux byte = 0x7f - - KeyLength = 56 -) - -func WriteHeader(w io.Writer, hexPassword [KeyLength]byte, command Command, socks5Addr []byte) error { - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - - buf.Write(hexPassword[:]) - buf.Write(crlf) - - buf.WriteByte(command) - buf.Write(socks5Addr) - buf.Write(crlf) - - _, err := w.Write(buf.Bytes()) - return err -} - -func writePacket(w io.Writer, socks5Addr, payload []byte) (int, error) { - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - - buf.Write(socks5Addr) - binary.Write(buf, binary.BigEndian, uint16(len(payload))) - buf.Write(crlf) - buf.Write(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 -} - -var _ N.EnhancePacketConn = (*PacketConn)(nil) - -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 (pc *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - pc.mux.Lock() - defer pc.mux.Unlock() - - destination, err := socks5.ReadAddr0(pc.Conn) - if err != nil { - return nil, nil, nil, err - } - addr = destination.UDPAddr() - - data = pool.Get(pool.UDPBufferSize) - put = func() { - _ = pool.Put(data) - } - - _, err = io.ReadFull(pc.Conn, data[:2+2]) // u16be length + CR LF - if err != nil { - if put != nil { - put() - } - return nil, nil, nil, err - } - length := binary.BigEndian.Uint16(data) - - if length > 0 { - data = data[:length] - _, err = io.ReadFull(pc.Conn, data) - if err != nil { - if put != nil { - put() - } - return nil, nil, nil, err - } - } else { - if put != nil { - put() - } - return nil, nil, addr, nil - } - - return -} - -func NewPacketConn(conn net.Conn) *PacketConn { - return &PacketConn{Conn: conn} -} - -func Key(password string) (key [56]byte) { - hash := sha256.Sum224([]byte(password)) - hex.Encode(key[:], hash[:]) - return -} diff --git a/transport/tuic/common/congestion.go b/transport/tuic/common/congestion.go deleted file mode 100644 index a0e2be68dd..0000000000 --- a/transport/tuic/common/congestion.go +++ /dev/null @@ -1,57 +0,0 @@ -package common - -import ( - "github.com/metacubex/mihomo/transport/tuic/congestion" - congestionv2 "github.com/metacubex/mihomo/transport/tuic/congestion_v2" - - "github.com/metacubex/quic-go" - c "github.com/metacubex/quic-go/congestion" -) - -const ( - DefaultStreamReceiveWindow = 15728640 // 15 MB/s - DefaultConnectionReceiveWindow = 67108864 // 64 MB/s -) - -func SetCongestionController(quicConn quic.Connection, cc string, cwnd int) { - if cwnd == 0 { - cwnd = 32 - } - switch cc { - case "cubic": - quicConn.SetCongestionControl( - congestion.NewCubicSender( - congestion.DefaultClock{}, - congestion.GetInitialPacketSize(quicConn), - false, - ), - ) - case "new_reno": - quicConn.SetCongestionControl( - congestion.NewCubicSender( - congestion.DefaultClock{}, - congestion.GetInitialPacketSize(quicConn), - true, - ), - ) - case "bbr_meta_v1": - quicConn.SetCongestionControl( - congestion.NewBBRSender( - congestion.DefaultClock{}, - congestion.GetInitialPacketSize(quicConn), - c.ByteCount(cwnd)*congestion.InitialMaxDatagramSize, - congestion.DefaultBBRMaxCongestionWindow*congestion.InitialMaxDatagramSize, - ), - ) - case "bbr_meta_v2": - fallthrough - case "bbr": - quicConn.SetCongestionControl( - congestionv2.NewBbrSender( - congestionv2.DefaultClock{}, - congestionv2.GetInitialPacketSize(quicConn), - c.ByteCount(cwnd), - ), - ) - } -} diff --git a/transport/tuic/common/stream.go b/transport/tuic/common/stream.go deleted file mode 100644 index e65f9a4948..0000000000 --- a/transport/tuic/common/stream.go +++ /dev/null @@ -1,67 +0,0 @@ -package common - -import ( - "net" - "sync" - "time" - - "github.com/metacubex/quic-go" -) - -type quicStreamConn struct { - quic.Stream - lock sync.Mutex - lAddr net.Addr - rAddr net.Addr - - closeDeferFn func() - - closeOnce sync.Once - closeErr error -} - -func (q *quicStreamConn) Write(p []byte) (n int, err error) { - q.lock.Lock() - defer q.lock.Unlock() - return q.Stream.Write(p) -} - -func (q *quicStreamConn) Close() error { - q.closeOnce.Do(func() { - q.closeErr = q.close() - }) - return q.closeErr -} - -func (q *quicStreamConn) close() error { - if q.closeDeferFn != nil { - defer q.closeDeferFn() - } - - // https://github.com/cloudflare/cloudflared/commit/ed2bac026db46b239699ac5ce4fcf122d7cab2cd - // Make sure a possible writer does not block the lock forever. We need it, so we can close the writer - // side of the stream safely. - _ = q.Stream.SetWriteDeadline(time.Now()) - - // This lock is eventually acquired despite Write also acquiring it, because we set a deadline to writes. - q.lock.Lock() - defer q.lock.Unlock() - - // We have to clean up the receiving stream ourselves since the Close in the bottom does not handle that. - q.Stream.CancelRead(0) - return q.Stream.Close() -} - -func (q *quicStreamConn) LocalAddr() net.Addr { - return q.lAddr -} - -func (q *quicStreamConn) RemoteAddr() net.Addr { - return q.rAddr -} - -var _ net.Conn = (*quicStreamConn)(nil) - -func NewQuicStreamConn(stream quic.Stream, lAddr, rAddr net.Addr, closeDeferFn func()) net.Conn { - return &quicStreamConn{Stream: stream, lAddr: lAddr, rAddr: rAddr, closeDeferFn: closeDeferFn} -} diff --git a/transport/tuic/common/type.go b/transport/tuic/common/type.go deleted file mode 100644 index c663fa0b9e..0000000000 --- a/transport/tuic/common/type.go +++ /dev/null @@ -1,46 +0,0 @@ -package common - -import ( - "bufio" - "context" - "errors" - "net" - "time" - - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - - "github.com/metacubex/quic-go" -) - -var ( - ClientClosed = errors.New("tuic: client closed") - TooManyOpenStreams = errors.New("tuic: too many open streams") -) - -type DialFunc func(ctx context.Context, dialer C.Dialer) (transport *quic.Transport, addr net.Addr, err error) - -type Client interface { - DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.Conn, error) - ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.PacketConn, error) - OpenStreams() int64 - DialerRef() C.Dialer - LastVisited() time.Time - SetLastVisited(last time.Time) - Close() -} - -type ServerHandler interface { - AuthOk() bool - HandleTimeout() - HandleStream(conn *N.BufferedConn) (err error) - HandleMessage(message []byte) (err error) - HandleUniStream(reader *bufio.Reader) (err error) -} - -type UdpRelayMode uint8 - -const ( - QUIC UdpRelayMode = iota - NATIVE -) diff --git a/transport/tuic/congestion/bandwidth.go b/transport/tuic/congestion/bandwidth.go deleted file mode 100644 index 2a6b3a2e0a..0000000000 --- a/transport/tuic/congestion/bandwidth.go +++ /dev/null @@ -1,25 +0,0 @@ -package congestion - -import ( - "math" - "time" - - "github.com/metacubex/quic-go/congestion" -) - -// Bandwidth of a connection -type Bandwidth uint64 - -const infBandwidth Bandwidth = math.MaxUint64 - -const ( - // BitsPerSecond is 1 bit per second - BitsPerSecond Bandwidth = 1 - // BytesPerSecond is 1 byte per second - BytesPerSecond = 8 * BitsPerSecond -) - -// BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta -func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth { - return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond -} diff --git a/transport/tuic/congestion/bandwidth_sampler.go b/transport/tuic/congestion/bandwidth_sampler.go deleted file mode 100644 index e415fe7a6e..0000000000 --- a/transport/tuic/congestion/bandwidth_sampler.go +++ /dev/null @@ -1,376 +0,0 @@ -package congestion - -import ( - "math" - "time" - - "github.com/metacubex/quic-go/congestion" -) - -var ( - InfiniteBandwidth = Bandwidth(math.MaxUint64) -) - -// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned -// to the caller when the packet is acked or lost. -type SendTimeState struct { - // Whether other states in this object is valid. - isValid bool - // Whether the sender is app limited at the time the packet was sent. - // App limited bandwidth sample might be artificially low because the sender - // did not have enough data to send in order to saturate the link. - isAppLimited bool - // Total number of sent bytes at the time the packet was sent. - // Includes the packet itself. - totalBytesSent congestion.ByteCount - // Total number of acked bytes at the time the packet was sent. - totalBytesAcked congestion.ByteCount - // Total number of lost bytes at the time the packet was sent. - totalBytesLost congestion.ByteCount -} - -// ConnectionStateOnSentPacket represents the information about a sent packet -// and the state of the connection at the moment the packet was sent, -// specifically the information about the most recently acknowledged packet at -// that moment. -type ConnectionStateOnSentPacket struct { - packetNumber congestion.PacketNumber - // Time at which the packet is sent. - sendTime time.Time - // Size of the packet. - size congestion.ByteCount - // The value of |totalBytesSentAtLastAckedPacket| at the time the - // packet was sent. - totalBytesSentAtLastAckedPacket congestion.ByteCount - // The value of |lastAckedPacketSentTime| at the time the packet was - // sent. - lastAckedPacketSentTime time.Time - // The value of |lastAckedPacketAckTime| at the time the packet was - // sent. - lastAckedPacketAckTime time.Time - // Send time states that are returned to the congestion controller when the - // packet is acked or lost. - sendTimeState SendTimeState -} - -// BandwidthSample -type BandwidthSample struct { - // The bandwidth at that particular sample. Zero if no valid bandwidth sample - // is available. - bandwidth Bandwidth - // The RTT measurement at this particular sample. Zero if no RTT sample is - // available. Does not correct for delayed ack time. - rtt time.Duration - // States captured when the packet was sent. - stateAtSend SendTimeState -} - -func NewBandwidthSample() *BandwidthSample { - return &BandwidthSample{ - // FIXME: the default value of original code is zero. - rtt: InfiniteRTT, - } -} - -// BandwidthSampler keeps track of sent and acknowledged packets and outputs a -// bandwidth sample for every packet acknowledged. The samples are taken for -// individual packets, and are not filtered; the consumer has to filter the -// bandwidth samples itself. In certain cases, the sampler will locally severely -// underestimate the bandwidth, hence a maximum filter with a size of at least -// one RTT is recommended. -// -// This class bases its samples on the slope of two curves: the number of bytes -// sent over time, and the number of bytes acknowledged as received over time. -// It produces a sample of both slopes for every packet that gets acknowledged, -// based on a slope between two points on each of the corresponding curves. Note -// that due to the packet loss, the number of bytes on each curve might get -// further and further away from each other, meaning that it is not feasible to -// compare byte values coming from different curves with each other. -// -// The obvious points for measuring slope sample are the ones corresponding to -// the packet that was just acknowledged. Let us denote them as S_1 (point at -// which the current packet was sent) and A_1 (point at which the current packet -// was acknowledged). However, taking a slope requires two points on each line, -// so estimating bandwidth requires picking a packet in the past with respect to -// which the slope is measured. -// -// For that purpose, BandwidthSampler always keeps track of the most recently -// acknowledged packet, and records it together with every outgoing packet. -// When a packet gets acknowledged (A_1), it has not only information about when -// it itself was sent (S_1), but also the information about the latest -// acknowledged packet right before it was sent (S_0 and A_0). -// -// Based on that data, send and ack rate are estimated as: -// -// send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0)) -// ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0)) -// -// Here, the ack rate is intuitively the rate we want to treat as bandwidth. -// However, in certain cases (e.g. ack compression) the ack rate at a point may -// end up higher than the rate at which the data was originally sent, which is -// not indicative of the real bandwidth. Hence, we use the send rate as an upper -// bound, and the sample value is -// -// rate_sample = min(send_rate, ack_rate) -// -// An important edge case handled by the sampler is tracking the app-limited -// samples. There are multiple meaning of "app-limited" used interchangeably, -// hence it is important to understand and to be able to distinguish between -// them. -// -// Meaning 1: connection state. The connection is said to be app-limited when -// there is no outstanding data to send. This means that certain bandwidth -// samples in the future would not be an accurate indication of the link -// capacity, and it is important to inform consumer about that. Whenever -// connection becomes app-limited, the sampler is notified via OnAppLimited() -// method. -// -// Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth -// sampler becomes notified about the connection being app-limited, it enters -// app-limited phase. In that phase, all *sent* packets are marked as -// app-limited. Note that the connection itself does not have to be -// app-limited during the app-limited phase, and in fact it will not be -// (otherwise how would it send packets?). The boolean flag below indicates -// whether the sampler is in that phase. -// -// Meaning 3: a flag on the sent packet and on the sample. If a sent packet is -// sent during the app-limited phase, the resulting sample related to the -// packet will be marked as app-limited. -// -// With the terminology issue out of the way, let us consider the question of -// what kind of situation it addresses. -// -// Consider a scenario where we first send packets 1 to 20 at a regular -// bandwidth, and then immediately run out of data. After a few seconds, we send -// packets 21 to 60, and only receive ack for 21 between sending packets 40 and -// 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0 -// we use to compute the slope is going to be packet 20, a few seconds apart -// from the current packet, hence the resulting estimate would be extremely low -// and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21, -// meaning that the bandwidth sample would exclude the quiescence. -// -// Based on the analysis of that scenario, we implement the following rule: once -// OnAppLimited() is called, all sent packets will produce app-limited samples -// up until an ack for a packet that was sent after OnAppLimited() was called. -// Note that while the scenario above is not the only scenario when the -// connection is app-limited, the approach works in other cases too. -type BandwidthSampler struct { - // The total number of congestion controlled bytes sent during the connection. - totalBytesSent congestion.ByteCount - // The total number of congestion controlled bytes which were acknowledged. - totalBytesAcked congestion.ByteCount - // The total number of congestion controlled bytes which were lost. - totalBytesLost congestion.ByteCount - // The value of |totalBytesSent| at the time the last acknowledged packet - // was sent. Valid only when |lastAckedPacketSentTime| is valid. - totalBytesSentAtLastAckedPacket congestion.ByteCount - // The time at which the last acknowledged packet was sent. Set to - // QuicTime::Zero() if no valid timestamp is available. - lastAckedPacketSentTime time.Time - // The time at which the most recent packet was acknowledged. - lastAckedPacketAckTime time.Time - // The most recently sent packet. - lastSendPacket congestion.PacketNumber - // Indicates whether the bandwidth sampler is currently in an app-limited - // phase. - isAppLimited bool - // The packet that will be acknowledged after this one will cause the sampler - // to exit the app-limited phase. - endOfAppLimitedPhase congestion.PacketNumber - // Record of the connection state at the point where each packet in flight was - // sent, indexed by the packet number. - connectionStats *ConnectionStates -} - -func NewBandwidthSampler() *BandwidthSampler { - return &BandwidthSampler{ - connectionStats: &ConnectionStates{ - stats: make(map[congestion.PacketNumber]*ConnectionStateOnSentPacket), - }, - } -} - -// OnPacketSent Inputs the sent packet information into the sampler. Assumes that all -// packets are sent in order. The information about the packet will not be -// released from the sampler until it the packet is either acknowledged or -// declared lost. -func (s *BandwidthSampler) OnPacketSent(sentTime time.Time, lastSentPacket congestion.PacketNumber, sentBytes, bytesInFlight congestion.ByteCount, hasRetransmittableData bool) { - s.lastSendPacket = lastSentPacket - - if !hasRetransmittableData { - return - } - - s.totalBytesSent += sentBytes - - // If there are no packets in flight, the time at which the new transmission - // opens can be treated as the A_0 point for the purpose of bandwidth - // sampling. This underestimates bandwidth to some extent, and produces some - // artificially low samples for most packets in flight, but it provides with - // samples at important points where we would not have them otherwise, most - // importantly at the beginning of the connection. - if bytesInFlight == 0 { - s.lastAckedPacketAckTime = sentTime - s.totalBytesSentAtLastAckedPacket = s.totalBytesSent - - // In this situation ack compression is not a concern, set send rate to - // effectively infinite. - s.lastAckedPacketSentTime = sentTime - } - - s.connectionStats.Insert(lastSentPacket, sentTime, sentBytes, s) -} - -// OnPacketAcked Notifies the sampler that the |lastAckedPacket| is acknowledged. Returns a -// bandwidth sample. If no bandwidth sample is available, -// QuicBandwidth::Zero() is returned. -func (s *BandwidthSampler) OnPacketAcked(ackTime time.Time, lastAckedPacket congestion.PacketNumber) *BandwidthSample { - sentPacketState := s.connectionStats.Get(lastAckedPacket) - if sentPacketState == nil { - return NewBandwidthSample() - } - - sample := s.onPacketAckedInner(ackTime, lastAckedPacket, sentPacketState) - s.connectionStats.Remove(lastAckedPacket) - - return sample -} - -// onPacketAckedInner Handles the actual bandwidth calculations, whereas the outer method handles -// retrieving and removing |sentPacket|. -func (s *BandwidthSampler) onPacketAckedInner(ackTime time.Time, lastAckedPacket congestion.PacketNumber, sentPacket *ConnectionStateOnSentPacket) *BandwidthSample { - s.totalBytesAcked += sentPacket.size - - s.totalBytesSentAtLastAckedPacket = sentPacket.sendTimeState.totalBytesSent - s.lastAckedPacketSentTime = sentPacket.sendTime - s.lastAckedPacketAckTime = ackTime - - // Exit app-limited phase once a packet that was sent while the connection is - // not app-limited is acknowledged. - if s.isAppLimited && lastAckedPacket > s.endOfAppLimitedPhase { - s.isAppLimited = false - } - - // There might have been no packets acknowledged at the moment when the - // current packet was sent. In that case, there is no bandwidth sample to - // make. - if sentPacket.lastAckedPacketSentTime.IsZero() { - return NewBandwidthSample() - } - - // Infinite rate indicates that the sampler is supposed to discard the - // current send rate sample and use only the ack rate. - sendRate := InfiniteBandwidth - if sentPacket.sendTime.After(sentPacket.lastAckedPacketSentTime) { - sendRate = BandwidthFromDelta(sentPacket.sendTimeState.totalBytesSent-sentPacket.totalBytesSentAtLastAckedPacket, sentPacket.sendTime.Sub(sentPacket.lastAckedPacketSentTime)) - } - - // During the slope calculation, ensure that ack time of the current packet is - // always larger than the time of the previous packet, otherwise division by - // zero or integer underflow can occur. - if !ackTime.After(sentPacket.lastAckedPacketAckTime) { - // TODO(wub): Compare this code count before and after fixing clock jitter - // issue. - // if sentPacket.lastAckedPacketAckTime.Equal(sentPacket.sendTime) { - // This is the 1st packet after quiescense. - // QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 1, 2); - // } else { - // QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 2, 2); - // } - - return NewBandwidthSample() - } - - ackRate := BandwidthFromDelta(s.totalBytesAcked-sentPacket.sendTimeState.totalBytesAcked, - ackTime.Sub(sentPacket.lastAckedPacketAckTime)) - - // Note: this sample does not account for delayed acknowledgement time. This - // means that the RTT measurements here can be artificially high, especially - // on low bandwidth connections. - sample := &BandwidthSample{ - bandwidth: minBandwidth(sendRate, ackRate), - rtt: ackTime.Sub(sentPacket.sendTime), - } - - SentPacketToSendTimeState(sentPacket, &sample.stateAtSend) - return sample -} - -// OnCongestionEvent Informs the sampler that a packet is considered lost and it should no -// longer keep track of it. -func (s *BandwidthSampler) OnCongestionEvent(packetNumber congestion.PacketNumber) SendTimeState { - ok, sentPacket := s.connectionStats.Remove(packetNumber) - sendTimeState := SendTimeState{ - isValid: ok, - } - if sentPacket != nil { - s.totalBytesLost += sentPacket.size - SentPacketToSendTimeState(sentPacket, &sendTimeState) - } - - return sendTimeState -} - -// OnAppLimited Informs the sampler that the connection is currently app-limited, causing -// the sampler to enter the app-limited phase. The phase will expire by -// itself. -func (s *BandwidthSampler) OnAppLimited() { - s.isAppLimited = true - s.endOfAppLimitedPhase = s.lastSendPacket -} - -// SentPacketToSendTimeState Copy a subset of the (private) ConnectionStateOnSentPacket to the (public) -// SendTimeState. Always set send_time_state->is_valid to true. -func SentPacketToSendTimeState(sentPacket *ConnectionStateOnSentPacket, sendTimeState *SendTimeState) { - sendTimeState.isAppLimited = sentPacket.sendTimeState.isAppLimited - sendTimeState.totalBytesSent = sentPacket.sendTimeState.totalBytesSent - sendTimeState.totalBytesAcked = sentPacket.sendTimeState.totalBytesAcked - sendTimeState.totalBytesLost = sentPacket.sendTimeState.totalBytesLost - sendTimeState.isValid = true -} - -// ConnectionStates Record of the connection state at the point where each packet in flight was -// sent, indexed by the packet number. -// FIXME: using LinkedList replace map to fast remove all the packets lower than the specified packet number. -type ConnectionStates struct { - stats map[congestion.PacketNumber]*ConnectionStateOnSentPacket -} - -func (s *ConnectionStates) Insert(packetNumber congestion.PacketNumber, sentTime time.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) bool { - if _, ok := s.stats[packetNumber]; ok { - return false - } - - s.stats[packetNumber] = NewConnectionStateOnSentPacket(packetNumber, sentTime, bytes, sampler) - return true -} - -func (s *ConnectionStates) Get(packetNumber congestion.PacketNumber) *ConnectionStateOnSentPacket { - return s.stats[packetNumber] -} - -func (s *ConnectionStates) Remove(packetNumber congestion.PacketNumber) (bool, *ConnectionStateOnSentPacket) { - state, ok := s.stats[packetNumber] - if ok { - delete(s.stats, packetNumber) - } - return ok, state -} - -func NewConnectionStateOnSentPacket(packetNumber congestion.PacketNumber, sentTime time.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) *ConnectionStateOnSentPacket { - return &ConnectionStateOnSentPacket{ - packetNumber: packetNumber, - sendTime: sentTime, - size: bytes, - lastAckedPacketSentTime: sampler.lastAckedPacketSentTime, - lastAckedPacketAckTime: sampler.lastAckedPacketAckTime, - totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket, - sendTimeState: SendTimeState{ - isValid: true, - isAppLimited: sampler.isAppLimited, - totalBytesSent: sampler.totalBytesSent, - totalBytesAcked: sampler.totalBytesAcked, - totalBytesLost: sampler.totalBytesLost, - }, - } -} diff --git a/transport/tuic/congestion/bbr_sender.go b/transport/tuic/congestion/bbr_sender.go deleted file mode 100644 index 5a8633621f..0000000000 --- a/transport/tuic/congestion/bbr_sender.go +++ /dev/null @@ -1,946 +0,0 @@ -package congestion - -// src from https://quiche.googlesource.com/quiche.git/+/66dea072431f94095dfc3dd2743cb94ef365f7ef/quic/core/congestion_control/bbr_sender.cc - -import ( - "fmt" - "math" - "time" - - "github.com/metacubex/quic-go" - "github.com/metacubex/quic-go/congestion" - "github.com/metacubex/randv2" -) - -const ( - // InitialMaxDatagramSize is the default maximum packet size used in QUIC for congestion window computations in bytes. - InitialMaxDatagramSize = 1280 - InitialPacketSize = 1280 - InitialCongestionWindow = 32 - DefaultBBRMaxCongestionWindow = 10000 -) - -func GetInitialPacketSize(quicConn quic.Connection) congestion.ByteCount { - return congestion.ByteCount(quicConn.Config().InitialPacketSize) -} - -var ( - - // Default initial rtt used before any samples are received. - InitialRtt = 100 * time.Millisecond - - // The gain used for the STARTUP, equal to 4*ln(2). - DefaultHighGain = 2.77 - - // The gain used in STARTUP after loss has been detected. - // 1.5 is enough to allow for 25% exogenous loss and still observe a 25% growth - // in measured bandwidth. - StartupAfterLossGain = 1.5 - - // The cycle of gains used during the PROBE_BW stage. - PacingGain = []float64{1.25, 0.75, 1, 1, 1, 1, 1, 1} - - // The length of the gain cycle. - GainCycleLength = len(PacingGain) - - // The size of the bandwidth filter window, in round-trips. - BandwidthWindowSize = GainCycleLength + 2 - - // The time after which the current min_rtt value expires. - MinRttExpiry = 10 * time.Second - - // The minimum time the connection can spend in PROBE_RTT mode. - ProbeRttTime = time.Millisecond * 200 - - // If the bandwidth does not increase by the factor of |kStartupGrowthTarget| - // within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection - // will exit the STARTUP mode. - StartupGrowthTarget = 1.25 - RoundTripsWithoutGrowthBeforeExitingStartup = int64(3) - - // Coefficient of target congestion window to use when basing PROBE_RTT on BDP. - ModerateProbeRttMultiplier = 0.75 - - // Coefficient to determine if a new RTT is sufficiently similar to min_rtt that - // we don't need to enter PROBE_RTT. - SimilarMinRttThreshold = 1.125 - - // Congestion window gain for QUIC BBR during PROBE_BW phase. - DefaultCongestionWindowGainConst = 2.0 -) - -type bbrMode int - -const ( - // Startup phase of the connection. - STARTUP = iota - // After achieving the highest possible bandwidth during the startup, lower - // the pacing rate in order to drain the queue. - DRAIN - // Cruising mode. - PROBE_BW - // Temporarily slow down sending in order to empty the buffer and measure - // the real minimum RTT. - PROBE_RTT -) - -type bbrRecoveryState int - -const ( - // Do not limit. - NOT_IN_RECOVERY = iota - - // Allow an extra outstanding byte for each byte acknowledged. - CONSERVATION - - // Allow two extra outstanding bytes for each byte acknowledged (slow - // start). - GROWTH -) - -type bbrSender struct { - mode bbrMode - clock Clock - rttStats congestion.RTTStatsProvider - bytesInFlight congestion.ByteCount - // return total bytes of unacked packets. - //GetBytesInFlight func() congestion.ByteCount - // Bandwidth sampler provides BBR with the bandwidth measurements at - // individual points. - sampler *BandwidthSampler - // The number of the round trips that have occurred during the connection. - roundTripCount int64 - // The packet number of the most recently sent packet. - lastSendPacket congestion.PacketNumber - // Acknowledgement of any packet after |current_round_trip_end_| will cause - // the round trip counter to advance. - currentRoundTripEnd congestion.PacketNumber - // The filter that tracks the maximum bandwidth over the multiple recent - // round-trips. - maxBandwidth *WindowedFilter - // Tracks the maximum number of bytes acked faster than the sending rate. - maxAckHeight *WindowedFilter - // The time this aggregation started and the number of bytes acked during it. - aggregationEpochStartTime time.Time - aggregationEpochBytes congestion.ByteCount - // Minimum RTT estimate. Automatically expires within 10 seconds (and - // triggers PROBE_RTT mode) if no new value is sampled during that period. - minRtt time.Duration - // The time at which the current value of |min_rtt_| was assigned. - minRttTimestamp time.Time - // The maximum allowed number of bytes in flight. - congestionWindow congestion.ByteCount - // The initial value of the |congestion_window_|. - initialCongestionWindow congestion.ByteCount - // The largest value the |congestion_window_| can achieve. - initialMaxCongestionWindow congestion.ByteCount - // The smallest value the |congestion_window_| can achieve. - //minCongestionWindow congestion.ByteCount - // The pacing gain applied during the STARTUP phase. - highGain float64 - // The CWND gain applied during the STARTUP phase. - highCwndGain float64 - // The pacing gain applied during the DRAIN phase. - drainGain float64 - // The current pacing rate of the connection. - pacingRate Bandwidth - // The gain currently applied to the pacing rate. - pacingGain float64 - // The gain currently applied to the congestion window. - congestionWindowGain float64 - // The gain used for the congestion window during PROBE_BW. Latched from - // quic_bbr_cwnd_gain flag. - congestionWindowGainConst float64 - // The number of RTTs to stay in STARTUP mode. Defaults to 3. - numStartupRtts int64 - // If true, exit startup if 1RTT has passed with no bandwidth increase and - // the connection is in recovery. - exitStartupOnLoss bool - // Number of round-trips in PROBE_BW mode, used for determining the current - // pacing gain cycle. - cycleCurrentOffset int - // The time at which the last pacing gain cycle was started. - lastCycleStart time.Time - // Indicates whether the connection has reached the full bandwidth mode. - isAtFullBandwidth bool - // Number of rounds during which there was no significant bandwidth increase. - roundsWithoutBandwidthGain int64 - // The bandwidth compared to which the increase is measured. - bandwidthAtLastRound Bandwidth - // Set to true upon exiting quiescence. - exitingQuiescence bool - // Time at which PROBE_RTT has to be exited. Setting it to zero indicates - // that the time is yet unknown as the number of packets in flight has not - // reached the required value. - exitProbeRttAt time.Time - // Indicates whether a round-trip has passed since PROBE_RTT became active. - probeRttRoundPassed bool - // Indicates whether the most recent bandwidth sample was marked as - // app-limited. - lastSampleIsAppLimited bool - // Indicates whether any non app-limited samples have been recorded. - hasNoAppLimitedSample bool - // Indicates app-limited calls should be ignored as long as there's - // enough data inflight to see more bandwidth when necessary. - flexibleAppLimited bool - // Current state of recovery. - recoveryState bbrRecoveryState - // Receiving acknowledgement of a packet after |end_recovery_at_| will cause - // BBR to exit the recovery mode. A value above zero indicates at least one - // loss has been detected, so it must not be set back to zero. - endRecoveryAt congestion.PacketNumber - // A window used to limit the number of bytes in flight during loss recovery. - recoveryWindow congestion.ByteCount - // If true, consider all samples in recovery app-limited. - isAppLimitedRecovery bool - // When true, pace at 1.5x and disable packet conservation in STARTUP. - slowerStartup bool - // When true, disables packet conservation in STARTUP. - rateBasedStartup bool - // When non-zero, decreases the rate in STARTUP by the total number of bytes - // lost in STARTUP divided by CWND. - startupRateReductionMultiplier int64 - // Sum of bytes lost in STARTUP. - startupBytesLost congestion.ByteCount - // When true, add the most recent ack aggregation measurement during STARTUP. - enableAckAggregationDuringStartup bool - // When true, expire the windowed ack aggregation values in STARTUP when - // bandwidth increases more than 25%. - expireAckAggregationInStartup bool - // If true, will not exit low gain mode until bytes_in_flight drops below BDP - // or it's time for high gain mode. - drainToTarget bool - // If true, use a CWND of 0.75*BDP during probe_rtt instead of 4 packets. - probeRttBasedOnBdp bool - // If true, skip probe_rtt and update the timestamp of the existing min_rtt to - // now if min_rtt over the last cycle is within 12.5% of the current min_rtt. - // Even if the min_rtt is 12.5% too low, the 25% gain cycling and 2x CWND gain - // should overcome an overly small min_rtt. - probeRttSkippedIfSimilarRtt bool - // If true, disable PROBE_RTT entirely as long as the connection was recently - // app limited. - probeRttDisabledIfAppLimited bool - appLimitedSinceLastProbeRtt bool - minRttSinceLastProbeRtt time.Duration - // Latched value of --quic_always_get_bw_sample_when_acked. - alwaysGetBwSampleWhenAcked bool - - pacer *pacer - - maxDatagramSize congestion.ByteCount -} - -func NewBBRSender( - clock Clock, - initialMaxDatagramSize, - initialCongestionWindow, - initialMaxCongestionWindow congestion.ByteCount, -) *bbrSender { - b := &bbrSender{ - mode: STARTUP, - clock: clock, - sampler: NewBandwidthSampler(), - maxBandwidth: NewWindowedFilter(int64(BandwidthWindowSize), MaxFilter), - maxAckHeight: NewWindowedFilter(int64(BandwidthWindowSize), MaxFilter), - congestionWindow: initialCongestionWindow, - initialCongestionWindow: initialCongestionWindow, - highGain: DefaultHighGain, - highCwndGain: DefaultHighGain, - drainGain: 1.0 / DefaultHighGain, - pacingGain: 1.0, - congestionWindowGain: 1.0, - congestionWindowGainConst: DefaultCongestionWindowGainConst, - numStartupRtts: RoundTripsWithoutGrowthBeforeExitingStartup, - recoveryState: NOT_IN_RECOVERY, - recoveryWindow: initialMaxCongestionWindow, - minRttSinceLastProbeRtt: InfiniteRTT, - maxDatagramSize: initialMaxDatagramSize, - } - b.pacer = newPacer(b.BandwidthEstimate) - return b -} - -func (b *bbrSender) maxCongestionWindow() congestion.ByteCount { - return b.maxDatagramSize * DefaultBBRMaxCongestionWindow -} - -func (b *bbrSender) minCongestionWindow() congestion.ByteCount { - return b.maxDatagramSize * b.initialCongestionWindow -} - -func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { - b.rttStats = provider -} - -func (b *bbrSender) GetBytesInFlight() congestion.ByteCount { - return b.bytesInFlight -} - -// TimeUntilSend returns when the next packet should be sent. -func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time { - b.bytesInFlight = bytesInFlight - return b.pacer.TimeUntilSend() -} - -func (b *bbrSender) HasPacingBudget(now time.Time) bool { - return b.pacer.Budget(now) >= b.maxDatagramSize -} - -func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { - if s < b.maxDatagramSize { - panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) - } - cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow() - b.maxDatagramSize = s - if cwndIsMinCwnd { - b.congestionWindow = b.minCongestionWindow() - } - b.pacer.SetMaxDatagramSize(s) -} - -func (b *bbrSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool) { - b.pacer.SentPacket(sentTime, bytes) - b.lastSendPacket = packetNumber - - b.bytesInFlight = bytesInFlight - if bytesInFlight == 0 && b.sampler.isAppLimited { - b.exitingQuiescence = true - } - - if b.aggregationEpochStartTime.IsZero() { - b.aggregationEpochStartTime = sentTime - } - - b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) -} - -func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool { - b.bytesInFlight = bytesInFlight - return bytesInFlight < b.GetCongestionWindow() -} - -func (b *bbrSender) GetCongestionWindow() congestion.ByteCount { - if b.mode == PROBE_RTT { - return b.ProbeRttCongestionWindow() - } - - if b.InRecovery() && !(b.rateBasedStartup && b.mode == STARTUP) { - return minByteCount(b.congestionWindow, b.recoveryWindow) - } - - return b.congestionWindow -} - -func (b *bbrSender) MaybeExitSlowStart() { - -} - -func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, priorInFlight congestion.ByteCount, eventTime time.Time) { - // Stub -} - -func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount, priorInFlight congestion.ByteCount) { - // Stub -} - -func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { - totalBytesAckedBefore := b.sampler.totalBytesAcked - isRoundStart, minRttExpired := false, false - - if lostPackets != nil { - b.DiscardLostPackets(lostPackets) - } - - // Input the new data into the BBR model of the connection. - var excessAcked congestion.ByteCount - if len(ackedPackets) > 0 { - lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber - isRoundStart = b.UpdateRoundTripCounter(lastAckedPacket) - minRttExpired = b.UpdateBandwidthAndMinRtt(eventTime, ackedPackets) - b.UpdateRecoveryState(len(lostPackets) > 0, isRoundStart) - bytesAcked := b.sampler.totalBytesAcked - totalBytesAckedBefore - excessAcked = b.UpdateAckAggregationBytes(eventTime, bytesAcked) - } - - // Handle logic specific to PROBE_BW mode. - if b.mode == PROBE_BW { - b.UpdateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) > 0) - } - - // Handle logic specific to STARTUP and DRAIN modes. - if isRoundStart && !b.isAtFullBandwidth { - b.CheckIfFullBandwidthReached() - } - b.MaybeExitStartupOrDrain(eventTime) - - // Handle logic specific to PROBE_RTT. - b.MaybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) - - // Calculate number of packets acked and lost. - bytesAcked := b.sampler.totalBytesAcked - totalBytesAckedBefore - bytesLost := congestion.ByteCount(0) - for _, packet := range lostPackets { - bytesLost += packet.BytesLost - } - - // After the model is updated, recalculate the pacing rate and congestion - // window. - b.CalculatePacingRate() - b.CalculateCongestionWindow(bytesAcked, excessAcked) - b.CalculateRecoveryWindow(bytesAcked, bytesLost) -} - -//func (b *bbrSender) SetNumEmulatedConnections(n int) { -// -//} - -func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { - -} - -//func (b *bbrSender) OnConnectionMigration() { -// -//} - -//// Experiments -//func (b *bbrSender) SetSlowStartLargeReduction(enabled bool) { -// -//} - -//func (b *bbrSender) BandwidthEstimate() Bandwidth { -// return Bandwidth(b.maxBandwidth.GetBest()) -//} - -// BandwidthEstimate returns the current bandwidth estimate -func (b *bbrSender) BandwidthEstimate() Bandwidth { - if b.rttStats == nil { - return infBandwidth - } - srtt := b.rttStats.SmoothedRTT() - if srtt == 0 { - // If we haven't measured an rtt, the bandwidth estimate is unknown. - return infBandwidth - } - return BandwidthFromDelta(b.GetCongestionWindow(), srtt) -} - -//func (b *bbrSender) HybridSlowStart() *HybridSlowStart { -// return nil -//} - -//func (b *bbrSender) SlowstartThreshold() congestion.ByteCount { -// return 0 -//} - -//func (b *bbrSender) RenoBeta() float32 { -// return 0.0 -//} - -func (b *bbrSender) InRecovery() bool { - return b.recoveryState != NOT_IN_RECOVERY -} - -func (b *bbrSender) InSlowStart() bool { - return b.mode == STARTUP -} - -//func (b *bbrSender) ShouldSendProbingPacket() bool { -// if b.pacingGain <= 1 { -// return false -// } -// // TODO(b/77975811): If the pipe is highly under-utilized, consider not -// // sending a probing transmission, because the extra bandwidth is not needed. -// // If flexible_app_limited is enabled, check if the pipe is sufficiently full. -// if b.flexibleAppLimited { -// return !b.IsPipeSufficientlyFull() -// } else { -// return true -// } -//} - -//func (b *bbrSender) IsPipeSufficientlyFull() bool { -// // See if we need more bytes in flight to see more bandwidth. -// if b.mode == STARTUP { -// // STARTUP exits if it doesn't observe a 25% bandwidth increase, so the CWND -// // must be more than 25% above the target. -// return b.GetBytesInFlight() >= b.GetTargetCongestionWindow(1.5) -// } -// if b.pacingGain > 1 { -// // Super-unity PROBE_BW doesn't exit until 1.25 * BDP is achieved. -// return b.GetBytesInFlight() >= b.GetTargetCongestionWindow(b.pacingGain) -// } -// // If bytes_in_flight are above the target congestion window, it should be -// // possible to observe the same or more bandwidth if it's available. -// return b.GetBytesInFlight() >= b.GetTargetCongestionWindow(1.1) -//} - -//func (b *bbrSender) SetFromConfig() { -// // TODO: not impl. -//} - -func (b *bbrSender) UpdateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool { - if b.currentRoundTripEnd == 0 || lastAckedPacket > b.currentRoundTripEnd { - b.currentRoundTripEnd = lastAckedPacket - b.roundTripCount++ - // if b.rttStats != nil && b.InSlowStart() { - // TODO: ++stats_->slowstart_num_rtts; - // } - return true - } - return false -} - -func (b *bbrSender) UpdateBandwidthAndMinRtt(now time.Time, ackedPackets []congestion.AckedPacketInfo) bool { - sampleMinRtt := InfiniteRTT - - for _, packet := range ackedPackets { - if !b.alwaysGetBwSampleWhenAcked && packet.BytesAcked == 0 { - // Skip acked packets with 0 in flight bytes when updating bandwidth. - return false - } - bandwidthSample := b.sampler.OnPacketAcked(now, packet.PacketNumber) - if b.alwaysGetBwSampleWhenAcked && !bandwidthSample.stateAtSend.isValid { - // From the sampler's perspective, the packet has never been sent, or the - // packet has been acked or marked as lost previously. - return false - } - b.lastSampleIsAppLimited = bandwidthSample.stateAtSend.isAppLimited - // has_non_app_limited_sample_ |= - // !bandwidth_sample.state_at_send.is_app_limited; - if !bandwidthSample.stateAtSend.isAppLimited { - b.hasNoAppLimitedSample = true - } - if bandwidthSample.rtt > 0 { - sampleMinRtt = minRtt(sampleMinRtt, bandwidthSample.rtt) - } - if !bandwidthSample.stateAtSend.isAppLimited || bandwidthSample.bandwidth > b.BandwidthEstimate() { - b.maxBandwidth.Update(int64(bandwidthSample.bandwidth), b.roundTripCount) - } - } - - // If none of the RTT samples are valid, return immediately. - if sampleMinRtt == InfiniteRTT { - return false - } - - b.minRttSinceLastProbeRtt = minRtt(b.minRttSinceLastProbeRtt, sampleMinRtt) - // Do not expire min_rtt if none was ever available. - minRttExpired := b.minRtt > 0 && (now.After(b.minRttTimestamp.Add(MinRttExpiry))) - if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 { - if minRttExpired && b.ShouldExtendMinRttExpiry() { - minRttExpired = false - } else { - b.minRtt = sampleMinRtt - } - b.minRttTimestamp = now - // Reset since_last_probe_rtt fields. - b.minRttSinceLastProbeRtt = InfiniteRTT - b.appLimitedSinceLastProbeRtt = false - } - - return minRttExpired -} - -func (b *bbrSender) ShouldExtendMinRttExpiry() bool { - if b.probeRttDisabledIfAppLimited && b.appLimitedSinceLastProbeRtt { - // Extend the current min_rtt if we've been app limited recently. - return true - } - - minRttIncreasedSinceLastProbe := b.minRttSinceLastProbeRtt > time.Duration(float64(b.minRtt)*SimilarMinRttThreshold) - if b.probeRttSkippedIfSimilarRtt && b.appLimitedSinceLastProbeRtt && !minRttIncreasedSinceLastProbe { - // Extend the current min_rtt if we've been app limited recently and an rtt - // has been measured in that time that's less than 12.5% more than the - // current min_rtt. - return true - } - - return false -} - -func (b *bbrSender) DiscardLostPackets(lostPackets []congestion.LostPacketInfo) { - for _, packet := range lostPackets { - b.sampler.OnCongestionEvent(packet.PacketNumber) - if b.mode == STARTUP { - // if b.rttStats != nil { - // TODO: slow start. - // } - if b.startupRateReductionMultiplier != 0 { - b.startupBytesLost += packet.BytesLost - } - } - } -} - -func (b *bbrSender) UpdateRecoveryState(hasLosses, isRoundStart bool) { - // Exit recovery when there are no losses for a round. - if !hasLosses { - b.endRecoveryAt = b.lastSendPacket - } - switch b.recoveryState { - case NOT_IN_RECOVERY: - // Enter conservation on the first loss. - if hasLosses { - b.recoveryState = CONSERVATION - // This will cause the |recovery_window_| to be set to the correct - // value in CalculateRecoveryWindow(). - b.recoveryWindow = 0 - // Since the conservation phase is meant to be lasting for a whole - // round, extend the current round as if it were started right now. - b.currentRoundTripEnd = b.lastSendPacket - if false && b.lastSampleIsAppLimited { - b.isAppLimitedRecovery = true - } - } - case CONSERVATION: - if isRoundStart { - b.recoveryState = GROWTH - } - fallthrough - case GROWTH: - // Exit recovery if appropriate. - if !hasLosses && b.lastSendPacket > b.endRecoveryAt { - b.recoveryState = NOT_IN_RECOVERY - b.isAppLimitedRecovery = false - } - } - - if b.recoveryState != NOT_IN_RECOVERY && b.isAppLimitedRecovery { - b.sampler.OnAppLimited() - } -} - -func (b *bbrSender) UpdateAckAggregationBytes(ackTime time.Time, ackedBytes congestion.ByteCount) congestion.ByteCount { - // Compute how many bytes are expected to be delivered, assuming max bandwidth - // is correct. - expectedAckedBytes := congestion.ByteCount(b.maxBandwidth.GetBest()) * - congestion.ByteCount((ackTime.Sub(b.aggregationEpochStartTime))) - // Reset the current aggregation epoch as soon as the ack arrival rate is less - // than or equal to the max bandwidth. - if b.aggregationEpochBytes <= expectedAckedBytes { - // Reset to start measuring a new aggregation epoch. - b.aggregationEpochBytes = ackedBytes - b.aggregationEpochStartTime = ackTime - return 0 - } - // Compute how many extra bytes were delivered vs max bandwidth. - // Include the bytes most recently acknowledged to account for stretch acks. - b.aggregationEpochBytes += ackedBytes - b.maxAckHeight.Update(int64(b.aggregationEpochBytes-expectedAckedBytes), b.roundTripCount) - return b.aggregationEpochBytes - expectedAckedBytes -} - -func (b *bbrSender) UpdateGainCyclePhase(now time.Time, priorInFlight congestion.ByteCount, hasLosses bool) { - bytesInFlight := b.GetBytesInFlight() - // In most cases, the cycle is advanced after an RTT passes. - shouldAdvanceGainCycling := now.Sub(b.lastCycleStart) > b.GetMinRtt() - - // If the pacing gain is above 1.0, the connection is trying to probe the - // bandwidth by increasing the number of bytes in flight to at least - // pacing_gain * BDP. Make sure that it actually reaches the target, as long - // as there are no losses suggesting that the buffers are not able to hold - // that much. - if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.GetTargetCongestionWindow(b.pacingGain) { - shouldAdvanceGainCycling = false - } - // If pacing gain is below 1.0, the connection is trying to drain the extra - // queue which could have been incurred by probing prior to it. If the number - // of bytes in flight falls down to the estimated BDP value earlier, conclude - // that the queue has been successfully drained and exit this cycle early. - if b.pacingGain < 1.0 && bytesInFlight <= b.GetTargetCongestionWindow(1.0) { - shouldAdvanceGainCycling = true - } - - if shouldAdvanceGainCycling { - b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % GainCycleLength - b.lastCycleStart = now - // Stay in low gain mode until the target BDP is hit. - // Low gain mode will be exited immediately when the target BDP is achieved. - if b.drainToTarget && b.pacingGain < 1.0 && PacingGain[b.cycleCurrentOffset] == 1.0 && - bytesInFlight > b.GetTargetCongestionWindow(1.0) { - return - } - b.pacingGain = PacingGain[b.cycleCurrentOffset] - } -} - -func (b *bbrSender) GetTargetCongestionWindow(gain float64) congestion.ByteCount { - bdp := congestion.ByteCount(b.GetMinRtt()) * congestion.ByteCount(b.BandwidthEstimate()) - congestionWindow := congestion.ByteCount(gain * float64(bdp)) - - // BDP estimate will be zero if no bandwidth samples are available yet. - if congestionWindow == 0 { - congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow)) - } - - return maxByteCount(congestionWindow, b.minCongestionWindow()) -} - -func (b *bbrSender) CheckIfFullBandwidthReached() { - if b.lastSampleIsAppLimited { - return - } - - target := Bandwidth(float64(b.bandwidthAtLastRound) * StartupGrowthTarget) - if b.BandwidthEstimate() >= target { - b.bandwidthAtLastRound = b.BandwidthEstimate() - b.roundsWithoutBandwidthGain = 0 - if b.expireAckAggregationInStartup { - // Expire old excess delivery measurements now that bandwidth increased. - b.maxAckHeight.Reset(0, b.roundTripCount) - } - return - } - b.roundsWithoutBandwidthGain++ - if b.roundsWithoutBandwidthGain >= b.numStartupRtts || (b.exitStartupOnLoss && b.InRecovery()) { - b.isAtFullBandwidth = true - } -} - -func (b *bbrSender) MaybeExitStartupOrDrain(now time.Time) { - if b.mode == STARTUP && b.isAtFullBandwidth { - b.OnExitStartup(now) - b.mode = DRAIN - b.pacingGain = b.drainGain - b.congestionWindowGain = b.highCwndGain - } - if b.mode == DRAIN && b.GetBytesInFlight() <= b.GetTargetCongestionWindow(1) { - b.EnterProbeBandwidthMode(now) - } -} - -func (b *bbrSender) EnterProbeBandwidthMode(now time.Time) { - b.mode = PROBE_BW - b.congestionWindowGain = b.congestionWindowGainConst - - // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is - // excluded because in that case increased gain and decreased gain would not - // follow each other. - b.cycleCurrentOffset = randv2.Int() % (GainCycleLength - 1) - if b.cycleCurrentOffset >= 1 { - b.cycleCurrentOffset += 1 - } - - b.lastCycleStart = now - b.pacingGain = PacingGain[b.cycleCurrentOffset] -} - -func (b *bbrSender) MaybeEnterOrExitProbeRtt(now time.Time, isRoundStart, minRttExpired bool) { - if minRttExpired && !b.exitingQuiescence && b.mode != PROBE_RTT { - if b.InSlowStart() { - b.OnExitStartup(now) - } - b.mode = PROBE_RTT - b.pacingGain = 1.0 - // Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight| - // is at the target small value. - b.exitProbeRttAt = time.Time{} - } - - if b.mode == PROBE_RTT { - b.sampler.OnAppLimited() - if b.exitProbeRttAt.IsZero() { - // If the window has reached the appropriate size, schedule exiting - // PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but - // we allow an extra packet since QUIC checks CWND before sending a - // packet. - if b.GetBytesInFlight() < b.ProbeRttCongestionWindow()+b.maxDatagramSize { - b.exitProbeRttAt = now.Add(ProbeRttTime) - b.probeRttRoundPassed = false - } - } else { - if isRoundStart { - b.probeRttRoundPassed = true - } - if !now.Before(b.exitProbeRttAt) && b.probeRttRoundPassed { - b.minRttTimestamp = now - if !b.isAtFullBandwidth { - b.EnterStartupMode(now) - } else { - b.EnterProbeBandwidthMode(now) - } - } - } - } - b.exitingQuiescence = false -} - -func (b *bbrSender) ProbeRttCongestionWindow() congestion.ByteCount { - if b.probeRttBasedOnBdp { - return b.GetTargetCongestionWindow(ModerateProbeRttMultiplier) - } else { - return b.minCongestionWindow() - } -} - -func (b *bbrSender) EnterStartupMode(now time.Time) { - // if b.rttStats != nil { - // TODO: slow start. - // } - b.mode = STARTUP - b.pacingGain = b.highGain - b.congestionWindowGain = b.highCwndGain -} - -func (b *bbrSender) OnExitStartup(now time.Time) { - if b.rttStats == nil { - return - } - // TODO: slow start. -} - -func (b *bbrSender) CalculatePacingRate() { - if b.BandwidthEstimate() == 0 { - return - } - - targetRate := Bandwidth(b.pacingGain * float64(b.BandwidthEstimate())) - if b.isAtFullBandwidth { - b.pacingRate = targetRate - return - } - - // Pace at the rate of initial_window / RTT as soon as RTT measurements are - // available. - if b.pacingRate == 0 && b.rttStats.MinRTT() > 0 { - b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT()) - return - } - // Slow the pacing rate in STARTUP once loss has ever been detected. - hasEverDetectedLoss := b.endRecoveryAt > 0 - if b.slowerStartup && hasEverDetectedLoss && b.hasNoAppLimitedSample { - b.pacingRate = Bandwidth(StartupAfterLossGain * float64(b.BandwidthEstimate())) - return - } - - // Slow the pacing rate in STARTUP by the bytes_lost / CWND. - if b.startupRateReductionMultiplier != 0 && hasEverDetectedLoss && b.hasNoAppLimitedSample { - b.pacingRate = Bandwidth((1.0 - (float64(b.startupBytesLost) * float64(b.startupRateReductionMultiplier) / float64(b.congestionWindow))) * float64(targetRate)) - // Ensure the pacing rate doesn't drop below the startup growth target times - // the bandwidth estimate. - b.pacingRate = maxBandwidth(b.pacingRate, Bandwidth(StartupGrowthTarget*float64(b.BandwidthEstimate()))) - return - } - - // Do not decrease the pacing rate during startup. - b.pacingRate = maxBandwidth(b.pacingRate, targetRate) -} - -func (b *bbrSender) CalculateCongestionWindow(ackedBytes, excessAcked congestion.ByteCount) { - if b.mode == PROBE_RTT { - return - } - - targetWindow := b.GetTargetCongestionWindow(b.congestionWindowGain) - if b.isAtFullBandwidth { - // Add the max recently measured ack aggregation to CWND. - targetWindow += congestion.ByteCount(b.maxAckHeight.GetBest()) - } else if b.enableAckAggregationDuringStartup { - // Add the most recent excess acked. Because CWND never decreases in - // STARTUP, this will automatically create a very localized max filter. - targetWindow += excessAcked - } - - // Instead of immediately setting the target CWND as the new one, BBR grows - // the CWND towards |target_window| by only increasing it |bytes_acked| at a - // time. - addBytesAcked := true || !b.InRecovery() - if b.isAtFullBandwidth { - b.congestionWindow = minByteCount(targetWindow, b.congestionWindow+ackedBytes) - } else if addBytesAcked && (b.congestionWindow < targetWindow || b.sampler.totalBytesAcked < b.initialCongestionWindow) { - // If the connection is not yet out of startup phase, do not decrease the - // window. - b.congestionWindow += ackedBytes - } - - // Enforce the limits on the congestion window. - b.congestionWindow = maxByteCount(b.congestionWindow, b.minCongestionWindow()) - b.congestionWindow = minByteCount(b.congestionWindow, b.maxCongestionWindow()) -} - -func (b *bbrSender) CalculateRecoveryWindow(ackedBytes, lostBytes congestion.ByteCount) { - if b.rateBasedStartup && b.mode == STARTUP { - return - } - - if b.recoveryState == NOT_IN_RECOVERY { - return - } - - // Set up the initial recovery window. - if b.recoveryWindow == 0 { - b.recoveryWindow = maxByteCount(b.GetBytesInFlight()+ackedBytes, b.minCongestionWindow()) - return - } - - // Remove losses from the recovery window, while accounting for a potential - // integer underflow. - if b.recoveryWindow >= lostBytes { - b.recoveryWindow -= lostBytes - } else { - b.recoveryWindow = congestion.ByteCount(b.maxDatagramSize) - } - // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, - // release additional |bytes_acked| to achieve a slow-start-like behavior. - if b.recoveryState == GROWTH { - b.recoveryWindow += ackedBytes - } - // Sanity checks. Ensure that we always allow to send at least an MSS or - // |bytes_acked| in response, whichever is larger. - b.recoveryWindow = maxByteCount(b.recoveryWindow, b.GetBytesInFlight()+ackedBytes) - b.recoveryWindow = maxByteCount(b.recoveryWindow, b.minCongestionWindow()) -} - -var _ congestion.CongestionControl = (*bbrSender)(nil) - -func (b *bbrSender) GetMinRtt() time.Duration { - if b.minRtt > 0 { - return b.minRtt - } else { - return InitialRtt - } -} - -func minRtt(a, b time.Duration) time.Duration { - if a < b { - return a - } else { - return b - } -} - -func minBandwidth(a, b Bandwidth) Bandwidth { - if a < b { - return a - } else { - return b - } -} - -func maxBandwidth(a, b Bandwidth) Bandwidth { - if a > b { - return a - } else { - return b - } -} - -func maxByteCount(a, b congestion.ByteCount) congestion.ByteCount { - if a > b { - return a - } else { - return b - } -} - -func minByteCount(a, b congestion.ByteCount) congestion.ByteCount { - if a < b { - return a - } else { - return b - } -} - -var ( - InfiniteRTT = time.Duration(math.MaxInt64) -) diff --git a/transport/tuic/congestion/clock.go b/transport/tuic/congestion/clock.go deleted file mode 100644 index 405fae70f9..0000000000 --- a/transport/tuic/congestion/clock.go +++ /dev/null @@ -1,18 +0,0 @@ -package congestion - -import "time" - -// A Clock returns the current time -type Clock interface { - Now() time.Time -} - -// DefaultClock implements the Clock interface using the Go stdlib clock. -type DefaultClock struct{} - -var _ Clock = DefaultClock{} - -// Now gets the current time -func (DefaultClock) Now() time.Time { - return time.Now() -} diff --git a/transport/tuic/congestion/cubic.go b/transport/tuic/congestion/cubic.go deleted file mode 100644 index a9bed43aa1..0000000000 --- a/transport/tuic/congestion/cubic.go +++ /dev/null @@ -1,213 +0,0 @@ -package congestion - -import ( - "math" - "time" - - "github.com/metacubex/quic-go/congestion" -) - -// This cubic implementation is based on the one found in Chromiums's QUIC -// implementation, in the files net/quic/congestion_control/cubic.{hh,cc}. - -// Constants based on TCP defaults. -// The following constants are in 2^10 fractions of a second instead of ms to -// allow a 10 shift right to divide. - -// 1024*1024^3 (first 1024 is from 0.100^3) -// where 0.100 is 100 ms which is the scaling round trip time. -const ( - cubeScale = 40 - cubeCongestionWindowScale = 410 - cubeFactor congestion.ByteCount = 1 << cubeScale / cubeCongestionWindowScale / maxDatagramSize - // TODO: when re-enabling cubic, make sure to use the actual packet size here - maxDatagramSize = congestion.ByteCount(InitialPacketSize) -) - -const defaultNumConnections = 1 - -// Default Cubic backoff factor -const beta float32 = 0.7 - -// Additional backoff factor when loss occurs in the concave part of the Cubic -// curve. This additional backoff factor is expected to give up bandwidth to -// new concurrent flows and speed up convergence. -const betaLastMax float32 = 0.85 - -// Cubic implements the cubic algorithm from TCP -type Cubic struct { - clock Clock - - // Number of connections to simulate. - numConnections int - - // Time when this cycle started, after last loss event. - epoch time.Time - - // Max congestion window used just before last loss event. - // Note: to improve fairness to other streams an additional back off is - // applied to this value if the new value is below our latest value. - lastMaxCongestionWindow congestion.ByteCount - - // Number of acked bytes since the cycle started (epoch). - ackedBytesCount congestion.ByteCount - - // TCP Reno equivalent congestion window in packets. - estimatedTCPcongestionWindow congestion.ByteCount - - // Origin point of cubic function. - originPointCongestionWindow congestion.ByteCount - - // Time to origin point of cubic function in 2^10 fractions of a second. - timeToOriginPoint uint32 - - // Last congestion window in packets computed by cubic function. - lastTargetCongestionWindow congestion.ByteCount -} - -// NewCubic returns a new Cubic instance -func NewCubic(clock Clock) *Cubic { - c := &Cubic{ - clock: clock, - numConnections: defaultNumConnections, - } - c.Reset() - return c -} - -// Reset is called after a timeout to reset the cubic state -func (c *Cubic) Reset() { - c.epoch = time.Time{} - c.lastMaxCongestionWindow = 0 - c.ackedBytesCount = 0 - c.estimatedTCPcongestionWindow = 0 - c.originPointCongestionWindow = 0 - c.timeToOriginPoint = 0 - c.lastTargetCongestionWindow = 0 -} - -func (c *Cubic) alpha() float32 { - // TCPFriendly alpha is described in Section 3.3 of the CUBIC paper. Note that - // beta here is a cwnd multiplier, and is equal to 1-beta from the paper. - // We derive the equivalent alpha for an N-connection emulation as: - b := c.beta() - return 3 * float32(c.numConnections) * float32(c.numConnections) * (1 - b) / (1 + b) -} - -func (c *Cubic) beta() float32 { - // kNConnectionBeta is the backoff factor after loss for our N-connection - // emulation, which emulates the effective backoff of an ensemble of N - // TCP-Reno connections on a single loss event. The effective multiplier is - // computed as: - return (float32(c.numConnections) - 1 + beta) / float32(c.numConnections) -} - -func (c *Cubic) betaLastMax() float32 { - // betaLastMax is the additional backoff factor after loss for our - // N-connection emulation, which emulates the additional backoff of - // an ensemble of N TCP-Reno connections on a single loss event. The - // effective multiplier is computed as: - return (float32(c.numConnections) - 1 + betaLastMax) / float32(c.numConnections) -} - -// OnApplicationLimited is called on ack arrival when sender is unable to use -// the available congestion window. Resets Cubic state during quiescence. -func (c *Cubic) OnApplicationLimited() { - // When sender is not using the available congestion window, the window does - // not grow. But to be RTT-independent, Cubic assumes that the sender has been - // using the entire window during the time since the beginning of the current - // "epoch" (the end of the last loss recovery period). Since - // application-limited periods break this assumption, we reset the epoch when - // in such a period. This reset effectively freezes congestion window growth - // through application-limited periods and allows Cubic growth to continue - // when the entire window is being used. - c.epoch = time.Time{} -} - -// CongestionWindowAfterPacketLoss computes a new congestion window to use after -// a loss event. Returns the new congestion window in packets. The new -// congestion window is a multiplicative decrease of our current window. -func (c *Cubic) CongestionWindowAfterPacketLoss(currentCongestionWindow congestion.ByteCount) congestion.ByteCount { - if currentCongestionWindow+maxDatagramSize < c.lastMaxCongestionWindow { - // We never reached the old max, so assume we are competing with another - // flow. Use our extra back off factor to allow the other flow to go up. - c.lastMaxCongestionWindow = congestion.ByteCount(c.betaLastMax() * float32(currentCongestionWindow)) - } else { - c.lastMaxCongestionWindow = currentCongestionWindow - } - c.epoch = time.Time{} // Reset time. - return congestion.ByteCount(float32(currentCongestionWindow) * c.beta()) -} - -// CongestionWindowAfterAck computes a new congestion window to use after a received ACK. -// Returns the new congestion window in packets. The new congestion window -// follows a cubic function that depends on the time passed since last -// packet loss. -func (c *Cubic) CongestionWindowAfterAck( - ackedBytes congestion.ByteCount, - currentCongestionWindow congestion.ByteCount, - delayMin time.Duration, - eventTime time.Time, -) congestion.ByteCount { - c.ackedBytesCount += ackedBytes - - if c.epoch.IsZero() { - // First ACK after a loss event. - c.epoch = eventTime // Start of epoch. - c.ackedBytesCount = ackedBytes // Reset count. - // Reset estimated_tcp_congestion_window_ to be in sync with cubic. - c.estimatedTCPcongestionWindow = currentCongestionWindow - if c.lastMaxCongestionWindow <= currentCongestionWindow { - c.timeToOriginPoint = 0 - c.originPointCongestionWindow = currentCongestionWindow - } else { - c.timeToOriginPoint = uint32(math.Cbrt(float64(cubeFactor * (c.lastMaxCongestionWindow - currentCongestionWindow)))) - c.originPointCongestionWindow = c.lastMaxCongestionWindow - } - } - - // Change the time unit from microseconds to 2^10 fractions per second. Take - // the round trip time in account. This is done to allow us to use shift as a - // divide operator. - elapsedTime := int64(eventTime.Add(delayMin).Sub(c.epoch)/time.Microsecond) << 10 / (1000 * 1000) - - // Right-shifts of negative, signed numbers have implementation-dependent - // behavior, so force the offset to be positive, as is done in the kernel. - offset := int64(c.timeToOriginPoint) - elapsedTime - if offset < 0 { - offset = -offset - } - - deltaCongestionWindow := congestion.ByteCount(cubeCongestionWindowScale*offset*offset*offset) * maxDatagramSize >> cubeScale - var targetCongestionWindow congestion.ByteCount - if elapsedTime > int64(c.timeToOriginPoint) { - targetCongestionWindow = c.originPointCongestionWindow + deltaCongestionWindow - } else { - targetCongestionWindow = c.originPointCongestionWindow - deltaCongestionWindow - } - // Limit the CWND increase to half the acked bytes. - targetCongestionWindow = Min(targetCongestionWindow, currentCongestionWindow+c.ackedBytesCount/2) - - // Increase the window by approximately Alpha * 1 MSS of bytes every - // time we ack an estimated tcp window of bytes. For small - // congestion windows (less than 25), the formula below will - // increase slightly slower than linearly per estimated tcp window - // of bytes. - c.estimatedTCPcongestionWindow += congestion.ByteCount(float32(c.ackedBytesCount) * c.alpha() * float32(maxDatagramSize) / float32(c.estimatedTCPcongestionWindow)) - c.ackedBytesCount = 0 - - // We have a new cubic congestion window. - c.lastTargetCongestionWindow = targetCongestionWindow - - // Compute target congestion_window based on cubic target and estimated TCP - // congestion_window, use highest (fastest). - if targetCongestionWindow < c.estimatedTCPcongestionWindow { - targetCongestionWindow = c.estimatedTCPcongestionWindow - } - return targetCongestionWindow -} - -// SetNumConnections sets the number of emulated connections -func (c *Cubic) SetNumConnections(n int) { - c.numConnections = n -} diff --git a/transport/tuic/congestion/cubic_sender.go b/transport/tuic/congestion/cubic_sender.go deleted file mode 100644 index f544cd749c..0000000000 --- a/transport/tuic/congestion/cubic_sender.go +++ /dev/null @@ -1,297 +0,0 @@ -package congestion - -import ( - "fmt" - "time" - - "github.com/metacubex/quic-go/congestion" -) - -const ( - maxBurstPackets = 3 - renoBeta = 0.7 // Reno backoff factor. - minCongestionWindowPackets = 2 - initialCongestionWindow = 32 -) - -const InvalidPacketNumber congestion.PacketNumber = -1 -const MaxCongestionWindowPackets = 20000 -const MaxByteCount = congestion.ByteCount(1<<62 - 1) - -type cubicSender struct { - hybridSlowStart HybridSlowStart - rttStats congestion.RTTStatsProvider - cubic *Cubic - pacer *pacer - clock Clock - - reno bool - - // Track the largest packet that has been sent. - largestSentPacketNumber congestion.PacketNumber - - // Track the largest packet that has been acked. - largestAckedPacketNumber congestion.PacketNumber - - // Track the largest packet number outstanding when a CWND cutback occurs. - largestSentAtLastCutback congestion.PacketNumber - - // Whether the last loss event caused us to exit slowstart. - // Used for stats collection of slowstartPacketsLost - lastCutbackExitedSlowstart bool - - // Congestion window in bytes. - congestionWindow congestion.ByteCount - - // Slow start congestion window in bytes, aka ssthresh. - slowStartThreshold congestion.ByteCount - - // ACK counter for the Reno implementation. - numAckedPackets uint64 - - initialCongestionWindow congestion.ByteCount - initialMaxCongestionWindow congestion.ByteCount - - maxDatagramSize congestion.ByteCount -} - -var ( - _ congestion.CongestionControl = &cubicSender{} -) - -// NewCubicSender makes a new cubic sender -func NewCubicSender( - clock Clock, - initialMaxDatagramSize congestion.ByteCount, - reno bool, -) *cubicSender { - return newCubicSender( - clock, - reno, - initialMaxDatagramSize, - initialCongestionWindow*initialMaxDatagramSize, - MaxCongestionWindowPackets*initialMaxDatagramSize, - ) -} - -func newCubicSender( - clock Clock, - reno bool, - initialMaxDatagramSize, - initialCongestionWindow, - initialMaxCongestionWindow congestion.ByteCount, -) *cubicSender { - c := &cubicSender{ - largestSentPacketNumber: InvalidPacketNumber, - largestAckedPacketNumber: InvalidPacketNumber, - largestSentAtLastCutback: InvalidPacketNumber, - initialCongestionWindow: initialCongestionWindow, - initialMaxCongestionWindow: initialMaxCongestionWindow, - congestionWindow: initialCongestionWindow, - slowStartThreshold: MaxByteCount, - cubic: NewCubic(clock), - clock: clock, - reno: reno, - maxDatagramSize: initialMaxDatagramSize, - } - c.pacer = newPacer(c.BandwidthEstimate) - return c -} - -func (c *cubicSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { - c.rttStats = provider -} - -// TimeUntilSend returns when the next packet should be sent. -func (c *cubicSender) TimeUntilSend(_ congestion.ByteCount) time.Time { - return c.pacer.TimeUntilSend() -} - -func (c *cubicSender) HasPacingBudget(now time.Time) bool { - return c.pacer.Budget(now) >= c.maxDatagramSize -} - -func (c *cubicSender) maxCongestionWindow() congestion.ByteCount { - return c.maxDatagramSize * MaxCongestionWindowPackets -} - -func (c *cubicSender) minCongestionWindow() congestion.ByteCount { - return c.maxDatagramSize * minCongestionWindowPackets -} - -func (c *cubicSender) OnPacketSent( - sentTime time.Time, - _ congestion.ByteCount, - packetNumber congestion.PacketNumber, - bytes congestion.ByteCount, - isRetransmittable bool, -) { - c.pacer.SentPacket(sentTime, bytes) - if !isRetransmittable { - return - } - c.largestSentPacketNumber = packetNumber - c.hybridSlowStart.OnPacketSent(packetNumber) -} - -func (c *cubicSender) CanSend(bytesInFlight congestion.ByteCount) bool { - return bytesInFlight < c.GetCongestionWindow() -} - -func (c *cubicSender) InRecovery() bool { - return c.largestAckedPacketNumber != InvalidPacketNumber && c.largestAckedPacketNumber <= c.largestSentAtLastCutback -} - -func (c *cubicSender) InSlowStart() bool { - return c.GetCongestionWindow() < c.slowStartThreshold -} - -func (c *cubicSender) GetCongestionWindow() congestion.ByteCount { - return c.congestionWindow -} - -func (c *cubicSender) MaybeExitSlowStart() { - if c.InSlowStart() && - c.hybridSlowStart.ShouldExitSlowStart(c.rttStats.LatestRTT(), c.rttStats.MinRTT(), c.GetCongestionWindow()/c.maxDatagramSize) { - // exit slow start - c.slowStartThreshold = c.congestionWindow - } -} - -func (c *cubicSender) OnPacketAcked( - ackedPacketNumber congestion.PacketNumber, - ackedBytes congestion.ByteCount, - priorInFlight congestion.ByteCount, - eventTime time.Time, -) { - c.largestAckedPacketNumber = Max(ackedPacketNumber, c.largestAckedPacketNumber) - if c.InRecovery() { - return - } - c.maybeIncreaseCwnd(ackedPacketNumber, ackedBytes, priorInFlight, eventTime) - if c.InSlowStart() { - c.hybridSlowStart.OnPacketAcked(ackedPacketNumber) - } -} - -func (c *cubicSender) OnCongestionEvent(packetNumber congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { - // TCP NewReno (RFC6582) says that once a loss occurs, any losses in packets - // already sent should be treated as a single loss event, since it's expected. - if packetNumber <= c.largestSentAtLastCutback { - return - } - c.lastCutbackExitedSlowstart = c.InSlowStart() - - if c.reno { - c.congestionWindow = congestion.ByteCount(float64(c.congestionWindow) * renoBeta) - } else { - c.congestionWindow = c.cubic.CongestionWindowAfterPacketLoss(c.congestionWindow) - } - if minCwnd := c.minCongestionWindow(); c.congestionWindow < minCwnd { - c.congestionWindow = minCwnd - } - c.slowStartThreshold = c.congestionWindow - c.largestSentAtLastCutback = c.largestSentPacketNumber - // reset packet count from congestion avoidance mode. We start - // counting again when we're out of recovery. - c.numAckedPackets = 0 -} - -func (b *cubicSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { - // Stub -} - -// Called when we receive an ack. Normal TCP tracks how many packets one ack -// represents, but quic has a separate ack for each packet. -func (c *cubicSender) maybeIncreaseCwnd( - _ congestion.PacketNumber, - ackedBytes congestion.ByteCount, - priorInFlight congestion.ByteCount, - eventTime time.Time, -) { - // Do not increase the congestion window unless the sender is close to using - // the current window. - if !c.isCwndLimited(priorInFlight) { - c.cubic.OnApplicationLimited() - return - } - if c.congestionWindow >= c.maxCongestionWindow() { - return - } - if c.InSlowStart() { - // TCP slow start, exponential growth, increase by one for each ACK. - c.congestionWindow += c.maxDatagramSize - return - } - // Congestion avoidance - if c.reno { - // Classic Reno congestion avoidance. - c.numAckedPackets++ - if c.numAckedPackets >= uint64(c.congestionWindow/c.maxDatagramSize) { - c.congestionWindow += c.maxDatagramSize - c.numAckedPackets = 0 - } - } else { - c.congestionWindow = Min(c.maxCongestionWindow(), c.cubic.CongestionWindowAfterAck(ackedBytes, c.congestionWindow, c.rttStats.MinRTT(), eventTime)) - } -} - -func (c *cubicSender) isCwndLimited(bytesInFlight congestion.ByteCount) bool { - congestionWindow := c.GetCongestionWindow() - if bytesInFlight >= congestionWindow { - return true - } - availableBytes := congestionWindow - bytesInFlight - slowStartLimited := c.InSlowStart() && bytesInFlight > congestionWindow/2 - return slowStartLimited || availableBytes <= maxBurstPackets*c.maxDatagramSize -} - -// BandwidthEstimate returns the current bandwidth estimate -func (c *cubicSender) BandwidthEstimate() Bandwidth { - if c.rttStats == nil { - return infBandwidth - } - srtt := c.rttStats.SmoothedRTT() - if srtt == 0 { - // If we haven't measured an rtt, the bandwidth estimate is unknown. - return infBandwidth - } - return BandwidthFromDelta(c.GetCongestionWindow(), srtt) -} - -// OnRetransmissionTimeout is called on an retransmission timeout -func (c *cubicSender) OnRetransmissionTimeout(packetsRetransmitted bool) { - c.largestSentAtLastCutback = InvalidPacketNumber - if !packetsRetransmitted { - return - } - c.hybridSlowStart.Restart() - c.cubic.Reset() - c.slowStartThreshold = c.congestionWindow / 2 - c.congestionWindow = c.minCongestionWindow() -} - -// OnConnectionMigration is called when the connection is migrated (?) -func (c *cubicSender) OnConnectionMigration() { - c.hybridSlowStart.Restart() - c.largestSentPacketNumber = InvalidPacketNumber - c.largestAckedPacketNumber = InvalidPacketNumber - c.largestSentAtLastCutback = InvalidPacketNumber - c.lastCutbackExitedSlowstart = false - c.cubic.Reset() - c.numAckedPackets = 0 - c.congestionWindow = c.initialCongestionWindow - c.slowStartThreshold = c.initialMaxCongestionWindow -} - -func (c *cubicSender) SetMaxDatagramSize(s congestion.ByteCount) { - if s < c.maxDatagramSize { - panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", c.maxDatagramSize, s)) - } - cwndIsMinCwnd := c.congestionWindow == c.minCongestionWindow() - c.maxDatagramSize = s - if cwndIsMinCwnd { - c.congestionWindow = c.minCongestionWindow() - } - c.pacer.SetMaxDatagramSize(s) -} diff --git a/transport/tuic/congestion/hybrid_slow_start.go b/transport/tuic/congestion/hybrid_slow_start.go deleted file mode 100644 index 7586774e68..0000000000 --- a/transport/tuic/congestion/hybrid_slow_start.go +++ /dev/null @@ -1,112 +0,0 @@ -package congestion - -import ( - "time" - - "github.com/metacubex/quic-go/congestion" -) - -// Note(pwestin): the magic clamping numbers come from the original code in -// tcp_cubic.c. -const hybridStartLowWindow = congestion.ByteCount(16) - -// Number of delay samples for detecting the increase of delay. -const hybridStartMinSamples = uint32(8) - -// Exit slow start if the min rtt has increased by more than 1/8th. -const hybridStartDelayFactorExp = 3 // 2^3 = 8 -// The original paper specifies 2 and 8ms, but those have changed over time. -const ( - hybridStartDelayMinThresholdUs = int64(4000) - hybridStartDelayMaxThresholdUs = int64(16000) -) - -// HybridSlowStart implements the TCP hybrid slow start algorithm -type HybridSlowStart struct { - endPacketNumber congestion.PacketNumber - lastSentPacketNumber congestion.PacketNumber - started bool - currentMinRTT time.Duration - rttSampleCount uint32 - hystartFound bool -} - -// StartReceiveRound is called for the start of each receive round (burst) in the slow start phase. -func (s *HybridSlowStart) StartReceiveRound(lastSent congestion.PacketNumber) { - s.endPacketNumber = lastSent - s.currentMinRTT = 0 - s.rttSampleCount = 0 - s.started = true -} - -// IsEndOfRound returns true if this ack is the last packet number of our current slow start round. -func (s *HybridSlowStart) IsEndOfRound(ack congestion.PacketNumber) bool { - return s.endPacketNumber < ack -} - -// ShouldExitSlowStart should be called on every new ack frame, since a new -// RTT measurement can be made then. -// rtt: the RTT for this ack packet. -// minRTT: is the lowest delay (RTT) we have seen during the session. -// congestionWindow: the congestion window in packets. -func (s *HybridSlowStart) ShouldExitSlowStart(latestRTT time.Duration, minRTT time.Duration, congestionWindow congestion.ByteCount) bool { - if !s.started { - // Time to start the hybrid slow start. - s.StartReceiveRound(s.lastSentPacketNumber) - } - if s.hystartFound { - return true - } - // Second detection parameter - delay increase detection. - // Compare the minimum delay (s.currentMinRTT) of the current - // burst of packets relative to the minimum delay during the session. - // Note: we only look at the first few(8) packets in each burst, since we - // only want to compare the lowest RTT of the burst relative to previous - // bursts. - s.rttSampleCount++ - if s.rttSampleCount <= hybridStartMinSamples { - if s.currentMinRTT == 0 || s.currentMinRTT > latestRTT { - s.currentMinRTT = latestRTT - } - } - // We only need to check this once per round. - if s.rttSampleCount == hybridStartMinSamples { - // Divide minRTT by 8 to get a rtt increase threshold for exiting. - minRTTincreaseThresholdUs := int64(minRTT / time.Microsecond >> hybridStartDelayFactorExp) - // Ensure the rtt threshold is never less than 2ms or more than 16ms. - minRTTincreaseThresholdUs = Min(minRTTincreaseThresholdUs, hybridStartDelayMaxThresholdUs) - minRTTincreaseThreshold := time.Duration(Max(minRTTincreaseThresholdUs, hybridStartDelayMinThresholdUs)) * time.Microsecond - - if s.currentMinRTT > (minRTT + minRTTincreaseThreshold) { - s.hystartFound = true - } - } - // Exit from slow start if the cwnd is greater than 16 and - // increasing delay is found. - return congestionWindow >= hybridStartLowWindow && s.hystartFound -} - -// OnPacketSent is called when a packet was sent -func (s *HybridSlowStart) OnPacketSent(packetNumber congestion.PacketNumber) { - s.lastSentPacketNumber = packetNumber -} - -// OnPacketAcked gets invoked after ShouldExitSlowStart, so it's best to end -// the round when the final packet of the burst is received and start it on -// the next incoming ack. -func (s *HybridSlowStart) OnPacketAcked(ackedPacketNumber congestion.PacketNumber) { - if s.IsEndOfRound(ackedPacketNumber) { - s.started = false - } -} - -// Started returns true if started -func (s *HybridSlowStart) Started() bool { - return s.started -} - -// Restart the slow start phase -func (s *HybridSlowStart) Restart() { - s.started = false - s.hystartFound = false -} diff --git a/transport/tuic/congestion/minmax.go b/transport/tuic/congestion/minmax.go deleted file mode 100644 index 0a8f4ad4ac..0000000000 --- a/transport/tuic/congestion/minmax.go +++ /dev/null @@ -1,56 +0,0 @@ -package congestion - -import ( - "math" - "time" -) - -// InfDuration is a duration of infinite length -const InfDuration = time.Duration(math.MaxInt64) - -// MinNonZeroDuration return the minimum duration that's not zero. -func MinNonZeroDuration(a, b time.Duration) time.Duration { - if a == 0 { - return b - } - if b == 0 { - return a - } - return Min(a, b) -} - -// AbsDuration returns the absolute value of a time duration -func AbsDuration(d time.Duration) time.Duration { - if d >= 0 { - return d - } - return -d -} - -// MinTime returns the earlier time -func MinTime(a, b time.Time) time.Time { - if a.After(b) { - return b - } - return a -} - -// MinNonZeroTime returns the earlist time that is not time.Time{} -// If both a and b are time.Time{}, it returns time.Time{} -func MinNonZeroTime(a, b time.Time) time.Time { - if a.IsZero() { - return b - } - if b.IsZero() { - return a - } - return MinTime(a, b) -} - -// MaxTime returns the later time -func MaxTime(a, b time.Time) time.Time { - if a.After(b) { - return a - } - return b -} diff --git a/transport/tuic/congestion/minmax_go120.go b/transport/tuic/congestion/minmax_go120.go deleted file mode 100644 index 1266edbc48..0000000000 --- a/transport/tuic/congestion/minmax_go120.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !go1.21 - -package congestion - -import "golang.org/x/exp/constraints" - -func Max[T constraints.Ordered](a, b T) T { - if a < b { - return b - } - return a -} - -func Min[T constraints.Ordered](a, b T) T { - if a < b { - return a - } - return b -} diff --git a/transport/tuic/congestion/minmax_go121.go b/transport/tuic/congestion/minmax_go121.go deleted file mode 100644 index 65b067265a..0000000000 --- a/transport/tuic/congestion/minmax_go121.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build go1.21 - -package congestion - -import "cmp" - -func Max[T cmp.Ordered](a, b T) T { - return max(a, b) -} - -func Min[T cmp.Ordered](a, b T) T { - return min(a, b) -} diff --git a/transport/tuic/congestion/pacer.go b/transport/tuic/congestion/pacer.go deleted file mode 100644 index f60ef5fe1b..0000000000 --- a/transport/tuic/congestion/pacer.go +++ /dev/null @@ -1,79 +0,0 @@ -package congestion - -import ( - "math" - "time" - - "github.com/metacubex/quic-go/congestion" -) - -const initialMaxDatagramSize = congestion.ByteCount(1252) -const MinPacingDelay = time.Millisecond -const TimerGranularity = time.Millisecond -const maxBurstSizePackets = 10 - -// The pacer implements a token bucket pacing algorithm. -type pacer struct { - budgetAtLastSent congestion.ByteCount - maxDatagramSize congestion.ByteCount - lastSentTime time.Time - getAdjustedBandwidth func() uint64 // in bytes/s -} - -func newPacer(getBandwidth func() Bandwidth) *pacer { - p := &pacer{ - maxDatagramSize: initialMaxDatagramSize, - getAdjustedBandwidth: func() uint64 { - // Bandwidth is in bits/s. We need the value in bytes/s. - bw := uint64(getBandwidth() / BytesPerSecond) - // Use a slightly higher value than the actual measured bandwidth. - // RTT variations then won't result in under-utilization of the congestion window. - // Ultimately, this will result in sending packets as acknowledgments are received rather than when timers fire, - // provided the congestion window is fully utilized and acknowledgments arrive at regular intervals. - return bw * 5 / 4 - }, - } - p.budgetAtLastSent = p.maxBurstSize() - return p -} - -func (p *pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) { - budget := p.Budget(sendTime) - if size > budget { - p.budgetAtLastSent = 0 - } else { - p.budgetAtLastSent = budget - size - } - p.lastSentTime = sendTime -} - -func (p *pacer) Budget(now time.Time) congestion.ByteCount { - if p.lastSentTime.IsZero() { - return p.maxBurstSize() - } - budget := p.budgetAtLastSent + (congestion.ByteCount(p.getAdjustedBandwidth())*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 - return Min(p.maxBurstSize(), budget) -} - -func (p *pacer) maxBurstSize() congestion.ByteCount { - return Max( - congestion.ByteCount(uint64((MinPacingDelay+TimerGranularity).Nanoseconds())*p.getAdjustedBandwidth())/1e9, - maxBurstSizePackets*p.maxDatagramSize, - ) -} - -// TimeUntilSend returns when the next packet should be sent. -// It returns the zero value of time.Time if a packet can be sent immediately. -func (p *pacer) TimeUntilSend() time.Time { - if p.budgetAtLastSent >= p.maxDatagramSize { - return time.Time{} - } - return p.lastSentTime.Add(Max( - MinPacingDelay, - time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/float64(p.getAdjustedBandwidth())))*time.Nanosecond, - )) -} - -func (p *pacer) SetMaxDatagramSize(s congestion.ByteCount) { - p.maxDatagramSize = s -} diff --git a/transport/tuic/congestion/windowed_filter.go b/transport/tuic/congestion/windowed_filter.go deleted file mode 100644 index 4da595b94f..0000000000 --- a/transport/tuic/congestion/windowed_filter.go +++ /dev/null @@ -1,132 +0,0 @@ -package congestion - -// WindowedFilter Use the following to construct a windowed filter object of type T. -// For example, a min filter using QuicTime as the time type: -// -// WindowedFilter, QuicTime, QuicTime::Delta> ObjectName; -// -// A max filter using 64-bit integers as the time type: -// -// WindowedFilter, uint64_t, int64_t> ObjectName; -// -// Specifically, this template takes four arguments: -// 1. T -- type of the measurement that is being filtered. -// 2. Compare -- MinFilter or MaxFilter, depending on the type of filter -// desired. -// 3. TimeT -- the type used to represent timestamps. -// 4. TimeDeltaT -- the type used to represent continuous time intervals between -// two timestamps. Has to be the type of (a - b) if both |a| and |b| are -// of type TimeT. -type WindowedFilter struct { - // Time length of window. - windowLength int64 - estimates []Sample - comparator func(int64, int64) bool -} - -type Sample struct { - sample int64 - time int64 -} - -// Compares two values and returns true if the first is greater than or equal -// to the second. -func MaxFilter(a, b int64) bool { - return a >= b -} - -// Compares two values and returns true if the first is less than or equal -// to the second. -func MinFilter(a, b int64) bool { - return a <= b -} - -func NewWindowedFilter(windowLength int64, comparator func(int64, int64) bool) *WindowedFilter { - return &WindowedFilter{ - windowLength: windowLength, - estimates: make([]Sample, 3), - comparator: comparator, - } -} - -// Changes the window length. Does not update any current samples. -func (f *WindowedFilter) SetWindowLength(windowLength int64) { - f.windowLength = windowLength -} - -func (f *WindowedFilter) GetBest() int64 { - return f.estimates[0].sample -} - -func (f *WindowedFilter) GetSecondBest() int64 { - return f.estimates[1].sample -} - -func (f *WindowedFilter) GetThirdBest() int64 { - return f.estimates[2].sample -} - -func (f *WindowedFilter) Update(sample int64, time int64) { - if f.estimates[0].time == 0 || f.comparator(sample, f.estimates[0].sample) || (time-f.estimates[2].time) > f.windowLength { - f.Reset(sample, time) - return - } - - if f.comparator(sample, f.estimates[1].sample) { - f.estimates[1].sample = sample - f.estimates[1].time = time - f.estimates[2].sample = sample - f.estimates[2].time = time - } else if f.comparator(sample, f.estimates[2].sample) { - f.estimates[2].sample = sample - f.estimates[2].time = time - } - - // Expire and update estimates as necessary. - if time-f.estimates[0].time > f.windowLength { - // The best estimate hasn't been updated for an entire window, so promote - // second and third best estimates. - f.estimates[0].sample = f.estimates[1].sample - f.estimates[0].time = f.estimates[1].time - f.estimates[1].sample = f.estimates[2].sample - f.estimates[1].time = f.estimates[2].time - f.estimates[2].sample = sample - f.estimates[2].time = time - // Need to iterate one more time. Check if the new best estimate is - // outside the window as well, since it may also have been recorded a - // long time ago. Don't need to iterate once more since we cover that - // case at the beginning of the method. - if time-f.estimates[0].time > f.windowLength { - f.estimates[0].sample = f.estimates[1].sample - f.estimates[0].time = f.estimates[1].time - f.estimates[1].sample = f.estimates[2].sample - f.estimates[1].time = f.estimates[2].time - } - return - } - if f.estimates[1].sample == f.estimates[0].sample && time-f.estimates[1].time > f.windowLength>>2 { - // A quarter of the window has passed without a better sample, so the - // second-best estimate is taken from the second quarter of the window. - f.estimates[1].sample = sample - f.estimates[1].time = time - f.estimates[2].sample = sample - f.estimates[2].time = time - return - } - - if f.estimates[2].sample == f.estimates[1].sample && time-f.estimates[2].time > f.windowLength>>1 { - // We've passed a half of the window without a better estimate, so take - // a third-best estimate from the second half of the window. - f.estimates[2].sample = sample - f.estimates[2].time = time - } -} - -func (f *WindowedFilter) Reset(newSample int64, newTime int64) { - f.estimates[0].sample = newSample - f.estimates[0].time = newTime - f.estimates[1].sample = newSample - f.estimates[1].time = newTime - f.estimates[2].sample = newSample - f.estimates[2].time = newTime -} diff --git a/transport/tuic/congestion_v2/bandwidth.go b/transport/tuic/congestion_v2/bandwidth.go deleted file mode 100644 index df39a07784..0000000000 --- a/transport/tuic/congestion_v2/bandwidth.go +++ /dev/null @@ -1,27 +0,0 @@ -package congestion - -import ( - "math" - "time" - - "github.com/metacubex/quic-go/congestion" -) - -const ( - infBandwidth = Bandwidth(math.MaxUint64) -) - -// Bandwidth of a connection -type Bandwidth uint64 - -const ( - // BitsPerSecond is 1 bit per second - BitsPerSecond Bandwidth = 1 - // BytesPerSecond is 1 byte per second - BytesPerSecond = 8 * BitsPerSecond -) - -// BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta -func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth { - return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond -} diff --git a/transport/tuic/congestion_v2/bandwidth_sampler.go b/transport/tuic/congestion_v2/bandwidth_sampler.go deleted file mode 100644 index 9028df64c3..0000000000 --- a/transport/tuic/congestion_v2/bandwidth_sampler.go +++ /dev/null @@ -1,874 +0,0 @@ -package congestion - -import ( - "math" - "time" - - "github.com/metacubex/quic-go/congestion" -) - -const ( - infRTT = time.Duration(math.MaxInt64) - defaultConnectionStateMapQueueSize = 256 - defaultCandidatesBufferSize = 256 -) - -type roundTripCount uint64 - -// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned -// to the caller when the packet is acked or lost. -type sendTimeState struct { - // Whether other states in this object is valid. - isValid bool - // Whether the sender is app limited at the time the packet was sent. - // App limited bandwidth sample might be artificially low because the sender - // did not have enough data to send in order to saturate the link. - isAppLimited bool - // Total number of sent bytes at the time the packet was sent. - // Includes the packet itself. - totalBytesSent congestion.ByteCount - // Total number of acked bytes at the time the packet was sent. - totalBytesAcked congestion.ByteCount - // Total number of lost bytes at the time the packet was sent. - totalBytesLost congestion.ByteCount - // Total number of inflight bytes at the time the packet was sent. - // Includes the packet itself. - // It should be equal to |total_bytes_sent| minus the sum of - // |total_bytes_acked|, |total_bytes_lost| and total neutered bytes. - bytesInFlight congestion.ByteCount -} - -func newSendTimeState( - isAppLimited bool, - totalBytesSent congestion.ByteCount, - totalBytesAcked congestion.ByteCount, - totalBytesLost congestion.ByteCount, - bytesInFlight congestion.ByteCount, -) *sendTimeState { - return &sendTimeState{ - isValid: true, - isAppLimited: isAppLimited, - totalBytesSent: totalBytesSent, - totalBytesAcked: totalBytesAcked, - totalBytesLost: totalBytesLost, - bytesInFlight: bytesInFlight, - } -} - -type extraAckedEvent struct { - // The excess bytes acknowlwedged in the time delta for this event. - extraAcked congestion.ByteCount - - // The bytes acknowledged and time delta from the event. - bytesAcked congestion.ByteCount - timeDelta time.Duration - // The round trip of the event. - round roundTripCount -} - -func maxExtraAckedEventFunc(a, b extraAckedEvent) int { - if a.extraAcked > b.extraAcked { - return 1 - } else if a.extraAcked < b.extraAcked { - return -1 - } - return 0 -} - -// BandwidthSample -type bandwidthSample struct { - // The bandwidth at that particular sample. Zero if no valid bandwidth sample - // is available. - bandwidth Bandwidth - // The RTT measurement at this particular sample. Zero if no RTT sample is - // available. Does not correct for delayed ack time. - rtt time.Duration - // |send_rate| is computed from the current packet being acked('P') and an - // earlier packet that is acked before P was sent. - sendRate Bandwidth - // States captured when the packet was sent. - stateAtSend sendTimeState -} - -func newBandwidthSample() *bandwidthSample { - return &bandwidthSample{ - sendRate: infBandwidth, - } -} - -// MaxAckHeightTracker is part of the BandwidthSampler. It is called after every -// ack event to keep track the degree of ack aggregation(a.k.a "ack height"). -type maxAckHeightTracker struct { - // Tracks the maximum number of bytes acked faster than the estimated - // bandwidth. - maxAckHeightFilter *WindowedFilter[extraAckedEvent, roundTripCount] - // The time this aggregation started and the number of bytes acked during it. - aggregationEpochStartTime time.Time - aggregationEpochBytes congestion.ByteCount - // The last sent packet number before the current aggregation epoch started. - lastSentPacketNumberBeforeEpoch congestion.PacketNumber - // The number of ack aggregation epochs ever started, including the ongoing - // one. Stats only. - numAckAggregationEpochs uint64 - ackAggregationBandwidthThreshold float64 - startNewAggregationEpochAfterFullRound bool - reduceExtraAckedOnBandwidthIncrease bool -} - -func newMaxAckHeightTracker(windowLength roundTripCount) *maxAckHeightTracker { - return &maxAckHeightTracker{ - maxAckHeightFilter: NewWindowedFilter(windowLength, maxExtraAckedEventFunc), - lastSentPacketNumberBeforeEpoch: invalidPacketNumber, - ackAggregationBandwidthThreshold: 1.0, - } -} - -func (m *maxAckHeightTracker) Get() congestion.ByteCount { - return m.maxAckHeightFilter.GetBest().extraAcked -} - -func (m *maxAckHeightTracker) Update( - bandwidthEstimate Bandwidth, - isNewMaxBandwidth bool, - roundTripCount roundTripCount, - lastSentPacketNumber congestion.PacketNumber, - lastAckedPacketNumber congestion.PacketNumber, - ackTime time.Time, - bytesAcked congestion.ByteCount, -) congestion.ByteCount { - forceNewEpoch := false - - if m.reduceExtraAckedOnBandwidthIncrease && isNewMaxBandwidth { - // Save and clear existing entries. - best := m.maxAckHeightFilter.GetBest() - secondBest := m.maxAckHeightFilter.GetSecondBest() - thirdBest := m.maxAckHeightFilter.GetThirdBest() - m.maxAckHeightFilter.Clear() - - // Reinsert the heights into the filter after recalculating. - expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, best.timeDelta) - if expectedBytesAcked < best.bytesAcked { - best.extraAcked = best.bytesAcked - expectedBytesAcked - m.maxAckHeightFilter.Update(best, best.round) - } - expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, secondBest.timeDelta) - if expectedBytesAcked < secondBest.bytesAcked { - secondBest.extraAcked = secondBest.bytesAcked - expectedBytesAcked - m.maxAckHeightFilter.Update(secondBest, secondBest.round) - } - expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, thirdBest.timeDelta) - if expectedBytesAcked < thirdBest.bytesAcked { - thirdBest.extraAcked = thirdBest.bytesAcked - expectedBytesAcked - m.maxAckHeightFilter.Update(thirdBest, thirdBest.round) - } - } - - // If any packet sent after the start of the epoch has been acked, start a new - // epoch. - if m.startNewAggregationEpochAfterFullRound && - m.lastSentPacketNumberBeforeEpoch != invalidPacketNumber && - lastAckedPacketNumber != invalidPacketNumber && - lastAckedPacketNumber > m.lastSentPacketNumberBeforeEpoch { - forceNewEpoch = true - } - if m.aggregationEpochStartTime.IsZero() || forceNewEpoch { - m.aggregationEpochBytes = bytesAcked - m.aggregationEpochStartTime = ackTime - m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber - m.numAckAggregationEpochs++ - return 0 - } - - // Compute how many bytes are expected to be delivered, assuming max bandwidth - // is correct. - aggregationDelta := ackTime.Sub(m.aggregationEpochStartTime) - expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, aggregationDelta) - // Reset the current aggregation epoch as soon as the ack arrival rate is less - // than or equal to the max bandwidth. - if m.aggregationEpochBytes <= congestion.ByteCount(m.ackAggregationBandwidthThreshold*float64(expectedBytesAcked)) { - // Reset to start measuring a new aggregation epoch. - m.aggregationEpochBytes = bytesAcked - m.aggregationEpochStartTime = ackTime - m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber - m.numAckAggregationEpochs++ - return 0 - } - - m.aggregationEpochBytes += bytesAcked - - // Compute how many extra bytes were delivered vs max bandwidth. - extraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked - newEvent := extraAckedEvent{ - extraAcked: expectedBytesAcked, - bytesAcked: m.aggregationEpochBytes, - timeDelta: aggregationDelta, - } - m.maxAckHeightFilter.Update(newEvent, roundTripCount) - return extraBytesAcked -} - -func (m *maxAckHeightTracker) SetFilterWindowLength(length roundTripCount) { - m.maxAckHeightFilter.SetWindowLength(length) -} - -func (m *maxAckHeightTracker) Reset(newHeight congestion.ByteCount, newTime roundTripCount) { - newEvent := extraAckedEvent{ - extraAcked: newHeight, - round: newTime, - } - m.maxAckHeightFilter.Reset(newEvent, newTime) -} - -func (m *maxAckHeightTracker) SetAckAggregationBandwidthThreshold(threshold float64) { - m.ackAggregationBandwidthThreshold = threshold -} - -func (m *maxAckHeightTracker) SetStartNewAggregationEpochAfterFullRound(value bool) { - m.startNewAggregationEpochAfterFullRound = value -} - -func (m *maxAckHeightTracker) SetReduceExtraAckedOnBandwidthIncrease(value bool) { - m.reduceExtraAckedOnBandwidthIncrease = value -} - -func (m *maxAckHeightTracker) AckAggregationBandwidthThreshold() float64 { - return m.ackAggregationBandwidthThreshold -} - -func (m *maxAckHeightTracker) NumAckAggregationEpochs() uint64 { - return m.numAckAggregationEpochs -} - -// AckPoint represents a point on the ack line. -type ackPoint struct { - ackTime time.Time - totalBytesAcked congestion.ByteCount -} - -// RecentAckPoints maintains the most recent 2 ack points at distinct times. -type recentAckPoints struct { - ackPoints [2]ackPoint -} - -func (r *recentAckPoints) Update(ackTime time.Time, totalBytesAcked congestion.ByteCount) { - if ackTime.Before(r.ackPoints[1].ackTime) { - r.ackPoints[1].ackTime = ackTime - } else if ackTime.After(r.ackPoints[1].ackTime) { - r.ackPoints[0] = r.ackPoints[1] - r.ackPoints[1].ackTime = ackTime - } - - r.ackPoints[1].totalBytesAcked = totalBytesAcked -} - -func (r *recentAckPoints) Clear() { - r.ackPoints[0] = ackPoint{} - r.ackPoints[1] = ackPoint{} -} - -func (r *recentAckPoints) MostRecentPoint() *ackPoint { - return &r.ackPoints[1] -} - -func (r *recentAckPoints) LessRecentPoint() *ackPoint { - if r.ackPoints[0].totalBytesAcked != 0 { - return &r.ackPoints[0] - } - - return &r.ackPoints[1] -} - -// ConnectionStateOnSentPacket represents the information about a sent packet -// and the state of the connection at the moment the packet was sent, -// specifically the information about the most recently acknowledged packet at -// that moment. -type connectionStateOnSentPacket struct { - // Time at which the packet is sent. - sentTime time.Time - // Size of the packet. - size congestion.ByteCount - // The value of |totalBytesSentAtLastAckedPacket| at the time the - // packet was sent. - totalBytesSentAtLastAckedPacket congestion.ByteCount - // The value of |lastAckedPacketSentTime| at the time the packet was - // sent. - lastAckedPacketSentTime time.Time - // The value of |lastAckedPacketAckTime| at the time the packet was - // sent. - lastAckedPacketAckTime time.Time - // Send time states that are returned to the congestion controller when the - // packet is acked or lost. - sendTimeState sendTimeState -} - -// Snapshot constructor. Records the current state of the bandwidth -// sampler. -// |bytes_in_flight| is the bytes in flight right after the packet is sent. -func newConnectionStateOnSentPacket( - sentTime time.Time, - size congestion.ByteCount, - bytesInFlight congestion.ByteCount, - sampler *bandwidthSampler, -) *connectionStateOnSentPacket { - return &connectionStateOnSentPacket{ - sentTime: sentTime, - size: size, - totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket, - lastAckedPacketSentTime: sampler.lastAckedPacketSentTime, - lastAckedPacketAckTime: sampler.lastAckedPacketAckTime, - sendTimeState: *newSendTimeState( - sampler.isAppLimited, - sampler.totalBytesSent, - sampler.totalBytesAcked, - sampler.totalBytesLost, - bytesInFlight, - ), - } -} - -// BandwidthSampler keeps track of sent and acknowledged packets and outputs a -// bandwidth sample for every packet acknowledged. The samples are taken for -// individual packets, and are not filtered; the consumer has to filter the -// bandwidth samples itself. In certain cases, the sampler will locally severely -// underestimate the bandwidth, hence a maximum filter with a size of at least -// one RTT is recommended. -// -// This class bases its samples on the slope of two curves: the number of bytes -// sent over time, and the number of bytes acknowledged as received over time. -// It produces a sample of both slopes for every packet that gets acknowledged, -// based on a slope between two points on each of the corresponding curves. Note -// that due to the packet loss, the number of bytes on each curve might get -// further and further away from each other, meaning that it is not feasible to -// compare byte values coming from different curves with each other. -// -// The obvious points for measuring slope sample are the ones corresponding to -// the packet that was just acknowledged. Let us denote them as S_1 (point at -// which the current packet was sent) and A_1 (point at which the current packet -// was acknowledged). However, taking a slope requires two points on each line, -// so estimating bandwidth requires picking a packet in the past with respect to -// which the slope is measured. -// -// For that purpose, BandwidthSampler always keeps track of the most recently -// acknowledged packet, and records it together with every outgoing packet. -// When a packet gets acknowledged (A_1), it has not only information about when -// it itself was sent (S_1), but also the information about the latest -// acknowledged packet right before it was sent (S_0 and A_0). -// -// Based on that data, send and ack rate are estimated as: -// -// send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0)) -// ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0)) -// -// Here, the ack rate is intuitively the rate we want to treat as bandwidth. -// However, in certain cases (e.g. ack compression) the ack rate at a point may -// end up higher than the rate at which the data was originally sent, which is -// not indicative of the real bandwidth. Hence, we use the send rate as an upper -// bound, and the sample value is -// -// rate_sample = Min(send_rate, ack_rate) -// -// An important edge case handled by the sampler is tracking the app-limited -// samples. There are multiple meaning of "app-limited" used interchangeably, -// hence it is important to understand and to be able to distinguish between -// them. -// -// Meaning 1: connection state. The connection is said to be app-limited when -// there is no outstanding data to send. This means that certain bandwidth -// samples in the future would not be an accurate indication of the link -// capacity, and it is important to inform consumer about that. Whenever -// connection becomes app-limited, the sampler is notified via OnAppLimited() -// method. -// -// Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth -// sampler becomes notified about the connection being app-limited, it enters -// app-limited phase. In that phase, all *sent* packets are marked as -// app-limited. Note that the connection itself does not have to be -// app-limited during the app-limited phase, and in fact it will not be -// (otherwise how would it send packets?). The boolean flag below indicates -// whether the sampler is in that phase. -// -// Meaning 3: a flag on the sent packet and on the sample. If a sent packet is -// sent during the app-limited phase, the resulting sample related to the -// packet will be marked as app-limited. -// -// With the terminology issue out of the way, let us consider the question of -// what kind of situation it addresses. -// -// Consider a scenario where we first send packets 1 to 20 at a regular -// bandwidth, and then immediately run out of data. After a few seconds, we send -// packets 21 to 60, and only receive ack for 21 between sending packets 40 and -// 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0 -// we use to compute the slope is going to be packet 20, a few seconds apart -// from the current packet, hence the resulting estimate would be extremely low -// and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21, -// meaning that the bandwidth sample would exclude the quiescence. -// -// Based on the analysis of that scenario, we implement the following rule: once -// OnAppLimited() is called, all sent packets will produce app-limited samples -// up until an ack for a packet that was sent after OnAppLimited() was called. -// Note that while the scenario above is not the only scenario when the -// connection is app-limited, the approach works in other cases too. - -type congestionEventSample struct { - // The maximum bandwidth sample from all acked packets. - // QuicBandwidth::Zero() if no samples are available. - sampleMaxBandwidth Bandwidth - // Whether |sample_max_bandwidth| is from a app-limited sample. - sampleIsAppLimited bool - // The minimum rtt sample from all acked packets. - // QuicTime::Delta::Infinite() if no samples are available. - sampleRtt time.Duration - // For each packet p in acked packets, this is the max value of INFLIGHT(p), - // where INFLIGHT(p) is the number of bytes acked while p is inflight. - sampleMaxInflight congestion.ByteCount - // The send state of the largest packet in acked_packets, unless it is - // empty. If acked_packets is empty, it's the send state of the largest - // packet in lost_packets. - lastPacketSendState sendTimeState - // The number of extra bytes acked from this ack event, compared to what is - // expected from the flow's bandwidth. Larger value means more ack - // aggregation. - extraAcked congestion.ByteCount -} - -func newCongestionEventSample() *congestionEventSample { - return &congestionEventSample{ - sampleRtt: infRTT, - } -} - -type bandwidthSampler struct { - // The total number of congestion controlled bytes sent during the connection. - totalBytesSent congestion.ByteCount - - // The total number of congestion controlled bytes which were acknowledged. - totalBytesAcked congestion.ByteCount - - // The total number of congestion controlled bytes which were lost. - totalBytesLost congestion.ByteCount - - // The total number of congestion controlled bytes which have been neutered. - totalBytesNeutered congestion.ByteCount - - // The value of |total_bytes_sent_| at the time the last acknowledged packet - // was sent. Valid only when |last_acked_packet_sent_time_| is valid. - totalBytesSentAtLastAckedPacket congestion.ByteCount - - // The time at which the last acknowledged packet was sent. Set to - // QuicTime::Zero() if no valid timestamp is available. - lastAckedPacketSentTime time.Time - - // The time at which the most recent packet was acknowledged. - lastAckedPacketAckTime time.Time - - // The most recently sent packet. - lastSentPacket congestion.PacketNumber - - // The most recently acked packet. - lastAckedPacket congestion.PacketNumber - - // Indicates whether the bandwidth sampler is currently in an app-limited - // phase. - isAppLimited bool - - // The packet that will be acknowledged after this one will cause the sampler - // to exit the app-limited phase. - endOfAppLimitedPhase congestion.PacketNumber - - // Record of the connection state at the point where each packet in flight was - // sent, indexed by the packet number. - connectionStateMap *packetNumberIndexedQueue[connectionStateOnSentPacket] - - recentAckPoints recentAckPoints - a0Candidates RingBuffer[ackPoint] - - // Maximum number of tracked packets. - maxTrackedPackets congestion.ByteCount - - maxAckHeightTracker *maxAckHeightTracker - totalBytesAckedAfterLastAckEvent congestion.ByteCount - - // True if connection option 'BSAO' is set. - overestimateAvoidance bool - - // True if connection option 'BBRB' is set. - limitMaxAckHeightTrackerBySendRate bool -} - -func newBandwidthSampler(maxAckHeightTrackerWindowLength roundTripCount) *bandwidthSampler { - b := &bandwidthSampler{ - maxAckHeightTracker: newMaxAckHeightTracker(maxAckHeightTrackerWindowLength), - connectionStateMap: newPacketNumberIndexedQueue[connectionStateOnSentPacket](defaultConnectionStateMapQueueSize), - lastSentPacket: invalidPacketNumber, - lastAckedPacket: invalidPacketNumber, - endOfAppLimitedPhase: invalidPacketNumber, - } - - b.a0Candidates.Init(defaultCandidatesBufferSize) - - return b -} - -func (b *bandwidthSampler) MaxAckHeight() congestion.ByteCount { - return b.maxAckHeightTracker.Get() -} - -func (b *bandwidthSampler) NumAckAggregationEpochs() uint64 { - return b.maxAckHeightTracker.NumAckAggregationEpochs() -} - -func (b *bandwidthSampler) SetMaxAckHeightTrackerWindowLength(length roundTripCount) { - b.maxAckHeightTracker.SetFilterWindowLength(length) -} - -func (b *bandwidthSampler) ResetMaxAckHeightTracker(newHeight congestion.ByteCount, newTime roundTripCount) { - b.maxAckHeightTracker.Reset(newHeight, newTime) -} - -func (b *bandwidthSampler) SetStartNewAggregationEpochAfterFullRound(value bool) { - b.maxAckHeightTracker.SetStartNewAggregationEpochAfterFullRound(value) -} - -func (b *bandwidthSampler) SetLimitMaxAckHeightTrackerBySendRate(value bool) { - b.limitMaxAckHeightTrackerBySendRate = value -} - -func (b *bandwidthSampler) SetReduceExtraAckedOnBandwidthIncrease(value bool) { - b.maxAckHeightTracker.SetReduceExtraAckedOnBandwidthIncrease(value) -} - -func (b *bandwidthSampler) EnableOverestimateAvoidance() { - if b.overestimateAvoidance { - return - } - - b.overestimateAvoidance = true - b.maxAckHeightTracker.SetAckAggregationBandwidthThreshold(2.0) -} - -func (b *bandwidthSampler) IsOverestimateAvoidanceEnabled() bool { - return b.overestimateAvoidance -} - -func (b *bandwidthSampler) OnPacketSent( - sentTime time.Time, - packetNumber congestion.PacketNumber, - bytes congestion.ByteCount, - bytesInFlight congestion.ByteCount, - isRetransmittable bool, -) { - b.lastSentPacket = packetNumber - - if !isRetransmittable { - return - } - - b.totalBytesSent += bytes - - // If there are no packets in flight, the time at which the new transmission - // opens can be treated as the A_0 point for the purpose of bandwidth - // sampling. This underestimates bandwidth to some extent, and produces some - // artificially low samples for most packets in flight, but it provides with - // samples at important points where we would not have them otherwise, most - // importantly at the beginning of the connection. - if bytesInFlight == 0 { - b.lastAckedPacketAckTime = sentTime - if b.overestimateAvoidance { - b.recentAckPoints.Clear() - b.recentAckPoints.Update(sentTime, b.totalBytesAcked) - b.a0Candidates.Clear() - b.a0Candidates.PushBack(*b.recentAckPoints.MostRecentPoint()) - } - b.totalBytesSentAtLastAckedPacket = b.totalBytesSent - - // In this situation ack compression is not a concern, set send rate to - // effectively infinite. - b.lastAckedPacketSentTime = sentTime - } - - b.connectionStateMap.Emplace(packetNumber, newConnectionStateOnSentPacket( - sentTime, - bytes, - bytesInFlight+bytes, - b, - )) -} - -func (b *bandwidthSampler) OnCongestionEvent( - ackTime time.Time, - ackedPackets []congestion.AckedPacketInfo, - lostPackets []congestion.LostPacketInfo, - maxBandwidth Bandwidth, - estBandwidthUpperBound Bandwidth, - roundTripCount roundTripCount, -) congestionEventSample { - eventSample := newCongestionEventSample() - - var lastLostPacketSendState sendTimeState - - for _, p := range lostPackets { - sendState := b.OnPacketLost(p.PacketNumber, p.BytesLost) - if sendState.isValid { - lastLostPacketSendState = sendState - } - } - - if len(ackedPackets) == 0 { - // Only populate send state for a loss-only event. - eventSample.lastPacketSendState = lastLostPacketSendState - return *eventSample - } - - var lastAckedPacketSendState sendTimeState - var maxSendRate Bandwidth - - for _, p := range ackedPackets { - sample := b.onPacketAcknowledged(ackTime, p.PacketNumber) - if !sample.stateAtSend.isValid { - continue - } - - lastAckedPacketSendState = sample.stateAtSend - - if sample.rtt != 0 { - eventSample.sampleRtt = Min(eventSample.sampleRtt, sample.rtt) - } - if sample.bandwidth > eventSample.sampleMaxBandwidth { - eventSample.sampleMaxBandwidth = sample.bandwidth - eventSample.sampleIsAppLimited = sample.stateAtSend.isAppLimited - } - if sample.sendRate != infBandwidth { - maxSendRate = Max(maxSendRate, sample.sendRate) - } - inflightSample := b.totalBytesAcked - lastAckedPacketSendState.totalBytesAcked - if inflightSample > eventSample.sampleMaxInflight { - eventSample.sampleMaxInflight = inflightSample - } - } - - if !lastLostPacketSendState.isValid { - eventSample.lastPacketSendState = lastAckedPacketSendState - } else if !lastAckedPacketSendState.isValid { - eventSample.lastPacketSendState = lastLostPacketSendState - } else { - // If two packets are inflight and an alarm is armed to lose a packet and it - // wakes up late, then the first of two in flight packets could have been - // acknowledged before the wakeup, which re-evaluates loss detection, and - // could declare the later of the two lost. - if lostPackets[len(lostPackets)-1].PacketNumber > ackedPackets[len(ackedPackets)-1].PacketNumber { - eventSample.lastPacketSendState = lastLostPacketSendState - } else { - eventSample.lastPacketSendState = lastAckedPacketSendState - } - } - - isNewMaxBandwidth := eventSample.sampleMaxBandwidth > maxBandwidth - maxBandwidth = Max(maxBandwidth, eventSample.sampleMaxBandwidth) - if b.limitMaxAckHeightTrackerBySendRate { - maxBandwidth = Max(maxBandwidth, maxSendRate) - } - - eventSample.extraAcked = b.onAckEventEnd(Min(estBandwidthUpperBound, maxBandwidth), isNewMaxBandwidth, roundTripCount) - - return *eventSample -} - -func (b *bandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber, bytesLost congestion.ByteCount) (s sendTimeState) { - b.totalBytesLost += bytesLost - if sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber); sentPacketPointer != nil { - sentPacketToSendTimeState(sentPacketPointer, &s) - } - return s -} - -func (b *bandwidthSampler) OnPacketNeutered(packetNumber congestion.PacketNumber) { - b.connectionStateMap.Remove(packetNumber, func(sentPacket connectionStateOnSentPacket) { - b.totalBytesNeutered += sentPacket.size - }) -} - -func (b *bandwidthSampler) OnAppLimited() { - b.isAppLimited = true - b.endOfAppLimitedPhase = b.lastSentPacket -} - -func (b *bandwidthSampler) RemoveObsoletePackets(leastUnacked congestion.PacketNumber) { - // A packet can become obsolete when it is removed from QuicUnackedPacketMap's - // view of inflight before it is acked or marked as lost. For example, when - // QuicSentPacketManager::RetransmitCryptoPackets retransmits a crypto packet, - // the packet is removed from QuicUnackedPacketMap's inflight, but is not - // marked as acked or lost in the BandwidthSampler. - b.connectionStateMap.RemoveUpTo(leastUnacked) -} - -func (b *bandwidthSampler) TotalBytesSent() congestion.ByteCount { - return b.totalBytesSent -} - -func (b *bandwidthSampler) TotalBytesLost() congestion.ByteCount { - return b.totalBytesLost -} - -func (b *bandwidthSampler) TotalBytesAcked() congestion.ByteCount { - return b.totalBytesAcked -} - -func (b *bandwidthSampler) TotalBytesNeutered() congestion.ByteCount { - return b.totalBytesNeutered -} - -func (b *bandwidthSampler) IsAppLimited() bool { - return b.isAppLimited -} - -func (b *bandwidthSampler) EndOfAppLimitedPhase() congestion.PacketNumber { - return b.endOfAppLimitedPhase -} - -func (b *bandwidthSampler) max_ack_height() congestion.ByteCount { - return b.maxAckHeightTracker.Get() -} - -func (b *bandwidthSampler) chooseA0Point(totalBytesAcked congestion.ByteCount, a0 *ackPoint) bool { - if b.a0Candidates.Empty() { - return false - } - - if b.a0Candidates.Len() == 1 { - *a0 = *b.a0Candidates.Front() - return true - } - - for i := 1; i < b.a0Candidates.Len(); i++ { - if b.a0Candidates.Offset(i).totalBytesAcked > totalBytesAcked { - *a0 = *b.a0Candidates.Offset(i - 1) - if i > 1 { - for j := 0; j < i-1; j++ { - b.a0Candidates.PopFront() - } - } - return true - } - } - - *a0 = *b.a0Candidates.Back() - for k := 0; k < b.a0Candidates.Len()-1; k++ { - b.a0Candidates.PopFront() - } - return true -} - -func (b *bandwidthSampler) onPacketAcknowledged(ackTime time.Time, packetNumber congestion.PacketNumber) bandwidthSample { - sample := newBandwidthSample() - b.lastAckedPacket = packetNumber - sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber) - if sentPacketPointer == nil { - return *sample - } - - // OnPacketAcknowledgedInner - b.totalBytesAcked += sentPacketPointer.size - b.totalBytesSentAtLastAckedPacket = sentPacketPointer.sendTimeState.totalBytesSent - b.lastAckedPacketSentTime = sentPacketPointer.sentTime - b.lastAckedPacketAckTime = ackTime - if b.overestimateAvoidance { - b.recentAckPoints.Update(ackTime, b.totalBytesAcked) - } - - if b.isAppLimited { - // Exit app-limited phase in two cases: - // (1) end_of_app_limited_phase_ is not initialized, i.e., so far all - // packets are sent while there are buffered packets or pending data. - // (2) The current acked packet is after the sent packet marked as the end - // of the app limit phase. - if b.endOfAppLimitedPhase == invalidPacketNumber || - packetNumber > b.endOfAppLimitedPhase { - b.isAppLimited = false - } - } - - // There might have been no packets acknowledged at the moment when the - // current packet was sent. In that case, there is no bandwidth sample to - // make. - if sentPacketPointer.lastAckedPacketSentTime.IsZero() { - return *sample - } - - // Infinite rate indicates that the sampler is supposed to discard the - // current send rate sample and use only the ack rate. - sendRate := infBandwidth - if sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) { - sendRate = BandwidthFromDelta( - sentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket, - sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime)) - } - - var a0 ackPoint - if b.overestimateAvoidance && b.chooseA0Point(sentPacketPointer.sendTimeState.totalBytesAcked, &a0) { - } else { - a0.ackTime = sentPacketPointer.lastAckedPacketAckTime - a0.totalBytesAcked = sentPacketPointer.sendTimeState.totalBytesAcked - } - - // During the slope calculation, ensure that ack time of the current packet is - // always larger than the time of the previous packet, otherwise division by - // zero or integer underflow can occur. - if ackTime.Sub(a0.ackTime) <= 0 { - return *sample - } - - ackRate := BandwidthFromDelta(b.totalBytesAcked-a0.totalBytesAcked, ackTime.Sub(a0.ackTime)) - - sample.bandwidth = Min(sendRate, ackRate) - // Note: this sample does not account for delayed acknowledgement time. This - // means that the RTT measurements here can be artificially high, especially - // on low bandwidth connections. - sample.rtt = ackTime.Sub(sentPacketPointer.sentTime) - sample.sendRate = sendRate - sentPacketToSendTimeState(sentPacketPointer, &sample.stateAtSend) - - return *sample -} - -func (b *bandwidthSampler) onAckEventEnd( - bandwidthEstimate Bandwidth, - isNewMaxBandwidth bool, - roundTripCount roundTripCount, -) congestion.ByteCount { - newlyAckedBytes := b.totalBytesAcked - b.totalBytesAckedAfterLastAckEvent - if newlyAckedBytes == 0 { - return 0 - } - b.totalBytesAckedAfterLastAckEvent = b.totalBytesAcked - extraAcked := b.maxAckHeightTracker.Update( - bandwidthEstimate, - isNewMaxBandwidth, - roundTripCount, - b.lastSentPacket, - b.lastAckedPacket, - b.lastAckedPacketAckTime, - newlyAckedBytes) - // If |extra_acked| is zero, i.e. this ack event marks the start of a new ack - // aggregation epoch, save LessRecentPoint, which is the last ack point of the - // previous epoch, as a A0 candidate. - if b.overestimateAvoidance && extraAcked == 0 { - b.a0Candidates.PushBack(*b.recentAckPoints.LessRecentPoint()) - } - return extraAcked -} - -func sentPacketToSendTimeState(sentPacket *connectionStateOnSentPacket, sendTimeState *sendTimeState) { - *sendTimeState = sentPacket.sendTimeState - sendTimeState.isValid = true -} - -// BytesFromBandwidthAndTimeDelta calculates the bytes -// from a bandwidth(bits per second) and a time delta -func bytesFromBandwidthAndTimeDelta(bandwidth Bandwidth, delta time.Duration) congestion.ByteCount { - return (congestion.ByteCount(bandwidth) * congestion.ByteCount(delta)) / - (congestion.ByteCount(time.Second) * 8) -} - -func timeDeltaFromBytesAndBandwidth(bytes congestion.ByteCount, bandwidth Bandwidth) time.Duration { - return time.Duration(bytes*8) * time.Second / time.Duration(bandwidth) -} diff --git a/transport/tuic/congestion_v2/bbr_sender.go b/transport/tuic/congestion_v2/bbr_sender.go deleted file mode 100644 index d8852fbc96..0000000000 --- a/transport/tuic/congestion_v2/bbr_sender.go +++ /dev/null @@ -1,935 +0,0 @@ -package congestion - -// src from https://github.com/google/quiche/blob/e7872fc9e12bb1d46a118949c3d4da36de58aa44/quiche/quic/core/congestion_control/bbr_sender.cc - -import ( - "fmt" - "time" - - "github.com/metacubex/quic-go" - "github.com/metacubex/quic-go/congestion" - - "github.com/metacubex/randv2" -) - -// BbrSender implements BBR congestion control algorithm. BBR aims to estimate -// the current available Bottleneck Bandwidth and RTT (hence the name), and -// regulates the pacing rate and the size of the congestion window based on -// those signals. -// -// BBR relies on pacing in order to function properly. Do not use BBR when -// pacing is disabled. -// - -const ( - minBps = 65536 // 64 kbps - - invalidPacketNumber = -1 - initialCongestionWindowPackets = 32 - - // Constants based on TCP defaults. - // The minimum CWND to ensure delayed acks don't reduce bandwidth measurements. - // Does not inflate the pacing rate. - defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSize) - - // The gain used for the STARTUP, equal to 2/ln(2). - defaultHighGain = 2.885 - // The newly derived gain for STARTUP, equal to 4 * ln(2) - derivedHighGain = 2.773 - // The newly derived CWND gain for STARTUP, 2. - derivedHighCWNDGain = 2.0 -) - -// The cycle of gains used during the PROBE_BW stage. -var pacingGain = [...]float64{1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0} - -const ( - // The length of the gain cycle. - gainCycleLength = len(pacingGain) - // The size of the bandwidth filter window, in round-trips. - bandwidthWindowSize = gainCycleLength + 2 - - // The time after which the current min_rtt value expires. - minRttExpiry = 10 * time.Second - // The minimum time the connection can spend in PROBE_RTT mode. - probeRttTime = 200 * time.Millisecond - // If the bandwidth does not increase by the factor of |kStartupGrowthTarget| - // within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection - // will exit the STARTUP mode. - startupGrowthTarget = 1.25 - roundTripsWithoutGrowthBeforeExitingStartup = int64(3) - - // Flag. - defaultStartupFullLossCount = 8 - quicBbr2DefaultLossThreshold = 0.02 - maxBbrBurstPackets = 10 -) - -type bbrMode int - -const ( - // Startup phase of the connection. - bbrModeStartup = iota - // After achieving the highest possible bandwidth during the startup, lower - // the pacing rate in order to drain the queue. - bbrModeDrain - // Cruising mode. - bbrModeProbeBw - // Temporarily slow down sending in order to empty the buffer and measure - // the real minimum RTT. - bbrModeProbeRtt -) - -// Indicates how the congestion control limits the amount of bytes in flight. -type bbrRecoveryState int - -const ( - // Do not limit. - bbrRecoveryStateNotInRecovery = iota - // Allow an extra outstanding byte for each byte acknowledged. - bbrRecoveryStateConservation - // Allow two extra outstanding bytes for each byte acknowledged (slow - // start). - bbrRecoveryStateGrowth -) - -type bbrSender struct { - rttStats congestion.RTTStatsProvider - clock Clock - pacer *Pacer - - mode bbrMode - - // Bandwidth sampler provides BBR with the bandwidth measurements at - // individual points. - sampler *bandwidthSampler - - // The number of the round trips that have occurred during the connection. - roundTripCount roundTripCount - - // The packet number of the most recently sent packet. - lastSentPacket congestion.PacketNumber - // Acknowledgement of any packet after |current_round_trip_end_| will cause - // the round trip counter to advance. - currentRoundTripEnd congestion.PacketNumber - - // Number of congestion events with some losses, in the current round. - numLossEventsInRound uint64 - - // Number of total bytes lost in the current round. - bytesLostInRound congestion.ByteCount - - // The filter that tracks the maximum bandwidth over the multiple recent - // round-trips. - maxBandwidth *WindowedFilter[Bandwidth, roundTripCount] - - // Minimum RTT estimate. Automatically expires within 10 seconds (and - // triggers PROBE_RTT mode) if no new value is sampled during that period. - minRtt time.Duration - // The time at which the current value of |min_rtt_| was assigned. - minRttTimestamp time.Time - - // The maximum allowed number of bytes in flight. - congestionWindow congestion.ByteCount - - // The initial value of the |congestion_window_|. - initialCongestionWindow congestion.ByteCount - - // The largest value the |congestion_window_| can achieve. - maxCongestionWindow congestion.ByteCount - - // The smallest value the |congestion_window_| can achieve. - minCongestionWindow congestion.ByteCount - - // The pacing gain applied during the STARTUP phase. - highGain float64 - - // The CWND gain applied during the STARTUP phase. - highCwndGain float64 - - // The pacing gain applied during the DRAIN phase. - drainGain float64 - - // The current pacing rate of the connection. - pacingRate Bandwidth - - // The gain currently applied to the pacing rate. - pacingGain float64 - // The gain currently applied to the congestion window. - congestionWindowGain float64 - - // The gain used for the congestion window during PROBE_BW. Latched from - // quic_bbr_cwnd_gain flag. - congestionWindowGainConstant float64 - // The number of RTTs to stay in STARTUP mode. Defaults to 3. - numStartupRtts int64 - - // Number of round-trips in PROBE_BW mode, used for determining the current - // pacing gain cycle. - cycleCurrentOffset int - // The time at which the last pacing gain cycle was started. - lastCycleStart time.Time - - // Indicates whether the connection has reached the full bandwidth mode. - isAtFullBandwidth bool - // Number of rounds during which there was no significant bandwidth increase. - roundsWithoutBandwidthGain int64 - // The bandwidth compared to which the increase is measured. - bandwidthAtLastRound Bandwidth - - // Set to true upon exiting quiescence. - exitingQuiescence bool - - // Time at which PROBE_RTT has to be exited. Setting it to zero indicates - // that the time is yet unknown as the number of packets in flight has not - // reached the required value. - exitProbeRttAt time.Time - // Indicates whether a round-trip has passed since PROBE_RTT became active. - probeRttRoundPassed bool - - // Indicates whether the most recent bandwidth sample was marked as - // app-limited. - lastSampleIsAppLimited bool - // Indicates whether any non app-limited samples have been recorded. - hasNoAppLimitedSample bool - - // Current state of recovery. - recoveryState bbrRecoveryState - // Receiving acknowledgement of a packet after |end_recovery_at_| will cause - // BBR to exit the recovery mode. A value above zero indicates at least one - // loss has been detected, so it must not be set back to zero. - endRecoveryAt congestion.PacketNumber - // A window used to limit the number of bytes in flight during loss recovery. - recoveryWindow congestion.ByteCount - // If true, consider all samples in recovery app-limited. - isAppLimitedRecovery bool // not used - - // When true, pace at 1.5x and disable packet conservation in STARTUP. - slowerStartup bool // not used - // When true, disables packet conservation in STARTUP. - rateBasedStartup bool // not used - - // When true, add the most recent ack aggregation measurement during STARTUP. - enableAckAggregationDuringStartup bool - // When true, expire the windowed ack aggregation values in STARTUP when - // bandwidth increases more than 25%. - expireAckAggregationInStartup bool - - // If true, will not exit low gain mode until bytes_in_flight drops below BDP - // or it's time for high gain mode. - drainToTarget bool - - // If true, slow down pacing rate in STARTUP when overshooting is detected. - detectOvershooting bool - // Bytes lost while detect_overshooting_ is true. - bytesLostWhileDetectingOvershooting congestion.ByteCount - // Slow down pacing rate if - // bytes_lost_while_detecting_overshooting_ * - // bytes_lost_multiplier_while_detecting_overshooting_ > IW. - bytesLostMultiplierWhileDetectingOvershooting uint8 - // When overshooting is detected, do not drop pacing_rate_ below this value / - // min_rtt. - cwndToCalculateMinPacingRate congestion.ByteCount - - // Max congestion window when adjusting network parameters. - maxCongestionWindowWithNetworkParametersAdjusted congestion.ByteCount // not used - - // Params. - maxDatagramSize congestion.ByteCount - // Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()| - bytesInFlight congestion.ByteCount -} - -var _ congestion.CongestionControl = &bbrSender{} - -func NewBbrSender( - clock Clock, - initialMaxDatagramSize congestion.ByteCount, - initialCongestionWindowPackets congestion.ByteCount, -) *bbrSender { - return newBbrSender( - clock, - initialMaxDatagramSize, - initialCongestionWindowPackets*initialMaxDatagramSize, - congestion.MaxCongestionWindowPackets*initialMaxDatagramSize, - ) -} - -func newBbrSender( - clock Clock, - initialMaxDatagramSize, - initialCongestionWindow, - initialMaxCongestionWindow congestion.ByteCount, -) *bbrSender { - b := &bbrSender{ - clock: clock, - mode: bbrModeStartup, - sampler: newBandwidthSampler(roundTripCount(bandwidthWindowSize)), - lastSentPacket: invalidPacketNumber, - currentRoundTripEnd: invalidPacketNumber, - maxBandwidth: NewWindowedFilter(roundTripCount(bandwidthWindowSize), MaxFilter[Bandwidth]), - congestionWindow: initialCongestionWindow, - initialCongestionWindow: initialCongestionWindow, - maxCongestionWindow: initialMaxCongestionWindow, - minCongestionWindow: defaultMinimumCongestionWindow, - highGain: defaultHighGain, - highCwndGain: defaultHighGain, - drainGain: 1.0 / defaultHighGain, - pacingGain: 1.0, - congestionWindowGain: 1.0, - congestionWindowGainConstant: 2.0, - numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, - recoveryState: bbrRecoveryStateNotInRecovery, - endRecoveryAt: invalidPacketNumber, - recoveryWindow: initialMaxCongestionWindow, - bytesLostMultiplierWhileDetectingOvershooting: 2, - cwndToCalculateMinPacingRate: initialCongestionWindow, - maxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow, - maxDatagramSize: initialMaxDatagramSize, - } - b.pacer = NewPacer(b.bandwidthForPacer) - - /* - if b.tracer != nil { - b.lastState = logging.CongestionStateStartup - b.tracer.UpdatedCongestionState(logging.CongestionStateStartup) - } - */ - - b.enterStartupMode(b.clock.Now()) - b.setHighCwndGain(derivedHighCWNDGain) - - return b -} - -func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { - b.rttStats = provider -} - -// TimeUntilSend implements the SendAlgorithm interface. -func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time { - return b.pacer.TimeUntilSend() -} - -// HasPacingBudget implements the SendAlgorithm interface. -func (b *bbrSender) HasPacingBudget(now time.Time) bool { - return b.pacer.Budget(now) >= b.maxDatagramSize -} - -// OnPacketSent implements the SendAlgorithm interface. -func (b *bbrSender) OnPacketSent( - sentTime time.Time, - bytesInFlight congestion.ByteCount, - packetNumber congestion.PacketNumber, - bytes congestion.ByteCount, - isRetransmittable bool, -) { - b.pacer.SentPacket(sentTime, bytes) - - b.lastSentPacket = packetNumber - b.bytesInFlight = bytesInFlight - - if bytesInFlight == 0 { - b.exitingQuiescence = true - } - - b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) - - b.maybeAppLimited(bytesInFlight) -} - -// CanSend implements the SendAlgorithm interface. -func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool { - return bytesInFlight < b.GetCongestionWindow() -} - -// MaybeExitSlowStart implements the SendAlgorithm interface. -func (b *bbrSender) MaybeExitSlowStart() { - // Do nothing -} - -// OnPacketAcked implements the SendAlgorithm interface. -func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes, priorInFlight congestion.ByteCount, eventTime time.Time) { - // Do nothing. -} - -// OnPacketLost implements the SendAlgorithm interface. -func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { - // Do nothing. -} - -// OnRetransmissionTimeout implements the SendAlgorithm interface. -func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { - // Do nothing. -} - -// SetMaxDatagramSize implements the SendAlgorithm interface. -func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { - if s < b.maxDatagramSize { - panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) - } - cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow - b.maxDatagramSize = s - if cwndIsMinCwnd { - b.congestionWindow = b.minCongestionWindow - } - b.pacer.SetMaxDatagramSize(s) -} - -// InSlowStart implements the SendAlgorithmWithDebugInfos interface. -func (b *bbrSender) InSlowStart() bool { - return b.mode == bbrModeStartup -} - -// InRecovery implements the SendAlgorithmWithDebugInfos interface. -func (b *bbrSender) InRecovery() bool { - return b.recoveryState != bbrRecoveryStateNotInRecovery -} - -// GetCongestionWindow implements the SendAlgorithmWithDebugInfos interface. -func (b *bbrSender) GetCongestionWindow() congestion.ByteCount { - if b.mode == bbrModeProbeRtt { - return b.probeRttCongestionWindow() - } - - if b.InRecovery() { - return Min(b.congestionWindow, b.recoveryWindow) - } - - return b.congestionWindow -} - -func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { - // Do nothing. -} - -func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { - totalBytesAckedBefore := b.sampler.TotalBytesAcked() - totalBytesLostBefore := b.sampler.TotalBytesLost() - - var isRoundStart, minRttExpired bool - var excessAcked, bytesLost congestion.ByteCount - - // The send state of the largest packet in acked_packets, unless it is - // empty. If acked_packets is empty, it's the send state of the largest - // packet in lost_packets. - var lastPacketSendState sendTimeState - - // Update bytesInFlight - b.bytesInFlight = priorInFlight - for _, p := range ackedPackets { - b.bytesInFlight -= p.BytesAcked - } - for _, p := range lostPackets { - b.bytesInFlight -= p.BytesLost - } - - if len(ackedPackets) != 0 { - lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber - isRoundStart = b.updateRoundTripCounter(lastAckedPacket) - b.updateRecoveryState(lastAckedPacket, len(lostPackets) != 0, isRoundStart) - } - - sample := b.sampler.OnCongestionEvent(eventTime, - ackedPackets, lostPackets, b.maxBandwidth.GetBest(), infBandwidth, b.roundTripCount) - if sample.lastPacketSendState.isValid { - b.lastSampleIsAppLimited = sample.lastPacketSendState.isAppLimited - b.hasNoAppLimitedSample = b.hasNoAppLimitedSample || !b.lastSampleIsAppLimited - } - // Avoid updating |max_bandwidth_| if a) this is a loss-only event, or b) all - // packets in |acked_packets| did not generate valid samples. (e.g. ack of - // ack-only packets). In both cases, sampler_.total_bytes_acked() will not - // change. - if totalBytesAckedBefore != b.sampler.TotalBytesAcked() { - if !sample.sampleIsAppLimited || sample.sampleMaxBandwidth > b.maxBandwidth.GetBest() { - b.maxBandwidth.Update(sample.sampleMaxBandwidth, b.roundTripCount) - } - } - - if sample.sampleRtt != infRTT { - minRttExpired = b.maybeUpdateMinRtt(eventTime, sample.sampleRtt) - } - bytesLost = b.sampler.TotalBytesLost() - totalBytesLostBefore - - excessAcked = sample.extraAcked - lastPacketSendState = sample.lastPacketSendState - - if len(lostPackets) != 0 { - b.numLossEventsInRound++ - b.bytesLostInRound += bytesLost - } - - // Handle logic specific to PROBE_BW mode. - if b.mode == bbrModeProbeBw { - b.updateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) != 0) - } - - // Handle logic specific to STARTUP and DRAIN modes. - if isRoundStart && !b.isAtFullBandwidth { - b.checkIfFullBandwidthReached(&lastPacketSendState) - } - - b.maybeExitStartupOrDrain(eventTime) - - // Handle logic specific to PROBE_RTT. - b.maybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) - - // Calculate number of packets acked and lost. - bytesAcked := b.sampler.TotalBytesAcked() - totalBytesAckedBefore - - // After the model is updated, recalculate the pacing rate and congestion - // window. - b.calculatePacingRate(bytesLost) - b.calculateCongestionWindow(bytesAcked, excessAcked) - b.calculateRecoveryWindow(bytesAcked, bytesLost) - - // Cleanup internal state. - // This is where we clean up obsolete (acked or lost) packets from the bandwidth sampler. - // The "least unacked" should actually be FirstOutstanding, but since we are not passing - // that through OnCongestionEventEx, we will only do an estimate using acked/lost packets - // for now. Because of fast retransmission, they should differ by no more than 2 packets. - // (this is controlled by packetThreshold in quic-go's sentPacketHandler) - var leastUnacked congestion.PacketNumber - if len(ackedPackets) != 0 { - leastUnacked = ackedPackets[len(ackedPackets)-1].PacketNumber - 2 - } else { - leastUnacked = lostPackets[len(lostPackets)-1].PacketNumber + 1 - } - b.sampler.RemoveObsoletePackets(leastUnacked) - - if isRoundStart { - b.numLossEventsInRound = 0 - b.bytesLostInRound = 0 - } -} - -func (b *bbrSender) PacingRate() Bandwidth { - if b.pacingRate == 0 { - return Bandwidth(b.highGain * float64( - BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt()))) - } - - return b.pacingRate -} - -func (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool { - return b.hasNonAppLimitedSample() -} - -func (b *bbrSender) hasNonAppLimitedSample() bool { - return b.hasNoAppLimitedSample -} - -// Sets the pacing gain used in STARTUP. Must be greater than 1. -func (b *bbrSender) setHighGain(highGain float64) { - b.highGain = highGain - if b.mode == bbrModeStartup { - b.pacingGain = highGain - } -} - -// Sets the CWND gain used in STARTUP. Must be greater than 1. -func (b *bbrSender) setHighCwndGain(highCwndGain float64) { - b.highCwndGain = highCwndGain - if b.mode == bbrModeStartup { - b.congestionWindowGain = highCwndGain - } -} - -// Sets the gain used in DRAIN. Must be less than 1. -func (b *bbrSender) setDrainGain(drainGain float64) { - b.drainGain = drainGain -} - -// Get the current bandwidth estimate. Note that Bandwidth is in bits per second. -func (b *bbrSender) bandwidthEstimate() Bandwidth { - return b.maxBandwidth.GetBest() -} - -func (b *bbrSender) bandwidthForPacer() congestion.ByteCount { - bps := congestion.ByteCount(float64(b.bandwidthEstimate()) * b.congestionWindowGain / float64(BytesPerSecond)) - if bps < minBps { - // We need to make sure that the bandwidth value for pacer is never zero, - // otherwise it will go into an edge case where HasPacingBudget = false - // but TimeUntilSend is before, causing the quic-go send loop to go crazy and get stuck. - return minBps - } - return bps -} - -// Returns the current estimate of the RTT of the connection. Outside of the -// edge cases, this is minimum RTT. -func (b *bbrSender) getMinRtt() time.Duration { - if b.minRtt != 0 { - return b.minRtt - } - // min_rtt could be available if the handshake packet gets neutered then - // gets acknowledged. This could only happen for QUIC crypto where we do not - // drop keys. - minRtt := b.rttStats.MinRTT() - if minRtt == 0 { - return 100 * time.Millisecond - } else { - return minRtt - } -} - -// Computes the target congestion window using the specified gain. -func (b *bbrSender) getTargetCongestionWindow(gain float64) congestion.ByteCount { - bdp := bdpFromRttAndBandwidth(b.getMinRtt(), b.bandwidthEstimate()) - congestionWindow := congestion.ByteCount(gain * float64(bdp)) - - // BDP estimate will be zero if no bandwidth samples are available yet. - if congestionWindow == 0 { - congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow)) - } - - return Max(congestionWindow, b.minCongestionWindow) -} - -// The target congestion window during PROBE_RTT. -func (b *bbrSender) probeRttCongestionWindow() congestion.ByteCount { - return b.minCongestionWindow -} - -func (b *bbrSender) maybeUpdateMinRtt(now time.Time, sampleMinRtt time.Duration) bool { - // Do not expire min_rtt if none was ever available. - minRttExpired := b.minRtt != 0 && now.After(b.minRttTimestamp.Add(minRttExpiry)) - if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 { - b.minRtt = sampleMinRtt - b.minRttTimestamp = now - } - - return minRttExpired -} - -// Enters the STARTUP mode. -func (b *bbrSender) enterStartupMode(now time.Time) { - b.mode = bbrModeStartup - // b.maybeTraceStateChange(logging.CongestionStateStartup) - b.pacingGain = b.highGain - b.congestionWindowGain = b.highCwndGain -} - -// Enters the PROBE_BW mode. -func (b *bbrSender) enterProbeBandwidthMode(now time.Time) { - b.mode = bbrModeProbeBw - // b.maybeTraceStateChange(logging.CongestionStateProbeBw) - b.congestionWindowGain = b.congestionWindowGainConstant - - // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is - // excluded because in that case increased gain and decreased gain would not - // follow each other. - b.cycleCurrentOffset = int(randv2.Int32N(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1) - if b.cycleCurrentOffset >= 1 { - b.cycleCurrentOffset += 1 - } - - b.lastCycleStart = now - b.pacingGain = pacingGain[b.cycleCurrentOffset] -} - -// Updates the round-trip counter if a round-trip has passed. Returns true if -// the counter has been advanced. -func (b *bbrSender) updateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool { - if b.currentRoundTripEnd == invalidPacketNumber || lastAckedPacket > b.currentRoundTripEnd { - b.roundTripCount++ - b.currentRoundTripEnd = b.lastSentPacket - return true - } - return false -} - -// Updates the current gain used in PROBE_BW mode. -func (b *bbrSender) updateGainCyclePhase(now time.Time, priorInFlight congestion.ByteCount, hasLosses bool) { - // In most cases, the cycle is advanced after an RTT passes. - shouldAdvanceGainCycling := now.After(b.lastCycleStart.Add(b.getMinRtt())) - // If the pacing gain is above 1.0, the connection is trying to probe the - // bandwidth by increasing the number of bytes in flight to at least - // pacing_gain * BDP. Make sure that it actually reaches the target, as long - // as there are no losses suggesting that the buffers are not able to hold - // that much. - if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.getTargetCongestionWindow(b.pacingGain) { - shouldAdvanceGainCycling = false - } - - // If pacing gain is below 1.0, the connection is trying to drain the extra - // queue which could have been incurred by probing prior to it. If the number - // of bytes in flight falls down to the estimated BDP value earlier, conclude - // that the queue has been successfully drained and exit this cycle early. - if b.pacingGain < 1.0 && b.bytesInFlight <= b.getTargetCongestionWindow(1) { - shouldAdvanceGainCycling = true - } - - if shouldAdvanceGainCycling { - b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength - b.lastCycleStart = now - // Stay in low gain mode until the target BDP is hit. - // Low gain mode will be exited immediately when the target BDP is achieved. - if b.drainToTarget && b.pacingGain < 1 && - pacingGain[b.cycleCurrentOffset] == 1 && - b.bytesInFlight > b.getTargetCongestionWindow(1) { - return - } - b.pacingGain = pacingGain[b.cycleCurrentOffset] - } -} - -// Tracks for how many round-trips the bandwidth has not increased -// significantly. -func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeState) { - if b.lastSampleIsAppLimited { - return - } - - target := Bandwidth(float64(b.bandwidthAtLastRound) * startupGrowthTarget) - if b.bandwidthEstimate() >= target { - b.bandwidthAtLastRound = b.bandwidthEstimate() - b.roundsWithoutBandwidthGain = 0 - if b.expireAckAggregationInStartup { - // Expire old excess delivery measurements now that bandwidth increased. - b.sampler.ResetMaxAckHeightTracker(0, b.roundTripCount) - } - return - } - - b.roundsWithoutBandwidthGain++ - if b.roundsWithoutBandwidthGain >= b.numStartupRtts || - b.shouldExitStartupDueToLoss(lastPacketSendState) { - b.isAtFullBandwidth = true - } -} - -func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) { - congestionWindow := b.GetCongestionWindow() - if bytesInFlight >= congestionWindow { - return - } - availableBytes := congestionWindow - bytesInFlight - if availableBytes > maxBbrBurstPackets*b.maxDatagramSize { - b.sampler.OnAppLimited() - } -} - -// Transitions from STARTUP to DRAIN and from DRAIN to PROBE_BW if -// appropriate. -func (b *bbrSender) maybeExitStartupOrDrain(now time.Time) { - if b.mode == bbrModeStartup && b.isAtFullBandwidth { - b.mode = bbrModeDrain - // b.maybeTraceStateChange(logging.CongestionStateDrain) - b.pacingGain = b.drainGain - b.congestionWindowGain = b.highCwndGain - } - if b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) { - b.enterProbeBandwidthMode(now) - } -} - -// Decides whether to enter or exit PROBE_RTT. -func (b *bbrSender) maybeEnterOrExitProbeRtt(now time.Time, isRoundStart, minRttExpired bool) { - if minRttExpired && !b.exitingQuiescence && b.mode != bbrModeProbeRtt { - b.mode = bbrModeProbeRtt - // b.maybeTraceStateChange(logging.CongestionStateProbRtt) - b.pacingGain = 1.0 - // Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight| - // is at the target small value. - b.exitProbeRttAt = time.Time{} - } - - if b.mode == bbrModeProbeRtt { - b.sampler.OnAppLimited() - // b.maybeTraceStateChange(logging.CongestionStateApplicationLimited) - - if b.exitProbeRttAt.IsZero() { - // If the window has reached the appropriate size, schedule exiting - // PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but - // we allow an extra packet since QUIC checks CWND before sending a - // packet. - if b.bytesInFlight < b.probeRttCongestionWindow()+congestion.MaxPacketBufferSize { - b.exitProbeRttAt = now.Add(probeRttTime) - b.probeRttRoundPassed = false - } - } else { - if isRoundStart { - b.probeRttRoundPassed = true - } - if now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed { - b.minRttTimestamp = now - if !b.isAtFullBandwidth { - b.enterStartupMode(now) - } else { - b.enterProbeBandwidthMode(now) - } - } - } - } - - b.exitingQuiescence = false -} - -// Determines whether BBR needs to enter, exit or advance state of the -// recovery. -func (b *bbrSender) updateRecoveryState(lastAckedPacket congestion.PacketNumber, hasLosses, isRoundStart bool) { - // Disable recovery in startup, if loss-based exit is enabled. - if !b.isAtFullBandwidth { - return - } - - // Exit recovery when there are no losses for a round. - if hasLosses { - b.endRecoveryAt = b.lastSentPacket - } - - switch b.recoveryState { - case bbrRecoveryStateNotInRecovery: - if hasLosses { - b.recoveryState = bbrRecoveryStateConservation - // This will cause the |recovery_window_| to be set to the correct - // value in CalculateRecoveryWindow(). - b.recoveryWindow = 0 - // Since the conservation phase is meant to be lasting for a whole - // round, extend the current round as if it were started right now. - b.currentRoundTripEnd = b.lastSentPacket - } - case bbrRecoveryStateConservation: - if isRoundStart { - b.recoveryState = bbrRecoveryStateGrowth - } - fallthrough - case bbrRecoveryStateGrowth: - // Exit recovery if appropriate. - if !hasLosses && lastAckedPacket > b.endRecoveryAt { - b.recoveryState = bbrRecoveryStateNotInRecovery - } - } -} - -// Determines the appropriate pacing rate for the connection. -func (b *bbrSender) calculatePacingRate(bytesLost congestion.ByteCount) { - if b.bandwidthEstimate() == 0 { - return - } - - targetRate := Bandwidth(b.pacingGain * float64(b.bandwidthEstimate())) - if b.isAtFullBandwidth { - b.pacingRate = targetRate - return - } - - // Pace at the rate of initial_window / RTT as soon as RTT measurements are - // available. - if b.pacingRate == 0 && b.rttStats.MinRTT() != 0 { - b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT()) - return - } - - if b.detectOvershooting { - b.bytesLostWhileDetectingOvershooting += bytesLost - // Check for overshooting with network parameters adjusted when pacing rate - // > target_rate and loss has been detected. - if b.pacingRate > targetRate && b.bytesLostWhileDetectingOvershooting > 0 { - if b.hasNoAppLimitedSample || - b.bytesLostWhileDetectingOvershooting*congestion.ByteCount(b.bytesLostMultiplierWhileDetectingOvershooting) > b.initialCongestionWindow { - // We are fairly sure overshoot happens if 1) there is at least one - // non app-limited bw sample or 2) half of IW gets lost. Slow pacing - // rate. - b.pacingRate = Max(targetRate, BandwidthFromDelta(b.cwndToCalculateMinPacingRate, b.rttStats.MinRTT())) - b.bytesLostWhileDetectingOvershooting = 0 - b.detectOvershooting = false - } - } - } - - // Do not decrease the pacing rate during startup. - b.pacingRate = Max(b.pacingRate, targetRate) -} - -// Determines the appropriate congestion window for the connection. -func (b *bbrSender) calculateCongestionWindow(bytesAcked, excessAcked congestion.ByteCount) { - if b.mode == bbrModeProbeRtt { - return - } - - targetWindow := b.getTargetCongestionWindow(b.congestionWindowGain) - if b.isAtFullBandwidth { - // Add the max recently measured ack aggregation to CWND. - targetWindow += b.sampler.MaxAckHeight() - } else if b.enableAckAggregationDuringStartup { - // Add the most recent excess acked. Because CWND never decreases in - // STARTUP, this will automatically create a very localized max filter. - targetWindow += excessAcked - } - - // Instead of immediately setting the target CWND as the new one, BBR grows - // the CWND towards |target_window| by only increasing it |bytes_acked| at a - // time. - if b.isAtFullBandwidth { - b.congestionWindow = Min(targetWindow, b.congestionWindow+bytesAcked) - } else if b.congestionWindow < targetWindow || - b.sampler.TotalBytesAcked() < b.initialCongestionWindow { - // If the connection is not yet out of startup phase, do not decrease the - // window. - b.congestionWindow += bytesAcked - } - - // Enforce the limits on the congestion window. - b.congestionWindow = Max(b.congestionWindow, b.minCongestionWindow) - b.congestionWindow = Min(b.congestionWindow, b.maxCongestionWindow) -} - -// Determines the appropriate window that constrains the in-flight during recovery. -func (b *bbrSender) calculateRecoveryWindow(bytesAcked, bytesLost congestion.ByteCount) { - if b.recoveryState == bbrRecoveryStateNotInRecovery { - return - } - - // Set up the initial recovery window. - if b.recoveryWindow == 0 { - b.recoveryWindow = b.bytesInFlight + bytesAcked - b.recoveryWindow = Max(b.minCongestionWindow, b.recoveryWindow) - return - } - - // Remove losses from the recovery window, while accounting for a potential - // integer underflow. - if b.recoveryWindow >= bytesLost { - b.recoveryWindow = b.recoveryWindow - bytesLost - } else { - b.recoveryWindow = b.maxDatagramSize - } - - // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, - // release additional |bytes_acked| to achieve a slow-start-like behavior. - if b.recoveryState == bbrRecoveryStateGrowth { - b.recoveryWindow += bytesAcked - } - - // Always allow sending at least |bytes_acked| in response. - b.recoveryWindow = Max(b.recoveryWindow, b.bytesInFlight+bytesAcked) - b.recoveryWindow = Max(b.minCongestionWindow, b.recoveryWindow) -} - -// Return whether we should exit STARTUP due to excessive loss. -func (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeState) bool { - if b.numLossEventsInRound < defaultStartupFullLossCount || !lastPacketSendState.isValid { - return false - } - - inflightAtSend := lastPacketSendState.bytesInFlight - - if inflightAtSend > 0 && b.bytesLostInRound > 0 { - if b.bytesLostInRound > congestion.ByteCount(float64(inflightAtSend)*quicBbr2DefaultLossThreshold) { - return true - } - return false - } - return false -} - -func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount { - return congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second) -} - -func GetInitialPacketSize(quicConn quic.Connection) congestion.ByteCount { - return congestion.ByteCount(quicConn.Config().InitialPacketSize) -} diff --git a/transport/tuic/congestion_v2/clock.go b/transport/tuic/congestion_v2/clock.go deleted file mode 100644 index 405fae70f9..0000000000 --- a/transport/tuic/congestion_v2/clock.go +++ /dev/null @@ -1,18 +0,0 @@ -package congestion - -import "time" - -// A Clock returns the current time -type Clock interface { - Now() time.Time -} - -// DefaultClock implements the Clock interface using the Go stdlib clock. -type DefaultClock struct{} - -var _ Clock = DefaultClock{} - -// Now gets the current time -func (DefaultClock) Now() time.Time { - return time.Now() -} diff --git a/transport/tuic/congestion_v2/minmax_go120.go b/transport/tuic/congestion_v2/minmax_go120.go deleted file mode 100644 index 1266edbc48..0000000000 --- a/transport/tuic/congestion_v2/minmax_go120.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !go1.21 - -package congestion - -import "golang.org/x/exp/constraints" - -func Max[T constraints.Ordered](a, b T) T { - if a < b { - return b - } - return a -} - -func Min[T constraints.Ordered](a, b T) T { - if a < b { - return a - } - return b -} diff --git a/transport/tuic/congestion_v2/minmax_go121.go b/transport/tuic/congestion_v2/minmax_go121.go deleted file mode 100644 index 65b067265a..0000000000 --- a/transport/tuic/congestion_v2/minmax_go121.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build go1.21 - -package congestion - -import "cmp" - -func Max[T cmp.Ordered](a, b T) T { - return max(a, b) -} - -func Min[T cmp.Ordered](a, b T) T { - return min(a, b) -} diff --git a/transport/tuic/congestion_v2/pacer.go b/transport/tuic/congestion_v2/pacer.go deleted file mode 100644 index 174b3dbe3f..0000000000 --- a/transport/tuic/congestion_v2/pacer.go +++ /dev/null @@ -1,74 +0,0 @@ -package congestion - -import ( - "math" - "time" - - "github.com/metacubex/quic-go/congestion" -) - -const ( - maxBurstPackets = 10 -) - -// Pacer implements a token bucket pacing algorithm. -type Pacer struct { - budgetAtLastSent congestion.ByteCount - maxDatagramSize congestion.ByteCount - lastSentTime time.Time - getBandwidth func() congestion.ByteCount // in bytes/s -} - -func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer { - p := &Pacer{ - budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSize, - maxDatagramSize: congestion.InitialPacketSize, - getBandwidth: getBandwidth, - } - return p -} - -func (p *Pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) { - budget := p.Budget(sendTime) - if size > budget { - p.budgetAtLastSent = 0 - } else { - p.budgetAtLastSent = budget - size - } - p.lastSentTime = sendTime -} - -func (p *Pacer) Budget(now time.Time) congestion.ByteCount { - if p.lastSentTime.IsZero() { - return p.maxBurstSize() - } - budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 - if budget < 0 { // protect against overflows - budget = congestion.ByteCount(1<<62 - 1) - } - return Min(p.maxBurstSize(), budget) -} - -func (p *Pacer) maxBurstSize() congestion.ByteCount { - return Max( - congestion.ByteCount((congestion.MinPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9, - maxBurstPackets*p.maxDatagramSize, - ) -} - -// TimeUntilSend returns when the next packet should be sent. -// It returns the zero value of time.Time if a packet can be sent immediately. -func (p *Pacer) TimeUntilSend() time.Time { - if p.budgetAtLastSent >= p.maxDatagramSize { - return time.Time{} - } - return p.lastSentTime.Add(Max( - congestion.MinPacingDelay, - time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/ - float64(p.getBandwidth())))*time.Nanosecond, - )) -} - -func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) { - p.maxDatagramSize = s -} diff --git a/transport/tuic/congestion_v2/packet_number_indexed_queue.go b/transport/tuic/congestion_v2/packet_number_indexed_queue.go deleted file mode 100644 index 119d36f615..0000000000 --- a/transport/tuic/congestion_v2/packet_number_indexed_queue.go +++ /dev/null @@ -1,199 +0,0 @@ -package congestion - -import ( - "github.com/metacubex/quic-go/congestion" -) - -// packetNumberIndexedQueue is a queue of mostly continuous numbered entries -// which supports the following operations: -// - adding elements to the end of the queue, or at some point past the end -// - removing elements in any order -// - retrieving elements -// If all elements are inserted in order, all of the operations above are -// amortized O(1) time. -// -// Internally, the data structure is a deque where each element is marked as -// present or not. The deque starts at the lowest present index. Whenever an -// element is removed, it's marked as not present, and the front of the deque is -// cleared of elements that are not present. -// -// The tail of the queue is not cleared due to the assumption of entries being -// inserted in order, though removing all elements of the queue will return it -// to its initial state. -// -// Note that this data structure is inherently hazardous, since an addition of -// just two entries will cause it to consume all of the memory available. -// Because of that, it is not a general-purpose container and should not be used -// as one. - -type entryWrapper[T any] struct { - present bool - entry T -} - -type packetNumberIndexedQueue[T any] struct { - entries RingBuffer[entryWrapper[T]] - numberOfPresentEntries int - firstPacket congestion.PacketNumber -} - -func newPacketNumberIndexedQueue[T any](size int) *packetNumberIndexedQueue[T] { - q := &packetNumberIndexedQueue[T]{ - firstPacket: invalidPacketNumber, - } - - q.entries.Init(size) - - return q -} - -// Emplace inserts data associated |packet_number| into (or past) the end of the -// queue, filling up the missing intermediate entries as necessary. Returns -// true if the element has been inserted successfully, false if it was already -// in the queue or inserted out of order. -func (p *packetNumberIndexedQueue[T]) Emplace(packetNumber congestion.PacketNumber, entry *T) bool { - if packetNumber == invalidPacketNumber || entry == nil { - return false - } - - if p.IsEmpty() { - p.entries.PushBack(entryWrapper[T]{ - present: true, - entry: *entry, - }) - p.numberOfPresentEntries = 1 - p.firstPacket = packetNumber - return true - } - - // Do not allow insertion out-of-order. - if packetNumber <= p.LastPacket() { - return false - } - - // Handle potentially missing elements. - offset := int(packetNumber - p.FirstPacket()) - if gap := offset - p.entries.Len(); gap > 0 { - for i := 0; i < gap; i++ { - p.entries.PushBack(entryWrapper[T]{}) - } - } - - p.entries.PushBack(entryWrapper[T]{ - present: true, - entry: *entry, - }) - p.numberOfPresentEntries++ - return true -} - -// GetEntry Retrieve the entry associated with the packet number. Returns the pointer -// to the entry in case of success, or nullptr if the entry does not exist. -func (p *packetNumberIndexedQueue[T]) GetEntry(packetNumber congestion.PacketNumber) *T { - ew := p.getEntryWraper(packetNumber) - if ew == nil { - return nil - } - - return &ew.entry -} - -// Remove, Same as above, but if an entry is present in the queue, also call f(entry) -// before removing it. -func (p *packetNumberIndexedQueue[T]) Remove(packetNumber congestion.PacketNumber, f func(T)) bool { - ew := p.getEntryWraper(packetNumber) - if ew == nil { - return false - } - if f != nil { - f(ew.entry) - } - ew.present = false - p.numberOfPresentEntries-- - - if packetNumber == p.FirstPacket() { - p.clearup() - } - - return true -} - -// RemoveUpTo, but not including |packet_number|. -// Unused slots in the front are also removed, which means when the function -// returns, |first_packet()| can be larger than |packet_number|. -func (p *packetNumberIndexedQueue[T]) RemoveUpTo(packetNumber congestion.PacketNumber) { - for !p.entries.Empty() && - p.firstPacket != invalidPacketNumber && - p.firstPacket < packetNumber { - if p.entries.Front().present { - p.numberOfPresentEntries-- - } - p.entries.PopFront() - p.firstPacket++ - } - p.clearup() - - return -} - -// IsEmpty return if queue is empty. -func (p *packetNumberIndexedQueue[T]) IsEmpty() bool { - return p.numberOfPresentEntries == 0 -} - -// NumberOfPresentEntries returns the number of entries in the queue. -func (p *packetNumberIndexedQueue[T]) NumberOfPresentEntries() int { - return p.numberOfPresentEntries -} - -// EntrySlotsUsed returns the number of entries allocated in the underlying deque. This is -// proportional to the memory usage of the queue. -func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int { - return p.entries.Len() -} - -// LastPacket returns packet number of the first entry in the queue. -func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) { - return p.firstPacket -} - -// LastPacket returns packet number of the last entry ever inserted in the queue. Note that the -// entry in question may have already been removed. Zero if the queue is -// empty. -func (p *packetNumberIndexedQueue[T]) LastPacket() (packetNumber congestion.PacketNumber) { - if p.IsEmpty() { - return invalidPacketNumber - } - - return p.firstPacket + congestion.PacketNumber(p.entries.Len()-1) -} - -func (p *packetNumberIndexedQueue[T]) clearup() { - for !p.entries.Empty() && !p.entries.Front().present { - p.entries.PopFront() - p.firstPacket++ - } - if p.entries.Empty() { - p.firstPacket = invalidPacketNumber - } -} - -func (p *packetNumberIndexedQueue[T]) getEntryWraper(packetNumber congestion.PacketNumber) *entryWrapper[T] { - if packetNumber == invalidPacketNumber || - p.IsEmpty() || - packetNumber < p.firstPacket { - return nil - } - - offset := int(packetNumber - p.firstPacket) - if offset >= p.entries.Len() { - return nil - } - - ew := p.entries.Offset(offset) - if ew == nil || !ew.present { - return nil - } - - return ew -} diff --git a/transport/tuic/congestion_v2/ringbuffer.go b/transport/tuic/congestion_v2/ringbuffer.go deleted file mode 100644 index e110c00f77..0000000000 --- a/transport/tuic/congestion_v2/ringbuffer.go +++ /dev/null @@ -1,118 +0,0 @@ -package congestion - -// A RingBuffer is a ring buffer. -// It acts as a heap that doesn't cause any allocations. -type RingBuffer[T any] struct { - ring []T - headPos, tailPos int - full bool -} - -// Init preallocs a buffer with a certain size. -func (r *RingBuffer[T]) Init(size int) { - r.ring = make([]T, size) -} - -// Len returns the number of elements in the ring buffer. -func (r *RingBuffer[T]) Len() int { - if r.full { - return len(r.ring) - } - if r.tailPos >= r.headPos { - return r.tailPos - r.headPos - } - return r.tailPos - r.headPos + len(r.ring) -} - -// Empty says if the ring buffer is empty. -func (r *RingBuffer[T]) Empty() bool { - return !r.full && r.headPos == r.tailPos -} - -// PushBack adds a new element. -// If the ring buffer is full, its capacity is increased first. -func (r *RingBuffer[T]) PushBack(t T) { - if r.full || len(r.ring) == 0 { - r.grow() - } - r.ring[r.tailPos] = t - r.tailPos++ - if r.tailPos == len(r.ring) { - r.tailPos = 0 - } - if r.tailPos == r.headPos { - r.full = true - } -} - -// PopFront returns the next element. -// It must not be called when the buffer is empty, that means that -// callers might need to check if there are elements in the buffer first. -func (r *RingBuffer[T]) PopFront() T { - if r.Empty() { - panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue") - } - r.full = false - t := r.ring[r.headPos] - r.ring[r.headPos] = *new(T) - r.headPos++ - if r.headPos == len(r.ring) { - r.headPos = 0 - } - return t -} - -// Offset returns the offset element. -// It must not be called when the buffer is empty, that means that -// callers might need to check if there are elements in the buffer first -// and check if the index larger than buffer length. -func (r *RingBuffer[T]) Offset(index int) *T { - if r.Empty() || index >= r.Len() { - panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: offset from invalid index") - } - offset := (r.headPos + index) % len(r.ring) - return &r.ring[offset] -} - -// Front returns the front element. -// It must not be called when the buffer is empty, that means that -// callers might need to check if there are elements in the buffer first. -func (r *RingBuffer[T]) Front() *T { - if r.Empty() { - panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: front from an empty queue") - } - return &r.ring[r.headPos] -} - -// Back returns the back element. -// It must not be called when the buffer is empty, that means that -// callers might need to check if there are elements in the buffer first. -func (r *RingBuffer[T]) Back() *T { - if r.Empty() { - panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: back from an empty queue") - } - return r.Offset(r.Len() - 1) -} - -// Grow the maximum size of the queue. -// This method assume the queue is full. -func (r *RingBuffer[T]) grow() { - oldRing := r.ring - newSize := len(oldRing) * 2 - if newSize == 0 { - newSize = 1 - } - r.ring = make([]T, newSize) - headLen := copy(r.ring, oldRing[r.headPos:]) - copy(r.ring[headLen:], oldRing[:r.headPos]) - r.headPos, r.tailPos, r.full = 0, len(oldRing), false -} - -// Clear removes all elements. -func (r *RingBuffer[T]) Clear() { - var zeroValue T - for i := range r.ring { - r.ring[i] = zeroValue - } - r.headPos, r.tailPos, r.full = 0, 0, false -} diff --git a/transport/tuic/congestion_v2/windowed_filter.go b/transport/tuic/congestion_v2/windowed_filter.go deleted file mode 100644 index 2421b48b4f..0000000000 --- a/transport/tuic/congestion_v2/windowed_filter.go +++ /dev/null @@ -1,162 +0,0 @@ -package congestion - -import ( - "golang.org/x/exp/constraints" -) - -// Implements Kathleen Nichols' algorithm for tracking the minimum (or maximum) -// estimate of a stream of samples over some fixed time interval. (E.g., -// the minimum RTT over the past five minutes.) The algorithm keeps track of -// the best, second best, and third best min (or max) estimates, maintaining an -// invariant that the measurement time of the n'th best >= n-1'th best. - -// The algorithm works as follows. On a reset, all three estimates are set to -// the same sample. The second best estimate is then recorded in the second -// quarter of the window, and a third best estimate is recorded in the second -// half of the window, bounding the worst case error when the true min is -// monotonically increasing (or true max is monotonically decreasing) over the -// window. -// -// A new best sample replaces all three estimates, since the new best is lower -// (or higher) than everything else in the window and it is the most recent. -// The window thus effectively gets reset on every new min. The same property -// holds true for second best and third best estimates. Specifically, when a -// sample arrives that is better than the second best but not better than the -// best, it replaces the second and third best estimates but not the best -// estimate. Similarly, a sample that is better than the third best estimate -// but not the other estimates replaces only the third best estimate. -// -// Finally, when the best expires, it is replaced by the second best, which in -// turn is replaced by the third best. The newest sample replaces the third -// best. - -type WindowedFilterValue interface { - any -} - -type WindowedFilterTime interface { - constraints.Integer | constraints.Float -} - -type WindowedFilter[V WindowedFilterValue, T WindowedFilterTime] struct { - // Time length of window. - windowLength T - estimates []entry[V, T] - comparator func(V, V) int -} - -type entry[V WindowedFilterValue, T WindowedFilterTime] struct { - sample V - time T -} - -// Compares two values and returns true if the first is greater than or equal -// to the second. -func MaxFilter[O constraints.Ordered](a, b O) int { - if a > b { - return 1 - } else if a < b { - return -1 - } - return 0 -} - -// Compares two values and returns true if the first is less than or equal -// to the second. -func MinFilter[O constraints.Ordered](a, b O) int { - if a < b { - return 1 - } else if a > b { - return -1 - } - return 0 -} - -func NewWindowedFilter[V WindowedFilterValue, T WindowedFilterTime](windowLength T, comparator func(V, V) int) *WindowedFilter[V, T] { - return &WindowedFilter[V, T]{ - windowLength: windowLength, - estimates: make([]entry[V, T], 3, 3), - comparator: comparator, - } -} - -// Changes the window length. Does not update any current samples. -func (f *WindowedFilter[V, T]) SetWindowLength(windowLength T) { - f.windowLength = windowLength -} - -func (f *WindowedFilter[V, T]) GetBest() V { - return f.estimates[0].sample -} - -func (f *WindowedFilter[V, T]) GetSecondBest() V { - return f.estimates[1].sample -} - -func (f *WindowedFilter[V, T]) GetThirdBest() V { - return f.estimates[2].sample -} - -// Updates best estimates with |sample|, and expires and updates best -// estimates as necessary. -func (f *WindowedFilter[V, T]) Update(newSample V, newTime T) { - // Reset all estimates if they have not yet been initialized, if new sample - // is a new best, or if the newest recorded estimate is too old. - if f.comparator(f.estimates[0].sample, *new(V)) == 0 || - f.comparator(newSample, f.estimates[0].sample) >= 0 || - newTime-f.estimates[2].time > f.windowLength { - f.Reset(newSample, newTime) - return - } - - if f.comparator(newSample, f.estimates[1].sample) >= 0 { - f.estimates[1] = entry[V, T]{newSample, newTime} - f.estimates[2] = f.estimates[1] - } else if f.comparator(newSample, f.estimates[2].sample) >= 0 { - f.estimates[2] = entry[V, T]{newSample, newTime} - } - - // Expire and update estimates as necessary. - if newTime-f.estimates[0].time > f.windowLength { - // The best estimate hasn't been updated for an entire window, so promote - // second and third best estimates. - f.estimates[0] = f.estimates[1] - f.estimates[1] = f.estimates[2] - f.estimates[2] = entry[V, T]{newSample, newTime} - // Need to iterate one more time. Check if the new best estimate is - // outside the window as well, since it may also have been recorded a - // long time ago. Don't need to iterate once more since we cover that - // case at the beginning of the method. - if newTime-f.estimates[0].time > f.windowLength { - f.estimates[0] = f.estimates[1] - f.estimates[1] = f.estimates[2] - } - return - } - if f.comparator(f.estimates[1].sample, f.estimates[0].sample) == 0 && - newTime-f.estimates[1].time > f.windowLength/4 { - // A quarter of the window has passed without a better sample, so the - // second-best estimate is taken from the second quarter of the window. - f.estimates[1] = entry[V, T]{newSample, newTime} - f.estimates[2] = f.estimates[1] - return - } - - if f.comparator(f.estimates[2].sample, f.estimates[1].sample) == 0 && - newTime-f.estimates[2].time > f.windowLength/2 { - // We've passed a half of the window without a better estimate, so take - // a third-best estimate from the second half of the window. - f.estimates[2] = entry[V, T]{newSample, newTime} - } -} - -// Resets all estimates to new sample. -func (f *WindowedFilter[V, T]) Reset(newSample V, newTime T) { - f.estimates[2] = entry[V, T]{newSample, newTime} - f.estimates[1] = f.estimates[2] - f.estimates[0] = f.estimates[1] -} - -func (f *WindowedFilter[V, T]) Clear() { - f.estimates = make([]entry[V, T], 3, 3) -} diff --git a/transport/tuic/pool_client.go b/transport/tuic/pool_client.go deleted file mode 100644 index b4c319d819..0000000000 --- a/transport/tuic/pool_client.go +++ /dev/null @@ -1,212 +0,0 @@ -package tuic - -import ( - "context" - "errors" - "net" - "runtime" - "sync" - "time" - - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - - "github.com/metacubex/quic-go" - - list "github.com/bahlo/generic-list-go" -) - -type dialResult struct { - transport *quic.Transport - addr net.Addr - err error -} - -type PoolClient struct { - newClientOptionV4 *ClientOptionV4 - newClientOptionV5 *ClientOptionV5 - dialResultMap map[C.Dialer]dialResult - dialResultMutex *sync.Mutex - tcpClients *list.List[Client] - tcpClientsMutex *sync.Mutex - udpClients *list.List[Client] - udpClientsMutex *sync.Mutex -} - -func (t *PoolClient) DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.Conn, error) { - newDialFn := func(ctx context.Context, dialer C.Dialer) (transport *quic.Transport, addr net.Addr, err error) { - return t.dial(ctx, dialer, dialFn) - } - conn, err := t.getClient(false, dialer).DialContextWithDialer(ctx, metadata, dialer, newDialFn) - if errors.Is(err, TooManyOpenStreams) { - conn, err = t.newClient(false, dialer).DialContextWithDialer(ctx, metadata, dialer, newDialFn) - } - if err != nil { - return nil, err - } - return N.NewRefConn(conn, t), err -} - -func (t *PoolClient) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.PacketConn, error) { - newDialFn := func(ctx context.Context, dialer C.Dialer) (transport *quic.Transport, addr net.Addr, err error) { - return t.dial(ctx, dialer, dialFn) - } - pc, err := t.getClient(true, dialer).ListenPacketWithDialer(ctx, metadata, dialer, newDialFn) - if errors.Is(err, TooManyOpenStreams) { - pc, err = t.newClient(true, dialer).ListenPacketWithDialer(ctx, metadata, dialer, newDialFn) - } - if err != nil { - return nil, err - } - return N.NewRefPacketConn(pc, t), nil -} - -func (t *PoolClient) dial(ctx context.Context, dialer C.Dialer, dialFn DialFunc) (transport *quic.Transport, addr net.Addr, err error) { - t.dialResultMutex.Lock() - dr, ok := t.dialResultMap[dialer] - t.dialResultMutex.Unlock() - if ok { - return dr.transport, dr.addr, dr.err - } - - transport, addr, err = dialFn(ctx, dialer) - if err != nil { - return nil, nil, err - } - - if _, ok := transport.Conn.(*net.UDPConn); ok { // only cache the system's UDPConn - transport.SetSingleUse(false) // don't close transport in each dial - dr.transport, dr.addr, dr.err = transport, addr, err - - t.dialResultMutex.Lock() - t.dialResultMap[dialer] = dr - t.dialResultMutex.Unlock() - } - - return transport, addr, err -} - -func (t *PoolClient) forceClose() { - t.dialResultMutex.Lock() - defer t.dialResultMutex.Unlock() - for key := range t.dialResultMap { - transport := t.dialResultMap[key].transport - if transport != nil { - _ = transport.Close() - } - delete(t.dialResultMap, key) - } -} - -func (t *PoolClient) newClient(udp bool, dialer C.Dialer) (client Client) { - clients := t.tcpClients - clientsMutex := t.tcpClientsMutex - if udp { - clients = t.udpClients - clientsMutex = t.udpClientsMutex - } - - clientsMutex.Lock() - defer clientsMutex.Unlock() - - if t.newClientOptionV4 != nil { - client = NewClientV4(t.newClientOptionV4, udp, dialer) - } else { - client = NewClientV5(t.newClientOptionV5, udp, dialer) - } - - client.SetLastVisited(time.Now()) - - clients.PushFront(client) - return client -} - -func (t *PoolClient) getClient(udp bool, dialer C.Dialer) Client { - clients := t.tcpClients - clientsMutex := t.tcpClientsMutex - if udp { - clients = t.udpClients - clientsMutex = t.udpClientsMutex - } - var bestClient Client - - func() { - clientsMutex.Lock() - defer clientsMutex.Unlock() - for it := clients.Front(); it != nil; { - client := it.Value - if client == nil { - next := it.Next() - clients.Remove(it) - it = next - continue - } - if client.DialerRef() == dialer { - if bestClient == nil { - bestClient = client - } else { - if client.OpenStreams() < bestClient.OpenStreams() { - bestClient = client - } - } - } - it = it.Next() - } - }() - for it := clients.Front(); it != nil; { - client := it.Value - if client != bestClient && client.OpenStreams() == 0 && time.Now().Sub(client.LastVisited()) > 30*time.Minute { - client.Close() - next := it.Next() - clients.Remove(it) - it = next - continue - } - it = it.Next() - } - - if bestClient == nil { - return t.newClient(udp, dialer) - } else { - bestClient.SetLastVisited(time.Now()) - return bestClient - } -} - -func NewPoolClientV4(clientOption *ClientOptionV4) *PoolClient { - p := &PoolClient{ - dialResultMap: make(map[C.Dialer]dialResult), - dialResultMutex: &sync.Mutex{}, - tcpClients: list.New[Client](), - tcpClientsMutex: &sync.Mutex{}, - udpClients: list.New[Client](), - udpClientsMutex: &sync.Mutex{}, - } - newClientOption := *clientOption - p.newClientOptionV4 = &newClientOption - runtime.SetFinalizer(p, closeClientPool) - log.Debugln("New TuicV4 PoolClient at %p", p) - return p -} - -func NewPoolClientV5(clientOption *ClientOptionV5) *PoolClient { - p := &PoolClient{ - dialResultMap: make(map[C.Dialer]dialResult), - dialResultMutex: &sync.Mutex{}, - tcpClients: list.New[Client](), - tcpClientsMutex: &sync.Mutex{}, - udpClients: list.New[Client](), - udpClientsMutex: &sync.Mutex{}, - } - newClientOption := *clientOption - p.newClientOptionV5 = &newClientOption - runtime.SetFinalizer(p, closeClientPool) - log.Debugln("New TuicV5 PoolClient at %p", p) - return p -} - -func closeClientPool(client *PoolClient) { - log.Debugln("Close Tuic PoolClient at %p", client) - client.forceClose() -} diff --git a/transport/tuic/server.go b/transport/tuic/server.go deleted file mode 100644 index d31a8625d5..0000000000 --- a/transport/tuic/server.go +++ /dev/null @@ -1,238 +0,0 @@ -package tuic - -import ( - "bufio" - "context" - "net" - "time" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" - "github.com/metacubex/mihomo/transport/tuic/common" - v4 "github.com/metacubex/mihomo/transport/tuic/v4" - v5 "github.com/metacubex/mihomo/transport/tuic/v5" - - "github.com/gofrs/uuid/v5" - "github.com/metacubex/quic-go" -) - -type ServerOption struct { - HandleTcpFn func(conn net.Conn, addr socks5.Addr, additions ...inbound.Addition) error - HandleUdpFn func(addr socks5.Addr, packet C.UDPPacket, additions ...inbound.Addition) error - - TlsConfig *tlsC.Config - QuicConfig *quic.Config - Tokens [][32]byte // V4 special - Users map[[16]byte]string // V5 special - CongestionController string - AuthenticationTimeout time.Duration - MaxUdpRelayPacketSize int - CWND int -} - -type Server struct { - *ServerOption - optionV4 *v4.ServerOption - optionV5 *v5.ServerOption - listener *quic.EarlyListener -} - -func (s *Server) Serve() error { - for { - conn, err := s.listener.Accept(context.Background()) - if err != nil { - return err - } - common.SetCongestionController(conn, s.CongestionController, s.CWND) - h := &serverHandler{ - Server: s, - quicConn: conn, - uuid: utils.NewUUIDV4(), - } - if h.optionV4 != nil { - h.v4Handler = v4.NewServerHandler(h.optionV4, conn, h.uuid) - } - if h.optionV5 != nil { - h.v5Handler = v5.NewServerHandler(h.optionV5, conn, h.uuid) - } - go h.handle() - } -} - -func (s *Server) Close() error { - return s.listener.Close() -} - -type serverHandler struct { - *Server - quicConn quic.EarlyConnection - uuid uuid.UUID - - v4Handler common.ServerHandler - v5Handler common.ServerHandler -} - -func (s *serverHandler) handle() { - go func() { - _ = s.handleUniStream() - }() - go func() { - _ = s.handleStream() - }() - go func() { - _ = s.handleMessage() - }() - - <-s.quicConn.HandshakeComplete() - time.AfterFunc(s.AuthenticationTimeout, func() { - if s.v4Handler != nil { - if s.v4Handler.AuthOk() { - return - } - } - - if s.v5Handler != nil { - if s.v5Handler.AuthOk() { - return - } - } - - if s.v4Handler != nil { - s.v4Handler.HandleTimeout() - } - - if s.v5Handler != nil { - s.v5Handler.HandleTimeout() - } - }) -} - -func (s *serverHandler) handleMessage() (err error) { - for { - var message []byte - message, err = s.quicConn.ReceiveDatagram(context.Background()) - if err != nil { - return err - } - go func() (err error) { - if len(message) > 0 { - switch message[0] { - case v4.VER: - if s.v4Handler != nil { - return s.v4Handler.HandleMessage(message) - } - case v5.VER: - if s.v5Handler != nil { - return s.v5Handler.HandleMessage(message) - } - } - } - return - }() - } -} - -func (s *serverHandler) handleStream() (err error) { - for { - var quicStream quic.Stream - quicStream, err = s.quicConn.AcceptStream(context.Background()) - if err != nil { - return err - } - go func() (err error) { - stream := common.NewQuicStreamConn( - quicStream, - s.quicConn.LocalAddr(), - s.quicConn.RemoteAddr(), - nil, - ) - conn := N.NewBufferedConn(stream) - - verBytes, err := conn.Peek(1) - if err != nil { - _ = conn.Close() - return err - } - - switch verBytes[0] { - case v4.VER: - if s.v4Handler != nil { - return s.v4Handler.HandleStream(conn) - } - case v5.VER: - if s.v5Handler != nil { - return s.v5Handler.HandleStream(conn) - } - } - return - }() - } -} - -func (s *serverHandler) handleUniStream() (err error) { - for { - var stream quic.ReceiveStream - stream, err = s.quicConn.AcceptUniStream(context.Background()) - if err != nil { - return err - } - go func() (err error) { - defer func() { - stream.CancelRead(0) - }() - reader := bufio.NewReader(stream) - verBytes, err := reader.Peek(1) - if err != nil { - return err - } - - switch verBytes[0] { - case v4.VER: - if s.v4Handler != nil { - return s.v4Handler.HandleUniStream(reader) - } - case v5.VER: - if s.v5Handler != nil { - return s.v5Handler.HandleUniStream(reader) - } - } - return - }() - } -} - -func NewServer(option *ServerOption, pc net.PacketConn) (*Server, error) { - listener, err := quic.ListenEarly(pc, option.TlsConfig, option.QuicConfig) - if err != nil { - return nil, err - } - server := &Server{ - ServerOption: option, - listener: listener, - } - if len(option.Tokens) > 0 { - server.optionV4 = &v4.ServerOption{ - HandleTcpFn: option.HandleTcpFn, - HandleUdpFn: option.HandleUdpFn, - Tokens: option.Tokens, - MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize, - } - } - if len(option.Users) > 0 { - maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize - if maxUdpRelayPacketSize > MaxFragSizeV5 { - maxUdpRelayPacketSize = MaxFragSizeV5 - } - server.optionV5 = &v5.ServerOption{ - HandleTcpFn: option.HandleTcpFn, - HandleUdpFn: option.HandleUdpFn, - Users: option.Users, - MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize, - } - } - return server, nil -} diff --git a/transport/tuic/tuic.go b/transport/tuic/tuic.go deleted file mode 100644 index 02aaa3ad36..0000000000 --- a/transport/tuic/tuic.go +++ /dev/null @@ -1,40 +0,0 @@ -package tuic - -import ( - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/tuic/common" - v4 "github.com/metacubex/mihomo/transport/tuic/v4" - v5 "github.com/metacubex/mihomo/transport/tuic/v5" -) - -type ClientOptionV4 = v4.ClientOption -type ClientOptionV5 = v5.ClientOption - -type Client = common.Client - -func NewClientV4(clientOption *ClientOptionV4, udp bool, dialerRef C.Dialer) Client { - return v4.NewClient(clientOption, udp, dialerRef) -} - -func NewClientV5(clientOption *ClientOptionV5, udp bool, dialerRef C.Dialer) Client { - return v5.NewClient(clientOption, udp, dialerRef) -} - -type DialFunc = common.DialFunc - -var TooManyOpenStreams = common.TooManyOpenStreams - -const DefaultStreamReceiveWindow = common.DefaultStreamReceiveWindow -const DefaultConnectionReceiveWindow = common.DefaultConnectionReceiveWindow - -var GenTKN = v4.GenTKN -var PacketOverHeadV4 = v4.PacketOverHead -var PacketOverHeadV5 = v5.PacketOverHead -var MaxFragSizeV5 = v5.MaxFragSize - -type UdpRelayMode = common.UdpRelayMode - -const ( - QUIC = common.QUIC - NATIVE = common.NATIVE -) diff --git a/transport/tuic/v4/client.go b/transport/tuic/v4/client.go deleted file mode 100644 index b5b3291746..0000000000 --- a/transport/tuic/v4/client.go +++ /dev/null @@ -1,436 +0,0 @@ -package v4 - -import ( - "bufio" - "bytes" - "context" - "errors" - "net" - "runtime" - "sync" - "sync/atomic" - "time" - - atomic2 "github.com/metacubex/mihomo/common/atomic" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/tuic/common" - - "github.com/metacubex/quic-go" - "github.com/metacubex/randv2" - "github.com/puzpuzpuz/xsync/v3" -) - -type ClientOption struct { - TlsConfig *tlsC.Config - QuicConfig *quic.Config - Token [32]byte - UdpRelayMode common.UdpRelayMode - CongestionController string - ReduceRtt bool - RequestTimeout time.Duration - MaxUdpRelayPacketSize int - FastOpen bool - MaxOpenStreams int64 - CWND int -} - -type clientImpl struct { - *ClientOption - udp bool - - quicConn quic.Connection - connMutex sync.Mutex - - openStreams atomic.Int64 - closed atomic.Bool - - udpInputMap *xsync.MapOf[uint32, net.Conn] - - // only ready for PoolClient - dialerRef C.Dialer - lastVisited atomic2.TypedValue[time.Time] -} - -func (t *clientImpl) OpenStreams() int64 { - return t.openStreams.Load() -} - -func (t *clientImpl) DialerRef() C.Dialer { - return t.dialerRef -} - -func (t *clientImpl) LastVisited() time.Time { - return t.lastVisited.Load() -} - -func (t *clientImpl) SetLastVisited(last time.Time) { - t.lastVisited.Store(last) -} - -func (t *clientImpl) getQuicConn(ctx context.Context, dialer C.Dialer, dialFn common.DialFunc) (quic.Connection, error) { - t.connMutex.Lock() - defer t.connMutex.Unlock() - if t.quicConn != nil { - return t.quicConn, nil - } - transport, addr, err := dialFn(ctx, dialer) - if err != nil { - return nil, err - } - var quicConn quic.Connection - if t.ReduceRtt { - quicConn, err = transport.DialEarly(ctx, addr, t.TlsConfig, t.QuicConfig) - } else { - quicConn, err = transport.Dial(ctx, addr, t.TlsConfig, t.QuicConfig) - } - if err != nil { - return nil, err - } - - common.SetCongestionController(quicConn, t.CongestionController, t.CWND) - - go func() { - _ = t.sendAuthentication(quicConn) - }() - - if t.udp { - go func() { - switch t.UdpRelayMode { - case common.QUIC: - _ = t.handleUniStream(quicConn) - default: // native - _ = t.handleMessage(quicConn) - } - }() - } - - t.quicConn = quicConn - t.openStreams.Store(0) - return quicConn, nil -} - -func (t *clientImpl) sendAuthentication(quicConn quic.Connection) (err error) { - defer func() { - t.deferQuicConn(quicConn, err) - }() - stream, err := quicConn.OpenUniStream() - if err != nil { - return err - } - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - err = NewAuthenticate(t.Token).WriteTo(buf) - if err != nil { - return err - } - _, err = buf.WriteTo(stream) - if err != nil { - return err - } - err = stream.Close() - if err != nil { - return - } - return nil -} - -func (t *clientImpl) handleUniStream(quicConn quic.Connection) (err error) { - defer func() { - t.deferQuicConn(quicConn, err) - }() - for { - var stream quic.ReceiveStream - stream, err = quicConn.AcceptUniStream(context.Background()) - if err != nil { - return err - } - go func() (err error) { - var assocId uint32 - defer func() { - t.deferQuicConn(quicConn, err) - if err != nil && assocId != 0 { - if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { - if conn, ok := val.(net.Conn); ok { - _ = conn.Close() - } - } - } - stream.CancelRead(0) - }() - reader := bufio.NewReader(stream) - commandHead, err := ReadCommandHead(reader) - if err != nil { - return - } - switch commandHead.TYPE { - case PacketType: - var packet Packet - packet, err = ReadPacketWithHead(commandHead, reader) - if err != nil { - return - } - if t.udp && t.UdpRelayMode == common.QUIC { - assocId = packet.ASSOC_ID - if val, ok := t.udpInputMap.Load(assocId); ok { - if conn, ok := val.(net.Conn); ok { - writer := bufio.NewWriterSize(conn, packet.BytesLen()) - _ = packet.WriteTo(writer) - _ = writer.Flush() - } - } - } - } - return - }() - } -} - -func (t *clientImpl) handleMessage(quicConn quic.Connection) (err error) { - defer func() { - t.deferQuicConn(quicConn, err) - }() - for { - var message []byte - message, err = quicConn.ReceiveDatagram(context.Background()) - if err != nil { - return err - } - go func() (err error) { - var assocId uint32 - defer func() { - t.deferQuicConn(quicConn, err) - if err != nil && assocId != 0 { - if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { - if conn, ok := val.(net.Conn); ok { - _ = conn.Close() - } - } - } - }() - reader := bytes.NewBuffer(message) - commandHead, err := ReadCommandHead(reader) - if err != nil { - return - } - switch commandHead.TYPE { - case PacketType: - var packet Packet - packet, err = ReadPacketWithHead(commandHead, reader) - if err != nil { - return - } - if t.udp && t.UdpRelayMode == common.NATIVE { - assocId = packet.ASSOC_ID - if val, ok := t.udpInputMap.Load(assocId); ok { - if conn, ok := val.(net.Conn); ok { - _, _ = conn.Write(message) - } - } - } - } - return - }() - } -} - -func (t *clientImpl) deferQuicConn(quicConn quic.Connection, err error) { - var netError net.Error - if err != nil && errors.As(err, &netError) { - t.forceClose(quicConn, err) - } -} - -func (t *clientImpl) forceClose(quicConn quic.Connection, err error) { - t.connMutex.Lock() - defer t.connMutex.Unlock() - if quicConn == nil { - quicConn = t.quicConn - } - if quicConn != nil { - if quicConn == t.quicConn { - t.quicConn = nil - } - } - errStr := "" - if err != nil { - errStr = err.Error() - } - if quicConn != nil { - _ = quicConn.CloseWithError(ProtocolError, errStr) - } - udpInputMap := t.udpInputMap - udpInputMap.Range(func(key uint32, value net.Conn) bool { - conn := value - _ = conn.Close() - udpInputMap.Delete(key) - return true - }) -} - -func (t *clientImpl) Close() { - t.closed.Store(true) - if t.openStreams.Load() == 0 { - t.forceClose(nil, common.ClientClosed) - } -} - -func (t *clientImpl) DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn common.DialFunc) (net.Conn, error) { - quicConn, err := t.getQuicConn(ctx, dialer, dialFn) - if err != nil { - return nil, err - } - openStreams := t.openStreams.Add(1) - if openStreams >= t.MaxOpenStreams { - t.openStreams.Add(-1) - return nil, common.TooManyOpenStreams - } - stream, err := func() (stream net.Conn, err error) { - defer func() { - t.deferQuicConn(quicConn, err) - }() - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - err = NewConnect(NewAddress(metadata)).WriteTo(buf) - if err != nil { - return nil, err - } - quicStream, err := quicConn.OpenStream() - if err != nil { - return nil, err - } - stream = common.NewQuicStreamConn( - quicStream, - quicConn.LocalAddr(), - quicConn.RemoteAddr(), - func() { - time.AfterFunc(C.DefaultTCPTimeout, func() { - openStreams := t.openStreams.Add(-1) - if openStreams == 0 && t.closed.Load() { - t.forceClose(quicConn, common.ClientClosed) - } - }) - }, - ) - _, err = buf.WriteTo(stream) - if err != nil { - _ = stream.Close() - return nil, err - } - return stream, err - }() - if err != nil { - return nil, err - } - - bufConn := N.NewBufferedConn(stream) - response := func() error { - if t.RequestTimeout > 0 { - _ = bufConn.SetReadDeadline(time.Now().Add(t.RequestTimeout)) - } - response, err := ReadResponse(bufConn) - if err != nil { - _ = bufConn.Close() - return err - } - if response.IsFailed() { - _ = bufConn.Close() - return errors.New("connect failed") - } - _ = bufConn.SetReadDeadline(time.Time{}) - return nil - } - if t.FastOpen { - return N.NewEarlyConn(bufConn, response), nil - } - err = response() - if err != nil { - return nil, err - } - return bufConn, nil -} - -func (t *clientImpl) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn common.DialFunc) (net.PacketConn, error) { - quicConn, err := t.getQuicConn(ctx, dialer, dialFn) - if err != nil { - return nil, err - } - openStreams := t.openStreams.Add(1) - if openStreams >= t.MaxOpenStreams { - t.openStreams.Add(-1) - return nil, common.TooManyOpenStreams - } - - pipe1, pipe2 := N.Pipe() - var connId uint32 - for { - connId = randv2.Uint32() - _, loaded := t.udpInputMap.LoadOrStore(connId, pipe1) - if !loaded { - break - } - } - pc := &quicStreamPacketConn{ - connId: connId, - quicConn: quicConn, - inputConn: N.NewBufferedConn(pipe2), - udpRelayMode: t.UdpRelayMode, - maxUdpRelayPacketSize: t.MaxUdpRelayPacketSize, - deferQuicConnFn: t.deferQuicConn, - closeDeferFn: func() { - t.udpInputMap.Delete(connId) - time.AfterFunc(C.DefaultUDPTimeout, func() { - openStreams := t.openStreams.Add(-1) - if openStreams == 0 && t.closed.Load() { - t.forceClose(quicConn, common.ClientClosed) - } - }) - }, - } - return pc, nil -} - -type Client struct { - *clientImpl // use an independent pointer to let Finalizer can work no matter somewhere handle an influence in clientImpl inner -} - -func (t *Client) DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn common.DialFunc) (net.Conn, error) { - conn, err := t.clientImpl.DialContextWithDialer(ctx, metadata, dialer, dialFn) - if err != nil { - return nil, err - } - return N.NewRefConn(conn, t), err -} - -func (t *Client) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn common.DialFunc) (net.PacketConn, error) { - pc, err := t.clientImpl.ListenPacketWithDialer(ctx, metadata, dialer, dialFn) - if err != nil { - return nil, err - } - return N.NewRefPacketConn(pc, t), nil -} - -func (t *Client) forceClose() { - t.clientImpl.forceClose(nil, common.ClientClosed) -} - -func NewClient(clientOption *ClientOption, udp bool, dialerRef C.Dialer) *Client { - ci := &clientImpl{ - ClientOption: clientOption, - udp: udp, - dialerRef: dialerRef, - udpInputMap: xsync.NewMapOf[uint32, net.Conn](), - } - c := &Client{ci} - runtime.SetFinalizer(c, closeClient) - log.Debugln("New TuicV4 Client at %p", c) - return c -} - -func closeClient(client *Client) { - log.Debugln("Close TuicV4 Client at %p", client) - client.forceClose() -} diff --git a/transport/tuic/v4/packet.go b/transport/tuic/v4/packet.go deleted file mode 100644 index 47484e780c..0000000000 --- a/transport/tuic/v4/packet.go +++ /dev/null @@ -1,178 +0,0 @@ -package v4 - -import ( - "net" - "sync" - "time" - - "github.com/metacubex/mihomo/common/atomic" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/transport/tuic/common" - - "github.com/metacubex/quic-go" -) - -type quicStreamPacketConn struct { - connId uint32 - quicConn quic.Connection - inputConn *N.BufferedConn - - udpRelayMode common.UdpRelayMode - maxUdpRelayPacketSize int - - deferQuicConnFn func(quicConn quic.Connection, err error) - closeDeferFn func() - writeClosed *atomic.Bool - - closeOnce sync.Once - closeErr error - closed bool -} - -func (q *quicStreamPacketConn) Close() error { - q.closeOnce.Do(func() { - q.closed = true - q.closeErr = q.close() - }) - return q.closeErr -} - -func (q *quicStreamPacketConn) close() (err error) { - if q.closeDeferFn != nil { - defer q.closeDeferFn() - } - if q.deferQuicConnFn != nil { - defer func() { - q.deferQuicConnFn(q.quicConn, err) - }() - } - if q.inputConn != nil { - _ = q.inputConn.Close() - q.inputConn = nil - - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - err = NewDissociate(q.connId).WriteTo(buf) - if err != nil { - return - } - var stream quic.SendStream - stream, err = q.quicConn.OpenUniStream() - if err != nil { - return - } - _, err = buf.WriteTo(stream) - if err != nil { - return - } - err = stream.Close() - if err != nil { - return - } - } - return -} - -func (q *quicStreamPacketConn) SetDeadline(t time.Time) error { - //TODO implement me - return nil -} - -func (q *quicStreamPacketConn) SetReadDeadline(t time.Time) error { - if q.inputConn != nil { - return q.inputConn.SetReadDeadline(t) - } - return nil -} - -func (q *quicStreamPacketConn) SetWriteDeadline(t time.Time) error { - //TODO implement me - return nil -} - -func (q *quicStreamPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - if q.inputConn != nil { - var packet Packet - packet, err = ReadPacket(q.inputConn) - if err != nil { - return - } - n = copy(p, packet.DATA) - addr = packet.ADDR.UDPAddr() - } else { - err = net.ErrClosed - } - return -} - -func (q *quicStreamPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - if q.inputConn != nil { - var packet Packet - packet, err = ReadPacket(q.inputConn) - if err != nil { - return - } - data = packet.DATA - addr = packet.ADDR.UDPAddr() - } else { - err = net.ErrClosed - } - return -} - -func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - if q.udpRelayMode != common.QUIC && len(p) > q.maxUdpRelayPacketSize { - return 0, &quic.DatagramTooLargeError{MaxDatagramPayloadSize: int64(q.maxUdpRelayPacketSize)} - } - if q.closed { - return 0, net.ErrClosed - } - if q.writeClosed != nil && q.writeClosed.Load() { - _ = q.Close() - return 0, net.ErrClosed - } - if q.deferQuicConnFn != nil { - defer func() { - q.deferQuicConnFn(q.quicConn, err) - }() - } - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - address, err := NewAddressNetAddr(addr) - if err != nil { - return - } - err = NewPacket(q.connId, uint16(len(p)), address, p).WriteTo(buf) - if err != nil { - return - } - switch q.udpRelayMode { - case common.QUIC: - var stream quic.SendStream - stream, err = q.quicConn.OpenUniStream() - if err != nil { - return - } - defer stream.Close() - _, err = buf.WriteTo(stream) - if err != nil { - return - } - default: // native - data := buf.Bytes() - err = q.quicConn.SendDatagram(data) - if err != nil { - return - } - } - n = len(p) - - return -} - -func (q *quicStreamPacketConn) LocalAddr() net.Addr { - return q.quicConn.LocalAddr() -} - -var _ net.PacketConn = (*quicStreamPacketConn)(nil) diff --git a/transport/tuic/v4/protocol.go b/transport/tuic/v4/protocol.go deleted file mode 100644 index cb04fad595..0000000000 --- a/transport/tuic/v4/protocol.go +++ /dev/null @@ -1,598 +0,0 @@ -package v4 - -import ( - "encoding/binary" - "fmt" - "io" - "net" - "net/netip" - "strconv" - - "github.com/metacubex/quic-go" - "lukechampine.com/blake3" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" -) - -type BufferedReader interface { - io.Reader - io.ByteReader -} - -type BufferedWriter interface { - io.Writer - io.ByteWriter -} - -type CommandType byte - -const ( - AuthenticateType = CommandType(0x00) - ConnectType = CommandType(0x01) - PacketType = CommandType(0x02) - DissociateType = CommandType(0x03) - HeartbeatType = CommandType(0x04) - ResponseType = CommandType(0xff) -) - -const VER byte = 0x04 - -func (c CommandType) String() string { - switch c { - case AuthenticateType: - return "Authenticate" - case ConnectType: - return "Connect" - case PacketType: - return "Packet" - case DissociateType: - return "Dissociate" - case HeartbeatType: - return "Heartbeat" - case ResponseType: - return "Response" - default: - return fmt.Sprintf("UnknowCommand: %#x", byte(c)) - } -} - -func (c CommandType) BytesLen() int { - return 1 -} - -type CommandHead struct { - VER byte - TYPE CommandType -} - -func NewCommandHead(TYPE CommandType) CommandHead { - return CommandHead{ - VER: VER, - TYPE: TYPE, - } -} - -func ReadCommandHead(reader BufferedReader) (c CommandHead, err error) { - c.VER, err = reader.ReadByte() - if err != nil { - return - } - TYPE, err := reader.ReadByte() - if err != nil { - return - } - c.TYPE = CommandType(TYPE) - return -} - -func (c CommandHead) WriteTo(writer BufferedWriter) (err error) { - err = writer.WriteByte(c.VER) - if err != nil { - return - } - err = writer.WriteByte(byte(c.TYPE)) - if err != nil { - return - } - return -} - -func (c CommandHead) BytesLen() int { - return 1 + c.TYPE.BytesLen() -} - -type Authenticate struct { - CommandHead - TKN [32]byte -} - -func NewAuthenticate(TKN [32]byte) Authenticate { - return Authenticate{ - CommandHead: NewCommandHead(AuthenticateType), - TKN: TKN, - } -} - -func ReadAuthenticateWithHead(head CommandHead, reader BufferedReader) (c Authenticate, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != AuthenticateType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - _, err = io.ReadFull(reader, c.TKN[:]) - if err != nil { - return - } - return -} - -func ReadAuthenticate(reader BufferedReader) (c Authenticate, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadAuthenticateWithHead(head, reader) -} - -func GenTKN(token string) [32]byte { - return blake3.Sum256([]byte(token)) -} - -func (c Authenticate) WriteTo(writer BufferedWriter) (err error) { - err = c.CommandHead.WriteTo(writer) - if err != nil { - return - } - _, err = writer.Write(c.TKN[:]) - if err != nil { - return - } - return -} - -func (c Authenticate) BytesLen() int { - return c.CommandHead.BytesLen() + 32 -} - -type Connect struct { - CommandHead - ADDR Address -} - -func NewConnect(ADDR Address) Connect { - return Connect{ - CommandHead: NewCommandHead(ConnectType), - ADDR: ADDR, - } -} - -func ReadConnectWithHead(head CommandHead, reader BufferedReader) (c Connect, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != ConnectType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - c.ADDR, err = ReadAddress(reader) - if err != nil { - return - } - return -} - -func ReadConnect(reader BufferedReader) (c Connect, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadConnectWithHead(head, reader) -} - -func (c Connect) WriteTo(writer BufferedWriter) (err error) { - err = c.CommandHead.WriteTo(writer) - if err != nil { - return - } - err = c.ADDR.WriteTo(writer) - if err != nil { - return - } - return -} - -func (c Connect) BytesLen() int { - return c.CommandHead.BytesLen() + c.ADDR.BytesLen() -} - -type Packet struct { - CommandHead - ASSOC_ID uint32 - LEN uint16 - ADDR Address - DATA []byte -} - -func NewPacket(ASSOC_ID uint32, LEN uint16, ADDR Address, DATA []byte) Packet { - return Packet{ - CommandHead: NewCommandHead(PacketType), - ASSOC_ID: ASSOC_ID, - LEN: LEN, - ADDR: ADDR, - DATA: DATA, - } -} - -func ReadPacketWithHead(head CommandHead, reader BufferedReader) (c Packet, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != PacketType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) - if err != nil { - return - } - err = binary.Read(reader, binary.BigEndian, &c.LEN) - if err != nil { - return - } - c.ADDR, err = ReadAddress(reader) - if err != nil { - return - } - c.DATA = make([]byte, c.LEN) - _, err = io.ReadFull(reader, c.DATA) - if err != nil { - return - } - return -} - -func ReadPacket(reader BufferedReader) (c Packet, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadPacketWithHead(head, reader) -} - -func (c Packet) WriteTo(writer BufferedWriter) (err error) { - err = c.CommandHead.WriteTo(writer) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.LEN) - if err != nil { - return - } - err = c.ADDR.WriteTo(writer) - if err != nil { - return - } - _, err = writer.Write(c.DATA) - if err != nil { - return - } - return -} - -func (c Packet) BytesLen() int { - return c.CommandHead.BytesLen() + 4 + 2 + c.ADDR.BytesLen() + len(c.DATA) -} - -var PacketOverHead = NewPacket(0, 0, NewAddressAddrPort(netip.AddrPortFrom(netip.IPv6Unspecified(), 0)), nil).BytesLen() - -type Dissociate struct { - CommandHead - ASSOC_ID uint32 -} - -func NewDissociate(ASSOC_ID uint32) Dissociate { - return Dissociate{ - CommandHead: NewCommandHead(DissociateType), - ASSOC_ID: ASSOC_ID, - } -} - -func ReadDissociateWithHead(head CommandHead, reader BufferedReader) (c Dissociate, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != DissociateType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) - if err != nil { - return - } - return -} - -func ReadDissociate(reader BufferedReader) (c Dissociate, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadDissociateWithHead(head, reader) -} - -func (c Dissociate) WriteTo(writer BufferedWriter) (err error) { - err = c.CommandHead.WriteTo(writer) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) - if err != nil { - return - } - return -} - -func (c Dissociate) BytesLen() int { - return c.CommandHead.BytesLen() + 4 -} - -type Heartbeat struct { - CommandHead -} - -func NewHeartbeat() Heartbeat { - return Heartbeat{ - CommandHead: NewCommandHead(HeartbeatType), - } -} - -func ReadHeartbeatWithHead(head CommandHead, reader BufferedReader) (c Heartbeat, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != HeartbeatType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - return -} - -func ReadHeartbeat(reader BufferedReader) (c Heartbeat, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadHeartbeatWithHead(head, reader) -} - -type Response struct { - CommandHead - REP byte -} - -func NewResponse(REP byte) Response { - return Response{ - CommandHead: NewCommandHead(ResponseType), - REP: REP, - } -} - -func NewResponseSucceed() Response { - return NewResponse(0x00) -} - -func NewResponseFailed() Response { - return NewResponse(0xff) -} - -func ReadResponseWithHead(head CommandHead, reader BufferedReader) (c Response, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != ResponseType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - c.REP, err = reader.ReadByte() - if err != nil { - return - } - return -} - -func ReadResponse(reader BufferedReader) (c Response, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadResponseWithHead(head, reader) -} - -func (c Response) WriteTo(writer BufferedWriter) (err error) { - err = c.CommandHead.WriteTo(writer) - if err != nil { - return - } - err = writer.WriteByte(c.REP) - if err != nil { - return - } - return -} - -func (c Response) IsSucceed() bool { - return c.REP == 0x00 -} - -func (c Response) IsFailed() bool { - return c.REP == 0xff -} - -func (c Response) BytesLen() int { - return c.CommandHead.BytesLen() + 1 -} - -// Addr types -const ( - AtypDomainName byte = 0 - AtypIPv4 byte = 1 - AtypIPv6 byte = 2 -) - -type Address struct { - TYPE byte - ADDR []byte - PORT uint16 -} - -func NewAddress(metadata *C.Metadata) Address { - var addrType byte - var addr []byte - switch metadata.AddrType() { - case C.AtypIPv4: - addrType = AtypIPv4 - addr = metadata.DstIP.AsSlice() - case C.AtypIPv6: - addrType = AtypIPv6 - addr = metadata.DstIP.AsSlice() - case C.AtypDomainName: - addrType = AtypDomainName - addr = make([]byte, len(metadata.Host)+1) - addr[0] = byte(len(metadata.Host)) - copy(addr[1:], metadata.Host) - } - - return Address{ - TYPE: addrType, - ADDR: addr, - PORT: metadata.DstPort, - } -} - -func NewAddressNetAddr(addr net.Addr) (Address, error) { - if addr, ok := addr.(interface{ AddrPort() netip.AddrPort }); ok { - if addrPort := addr.AddrPort(); addrPort.IsValid() { // sing's M.Socksaddr maybe return an invalid AddrPort if it's a DomainName - return NewAddressAddrPort(addrPort), nil - } - } - addrStr := addr.String() - if addrPort, err := netip.ParseAddrPort(addrStr); err == nil { - return NewAddressAddrPort(addrPort), nil - } - metadata := &C.Metadata{} - if err := metadata.SetRemoteAddress(addrStr); err != nil { - return Address{}, err - } - return NewAddress(metadata), nil -} - -func NewAddressAddrPort(addrPort netip.AddrPort) Address { - var addrType byte - port := addrPort.Port() - addr := addrPort.Addr().Unmap() - if addr.Is4() { - addrType = AtypIPv4 - } else { - addrType = AtypIPv6 - } - return Address{ - TYPE: addrType, - ADDR: addr.AsSlice(), - PORT: port, - } -} - -func ReadAddress(reader BufferedReader) (c Address, err error) { - c.TYPE, err = reader.ReadByte() - if err != nil { - return - } - switch c.TYPE { - case AtypIPv4: - c.ADDR = make([]byte, net.IPv4len) - _, err = io.ReadFull(reader, c.ADDR) - if err != nil { - return - } - case AtypIPv6: - c.ADDR = make([]byte, net.IPv6len) - _, err = io.ReadFull(reader, c.ADDR) - if err != nil { - return - } - case AtypDomainName: - var addrLen byte - addrLen, err = reader.ReadByte() - if err != nil { - return - } - c.ADDR = make([]byte, addrLen+1) - c.ADDR[0] = addrLen - _, err = io.ReadFull(reader, c.ADDR[1:]) - if err != nil { - return - } - } - - err = binary.Read(reader, binary.BigEndian, &c.PORT) - if err != nil { - return - } - return -} - -func (c Address) WriteTo(writer BufferedWriter) (err error) { - err = writer.WriteByte(c.TYPE) - if err != nil { - return - } - _, err = writer.Write(c.ADDR[:]) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.PORT) - if err != nil { - return - } - return -} - -func (c Address) String() string { - switch c.TYPE { - case AtypDomainName: - return net.JoinHostPort(string(c.ADDR[1:]), strconv.Itoa(int(c.PORT))) - default: - addr, _ := netip.AddrFromSlice(c.ADDR) - addrPort := netip.AddrPortFrom(addr, c.PORT) - return addrPort.String() - } -} - -func (c Address) SocksAddr() socks5.Addr { - addr := make([]byte, 1+len(c.ADDR)+2) - switch c.TYPE { - case AtypIPv4: - addr[0] = socks5.AtypIPv4 - case AtypIPv6: - addr[0] = socks5.AtypIPv6 - case AtypDomainName: - addr[0] = socks5.AtypDomainName - } - copy(addr[1:], c.ADDR) - binary.BigEndian.PutUint16(addr[len(addr)-2:], c.PORT) - return addr -} - -func (c Address) UDPAddr() *net.UDPAddr { - return &net.UDPAddr{ - IP: c.ADDR, - Port: int(c.PORT), - Zone: "", - } -} - -func (c Address) BytesLen() int { - return 1 + len(c.ADDR) + 2 -} - -const ( - ProtocolError = quic.ApplicationErrorCode(0xfffffff0) - AuthenticationFailed = quic.ApplicationErrorCode(0xfffffff1) - AuthenticationTimeout = quic.ApplicationErrorCode(0xfffffff2) - BadCommand = quic.ApplicationErrorCode(0xfffffff3) -) diff --git a/transport/tuic/v4/server.go b/transport/tuic/v4/server.go deleted file mode 100644 index 2430866fac..0000000000 --- a/transport/tuic/v4/server.go +++ /dev/null @@ -1,218 +0,0 @@ -package v4 - -import ( - "bufio" - "bytes" - "fmt" - "net" - "sync" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/atomic" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" - "github.com/metacubex/mihomo/transport/tuic/common" - - "github.com/gofrs/uuid/v5" - "github.com/metacubex/quic-go" - "github.com/puzpuzpuz/xsync/v3" -) - -type ServerOption struct { - HandleTcpFn func(conn net.Conn, addr socks5.Addr, additions ...inbound.Addition) error - HandleUdpFn func(addr socks5.Addr, packet C.UDPPacket, additions ...inbound.Addition) error - - Tokens [][32]byte - MaxUdpRelayPacketSize int -} - -func NewServerHandler(option *ServerOption, quicConn quic.EarlyConnection, uuid uuid.UUID) common.ServerHandler { - return &serverHandler{ - ServerOption: option, - quicConn: quicConn, - uuid: uuid, - authCh: make(chan struct{}), - udpInputMap: xsync.NewMapOf[uint32, *atomic.Bool](), - } -} - -type serverHandler struct { - *ServerOption - quicConn quic.EarlyConnection - uuid uuid.UUID - - authCh chan struct{} - authOk atomic.Bool - authOnce sync.Once - - udpInputMap *xsync.MapOf[uint32, *atomic.Bool] -} - -func (s *serverHandler) AuthOk() bool { - return s.authOk.Load() -} - -func (s *serverHandler) HandleTimeout() { - s.authOnce.Do(func() { - _ = s.quicConn.CloseWithError(AuthenticationTimeout, "AuthenticationTimeout") - s.authOk.Store(false) - close(s.authCh) - }) -} - -func (s *serverHandler) HandleMessage(message []byte) (err error) { - buffer := bytes.NewBuffer(message) - packet, err := ReadPacket(buffer) - if err != nil { - return - } - return s.parsePacket(&packet, common.NATIVE) -} - -func (s *serverHandler) parsePacket(packet *Packet, udpRelayMode common.UdpRelayMode) (err error) { - <-s.authCh - if !s.authOk.Load() { - return - } - var assocId uint32 - - assocId = packet.ASSOC_ID - - writeClosed, _ := s.udpInputMap.LoadOrCompute(assocId, func() *atomic.Bool { return &atomic.Bool{} }) - if writeClosed.Load() { - return nil - } - - pc := &quicStreamPacketConn{ - connId: assocId, - quicConn: s.quicConn, - inputConn: nil, - udpRelayMode: udpRelayMode, - maxUdpRelayPacketSize: s.MaxUdpRelayPacketSize, - deferQuicConnFn: nil, - closeDeferFn: nil, - writeClosed: writeClosed, - } - - return s.HandleUdpFn(packet.ADDR.SocksAddr(), &serverUDPPacket{ - pc: pc, - packet: packet, - rAddr: N.NewCustomAddr("tuic", fmt.Sprintf("tuic-%s-%d", s.uuid, assocId), s.quicConn.RemoteAddr()), // for tunnel's handleUDPConn - }) -} - -func (s *serverHandler) HandleStream(conn *N.BufferedConn) (err error) { - connect, err := ReadConnect(conn) - if err != nil { - return err - } - <-s.authCh - if !s.authOk.Load() { - return conn.Close() - } - - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - err = s.HandleTcpFn(conn, connect.ADDR.SocksAddr()) - if err != nil { - err = NewResponseFailed().WriteTo(buf) - defer conn.Close() - } else { - err = NewResponseSucceed().WriteTo(buf) - } - if err != nil { - _ = conn.Close() - return err - } - _, err = buf.WriteTo(conn) - if err != nil { - _ = conn.Close() - return err - } - - return -} - -func (s *serverHandler) HandleUniStream(reader *bufio.Reader) (err error) { - commandHead, err := ReadCommandHead(reader) - if err != nil { - return - } - switch commandHead.TYPE { - case AuthenticateType: - var authenticate Authenticate - authenticate, err = ReadAuthenticateWithHead(commandHead, reader) - if err != nil { - return - } - authOk := false - for _, tkn := range s.Tokens { - if authenticate.TKN == tkn { - authOk = true - break - } - } - s.authOnce.Do(func() { - if !authOk { - _ = s.quicConn.CloseWithError(AuthenticationFailed, "AuthenticationFailed") - } - s.authOk.Store(authOk) - close(s.authCh) - }) - case PacketType: - var packet Packet - packet, err = ReadPacketWithHead(commandHead, reader) - if err != nil { - return - } - return s.parsePacket(&packet, common.QUIC) - case DissociateType: - var disassociate Dissociate - disassociate, err = ReadDissociateWithHead(commandHead, reader) - if err != nil { - return - } - if writeClosed, loaded := s.udpInputMap.LoadAndDelete(disassociate.ASSOC_ID); loaded { - writeClosed.Store(true) - } - case HeartbeatType: - var heartbeat Heartbeat - heartbeat, err = ReadHeartbeatWithHead(commandHead, reader) - if err != nil { - return - } - heartbeat.BytesLen() - } - return -} - -type serverUDPPacket struct { - pc *quicStreamPacketConn - packet *Packet - rAddr net.Addr -} - -func (s *serverUDPPacket) InAddr() net.Addr { - return s.pc.LocalAddr() -} - -func (s *serverUDPPacket) LocalAddr() net.Addr { - return s.rAddr -} - -func (s *serverUDPPacket) Data() []byte { - return s.packet.DATA -} - -func (s *serverUDPPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) { - return s.pc.WriteTo(b, addr) -} - -func (s *serverUDPPacket) Drop() { - s.packet.DATA = nil -} - -var _ C.UDPPacket = (*serverUDPPacket)(nil) -var _ C.UDPPacketInAddr = (*serverUDPPacket)(nil) diff --git a/transport/tuic/v5/client.go b/transport/tuic/v5/client.go deleted file mode 100644 index d8660cad99..0000000000 --- a/transport/tuic/v5/client.go +++ /dev/null @@ -1,420 +0,0 @@ -package v5 - -import ( - "bufio" - "bytes" - "context" - "errors" - "net" - "runtime" - "sync" - "sync/atomic" - "time" - - atomic2 "github.com/metacubex/mihomo/common/atomic" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - tlsC "github.com/metacubex/mihomo/component/tls" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/transport/tuic/common" - - "github.com/metacubex/quic-go" - "github.com/metacubex/randv2" - "github.com/puzpuzpuz/xsync/v3" -) - -type ClientOption struct { - TlsConfig *tlsC.Config - QuicConfig *quic.Config - Uuid [16]byte - Password string - UdpRelayMode common.UdpRelayMode - CongestionController string - ReduceRtt bool - MaxUdpRelayPacketSize int - MaxOpenStreams int64 - CWND int -} - -type clientImpl struct { - *ClientOption - udp bool - - quicConn quic.Connection - connMutex sync.Mutex - - openStreams atomic.Int64 - closed atomic.Bool - - udpInputMap *xsync.MapOf[uint16, net.Conn] - - // only ready for PoolClient - dialerRef C.Dialer - lastVisited atomic2.TypedValue[time.Time] -} - -func (t *clientImpl) OpenStreams() int64 { - return t.openStreams.Load() -} - -func (t *clientImpl) DialerRef() C.Dialer { - return t.dialerRef -} - -func (t *clientImpl) LastVisited() time.Time { - return t.lastVisited.Load() -} - -func (t *clientImpl) SetLastVisited(last time.Time) { - t.lastVisited.Store(last) -} - -func (t *clientImpl) getQuicConn(ctx context.Context, dialer C.Dialer, dialFn common.DialFunc) (quic.Connection, error) { - t.connMutex.Lock() - defer t.connMutex.Unlock() - if t.quicConn != nil { - return t.quicConn, nil - } - transport, addr, err := dialFn(ctx, dialer) - if err != nil { - return nil, err - } - var quicConn quic.Connection - if t.ReduceRtt { - quicConn, err = transport.DialEarly(ctx, addr, t.TlsConfig, t.QuicConfig) - } else { - quicConn, err = transport.Dial(ctx, addr, t.TlsConfig, t.QuicConfig) - } - if err != nil { - return nil, err - } - - common.SetCongestionController(quicConn, t.CongestionController, t.CWND) - - go func() { - _ = t.sendAuthentication(quicConn) - }() - - if t.udp && t.UdpRelayMode == common.QUIC { - go func() { - _ = t.handleUniStream(quicConn) - }() - } - go func() { - _ = t.handleMessage(quicConn) // always handleMessage because tuicV5 using datagram to send the Heartbeat - }() - - t.quicConn = quicConn - t.openStreams.Store(0) - return quicConn, nil -} - -func (t *clientImpl) sendAuthentication(quicConn quic.Connection) (err error) { - defer func() { - t.deferQuicConn(quicConn, err) - }() - stream, err := quicConn.OpenUniStream() - if err != nil { - return err - } - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - token, err := GenToken(quicConn.ConnectionState(), t.Uuid, t.Password) - if err != nil { - return err - } - err = NewAuthenticate(t.Uuid, token).WriteTo(buf) - if err != nil { - return err - } - _, err = buf.WriteTo(stream) - if err != nil { - return err - } - err = stream.Close() - if err != nil { - return - } - return nil -} - -func (t *clientImpl) handleUniStream(quicConn quic.Connection) (err error) { - defer func() { - t.deferQuicConn(quicConn, err) - }() - for { - var stream quic.ReceiveStream - stream, err = quicConn.AcceptUniStream(context.Background()) - if err != nil { - return err - } - go func() (err error) { - var assocId uint16 - defer func() { - t.deferQuicConn(quicConn, err) - if err != nil && assocId != 0 { - if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { - if conn, ok := val.(net.Conn); ok { - _ = conn.Close() - } - } - } - stream.CancelRead(0) - }() - reader := bufio.NewReader(stream) - commandHead, err := ReadCommandHead(reader) - if err != nil { - return - } - switch commandHead.TYPE { - case PacketType: - var packet Packet - packet, err = ReadPacketWithHead(commandHead, reader) - if err != nil { - return - } - if t.udp && t.UdpRelayMode == common.QUIC { - assocId = packet.ASSOC_ID - if val, ok := t.udpInputMap.Load(assocId); ok { - if conn, ok := val.(net.Conn); ok { - writer := bufio.NewWriterSize(conn, packet.BytesLen()) - _ = packet.WriteTo(writer) - _ = writer.Flush() - } - } - } - } - return - }() - } -} - -func (t *clientImpl) handleMessage(quicConn quic.Connection) (err error) { - defer func() { - t.deferQuicConn(quicConn, err) - }() - for { - var message []byte - message, err = quicConn.ReceiveDatagram(context.Background()) - if err != nil { - return err - } - go func() (err error) { - var assocId uint16 - defer func() { - t.deferQuicConn(quicConn, err) - if err != nil && assocId != 0 { - if val, ok := t.udpInputMap.LoadAndDelete(assocId); ok { - if conn, ok := val.(net.Conn); ok { - _ = conn.Close() - } - } - } - }() - reader := bytes.NewBuffer(message) - commandHead, err := ReadCommandHead(reader) - if err != nil { - return - } - switch commandHead.TYPE { - case PacketType: - var packet Packet - packet, err = ReadPacketWithHead(commandHead, reader) - if err != nil { - return - } - if t.udp && t.UdpRelayMode == common.NATIVE { - assocId = packet.ASSOC_ID - if val, ok := t.udpInputMap.Load(assocId); ok { - if conn, ok := val.(net.Conn); ok { - _, _ = conn.Write(message) - } - } - } - case HeartbeatType: - var heartbeat Heartbeat - heartbeat, err = ReadHeartbeatWithHead(commandHead, reader) - if err != nil { - return - } - heartbeat.BytesLen() - } - return - }() - } -} - -func (t *clientImpl) deferQuicConn(quicConn quic.Connection, err error) { - var netError net.Error - if err != nil && errors.As(err, &netError) { - t.forceClose(quicConn, err) - } -} - -func (t *clientImpl) forceClose(quicConn quic.Connection, err error) { - t.connMutex.Lock() - defer t.connMutex.Unlock() - if quicConn == nil { - quicConn = t.quicConn - } - if quicConn != nil { - if quicConn == t.quicConn { - t.quicConn = nil - } - } - errStr := "" - if err != nil { - errStr = err.Error() - } - if quicConn != nil { - _ = quicConn.CloseWithError(ProtocolError, errStr) - } - udpInputMap := t.udpInputMap - udpInputMap.Range(func(key uint16, value net.Conn) bool { - conn := value - _ = conn.Close() - udpInputMap.Delete(key) - return true - }) -} - -func (t *clientImpl) Close() { - t.closed.Store(true) - if t.openStreams.Load() == 0 { - t.forceClose(nil, common.ClientClosed) - } -} - -func (t *clientImpl) DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn common.DialFunc) (net.Conn, error) { - quicConn, err := t.getQuicConn(ctx, dialer, dialFn) - if err != nil { - return nil, err - } - openStreams := t.openStreams.Add(1) - if openStreams >= t.MaxOpenStreams { - t.openStreams.Add(-1) - return nil, common.TooManyOpenStreams - } - stream, err := func() (stream net.Conn, err error) { - defer func() { - t.deferQuicConn(quicConn, err) - }() - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - err = NewConnect(NewAddress(metadata)).WriteTo(buf) - if err != nil { - return nil, err - } - quicStream, err := quicConn.OpenStream() - if err != nil { - return nil, err - } - stream = common.NewQuicStreamConn( - quicStream, - quicConn.LocalAddr(), - quicConn.RemoteAddr(), - func() { - time.AfterFunc(C.DefaultTCPTimeout, func() { - openStreams := t.openStreams.Add(-1) - if openStreams == 0 && t.closed.Load() { - t.forceClose(quicConn, common.ClientClosed) - } - }) - }, - ) - _, err = buf.WriteTo(stream) - if err != nil { - _ = stream.Close() - return nil, err - } - return stream, err - }() - if err != nil { - return nil, err - } - - return stream, nil -} - -func (t *clientImpl) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn common.DialFunc) (net.PacketConn, error) { - quicConn, err := t.getQuicConn(ctx, dialer, dialFn) - if err != nil { - return nil, err - } - openStreams := t.openStreams.Add(1) - if openStreams >= t.MaxOpenStreams { - t.openStreams.Add(-1) - return nil, common.TooManyOpenStreams - } - - pipe1, pipe2 := N.Pipe() - var connId uint16 - for { - connId = uint16(randv2.IntN(0xFFFF)) - _, loaded := t.udpInputMap.LoadOrStore(connId, pipe1) - if !loaded { - break - } - } - pc := &quicStreamPacketConn{ - connId: connId, - quicConn: quicConn, - inputConn: N.NewBufferedConn(pipe2), - udpRelayMode: t.UdpRelayMode, - maxUdpRelayPacketSize: t.MaxUdpRelayPacketSize, - deferQuicConnFn: t.deferQuicConn, - closeDeferFn: func() { - t.udpInputMap.Delete(connId) - time.AfterFunc(C.DefaultUDPTimeout, func() { - openStreams := t.openStreams.Add(-1) - if openStreams == 0 && t.closed.Load() { - t.forceClose(quicConn, common.ClientClosed) - } - }) - }, - } - return pc, nil -} - -type Client struct { - *clientImpl // use an independent pointer to let Finalizer can work no matter somewhere handle an influence in clientImpl inner -} - -func (t *Client) DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn common.DialFunc) (net.Conn, error) { - conn, err := t.clientImpl.DialContextWithDialer(ctx, metadata, dialer, dialFn) - if err != nil { - return nil, err - } - return N.NewRefConn(conn, t), err -} - -func (t *Client) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn common.DialFunc) (net.PacketConn, error) { - pc, err := t.clientImpl.ListenPacketWithDialer(ctx, metadata, dialer, dialFn) - if err != nil { - return nil, err - } - return N.NewRefPacketConn(pc, t), nil -} - -func (t *Client) forceClose() { - t.clientImpl.forceClose(nil, common.ClientClosed) -} - -func NewClient(clientOption *ClientOption, udp bool, dialerRef C.Dialer) *Client { - ci := &clientImpl{ - ClientOption: clientOption, - udp: udp, - dialerRef: dialerRef, - udpInputMap: xsync.NewMapOf[uint16, net.Conn](), - } - c := &Client{ci} - runtime.SetFinalizer(c, closeClient) - log.Debugln("New TuicV5 Client at %p", c) - return c -} - -func closeClient(client *Client) { - log.Debugln("Close TuicV5 Client at %p", client) - client.forceClose() -} diff --git a/transport/tuic/v5/frag.go b/transport/tuic/v5/frag.go deleted file mode 100644 index d680a5d01f..0000000000 --- a/transport/tuic/v5/frag.go +++ /dev/null @@ -1,116 +0,0 @@ -package v5 - -import ( - "bytes" - "sync" - - "github.com/metacubex/mihomo/common/lru" - - "github.com/metacubex/quic-go" -) - -// MaxFragSize is a safe udp relay packet size -// because tuicv5 support udp fragment so we unneeded to do a magic modify for quic-go to increase MaxDatagramFrameSize -// it may not work fine in some platform -// "1200" from quic-go's MaxDatagramSize -// "-3" from quic-go's DatagramFrame.MaxDataLen -var MaxFragSize = 1200 - PacketOverHead - 3 - -func fragWriteNative(quicConn quic.Connection, packet Packet, buf *bytes.Buffer, fragSize int) (err error) { - fullPayload := packet.DATA - off := 0 - fragID := uint8(0) - fragCount := uint8((len(fullPayload) + fragSize - 1) / fragSize) // round up - packet.FRAG_TOTAL = fragCount - for off < len(fullPayload) { - payloadSize := len(fullPayload) - off - if payloadSize > fragSize { - payloadSize = fragSize - } - frag := packet - frag.FRAG_ID = fragID - frag.SIZE = uint16(payloadSize) - frag.DATA = fullPayload[off : off+payloadSize] - off += payloadSize - fragID++ - buf.Reset() - err = frag.WriteTo(buf) - if err != nil { - return - } - data := buf.Bytes() - err = quicConn.SendDatagram(data) - if err != nil { - return - } - packet.ADDR.TYPE = AtypNone // avoid "fragment 2/2: address in non-first fragment" - } - return -} - -type deFragger struct { - lru *lru.LruCache[uint16, *packetBag] - once sync.Once -} - -type packetBag struct { - frags []*Packet - count uint8 - mutex sync.Mutex -} - -func newPacketBag() *packetBag { - return new(packetBag) -} - -func (d *deFragger) init() { - if d.lru == nil { - d.lru = lru.New( - lru.WithAge[uint16, *packetBag](10), - lru.WithUpdateAgeOnGet[uint16, *packetBag](), - ) - } -} - -func (d *deFragger) Feed(m *Packet) *Packet { - if m.FRAG_TOTAL <= 1 { - return m - } - if m.FRAG_ID >= m.FRAG_TOTAL { - // wtf is this? - return nil - } - d.once.Do(d.init) // lazy init - bag, _ := d.lru.GetOrStore(m.PKT_ID, newPacketBag) - bag.mutex.Lock() - defer bag.mutex.Unlock() - if int(m.FRAG_TOTAL) != len(bag.frags) { - // new message, clear previous state - bag.frags = make([]*Packet, m.FRAG_TOTAL) - bag.count = 1 - bag.frags[m.FRAG_ID] = m - return nil - } - if bag.frags[m.FRAG_ID] != nil { - return nil - } - bag.frags[m.FRAG_ID] = m - bag.count++ - if int(bag.count) != len(bag.frags) { - return nil - } - - // all fragments received, assemble - var data []byte - for _, frag := range bag.frags { - data = append(data, frag.DATA...) - } - p := *bag.frags[0] // recover from first fragment - p.SIZE = uint16(len(data)) - p.DATA = data - p.FRAG_ID = 0 - p.FRAG_TOTAL = 1 - bag.frags = nil - d.lru.Delete(m.PKT_ID) - return &p -} diff --git a/transport/tuic/v5/packet.go b/transport/tuic/v5/packet.go deleted file mode 100644 index 8281a11eff..0000000000 --- a/transport/tuic/v5/packet.go +++ /dev/null @@ -1,207 +0,0 @@ -package v5 - -import ( - "errors" - "net" - "sync" - "time" - - "github.com/metacubex/mihomo/common/atomic" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/pool" - "github.com/metacubex/mihomo/transport/tuic/common" - - "github.com/metacubex/quic-go" - "github.com/metacubex/randv2" -) - -type quicStreamPacketConn struct { - connId uint16 - quicConn quic.Connection - inputConn *N.BufferedConn - - udpRelayMode common.UdpRelayMode - maxUdpRelayPacketSize int - - deferQuicConnFn func(quicConn quic.Connection, err error) - closeDeferFn func() - writeClosed *atomic.Bool - - closeOnce sync.Once - closeErr error - closed bool - - deFragger -} - -func (q *quicStreamPacketConn) Close() error { - q.closeOnce.Do(func() { - q.closed = true - q.closeErr = q.close() - }) - return q.closeErr -} - -func (q *quicStreamPacketConn) close() (err error) { - if q.closeDeferFn != nil { - defer q.closeDeferFn() - } - if q.deferQuicConnFn != nil { - defer func() { - q.deferQuicConnFn(q.quicConn, err) - }() - } - if q.inputConn != nil { - _ = q.inputConn.Close() - q.inputConn = nil - - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - err = NewDissociate(q.connId).WriteTo(buf) - if err != nil { - return - } - var stream quic.SendStream - stream, err = q.quicConn.OpenUniStream() - if err != nil { - return - } - _, err = buf.WriteTo(stream) - if err != nil { - return - } - err = stream.Close() - if err != nil { - return - } - } - return -} - -func (q *quicStreamPacketConn) SetDeadline(t time.Time) error { - //TODO implement me - return nil -} - -func (q *quicStreamPacketConn) SetReadDeadline(t time.Time) error { - if q.inputConn != nil { - return q.inputConn.SetReadDeadline(t) - } - return nil -} - -func (q *quicStreamPacketConn) SetWriteDeadline(t time.Time) error { - //TODO implement me - return nil -} - -func (q *quicStreamPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - if inputConn := q.inputConn; inputConn != nil { // copy inputConn avoid be nil in for loop - for { - var packet Packet - packet, err = ReadPacket(inputConn) - if err != nil { - return - } - if packetPtr := q.deFragger.Feed(&packet); packetPtr != nil { - n = copy(p, packet.DATA) - addr = packetPtr.ADDR.UDPAddr() - return - } - } - } else { - err = net.ErrClosed - } - return -} - -func (q *quicStreamPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - if inputConn := q.inputConn; inputConn != nil { // copy inputConn avoid be nil in for loop - for { - var packet Packet - packet, err = ReadPacket(inputConn) - if err != nil { - return - } - if packetPtr := q.deFragger.Feed(&packet); packetPtr != nil { - data = packetPtr.DATA - addr = packetPtr.ADDR.UDPAddr() - return - } - } - } else { - err = net.ErrClosed - } - return -} - -func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - if len(p) > 0xffff { // uint16 max - return 0, &quic.DatagramTooLargeError{MaxDatagramPayloadSize: 0xffff} - } - if q.closed { - return 0, net.ErrClosed - } - if q.writeClosed != nil && q.writeClosed.Load() { - _ = q.Close() - return 0, net.ErrClosed - } - if q.deferQuicConnFn != nil { - defer func() { - q.deferQuicConnFn(q.quicConn, err) - }() - } - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - address, err := NewAddressNetAddr(addr) - if err != nil { - return - } - pktId := uint16(randv2.Uint32()) - packet := NewPacket(q.connId, pktId, 1, 0, uint16(len(p)), address, p) - switch q.udpRelayMode { - case common.QUIC: - err = packet.WriteTo(buf) - if err != nil { - return - } - var stream quic.SendStream - stream, err = q.quicConn.OpenUniStream() - if err != nil { - return - } - defer stream.Close() - _, err = buf.WriteTo(stream) - if err != nil { - return - } - default: // native - if len(p) > q.maxUdpRelayPacketSize { - err = fragWriteNative(q.quicConn, packet, buf, q.maxUdpRelayPacketSize) - } else { - err = packet.WriteTo(buf) - if err != nil { - return - } - data := buf.Bytes() - err = q.quicConn.SendDatagram(data) - } - - var tooLarge *quic.DatagramTooLargeError - if errors.As(err, &tooLarge) { - err = fragWriteNative(q.quicConn, packet, buf, int(tooLarge.MaxDatagramPayloadSize)-PacketOverHead) - } - if err != nil { - return - } - } - n = len(p) - - return -} - -func (q *quicStreamPacketConn) LocalAddr() net.Addr { - return q.quicConn.LocalAddr() -} - -var _ net.PacketConn = (*quicStreamPacketConn)(nil) diff --git a/transport/tuic/v5/protocol.go b/transport/tuic/v5/protocol.go deleted file mode 100644 index 3d96c8e2c5..0000000000 --- a/transport/tuic/v5/protocol.go +++ /dev/null @@ -1,583 +0,0 @@ -package v5 - -import ( - "encoding/binary" - "fmt" - "io" - "net" - "net/netip" - "strconv" - - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" - - "github.com/metacubex/quic-go" -) - -type BufferedReader interface { - io.Reader - io.ByteReader -} - -type BufferedWriter interface { - io.Writer - io.ByteWriter -} - -type CommandType byte - -const ( - AuthenticateType = CommandType(0x00) - ConnectType = CommandType(0x01) - PacketType = CommandType(0x02) - DissociateType = CommandType(0x03) - HeartbeatType = CommandType(0x04) -) - -const VER byte = 0x05 - -func (c CommandType) String() string { - switch c { - case AuthenticateType: - return "Authenticate" - case ConnectType: - return "Connect" - case PacketType: - return "Packet" - case DissociateType: - return "Dissociate" - case HeartbeatType: - return "Heartbeat" - default: - return fmt.Sprintf("UnknowCommand: %#x", byte(c)) - } -} - -func (c CommandType) BytesLen() int { - return 1 -} - -type CommandHead struct { - VER byte - TYPE CommandType -} - -func NewCommandHead(TYPE CommandType) CommandHead { - return CommandHead{ - VER: VER, - TYPE: TYPE, - } -} - -func ReadCommandHead(reader BufferedReader) (c CommandHead, err error) { - c.VER, err = reader.ReadByte() - if err != nil { - return - } - TYPE, err := reader.ReadByte() - if err != nil { - return - } - c.TYPE = CommandType(TYPE) - return -} - -func (c CommandHead) WriteTo(writer BufferedWriter) (err error) { - err = writer.WriteByte(c.VER) - if err != nil { - return - } - err = writer.WriteByte(byte(c.TYPE)) - if err != nil { - return - } - return -} - -func (c CommandHead) BytesLen() int { - return 1 + c.TYPE.BytesLen() -} - -type Authenticate struct { - CommandHead - UUID [16]byte - TOKEN [32]byte -} - -func NewAuthenticate(UUID [16]byte, TOKEN [32]byte) Authenticate { - return Authenticate{ - CommandHead: NewCommandHead(AuthenticateType), - UUID: UUID, - TOKEN: TOKEN, - } -} - -func ReadAuthenticateWithHead(head CommandHead, reader BufferedReader) (c Authenticate, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != AuthenticateType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - _, err = io.ReadFull(reader, c.UUID[:]) - if err != nil { - return - } - _, err = io.ReadFull(reader, c.TOKEN[:]) - if err != nil { - return - } - return -} - -func ReadAuthenticate(reader BufferedReader) (c Authenticate, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadAuthenticateWithHead(head, reader) -} - -func GenToken(state quic.ConnectionState, uuid [16]byte, password string) (token [32]byte, err error) { - var tokenBytes []byte - tokenBytes, err = state.TLS.ExportKeyingMaterial(utils.StringFromImmutableBytes(uuid[:]), utils.ImmutableBytesFromString(password), 32) - if err != nil { - return - } - copy(token[:], tokenBytes) - return -} - -func (c Authenticate) WriteTo(writer BufferedWriter) (err error) { - err = c.CommandHead.WriteTo(writer) - if err != nil { - return - } - _, err = writer.Write(c.UUID[:]) - if err != nil { - return - } - _, err = writer.Write(c.TOKEN[:]) - if err != nil { - return - } - return -} - -func (c Authenticate) BytesLen() int { - return c.CommandHead.BytesLen() + 16 + 32 -} - -type Connect struct { - CommandHead - ADDR Address -} - -func NewConnect(ADDR Address) Connect { - return Connect{ - CommandHead: NewCommandHead(ConnectType), - ADDR: ADDR, - } -} - -func ReadConnectWithHead(head CommandHead, reader BufferedReader) (c Connect, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != ConnectType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - c.ADDR, err = ReadAddress(reader) - if err != nil { - return - } - return -} - -func ReadConnect(reader BufferedReader) (c Connect, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadConnectWithHead(head, reader) -} - -func (c Connect) WriteTo(writer BufferedWriter) (err error) { - err = c.CommandHead.WriteTo(writer) - if err != nil { - return - } - err = c.ADDR.WriteTo(writer) - if err != nil { - return - } - return -} - -func (c Connect) BytesLen() int { - return c.CommandHead.BytesLen() + c.ADDR.BytesLen() -} - -type Packet struct { - CommandHead - ASSOC_ID uint16 - PKT_ID uint16 - FRAG_TOTAL uint8 - FRAG_ID uint8 - SIZE uint16 - ADDR Address - DATA []byte -} - -func NewPacket(ASSOC_ID uint16, PKT_ID uint16, FRGA_TOTAL uint8, FRAG_ID uint8, SIZE uint16, ADDR Address, DATA []byte) Packet { - return Packet{ - CommandHead: NewCommandHead(PacketType), - ASSOC_ID: ASSOC_ID, - PKT_ID: PKT_ID, - FRAG_ID: FRAG_ID, - FRAG_TOTAL: FRGA_TOTAL, - SIZE: SIZE, - ADDR: ADDR, - DATA: DATA, - } -} - -func ReadPacketWithHead(head CommandHead, reader BufferedReader) (c Packet, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != PacketType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) - if err != nil { - return - } - err = binary.Read(reader, binary.BigEndian, &c.PKT_ID) - if err != nil { - return - } - err = binary.Read(reader, binary.BigEndian, &c.FRAG_TOTAL) - if err != nil { - return - } - err = binary.Read(reader, binary.BigEndian, &c.FRAG_ID) - if err != nil { - return - } - err = binary.Read(reader, binary.BigEndian, &c.SIZE) - if err != nil { - return - } - c.ADDR, err = ReadAddress(reader) - if err != nil { - return - } - c.DATA = make([]byte, c.SIZE) - _, err = io.ReadFull(reader, c.DATA) - if err != nil { - return - } - return -} - -func ReadPacket(reader BufferedReader) (c Packet, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadPacketWithHead(head, reader) -} - -func (c Packet) WriteTo(writer BufferedWriter) (err error) { - err = c.CommandHead.WriteTo(writer) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.PKT_ID) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.FRAG_TOTAL) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.FRAG_ID) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.SIZE) - if err != nil { - return - } - err = c.ADDR.WriteTo(writer) - if err != nil { - return - } - _, err = writer.Write(c.DATA) - if err != nil { - return - } - return -} - -func (c Packet) BytesLen() int { - return c.CommandHead.BytesLen() + 4 + 2 + c.ADDR.BytesLen() + len(c.DATA) -} - -var PacketOverHead = NewPacket(0, 0, 0, 0, 0, NewAddressAddrPort(netip.AddrPortFrom(netip.IPv6Unspecified(), 0)), nil).BytesLen() - -type Dissociate struct { - CommandHead - ASSOC_ID uint16 -} - -func NewDissociate(ASSOC_ID uint16) Dissociate { - return Dissociate{ - CommandHead: NewCommandHead(DissociateType), - ASSOC_ID: ASSOC_ID, - } -} - -func ReadDissociateWithHead(head CommandHead, reader BufferedReader) (c Dissociate, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != DissociateType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - err = binary.Read(reader, binary.BigEndian, &c.ASSOC_ID) - if err != nil { - return - } - return -} - -func ReadDissociate(reader BufferedReader) (c Dissociate, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadDissociateWithHead(head, reader) -} - -func (c Dissociate) WriteTo(writer BufferedWriter) (err error) { - err = c.CommandHead.WriteTo(writer) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.ASSOC_ID) - if err != nil { - return - } - return -} - -func (c Dissociate) BytesLen() int { - return c.CommandHead.BytesLen() + 4 -} - -type Heartbeat struct { - CommandHead -} - -func NewHeartbeat() Heartbeat { - return Heartbeat{ - CommandHead: NewCommandHead(HeartbeatType), - } -} - -func ReadHeartbeatWithHead(head CommandHead, reader BufferedReader) (c Heartbeat, err error) { - c.CommandHead = head - if c.CommandHead.TYPE != HeartbeatType { - err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) - return - } - return -} - -func ReadHeartbeat(reader BufferedReader) (c Heartbeat, err error) { - head, err := ReadCommandHead(reader) - if err != nil { - return - } - return ReadHeartbeatWithHead(head, reader) -} - -// Addr types -const ( - AtypDomainName byte = 0 - AtypIPv4 byte = 1 - AtypIPv6 byte = 2 - AtypNone byte = 255 // Address type None is used in Packet commands that is not the first fragment of a UDP packet. -) - -type Address struct { - TYPE byte - ADDR []byte - PORT uint16 -} - -func NewAddress(metadata *C.Metadata) Address { - var addrType byte - var addr []byte - switch metadata.AddrType() { - case C.AtypIPv4: - addrType = AtypIPv4 - addr = metadata.DstIP.AsSlice() - case C.AtypIPv6: - addrType = AtypIPv6 - addr = metadata.DstIP.AsSlice() - case C.AtypDomainName: - addrType = AtypDomainName - addr = make([]byte, len(metadata.Host)+1) - addr[0] = byte(len(metadata.Host)) - copy(addr[1:], metadata.Host) - } - - return Address{ - TYPE: addrType, - ADDR: addr, - PORT: metadata.DstPort, - } -} - -func NewAddressNetAddr(addr net.Addr) (Address, error) { - if addr, ok := addr.(interface{ AddrPort() netip.AddrPort }); ok { - if addrPort := addr.AddrPort(); addrPort.IsValid() { // sing's M.Socksaddr maybe return an invalid AddrPort if it's a DomainName - return NewAddressAddrPort(addrPort), nil - } - } - addrStr := addr.String() - if addrPort, err := netip.ParseAddrPort(addrStr); err == nil { - return NewAddressAddrPort(addrPort), nil - } - metadata := &C.Metadata{} - if err := metadata.SetRemoteAddress(addrStr); err != nil { - return Address{}, err - } - return NewAddress(metadata), nil -} - -func NewAddressAddrPort(addrPort netip.AddrPort) Address { - var addrType byte - port := addrPort.Port() - addr := addrPort.Addr().Unmap() - if addr.Is4() { - addrType = AtypIPv4 - } else { - addrType = AtypIPv6 - } - return Address{ - TYPE: addrType, - ADDR: addr.AsSlice(), - PORT: port, - } -} - -func ReadAddress(reader BufferedReader) (c Address, err error) { - c.TYPE, err = reader.ReadByte() - if err != nil { - return - } - switch c.TYPE { - case AtypIPv4: - c.ADDR = make([]byte, net.IPv4len) - _, err = io.ReadFull(reader, c.ADDR) - if err != nil { - return - } - case AtypIPv6: - c.ADDR = make([]byte, net.IPv6len) - _, err = io.ReadFull(reader, c.ADDR) - if err != nil { - return - } - case AtypDomainName: - var addrLen byte - addrLen, err = reader.ReadByte() - if err != nil { - return - } - c.ADDR = make([]byte, addrLen+1) - c.ADDR[0] = addrLen - _, err = io.ReadFull(reader, c.ADDR[1:]) - if err != nil { - return - } - } - - if c.TYPE == AtypNone { - return - } - err = binary.Read(reader, binary.BigEndian, &c.PORT) - if err != nil { - return - } - return -} - -func (c Address) WriteTo(writer BufferedWriter) (err error) { - err = writer.WriteByte(c.TYPE) - if err != nil { - return - } - if c.TYPE == AtypNone { - return - } - _, err = writer.Write(c.ADDR[:]) - if err != nil { - return - } - err = binary.Write(writer, binary.BigEndian, c.PORT) - if err != nil { - return - } - return -} - -func (c Address) String() string { - switch c.TYPE { - case AtypDomainName: - return net.JoinHostPort(string(c.ADDR[1:]), strconv.Itoa(int(c.PORT))) - default: - addr, _ := netip.AddrFromSlice(c.ADDR) - addrPort := netip.AddrPortFrom(addr, c.PORT) - return addrPort.String() - } -} - -func (c Address) SocksAddr() socks5.Addr { - addr := make([]byte, 1+len(c.ADDR)+2) - switch c.TYPE { - case AtypIPv4: - addr[0] = socks5.AtypIPv4 - case AtypIPv6: - addr[0] = socks5.AtypIPv6 - case AtypDomainName: - addr[0] = socks5.AtypDomainName - } - copy(addr[1:], c.ADDR) - binary.BigEndian.PutUint16(addr[len(addr)-2:], c.PORT) - return addr -} - -func (c Address) UDPAddr() *net.UDPAddr { - return &net.UDPAddr{ - IP: c.ADDR, - Port: int(c.PORT), - Zone: "", - } -} - -func (c Address) BytesLen() int { - return 1 + len(c.ADDR) + 2 -} - -const ( - ProtocolError = quic.ApplicationErrorCode(0xfffffff0) - AuthenticationFailed = quic.ApplicationErrorCode(0xfffffff1) - AuthenticationTimeout = quic.ApplicationErrorCode(0xfffffff2) - BadCommand = quic.ApplicationErrorCode(0xfffffff3) -) diff --git a/transport/tuic/v5/server.go b/transport/tuic/v5/server.go deleted file mode 100644 index 8454b64c3a..0000000000 --- a/transport/tuic/v5/server.go +++ /dev/null @@ -1,229 +0,0 @@ -package v5 - -import ( - "bufio" - "bytes" - "fmt" - "net" - "sync" - - "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/atomic" - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" - "github.com/metacubex/mihomo/transport/tuic/common" - - "github.com/gofrs/uuid/v5" - "github.com/metacubex/quic-go" - "github.com/puzpuzpuz/xsync/v3" -) - -type ServerOption struct { - HandleTcpFn func(conn net.Conn, addr socks5.Addr, additions ...inbound.Addition) error - HandleUdpFn func(addr socks5.Addr, packet C.UDPPacket, additions ...inbound.Addition) error - - Users map[[16]byte]string - MaxUdpRelayPacketSize int -} - -func NewServerHandler(option *ServerOption, quicConn quic.EarlyConnection, uuid uuid.UUID) common.ServerHandler { - return &serverHandler{ - ServerOption: option, - quicConn: quicConn, - uuid: uuid, - authCh: make(chan struct{}), - udpInputMap: xsync.NewMapOf[uint16, *serverUDPInput](), - } -} - -type serverHandler struct { - *ServerOption - quicConn quic.EarlyConnection - uuid uuid.UUID - - authCh chan struct{} - authOk atomic.Bool - authUUID atomic.TypedValue[string] - authOnce sync.Once - - udpInputMap *xsync.MapOf[uint16, *serverUDPInput] -} - -func (s *serverHandler) AuthOk() bool { - return s.authOk.Load() -} - -func (s *serverHandler) HandleTimeout() { - s.authOnce.Do(func() { - _ = s.quicConn.CloseWithError(AuthenticationTimeout, "AuthenticationTimeout") - s.authOk.Store(false) - close(s.authCh) - }) -} - -func (s *serverHandler) HandleMessage(message []byte) (err error) { - reader := bytes.NewBuffer(message) - commandHead, err := ReadCommandHead(reader) - if err != nil { - return - } - switch commandHead.TYPE { - case PacketType: - var packet Packet - packet, err = ReadPacketWithHead(commandHead, reader) - if err != nil { - return - } - return s.parsePacket(&packet, common.NATIVE) - case HeartbeatType: - var heartbeat Heartbeat - heartbeat, err = ReadHeartbeatWithHead(commandHead, reader) - if err != nil { - return - } - heartbeat.BytesLen() - } - return -} - -func (s *serverHandler) parsePacket(packet *Packet, udpRelayMode common.UdpRelayMode) (err error) { - <-s.authCh - if !s.authOk.Load() { - return - } - var assocId uint16 - - assocId = packet.ASSOC_ID - - input, _ := s.udpInputMap.LoadOrCompute(assocId, func() *serverUDPInput { return &serverUDPInput{} }) - if input.writeClosed.Load() { - return nil - } - packetPtr := input.Feed(packet) - if packetPtr == nil { - return - } - - pc := &quicStreamPacketConn{ - connId: assocId, - quicConn: s.quicConn, - inputConn: nil, - udpRelayMode: udpRelayMode, - maxUdpRelayPacketSize: s.MaxUdpRelayPacketSize, - deferQuicConnFn: nil, - closeDeferFn: nil, - writeClosed: &input.writeClosed, - } - - return s.HandleUdpFn(packetPtr.ADDR.SocksAddr(), &serverUDPPacket{ - pc: pc, - packet: packetPtr, - rAddr: N.NewCustomAddr("tuic", fmt.Sprintf("tuic-%s-%d", s.uuid, assocId), s.quicConn.RemoteAddr()), // for tunnel's handleUDPConn - }, inbound.WithInUser(s.authUUID.Load())) -} - -func (s *serverHandler) HandleStream(conn *N.BufferedConn) (err error) { - connect, err := ReadConnect(conn) - if err != nil { - return err - } - <-s.authCh - if !s.authOk.Load() { - return conn.Close() - } - - err = s.HandleTcpFn(conn, connect.ADDR.SocksAddr(), inbound.WithInUser(s.authUUID.Load())) - if err != nil { - _ = conn.Close() - return err - } - return -} - -func (s *serverHandler) HandleUniStream(reader *bufio.Reader) (err error) { - commandHead, err := ReadCommandHead(reader) - if err != nil { - return - } - switch commandHead.TYPE { - case AuthenticateType: - var authenticate Authenticate - authenticate, err = ReadAuthenticateWithHead(commandHead, reader) - if err != nil { - return - } - authOk := false - var authUUID uuid.UUID - var token [32]byte - if password, ok := s.Users[authenticate.UUID]; ok { - token, err = GenToken(s.quicConn.ConnectionState(), authenticate.UUID, password) - if err != nil { - return - } - if token == authenticate.TOKEN { - authOk = true - authUUID = authenticate.UUID - } - } - s.authOnce.Do(func() { - if !authOk { - _ = s.quicConn.CloseWithError(AuthenticationFailed, "AuthenticationFailed") - } - s.authOk.Store(authOk) - s.authUUID.Store(authUUID.String()) - close(s.authCh) - }) - case PacketType: - var packet Packet - packet, err = ReadPacketWithHead(commandHead, reader) - if err != nil { - return - } - return s.parsePacket(&packet, common.QUIC) - case DissociateType: - var disassociate Dissociate - disassociate, err = ReadDissociateWithHead(commandHead, reader) - if err != nil { - return - } - if input, loaded := s.udpInputMap.LoadAndDelete(disassociate.ASSOC_ID); loaded { - input.writeClosed.Store(true) - } - } - return -} - -type serverUDPInput struct { - writeClosed atomic.Bool - deFragger -} - -type serverUDPPacket struct { - pc *quicStreamPacketConn - packet *Packet - rAddr net.Addr -} - -func (s *serverUDPPacket) InAddr() net.Addr { - return s.pc.LocalAddr() -} - -func (s *serverUDPPacket) LocalAddr() net.Addr { - return s.rAddr -} - -func (s *serverUDPPacket) Data() []byte { - return s.packet.DATA -} - -func (s *serverUDPPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) { - return s.pc.WriteTo(b, addr) -} - -func (s *serverUDPPacket) Drop() { - s.packet.DATA = nil -} - -var _ C.UDPPacket = (*serverUDPPacket)(nil) -var _ C.UDPPacketInAddr = (*serverUDPPacket)(nil) diff --git a/transport/v2ray-plugin/mux.go b/transport/v2ray-plugin/mux.go deleted file mode 100644 index 6bff27bfb2..0000000000 --- a/transport/v2ray-plugin/mux.go +++ /dev/null @@ -1,173 +0,0 @@ -package obfs - -import ( - "bytes" - "encoding/binary" - "errors" - "io" - "net" -) - -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 bytes.Buffer - 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() - binary.Write(&m.buf, binary.BigEndian, uint16(4)) - m.buf.Write(m.id[:]) - m.buf.WriteByte(SessionStatusKeep) - m.buf.WriteByte(OptionData) - binary.Write(&m.buf, binary.BigEndian, 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 := &bytes.Buffer{} - - // fill empty length - buf.Write([]byte{0x0, 0x0}) - buf.Write(option.ID[:]) - buf.WriteByte(SessionStatusNew) - buf.WriteByte(OptionNone) - - // tcp - netType := byte(0x1) - if option.Type == "udp" { - netType = byte(0x2) - } - buf.WriteByte(netType) - - // port - binary.Write(buf, binary.BigEndian, option.Port) - - // address - ip := net.ParseIP(option.Host) - if ip == nil { - buf.WriteByte(0x2) - buf.WriteString(option.Host) - } else if ipv4 := ip.To4(); ipv4 != nil { - buf.WriteByte(0x1) - buf.Write(ipv4) - } else { - buf.WriteByte(0x3) - buf.Write(ip.To16()) - } - - 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 deleted file mode 100644 index 90ff5efe20..0000000000 --- a/transport/v2ray-plugin/websocket.go +++ /dev/null @@ -1,75 +0,0 @@ -package obfs - -import ( - "context" - "crypto/tls" - "net" - "net/http" - - "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/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 - Fingerprint string - Mux bool - V2rayHttpUpgrade bool - V2rayHttpUpgradeFastOpen bool -} - -// NewV2rayObfs return a HTTPObfs -func NewV2rayObfs(ctx context.Context, 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, - V2rayHttpUpgrade: option.V2rayHttpUpgrade, - V2rayHttpUpgradeFastOpen: option.V2rayHttpUpgradeFastOpen, - Headers: header, - } - - if option.TLS { - config.TLS = true - tlsConfig := &tls.Config{ - ServerName: option.Host, - InsecureSkipVerify: option.SkipCertVerify, - NextProtos: []string{"http/1.1"}, - } - var err error - config.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) - if err != nil { - return nil, err - } - - if host := config.Headers.Get("Host"); host != "" { - config.TLSConfig.ServerName = host - } - } - - var err error - conn, err = vmess.StreamWebsocketConn(ctx, 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/vless/config.pb.go b/transport/vless/config.pb.go deleted file mode 100644 index 14fcc5f968..0000000000 --- a/transport/vless/config.pb.go +++ /dev/null @@ -1,158 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.28.0 -// protoc v3.19.1 -// source: transport/vless/config.proto - -package vless - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Addons struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Flow string `protobuf:"bytes,1,opt,name=Flow,proto3" json:"Flow,omitempty"` - Seed []byte `protobuf:"bytes,2,opt,name=Seed,proto3" json:"Seed,omitempty"` -} - -func (x *Addons) Reset() { - *x = Addons{} - if protoimpl.UnsafeEnabled { - mi := &file_transport_vless_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Addons) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Addons) ProtoMessage() {} - -func (x *Addons) ProtoReflect() protoreflect.Message { - mi := &file_transport_vless_config_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Addons.ProtoReflect.Descriptor instead. -func (*Addons) Descriptor() ([]byte, []int) { - return file_transport_vless_config_proto_rawDescGZIP(), []int{0} -} - -func (x *Addons) GetFlow() string { - if x != nil { - return x.Flow - } - return "" -} - -func (x *Addons) GetSeed() []byte { - if x != nil { - return x.Seed - } - return nil -} - -var File_transport_vless_config_proto protoreflect.FileDescriptor - -var file_transport_vless_config_proto_rawDesc = []byte{ - 0x0a, 0x1c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x76, 0x6c, 0x65, 0x73, - 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x15, - 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x30, 0x0a, 0x06, 0x41, 0x64, 0x64, 0x6f, 0x6e, 0x73, 0x12, - 0x12, 0x0a, 0x04, 0x46, 0x6c, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x46, - 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x04, 0x53, 0x65, 0x65, 0x64, 0x42, 0x61, 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x63, - 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x76, - 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x44, 0x72, 0x65, 0x61, 0x6d, 0x61, 0x63, 0x72, 0x6f, 0x2f, 0x63, 0x6c, 0x61, - 0x73, 0x68, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x76, 0x6c, 0x65, - 0x73, 0x73, 0xaa, 0x02, 0x15, 0x43, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, -} - -var ( - file_transport_vless_config_proto_rawDescOnce sync.Once - file_transport_vless_config_proto_rawDescData = file_transport_vless_config_proto_rawDesc -) - -func file_transport_vless_config_proto_rawDescGZIP() []byte { - file_transport_vless_config_proto_rawDescOnce.Do(func() { - file_transport_vless_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_vless_config_proto_rawDescData) - }) - return file_transport_vless_config_proto_rawDescData -} - -var file_transport_vless_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_transport_vless_config_proto_goTypes = []interface{}{ - (*Addons)(nil), // 0: mihomo.transport.vless.Addons -} -var file_transport_vless_config_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_transport_vless_config_proto_init() } -func file_transport_vless_config_proto_init() { - if File_transport_vless_config_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_transport_vless_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Addons); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_transport_vless_config_proto_rawDesc, - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_transport_vless_config_proto_goTypes, - DependencyIndexes: file_transport_vless_config_proto_depIdxs, - MessageInfos: file_transport_vless_config_proto_msgTypes, - }.Build() - File_transport_vless_config_proto = out.File - file_transport_vless_config_proto_rawDesc = nil - file_transport_vless_config_proto_goTypes = nil - file_transport_vless_config_proto_depIdxs = nil -} diff --git a/transport/vless/config.proto b/transport/vless/config.proto deleted file mode 100644 index 44cad4793d..0000000000 --- a/transport/vless/config.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto3"; - -package mihomo.transport.vless; -option csharp_namespace = "Mihomo.Transport.Vless"; -option go_package = "github.com/metacubex/mihomo/transport/vless"; -option java_package = "com.mihomo.transport.vless"; -option java_multiple_files = true; - -message Addons { - string Flow = 1; - bytes Seed = 2; -} \ No newline at end of file diff --git a/transport/vless/conn.go b/transport/vless/conn.go deleted file mode 100644 index 02224892e2..0000000000 --- a/transport/vless/conn.go +++ /dev/null @@ -1,213 +0,0 @@ -package vless - -import ( - "encoding/binary" - "errors" - "io" - "net" - "sync" - - "github.com/metacubex/mihomo/common/buf" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/transport/vless/vision" - - "github.com/gofrs/uuid/v5" - "google.golang.org/protobuf/proto" -) - -type Conn struct { - N.ExtendedWriter - N.ExtendedReader - net.Conn - dst *DstAddr - id *uuid.UUID - addons *Addons - received bool - - handshakeMutex sync.Mutex - needHandshake bool - err error -} - -func (vc *Conn) Read(b []byte) (int, error) { - if vc.received { - return vc.ExtendedReader.Read(b) - } - - if err := vc.recvResponse(); err != nil { - return 0, err - } - vc.received = true - return vc.ExtendedReader.Read(b) -} - -func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { - if vc.received { - return vc.ExtendedReader.ReadBuffer(buffer) - } - - if err := vc.recvResponse(); err != nil { - return err - } - vc.received = true - return vc.ExtendedReader.ReadBuffer(buffer) -} - -func (vc *Conn) Write(p []byte) (int, error) { - if vc.needHandshake { - vc.handshakeMutex.Lock() - if vc.needHandshake { - vc.needHandshake = false - if vc.sendRequest(p) { - vc.handshakeMutex.Unlock() - if vc.err != nil { - return 0, vc.err - } - return len(p), vc.err - } - if vc.err != nil { - vc.handshakeMutex.Unlock() - return 0, vc.err - } - } - vc.handshakeMutex.Unlock() - } - - return vc.ExtendedWriter.Write(p) -} - -func (vc *Conn) WriteBuffer(buffer *buf.Buffer) error { - if vc.needHandshake { - vc.handshakeMutex.Lock() - if vc.needHandshake { - vc.needHandshake = false - if vc.sendRequest(buffer.Bytes()) { - vc.handshakeMutex.Unlock() - return vc.err - } - if vc.err != nil { - vc.handshakeMutex.Unlock() - return vc.err - } - } - vc.handshakeMutex.Unlock() - } - - return vc.ExtendedWriter.WriteBuffer(buffer) -} - -func (vc *Conn) sendRequest(p []byte) bool { - var addonsBytes []byte - if vc.addons != nil { - addonsBytes, vc.err = proto.Marshal(vc.addons) - if vc.err != nil { - return true - } - } - - var buffer *buf.Buffer - if vc.IsXTLSVisionEnabled() { - buffer = buf.New() - defer buffer.Release() - } else { - requestLen := 1 // protocol version - requestLen += 16 // UUID - requestLen += 1 // addons length - requestLen += len(addonsBytes) - requestLen += 1 // command - if !vc.dst.Mux { - requestLen += 2 // port - requestLen += 1 // addr type - requestLen += len(vc.dst.Addr) - } - requestLen += len(p) - - buffer = buf.NewSize(requestLen) - defer buffer.Release() - } - - buf.Must( - buffer.WriteByte(Version), // protocol version - buf.Error(buffer.Write(vc.id.Bytes())), // 16 bytes of uuid - buffer.WriteByte(byte(len(addonsBytes))), - buf.Error(buffer.Write(addonsBytes)), - ) - - if vc.dst.Mux { - buf.Must(buffer.WriteByte(CommandMux)) - } else { - if vc.dst.UDP { - buf.Must(buffer.WriteByte(CommandUDP)) - } else { - buf.Must(buffer.WriteByte(CommandTCP)) - } - - binary.BigEndian.PutUint16(buffer.Extend(2), vc.dst.Port) - buf.Must( - buffer.WriteByte(vc.dst.AddrType), - buf.Error(buffer.Write(vc.dst.Addr)), - ) - } - - buf.Must(buf.Error(buffer.Write(p))) - - _, vc.err = vc.ExtendedWriter.Write(buffer.Bytes()) - return true -} - -func (vc *Conn) recvResponse() error { - var buffer [2]byte - _, vc.err = io.ReadFull(vc.ExtendedReader, buffer[:]) - if vc.err != nil { - return vc.err - } - - if buffer[0] != Version { - return errors.New("unexpected response version") - } - - length := int64(buffer[1]) - if length != 0 { // addon data length > 0 - io.CopyN(io.Discard, vc.ExtendedReader, length) // just discard - } - - return nil -} - -func (vc *Conn) Upstream() any { - return vc.Conn -} - -func (vc *Conn) NeedHandshake() bool { - return vc.needHandshake -} - -func (vc *Conn) IsXTLSVisionEnabled() bool { - return vc.addons != nil && vc.addons.Flow == XRV -} - -// newConn return a Conn instance -func newConn(conn net.Conn, client *Client, dst *DstAddr) (net.Conn, error) { - c := &Conn{ - ExtendedReader: N.NewExtendedReader(conn), - ExtendedWriter: N.NewExtendedWriter(conn), - Conn: conn, - id: client.uuid, - dst: dst, - needHandshake: true, - } - - if client.Addons != nil { - switch client.Addons.Flow { - case XRV: - visionConn, err := vision.NewConn(c, c.id) - if err != nil { - return nil, err - } - c.addons = client.Addons - return visionConn, nil - } - } - - return c, nil -} diff --git a/transport/vless/vision/conn.go b/transport/vless/vision/conn.go deleted file mode 100644 index 56684808cb..0000000000 --- a/transport/vless/vision/conn.go +++ /dev/null @@ -1,304 +0,0 @@ -package vision - -import ( - "bytes" - "crypto/subtle" - gotls "crypto/tls" - "encoding/binary" - "errors" - "fmt" - "io" - "net" - - "github.com/metacubex/mihomo/common/buf" - N "github.com/metacubex/mihomo/common/net" - tlsC "github.com/metacubex/mihomo/component/tls" - "github.com/metacubex/mihomo/log" - - "github.com/gofrs/uuid/v5" -) - -var ( - _ N.ExtendedConn = (*Conn)(nil) -) - -type Conn struct { - net.Conn - N.ExtendedReader - N.ExtendedWriter - upstream net.Conn - userUUID *uuid.UUID - - tlsConn net.Conn - input *bytes.Reader - rawInput *bytes.Buffer - - needHandshake bool - packetsToFilter int - isTLS bool - isTLS12orAbove bool - enableXTLS bool - cipher uint16 - remainingServerHello uint16 - readRemainingContent int - readRemainingPadding int - readProcess bool - readFilterUUID bool - readLastCommand byte - writeFilterApplicationData bool - writeDirect bool -} - -func (vc *Conn) Read(b []byte) (int, error) { - if vc.readProcess { - buffer := buf.With(b) - err := vc.ReadBuffer(buffer) - return buffer.Len(), err - } - return vc.ExtendedReader.Read(b) -} - -func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { - toRead := buffer.FreeBytes() - if vc.readRemainingContent > 0 { - if vc.readRemainingContent < buffer.FreeLen() { - toRead = toRead[:vc.readRemainingContent] - } - n, err := vc.ExtendedReader.Read(toRead) - buffer.Truncate(n) - vc.readRemainingContent -= n - vc.FilterTLS(toRead) - return err - } - if vc.readRemainingPadding > 0 { - _, err := io.CopyN(io.Discard, vc.ExtendedReader, int64(vc.readRemainingPadding)) - if err != nil { - return err - } - vc.readRemainingPadding = 0 - } - if vc.readProcess { - switch vc.readLastCommand { - case commandPaddingContinue: - //if vc.isTLS || vc.packetsToFilter > 0 { - headerUUIDLen := 0 - if vc.readFilterUUID { - headerUUIDLen = uuid.Size - } - var header []byte - if need := headerUUIDLen + PaddingHeaderLen - uuid.Size; buffer.FreeLen() < need { - header = make([]byte, need) - } else { - header = buffer.FreeBytes()[:need] - } - _, err := io.ReadFull(vc.ExtendedReader, header) - if err != nil { - return err - } - if vc.readFilterUUID { - vc.readFilterUUID = false - if subtle.ConstantTimeCompare(vc.userUUID.Bytes(), header[:uuid.Size]) != 1 { - err = fmt.Errorf("XTLS Vision server responded unknown UUID: %s", - uuid.FromBytesOrNil(header[:uuid.Size]).String()) - log.Errorln(err.Error()) - return err - } - header = header[uuid.Size:] - } - vc.readRemainingPadding = int(binary.BigEndian.Uint16(header[3:])) - vc.readRemainingContent = int(binary.BigEndian.Uint16(header[1:])) - vc.readLastCommand = header[0] - log.Debugln("XTLS Vision read padding: command=%d, payloadLen=%d, paddingLen=%d", - vc.readLastCommand, vc.readRemainingContent, vc.readRemainingPadding) - return vc.ReadBuffer(buffer) - //} - case commandPaddingEnd: - vc.readProcess = false - return vc.ReadBuffer(buffer) - case commandPaddingDirect: - needReturn := false - if vc.input != nil { - _, err := buffer.ReadOnceFrom(vc.input) - if err != nil { - if !errors.Is(err, io.EOF) { - return err - } - } - if vc.input.Len() == 0 { - needReturn = true - vc.input = nil - } else { // buffer is full - return nil - } - } - if vc.rawInput != nil { - _, err := buffer.ReadOnceFrom(vc.rawInput) - if err != nil { - if !errors.Is(err, io.EOF) { - return err - } - } - needReturn = true - if vc.rawInput.Len() == 0 { - vc.rawInput = nil - } - } - if vc.input == nil && vc.rawInput == nil { - vc.readProcess = false - vc.ExtendedReader = N.NewExtendedReader(vc.Conn) - log.Debugln("XTLS Vision direct read start") - } - if needReturn { - return nil - } - default: - err := fmt.Errorf("XTLS Vision read unknown command: %d", vc.readLastCommand) - log.Debugln(err.Error()) - return err - } - } - return vc.ExtendedReader.ReadBuffer(buffer) -} - -func (vc *Conn) Write(p []byte) (int, error) { - if vc.writeFilterApplicationData { - return N.WriteBuffer(vc, buf.As(p)) - } - return vc.ExtendedWriter.Write(p) -} - -func (vc *Conn) WriteBuffer(buffer *buf.Buffer) (err error) { - if vc.needHandshake { - vc.needHandshake = false - if buffer.IsEmpty() { - ApplyPadding(buffer, commandPaddingContinue, vc.userUUID, false) - } else { - vc.FilterTLS(buffer.Bytes()) - ApplyPadding(buffer, commandPaddingContinue, vc.userUUID, vc.isTLS) - } - err = vc.ExtendedWriter.WriteBuffer(buffer) - if err != nil { - buffer.Release() - return err - } - switch underlying := vc.tlsConn.(type) { - case *gotls.Conn: - if underlying.ConnectionState().Version != gotls.VersionTLS13 { - buffer.Release() - return ErrNotTLS13 - } - case *tlsC.UConn: - if underlying.ConnectionState().Version != tlsC.VersionTLS13 { - buffer.Release() - return ErrNotTLS13 - } - } - vc.tlsConn = nil - return nil - } - - if vc.writeFilterApplicationData { - buffer2 := ReshapeBuffer(buffer) - defer buffer2.Release() - vc.FilterTLS(buffer.Bytes()) - command := commandPaddingContinue - if !vc.isTLS { - command = commandPaddingEnd - - // disable XTLS - //vc.readProcess = false - vc.writeFilterApplicationData = false - vc.packetsToFilter = 0 - } else if buffer.Len() > 6 && bytes.Equal(buffer.To(3), tlsApplicationDataStart) || vc.packetsToFilter <= 0 { - command = commandPaddingEnd - if vc.enableXTLS { - command = commandPaddingDirect - vc.writeDirect = true - } - vc.writeFilterApplicationData = false - } - ApplyPadding(buffer, command, nil, vc.isTLS) - err = vc.ExtendedWriter.WriteBuffer(buffer) - if err != nil { - return err - } - if vc.writeDirect { - vc.ExtendedWriter = N.NewExtendedWriter(vc.Conn) - log.Debugln("XTLS Vision direct write start") - //time.Sleep(5 * time.Millisecond) - } - if buffer2 != nil { - if vc.writeDirect || !vc.isTLS { - return vc.ExtendedWriter.WriteBuffer(buffer2) - } - vc.FilterTLS(buffer2.Bytes()) - command = commandPaddingContinue - if buffer2.Len() > 6 && bytes.Equal(buffer2.To(3), tlsApplicationDataStart) || vc.packetsToFilter <= 0 { - command = commandPaddingEnd - if vc.enableXTLS { - command = commandPaddingDirect - vc.writeDirect = true - } - vc.writeFilterApplicationData = false - } - ApplyPadding(buffer2, command, nil, vc.isTLS) - err = vc.ExtendedWriter.WriteBuffer(buffer2) - if vc.writeDirect { - vc.ExtendedWriter = N.NewExtendedWriter(vc.Conn) - log.Debugln("XTLS Vision direct write start") - //time.Sleep(10 * time.Millisecond) - } - } - return err - } - /*if vc.writeDirect { - log.Debugln("XTLS Vision Direct write, payloadLen=%d", buffer.Len()) - }*/ - return vc.ExtendedWriter.WriteBuffer(buffer) -} - -func (vc *Conn) FrontHeadroom() int { - if vc.readFilterUUID { - return PaddingHeaderLen - } - return PaddingHeaderLen - uuid.Size -} - -func (vc *Conn) RearHeadroom() int { - return 500 + 900 -} - -func (vc *Conn) NeedHandshake() bool { - return vc.needHandshake -} - -func (vc *Conn) Upstream() any { - if vc.writeDirect || - vc.readLastCommand == commandPaddingDirect { - return vc.Conn - } - return vc.upstream -} - -func (vc *Conn) ReaderPossiblyReplaceable() bool { - return vc.readProcess -} - -func (vc *Conn) ReaderReplaceable() bool { - if !vc.readProcess && - vc.readLastCommand == commandPaddingDirect { - return true - } - return false -} - -func (vc *Conn) WriterPossiblyReplaceable() bool { - return vc.writeFilterApplicationData -} - -func (vc *Conn) WriterReplaceable() bool { - if vc.writeDirect { - return true - } - return false -} diff --git a/transport/vless/vision/filter.go b/transport/vless/vision/filter.go deleted file mode 100644 index 55b5663f6f..0000000000 --- a/transport/vless/vision/filter.go +++ /dev/null @@ -1,90 +0,0 @@ -package vision - -import ( - "bytes" - "encoding/binary" - - "github.com/metacubex/mihomo/log" -) - -var ( - tls13SupportedVersions = []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04} - tlsClientHandshakeStart = []byte{0x16, 0x03} - tlsServerHandshakeStart = []byte{0x16, 0x03, 0x03} - tlsApplicationDataStart = []byte{0x17, 0x03, 0x03} - - tls13CipherSuiteMap = map[uint16]string{ - 0x1301: "TLS_AES_128_GCM_SHA256", - 0x1302: "TLS_AES_256_GCM_SHA384", - 0x1303: "TLS_CHACHA20_POLY1305_SHA256", - 0x1304: "TLS_AES_128_CCM_SHA256", - 0x1305: "TLS_AES_128_CCM_8_SHA256", - } -) - -const ( - tlsHandshakeTypeClientHello byte = 0x01 - tlsHandshakeTypeServerHello byte = 0x02 -) - -func (vc *Conn) FilterTLS(buffer []byte) (index int) { - if vc.packetsToFilter <= 0 { - return 0 - } - lenP := len(buffer) - vc.packetsToFilter-- - if index = bytes.Index(buffer, tlsServerHandshakeStart); index != -1 { - if lenP > index+5 { - if buffer[0] == 22 && buffer[1] == 3 && buffer[2] == 3 { - vc.isTLS = true - if buffer[5] == tlsHandshakeTypeServerHello { - //log.Debugln("isTLS12orAbove") - vc.remainingServerHello = binary.BigEndian.Uint16(buffer[index+3:]) + 5 - vc.isTLS12orAbove = true - if lenP-index >= 79 && vc.remainingServerHello >= 79 { - sessionIDLen := int(buffer[index+43]) - vc.cipher = binary.BigEndian.Uint16(buffer[index+43+sessionIDLen+1:]) - } - } - } - } - } else if index = bytes.Index(buffer, tlsClientHandshakeStart); index != -1 { - if lenP > index+5 && buffer[index+5] == tlsHandshakeTypeClientHello { - vc.isTLS = true - } - } - - if vc.remainingServerHello > 0 { - end := int(vc.remainingServerHello) - i := index - if i < 0 { - i = 0 - } - if i+end > lenP { - end = lenP - vc.remainingServerHello -= uint16(end - i) - } else { - vc.remainingServerHello -= uint16(end) - end += i - } - if bytes.Contains(buffer[i:end], tls13SupportedVersions) { - // TLS 1.3 Client Hello - cs, ok := tls13CipherSuiteMap[vc.cipher] - if ok && cs != "TLS_AES_128_CCM_8_SHA256" { - vc.enableXTLS = true - } - log.Debugln("XTLS Vision found TLS 1.3, packetLength=%d, CipherSuite=%s", lenP, cs) - vc.packetsToFilter = 0 - return - } else if vc.remainingServerHello <= 0 { - log.Debugln("XTLS Vision found TLS 1.2, packetLength=%d", lenP) - vc.packetsToFilter = 0 - return - } - log.Debugln("XTLS Vision found inconclusive server hello, packetLength=%d, remainingServerHelloBytes=%d", lenP, vc.remainingServerHello) - } - if vc.packetsToFilter <= 0 { - log.Debugln("XTLS Vision stop filtering") - } - return -} diff --git a/transport/vless/vision/padding.go b/transport/vless/vision/padding.go deleted file mode 100644 index dd9cb261a3..0000000000 --- a/transport/vless/vision/padding.go +++ /dev/null @@ -1,81 +0,0 @@ -package vision - -import ( - "bytes" - "encoding/binary" - - "github.com/metacubex/mihomo/common/buf" - "github.com/metacubex/mihomo/log" - - "github.com/gofrs/uuid/v5" - "github.com/metacubex/randv2" -) - -const ( - PaddingHeaderLen = uuid.Size + 1 + 2 + 2 // =21 - - commandPaddingContinue byte = 0x00 - commandPaddingEnd byte = 0x01 - commandPaddingDirect byte = 0x02 -) - -func WriteWithPadding(buffer *buf.Buffer, p []byte, command byte, userUUID *uuid.UUID, paddingTLS bool) { - contentLen := int32(len(p)) - var paddingLen int32 - if contentLen < 900 { - if paddingTLS { - //log.Debugln("long padding") - paddingLen = randv2.Int32N(500) + 900 - contentLen - } else { - paddingLen = randv2.Int32N(256) - } - } - if userUUID != nil { - buffer.Write(userUUID.Bytes()) - } - - buffer.WriteByte(command) - binary.BigEndian.PutUint16(buffer.Extend(2), uint16(contentLen)) - binary.BigEndian.PutUint16(buffer.Extend(2), uint16(paddingLen)) - buffer.Write(p) - - buffer.Extend(int(paddingLen)) - log.Debugln("XTLS Vision write padding1: command=%v, payloadLen=%v, paddingLen=%v", command, contentLen, paddingLen) -} - -func ApplyPadding(buffer *buf.Buffer, command byte, userUUID *uuid.UUID, paddingTLS bool) { - contentLen := int32(buffer.Len()) - var paddingLen int32 - if contentLen < 900 { - if paddingTLS { - //log.Debugln("long padding") - paddingLen = randv2.Int32N(500) + 900 - contentLen - } else { - paddingLen = randv2.Int32N(256) - } - } - - binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(paddingLen)) - binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(contentLen)) - buffer.ExtendHeader(1)[0] = command - if userUUID != nil { - copy(buffer.ExtendHeader(uuid.Size), userUUID.Bytes()) - } - - buffer.Extend(int(paddingLen)) - log.Debugln("XTLS Vision write padding2: command=%d, payloadLen=%d, paddingLen=%d", command, contentLen, paddingLen) -} - -func ReshapeBuffer(buffer *buf.Buffer) *buf.Buffer { - if buffer.Len() <= buf.BufferSize-PaddingHeaderLen { - return nil - } - cutAt := bytes.LastIndex(buffer.Bytes(), tlsApplicationDataStart) - if cutAt == -1 { - cutAt = buf.BufferSize / 2 - } - buffer2 := buf.New() - buffer2.Write(buffer.From(cutAt)) - buffer.Truncate(cutAt) - return buffer2 -} diff --git a/transport/vless/vision/vision.go b/transport/vless/vision/vision.go deleted file mode 100644 index 28fd2fceaf..0000000000 --- a/transport/vless/vision/vision.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package vision implements VLESS flow `xtls-rprx-vision` introduced by Xray-core. -package vision - -import ( - "bytes" - gotls "crypto/tls" - "errors" - "fmt" - "net" - "reflect" - "unsafe" - - N "github.com/metacubex/mihomo/common/net" - tlsC "github.com/metacubex/mihomo/component/tls" - - "github.com/gofrs/uuid/v5" - "github.com/metacubex/sing/common" -) - -var ErrNotTLS13 = errors.New("XTLS Vision based on TLS 1.3 outer connection") - -type connWithUpstream interface { - net.Conn - common.WithUpstream -} - -func NewConn(conn connWithUpstream, userUUID *uuid.UUID) (*Conn, error) { - c := &Conn{ - ExtendedReader: N.NewExtendedReader(conn), - ExtendedWriter: N.NewExtendedWriter(conn), - upstream: conn, - userUUID: userUUID, - packetsToFilter: 6, - needHandshake: true, - readProcess: true, - readFilterUUID: true, - writeFilterApplicationData: true, - } - var t reflect.Type - var p unsafe.Pointer - switch underlying := conn.Upstream().(type) { - case *gotls.Conn: - //log.Debugln("type tls") - c.Conn = underlying.NetConn() - c.tlsConn = underlying - t = reflect.TypeOf(underlying).Elem() - p = unsafe.Pointer(underlying) - case *tlsC.UConn: - //log.Debugln("type *tlsC.UConn") - c.Conn = underlying.NetConn() - c.tlsConn = underlying - t = reflect.TypeOf(underlying.Conn).Elem() - //log.Debugln("t:%v", t) - p = unsafe.Pointer(underlying.Conn) - default: - return nil, fmt.Errorf(`failed to use vision, maybe "security" is not "tls" or "utls"`) - } - i, _ := t.FieldByName("input") - r, _ := t.FieldByName("rawInput") - c.input = (*bytes.Reader)(unsafe.Add(p, i.Offset)) - c.rawInput = (*bytes.Buffer)(unsafe.Add(p, r.Offset)) - return c, nil -} diff --git a/transport/vless/vless.go b/transport/vless/vless.go deleted file mode 100644 index ce07cdb415..0000000000 --- a/transport/vless/vless.go +++ /dev/null @@ -1,65 +0,0 @@ -package vless - -import ( - "net" - - "github.com/metacubex/mihomo/common/utils" - - "github.com/gofrs/uuid/v5" -) - -const ( - XRO = "xtls-rprx-origin" - XRD = "xtls-rprx-direct" - XRS = "xtls-rprx-splice" - XRV = "xtls-rprx-vision" - - Version byte = 0 // protocol version. preview version is 0 -) - -// Command types -const ( - CommandTCP byte = 1 - CommandUDP byte = 2 - CommandMux byte = 3 -) - -// 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 uint16 - Mux bool // currently used for XUDP only -} - -// Client is vless connection generator -type Client struct { - uuid *uuid.UUID - Addons *Addons -} - -// StreamConn return a Conn with net.Conn and DstAddr -func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) { - return newConn(conn, c, dst) -} - -// NewClient return Client instance -func NewClient(uuidStr string, addons *Addons) (*Client, error) { - uid, err := utils.UUIDMap(uuidStr) - if err != nil { - return nil, err - } - - return &Client{ - uuid: &uid, - Addons: addons, - }, nil -} diff --git a/transport/vmess/aead.go b/transport/vmess/aead.go deleted file mode 100644 index 89ec6a3b70..0000000000 --- a/transport/vmess/aead.go +++ /dev/null @@ -1,124 +0,0 @@ -package vmess - -import ( - "crypto/cipher" - "encoding/binary" - "errors" - "io" - "sync" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index f52fc82cf5..0000000000 --- a/transport/vmess/chunk.go +++ /dev/null @@ -1,102 +0,0 @@ -package vmess - -import ( - "encoding/binary" - "errors" - "io" - - "github.com/metacubex/mihomo/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 deleted file mode 100644 index b65a447d33..0000000000 --- a/transport/vmess/conn.go +++ /dev/null @@ -1,273 +0,0 @@ -package vmess - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/hmac" - "crypto/md5" - "crypto/rand" - "crypto/sha256" - "encoding/binary" - "errors" - "hash/fnv" - "io" - "net" - "time" - - "github.com/metacubex/randv2" - "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 - 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 := &bytes.Buffer{} - - if !vc.isAead { - h := hmac.New(md5.New, vc.id.UUID.Bytes()) - binary.Write(h, binary.BigEndian, uint64(timestamp.Unix())) - mbuf.Write(h.Sum(nil)) - } - - buf := &bytes.Buffer{} - - // Ver IV Key V Opt - buf.WriteByte(Version) - buf.Write(vc.reqBodyIV[:]) - buf.Write(vc.reqBodyKey[:]) - buf.WriteByte(vc.respV) - buf.WriteByte(OptionChunkStream) - - p := randv2.IntN(16) - // P Sec Reserve Cmd - buf.WriteByte(byte(p<<4) | byte(vc.security)) - buf.WriteByte(0) - if vc.dst.UDP { - buf.WriteByte(CommandUDP) - } else { - buf.WriteByte(CommandTCP) - } - - // Port AddrType Addr - binary.Write(buf, binary.BigEndian, uint16(vc.dst.Port)) - buf.WriteByte(vc.dst.AddrType) - buf.Write(vc.dst.Addr) - - // padding - if p > 0 { - padding := make([]byte, p) - rand.Read(padding) - buf.Write(padding) - } - - fnv1a := fnv.New32a() - fnv1a.Write(buf.Bytes()) - buf.Write(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.Write(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] - - 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 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, - 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 deleted file mode 100644 index 5ad24f3d27..0000000000 --- a/transport/vmess/h2.go +++ /dev/null @@ -1,123 +0,0 @@ -package vmess - -import ( - "context" - "io" - "net" - "net/http" - "net/url" - - N "github.com/metacubex/mihomo/common/net" - - "github.com/metacubex/randv2" - "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[randv2.IntN(len(hc.cfg.Hosts))] - path := hc.cfg.Path - // TODO: connect use VMess Host instead of H2 Host - req := http.Request{ - Method: "PUT", - 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 hc.pwriter != nil { - if err := hc.pwriter.Close(); err != nil { - return err - } - } - ctx := context.Background() - if hc.res != nil { - ctx = hc.res.Request.Context() - } - if err := hc.ClientConn.Shutdown(ctx); err != nil { - return err - } - return hc.Conn.Close() -} - -func StreamH2Conn(ctx context.Context, conn net.Conn, cfg *H2Config) (_ net.Conn, err error) { - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, conn) - defer done(&err) - } - - 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 deleted file mode 100644 index 67d407e6f1..0000000000 --- a/transport/vmess/header.go +++ /dev/null @@ -1,103 +0,0 @@ -package vmess - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/hmac" - "crypto/rand" - "crypto/sha256" - "encoding/binary" - "hash" - "hash/crc32" - "time" -) - -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 := &bytes.Buffer{} - binary.Write(buf, binary.BigEndian, time) - - random := make([]byte, 4) - rand.Read(random) - buf.Write(random) - zero := crc32.ChecksumIEEE(buf.Bytes()) - binary.Write(buf, binary.BigEndian, 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 := &bytes.Buffer{} - - outputBuffer.Write(generatedAuthID[:]) - outputBuffer.Write(payloadHeaderLengthAEADEncrypted) - outputBuffer.Write(connectionNonce) - outputBuffer.Write(payloadHeaderAEADEncrypted) - - return outputBuffer.Bytes() -} diff --git a/transport/vmess/http.go b/transport/vmess/http.go deleted file mode 100644 index 3c66fe6b24..0000000000 --- a/transport/vmess/http.go +++ /dev/null @@ -1,93 +0,0 @@ -package vmess - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "net" - "net/http" - "net/textproto" - - "github.com/metacubex/mihomo/common/utils" - - "github.com/metacubex/randv2" -) - -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) - } - - if len(hc.cfg.Path) == 0 { - return -1, errors.New("path is empty") - } - - path := hc.cfg.Path[randv2.IntN(len(hc.cfg.Path))] - host := hc.cfg.Host - if header := hc.cfg.Headers["Host"]; len(header) != 0 { - host = header[randv2.IntN(len(header))] - } - - u := fmt.Sprintf("http://%s%s", net.JoinHostPort(host, "80"), path) - req, err := http.NewRequest(utils.EmptyOr(hc.cfg.Method, http.MethodGet), u, bytes.NewBuffer(b)) - if err != nil { - return 0, err - } - for key, list := range hc.cfg.Headers { - req.Header.Set(key, list[randv2.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 deleted file mode 100644 index 588c159aa8..0000000000 --- a/transport/vmess/tls.go +++ /dev/null @@ -1,55 +0,0 @@ -package vmess - -import ( - "context" - "crypto/tls" - "errors" - "net" - - "github.com/metacubex/mihomo/component/ca" - tlsC "github.com/metacubex/mihomo/component/tls" -) - -type TLSConfig struct { - Host string - SkipCertVerify bool - FingerPrint string - ClientFingerprint string - NextProtos []string - Reality *tlsC.RealityConfig -} - -func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) { - tlsConfig := &tls.Config{ - ServerName: cfg.Host, - InsecureSkipVerify: cfg.SkipCertVerify, - NextProtos: cfg.NextProtos, - } - - var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, cfg.FingerPrint) - if err != nil { - return nil, err - } - - if clientFingerprint, ok := tlsC.GetFingerprint(cfg.ClientFingerprint); ok { - if cfg.Reality == nil { - tlsConn := tlsC.UClient(conn, tlsC.UConfig(tlsConfig), clientFingerprint) - err = tlsConn.HandshakeContext(ctx) - if err != nil { - return nil, err - } - return tlsConn, nil - } else { - return tlsC.GetRealityConn(ctx, conn, clientFingerprint, tlsConfig, cfg.Reality) - } - } - if cfg.Reality != nil { - return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint") - } - - tlsConn := tls.Client(conn, tlsConfig) - - err = tlsConn.HandshakeContext(ctx) - return tlsConn, err -} diff --git a/transport/vmess/user.go b/transport/vmess/user.go deleted file mode 100644 index 091df0a889..0000000000 --- a/transport/vmess/user.go +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 22c77ab7d5..0000000000 --- a/transport/vmess/vmess.go +++ /dev/null @@ -1,115 +0,0 @@ -package vmess - -import ( - "fmt" - "net" - "runtime" - - "github.com/metacubex/mihomo/common/utils" - - "github.com/gofrs/uuid/v5" - "github.com/metacubex/randv2" -) - -// 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 -) - -// CipherMapping return -var CipherMapping = map[string]byte{ - "none": SecurityNone, - "aes-128-gcm": SecurityAES128GCM, - "chacha20-poly1305": SecurityCHACHA20POLY1305, -} - -// 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 := randv2.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 := utils.UUIDMap(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 "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 deleted file mode 100644 index 7e8886b6b7..0000000000 --- a/transport/vmess/websocket.go +++ /dev/null @@ -1,594 +0,0 @@ -package vmess - -import ( - "bufio" - "bytes" - "context" - "crypto/rand" - "crypto/sha1" - "crypto/tls" - "encoding/base64" - "encoding/binary" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/metacubex/mihomo/common/buf" - N "github.com/metacubex/mihomo/common/net" - tlsC "github.com/metacubex/mihomo/component/tls" - "github.com/metacubex/mihomo/log" - - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" - "github.com/metacubex/randv2" -) - -type websocketConn struct { - net.Conn - state ws.State - reader *wsutil.Reader - controlHandler wsutil.FrameHandlerFunc - - rawWriter N.ExtendedWriter -} - -type websocketWithEarlyDataConn struct { - net.Conn - wsWriter N.ExtendedWriter - 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 - ClientFingerprint string - V2rayHttpUpgrade bool - V2rayHttpUpgradeFastOpen bool -} - -// Read implements net.Conn.Read() -// modify from gobwas/ws/wsutil.readData -func (wsc *websocketConn) Read(b []byte) (n int, err error) { - defer func() { // avoid gobwas/ws pbytes.GetLen panic - if value := recover(); value != nil { - err = fmt.Errorf("websocket error: %s", value) - } - }() - var header ws.Header - for { - n, err = wsc.reader.Read(b) - // in gobwas/ws: "The error is io.EOF only if all of message bytes were read." - // but maybe next frame still have data, so drop it - if errors.Is(err, io.EOF) { - err = nil - } - if !errors.Is(err, wsutil.ErrNoFrameAdvance) { - return - } - header, err = wsc.reader.NextFrame() - if err != nil { - return - } - if header.OpCode.IsControl() { - err = wsc.controlHandler(header, wsc.reader) - if err != nil { - return - } - continue - } - if header.OpCode&(ws.OpBinary|ws.OpText) == 0 { - err = wsc.reader.Discard() - if err != nil { - return - } - continue - } - } -} - -// Write implements io.Writer. -func (wsc *websocketConn) Write(b []byte) (n int, err error) { - err = wsutil.WriteMessage(wsc.Conn, wsc.state, ws.OpBinary, b) - if err != nil { - return - } - n = len(b) - return -} - -func (wsc *websocketConn) WriteBuffer(buffer *buf.Buffer) error { - var payloadBitLength int - dataLen := buffer.Len() - data := buffer.Bytes() - if dataLen < 126 { - payloadBitLength = 1 - } else if dataLen < 65536 { - payloadBitLength = 3 - } else { - payloadBitLength = 9 - } - - var headerLen int - headerLen += 1 // FIN / RSV / OPCODE - headerLen += payloadBitLength - if wsc.state.ClientSide() { - headerLen += 4 // MASK KEY - } - - header := buffer.ExtendHeader(headerLen) - header[0] = byte(ws.OpBinary) | 0x80 - if wsc.state.ClientSide() { - header[1] = 1 << 7 - } else { - header[1] = 0 - } - - if dataLen < 126 { - header[1] |= byte(dataLen) - } else if dataLen < 65536 { - header[1] |= 126 - binary.BigEndian.PutUint16(header[2:], uint16(dataLen)) - } else { - header[1] |= 127 - binary.BigEndian.PutUint64(header[2:], uint64(dataLen)) - } - - if wsc.state.ClientSide() { - maskKey := randv2.Uint32() - binary.LittleEndian.PutUint32(header[1+payloadBitLength:], maskKey) - N.MaskWebSocket(maskKey, data) - } - - return wsc.rawWriter.WriteBuffer(buffer) -} - -func (wsc *websocketConn) FrontHeadroom() int { - return 14 -} - -func (wsc *websocketConn) Upstream() any { - return wsc.Conn -} - -func (wsc *websocketConn) Close() error { - _ = wsc.Conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) - _ = wsutil.WriteMessage(wsc.Conn, wsc.state, ws.OpClose, ws.NewCloseFrameBody(ws.StatusNormalClosure, "")) - _ = wsc.Conn.Close() - return nil -} - -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 fmt.Errorf("failed to encode early data: %w", err) - } - - if errc := base64EarlyDataEncoder.Close(); errc != nil { - return fmt.Errorf("failed to encode early data tail: %w", errc) - } - - var err error - if wsedc.Conn, err = streamWebsocketConn(wsedc.ctx, wsedc.underlay, wsedc.config, base64DataBuf); err != nil { - wsedc.Close() - return fmt.Errorf("failed to dial WebSocket: %w", err) - } - - wsedc.dialed <- true - wsedc.wsWriter = N.NewExtendedWriter(wsedc.Conn) - 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) WriteBuffer(buffer *buf.Buffer) error { - if wsedc.closed { - return io.ErrClosedPipe - } - if wsedc.Conn == nil { - if err := wsedc.Dial(buffer.Bytes()); err != nil { - return err - } - return nil - } - - return wsedc.wsWriter.WriteBuffer(buffer) -} - -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 { // is dialing or not dialed - return wsedc.underlay.Close() - } - 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 (wsedc *websocketWithEarlyDataConn) FrontHeadroom() int { - return 14 -} - -func (wsedc *websocketWithEarlyDataConn) Upstream() any { - return wsedc.underlay -} - -//func (wsedc *websocketWithEarlyDataConn) LazyHeadroom() bool { -// return wsedc.Conn == nil -//} -// -//func (wsedc *websocketWithEarlyDataConn) Upstream() any { -// if wsedc.Conn == nil { // ensure return a nil interface not an interface with nil value -// return nil -// } -// return wsedc.Conn -//} - -func (wsedc *websocketWithEarlyDataConn) NeedHandshake() bool { - return wsedc.Conn == nil -} - -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, - } - // websocketWithEarlyDataConn can't correct handle Deadline - // it will not apply the already set Deadline after Dial() - // so call N.NewDeadlineConn to add a safe wrapper - return N.NewDeadlineConn(conn), nil -} - -func streamWebsocketConn(ctx context.Context, conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buffer) (_ net.Conn, err error) { - 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: "ws", - Host: net.JoinHostPort(c.Host, c.Port), - Path: u.Path, - RawQuery: u.RawQuery, - } - - if !strings.HasPrefix(uri.Path, "/") { - uri.Path = "/" + uri.Path - } - - if c.TLS { - uri.Scheme = "wss" - config := c.TLSConfig - if config == nil { // The config cannot be nil - config = &tls.Config{NextProtos: []string{"http/1.1"}} - } - if config.ServerName == "" && !config.InsecureSkipVerify { // users must set either ServerName or InsecureSkipVerify in the config. - config = config.Clone() - config.ServerName = c.Host - } - - if clientFingerprint, ok := tlsC.GetFingerprint(c.ClientFingerprint); ok { - tlsConn := tlsC.UClient(conn, tlsC.UConfig(config), clientFingerprint) - if err = tlsC.BuildWebsocketHandshakeState(tlsConn); err != nil { - return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) - } - err = tlsConn.HandshakeContext(ctx) - if err != nil { - return nil, err - } - conn = tlsConn - } else { - tlsConn := tls.Client(conn, config) - err = tlsConn.HandshakeContext(ctx) - if err != nil { - return nil, err - } - conn = tlsConn - } - } - - request := &http.Request{ - Method: http.MethodGet, - URL: &uri, - Header: c.Headers.Clone(), - Host: c.Host, - } - - request.Header.Set("Connection", "Upgrade") - request.Header.Set("Upgrade", "websocket") - - if host := request.Header.Get("Host"); host != "" { - // For client requests, Host optionally overrides the Host - // header to send. If empty, the Request.Write method uses - // the value of URL.Host. Host may contain an international - // domain name. - request.Host = host - } - request.Header.Del("Host") - - var secKey string - if !c.V2rayHttpUpgrade { - const nonceKeySize = 16 - // NOTE: bts does not escape. - bts := make([]byte, nonceKeySize) - if _, err = rand.Read(bts); err != nil { - return nil, fmt.Errorf("rand read error: %w", err) - } - secKey = base64.StdEncoding.EncodeToString(bts) - request.Header.Set("Sec-WebSocket-Version", "13") - request.Header.Set("Sec-WebSocket-Key", secKey) - } - - if earlyData != nil { - earlyDataString := earlyData.String() - if c.EarlyDataHeaderName == "" { - uri.Path += earlyDataString - } else { - request.Header.Set(c.EarlyDataHeaderName, earlyDataString) - } - } - - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, conn) - defer done(&err) - } - - err = request.Write(conn) - if err != nil { - return nil, err - } - bufferedConn := N.NewBufferedConn(conn) - - if c.V2rayHttpUpgrade && c.V2rayHttpUpgradeFastOpen { - return N.NewEarlyConn(bufferedConn, func() error { - response, err := http.ReadResponse(bufferedConn.Reader(), request) - if err != nil { - return err - } - if response.StatusCode != http.StatusSwitchingProtocols || - !strings.EqualFold(response.Header.Get("Connection"), "upgrade") || - !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") { - return fmt.Errorf("unexpected status: %s", response.Status) - } - return nil - }), nil - } - - response, err := http.ReadResponse(bufferedConn.Reader(), request) - if err != nil { - return nil, err - } - if response.StatusCode != http.StatusSwitchingProtocols || - !strings.EqualFold(response.Header.Get("Connection"), "upgrade") || - !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") { - return nil, fmt.Errorf("unexpected status: %s", response.Status) - } - - if c.V2rayHttpUpgrade { - return bufferedConn, nil - } - - if log.Level() == log.DEBUG { // we might not check this for performance - secAccept := response.Header.Get("Sec-Websocket-Accept") - const acceptSize = 28 // base64.StdEncoding.EncodedLen(sha1.Size) - if lenSecAccept := len(secAccept); lenSecAccept != acceptSize { - return nil, fmt.Errorf("unexpected Sec-Websocket-Accept length: %d", lenSecAccept) - } - if getSecAccept(secKey) != secAccept { - return nil, errors.New("unexpected Sec-Websocket-Accept") - } - } - - conn = newWebsocketConn(bufferedConn, ws.StateClientSide) - // websocketConn can't correct handle ReadDeadline - // so call N.NewDeadlineConn to add a safe wrapper - return N.NewDeadlineConn(conn), nil -} - -func getSecAccept(secKey string) string { - const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - const nonceSize = 24 // base64.StdEncoding.EncodedLen(nonceKeySize) - p := make([]byte, nonceSize+len(magic)) - copy(p[:nonceSize], secKey) - copy(p[nonceSize:], magic) - sum := sha1.Sum(p) - return base64.StdEncoding.EncodeToString(sum[:]) -} - -func StreamWebsocketConn(ctx context.Context, 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(ctx, conn, c, nil) -} - -func newWebsocketConn(conn net.Conn, state ws.State) *websocketConn { - controlHandler := wsutil.ControlFrameHandler(conn, state) - return &websocketConn{ - Conn: conn, - state: state, - reader: &wsutil.Reader{ - Source: conn, - State: state, - SkipHeaderCheck: true, - CheckUTF8: false, - OnIntermediate: controlHandler, - }, - controlHandler: controlHandler, - rawWriter: N.NewExtendedWriter(conn), - } -} - -var replacer = strings.NewReplacer("+", "-", "/", "_", "=", "") - -func decodeEd(s string) ([]byte, error) { - return base64.RawURLEncoding.DecodeString(replacer.Replace(s)) -} - -func decodeXray0rtt(requestHeader http.Header) []byte { - // read inHeader's `Sec-WebSocket-Protocol` for Xray's 0rtt ws - if secProtocol := requestHeader.Get("Sec-WebSocket-Protocol"); len(secProtocol) > 0 { - if edBuf, err := decodeEd(secProtocol); err == nil { // sure could base64 decode - return edBuf - } - } - return nil -} - -func IsWebSocketUpgrade(r *http.Request) bool { - return r.Header.Get("Upgrade") == "websocket" -} - -func IsV2rayHttpUpdate(r *http.Request) bool { - return IsWebSocketUpgrade(r) && r.Header.Get("Sec-WebSocket-Key") == "" -} - -func StreamUpgradedWebsocketConn(w http.ResponseWriter, r *http.Request) (net.Conn, error) { - var conn net.Conn - var rw *bufio.ReadWriter - var err error - isRaw := IsV2rayHttpUpdate(r) - w.Header().Set("Connection", "upgrade") - w.Header().Set("Upgrade", "websocket") - if !isRaw { - w.Header().Set("Sec-Websocket-Accept", getSecAccept(r.Header.Get("Sec-WebSocket-Key"))) - } - w.WriteHeader(http.StatusSwitchingProtocols) - if flusher, isFlusher := w.(interface{ FlushError() error }); isFlusher && writeHeaderShouldFlush { - err = flusher.FlushError() - if err != nil { - return nil, fmt.Errorf("flush response: %w", err) - } - } - hijacker, canHijack := w.(http.Hijacker) - if !canHijack { - return nil, errors.New("invalid connection, maybe HTTP/2") - } - conn, rw, err = hijacker.Hijack() - if err != nil { - return nil, fmt.Errorf("hijack failed: %w", err) - } - - // rw.Writer was flushed, so we only need warp rw.Reader - conn = N.WarpConnWithBioReader(conn, rw.Reader) - - if !isRaw { - conn = newWebsocketConn(conn, ws.StateServerSide) - // websocketConn can't correct handle ReadDeadline - // so call N.NewDeadlineConn to add a safe wrapper - conn = N.NewDeadlineConn(conn) - } - - if edBuf := decodeXray0rtt(r.Header); len(edBuf) > 0 { - appendOk := false - if bufConn, ok := conn.(*N.BufferedConn); ok { - appendOk = bufConn.AppendData(edBuf) - } - if !appendOk { - conn = N.NewCachedConn(conn, edBuf) - } - - } - - return conn, nil -} diff --git a/transport/vmess/websocket_go120.go b/transport/vmess/websocket_go120.go deleted file mode 100644 index 0b9ff5e3d6..0000000000 --- a/transport/vmess/websocket_go120.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !go1.21 - -package vmess - -// Golang1.20's net.http Flush will mistakenly call w.WriteHeader(StatusOK) internally after w.WriteHeader(http.StatusSwitchingProtocols) -// https://github.com/golang/go/issues/59564 -const writeHeaderShouldFlush = false diff --git a/transport/vmess/websocket_go121.go b/transport/vmess/websocket_go121.go deleted file mode 100644 index 9419cd50a3..0000000000 --- a/transport/vmess/websocket_go121.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build go1.21 - -package vmess - -const writeHeaderShouldFlush = true diff --git a/tunnel/connection.go b/tunnel/connection.go deleted file mode 100644 index 1ea0678cd7..0000000000 --- a/tunnel/connection.go +++ /dev/null @@ -1,181 +0,0 @@ -package tunnel - -import ( - "context" - "errors" - "net" - "net/netip" - "time" - - "github.com/metacubex/mihomo/common/lru" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type packetSender struct { - ctx context.Context - cancel context.CancelFunc - ch chan C.PacketAdapter - cache *lru.LruCache[string, netip.Addr] -} - -// newPacketSender return a chan based C.PacketSender -// It ensures that packets can be sent sequentially and without blocking -func newPacketSender() C.PacketSender { - ctx, cancel := context.WithCancel(context.Background()) - ch := make(chan C.PacketAdapter, senderCapacity) - return &packetSender{ - ctx: ctx, - cancel: cancel, - ch: ch, - cache: lru.New[string, netip.Addr](lru.WithSize[string, netip.Addr](senderCapacity)), - } -} - -func (s *packetSender) Process(pc C.PacketConn, proxy C.WriteBackProxy) { - for { - select { - case <-s.ctx.Done(): - return // sender closed - case packet := <-s.ch: - if proxy != nil { - proxy.UpdateWriteBack(packet) - } - if err := s.ResolveUDP(packet.Metadata()); err != nil { - log.Warnln("[UDP] Resolve Ip error: %s", err) - } else { - _ = handleUDPToRemote(packet, pc, packet.Metadata()) - } - packet.Drop() - } - } -} - -func (s *packetSender) dropAll() { - for { - select { - case data := <-s.ch: - data.Drop() // drop all data still in chan - default: - return // no data, exit goroutine - } - } -} - -func (s *packetSender) Send(packet C.PacketAdapter) { - select { - case <-s.ctx.Done(): - packet.Drop() // sender closed before Send() - return - default: - } - - select { - case s.ch <- packet: - // put ok, so don't drop packet, will process by other side of chan - case <-s.ctx.Done(): - packet.Drop() // sender closed when putting data to chan - default: - packet.Drop() // chan is full - } -} - -func (s *packetSender) Close() { - s.cancel() - s.dropAll() -} - -func (s *packetSender) ResolveUDP(metadata *C.Metadata) (err error) { - // local resolve UDP dns - if !metadata.Resolved() { - ip, ok := s.cache.Get(metadata.Host) - if !ok { - ip, err = resolver.ResolveIP(s.ctx, metadata.Host) - if err != nil { - return err - } - s.cache.Set(metadata.Host, ip) - } - - metadata.DstIP = ip - } - return nil -} - -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(writeBack C.WriteBack, pc N.EnhancePacketConn, sender C.PacketSender, key string, oAddrPort netip.AddrPort, fAddr netip.Addr) { - defer func() { - sender.Close() - _ = pc.Close() - closeAllLocalCoon(key) - natTable.Delete(key) - }() - - for { - _ = pc.SetReadDeadline(time.Now().Add(udpTimeout)) - data, put, from, err := pc.WaitReadFrom() - if err != nil { - return - } - - fromUDPAddr, isUDPAddr := from.(*net.UDPAddr) - if !isUDPAddr { - fromUDPAddr = net.UDPAddrFromAddrPort(oAddrPort) // oAddrPort was Unmapped - log.Warnln("server return a [%T](%s) which isn't a *net.UDPAddr, force replace to (%s), this may be caused by a wrongly implemented server", from, from, oAddrPort) - } else if fromUDPAddr == nil { - fromUDPAddr = net.UDPAddrFromAddrPort(oAddrPort) // oAddrPort was Unmapped - log.Warnln("server return a nil *net.UDPAddr, force replace to (%s), this may be caused by a wrongly implemented server", oAddrPort) - } else { - _fromUDPAddr := *fromUDPAddr - fromUDPAddr = &_fromUDPAddr // make a copy - if fromAddr, ok := netip.AddrFromSlice(fromUDPAddr.IP); ok { - fromAddr = fromAddr.Unmap() - if fAddr.IsValid() && (oAddrPort.Addr() == fromAddr) { // oAddrPort was Unmapped - fromAddr = fAddr.Unmap() - } - fromUDPAddr.IP = fromAddr.AsSlice() - if fromAddr.Is4() { - fromUDPAddr.Zone = "" // only ipv6 can have the zone - } - } - } - - _, err = writeBack.WriteBack(data, fromUDPAddr) - if put != nil { - put() - } - if err != nil { - return - } - } -} - -func closeAllLocalCoon(lAddr string) { - natTable.RangeForLocalConn(lAddr, func(key string, value *net.UDPConn) bool { - conn := value - - conn.Close() - log.Debugln("Closing TProxy local conn... lAddr=%s rAddr=%s", lAddr, key) - return true - }) -} - -func handleSocket(inbound, outbound net.Conn) { - N.Relay(inbound, outbound) -} diff --git a/tunnel/dns_dialer.go b/tunnel/dns_dialer.go deleted file mode 100644 index c141d96648..0000000000 --- a/tunnel/dns_dialer.go +++ /dev/null @@ -1,185 +0,0 @@ -package tunnel - -// WARNING: all function in this file should only be using in dns module - -import ( - "context" - "fmt" - "net" - "strings" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/tunnel/statistic" -) - -const DnsRespectRules = "RULES" - -type DNSDialer struct { - r resolver.Resolver - proxyAdapter C.ProxyAdapter - proxyName string -} - -func NewDNSDialer(r resolver.Resolver, proxyAdapter C.ProxyAdapter, proxyName string) *DNSDialer { - return &DNSDialer{r: r, proxyAdapter: proxyAdapter, proxyName: proxyName} -} - -func (d *DNSDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { - r := d.r - proxyName := d.proxyName - proxyAdapter := d.proxyAdapter - var opts []dialer.Option - var rule C.Rule - metadata := &C.Metadata{ - NetWork: C.TCP, - Type: C.INNER, - } - err := metadata.SetRemoteAddress(addr) // tcp can resolve host by remote - if err != nil { - return nil, err - } - if !strings.Contains(network, "tcp") { - metadata.NetWork = C.UDP - if !metadata.Resolved() { - // udp must resolve host first - dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) - if err != nil { - return nil, err - } - metadata.DstIP = dstIP - } - } - - if proxyAdapter == nil && len(proxyName) != 0 { - if proxyName == DnsRespectRules { - if !metadata.Resolved() { - // resolve here before resolveMetadata to avoid its inner resolver.ResolveIP - dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) - if err != nil { - return nil, err - } - metadata.DstIP = dstIP - } - proxyAdapter, rule, err = resolveMetadata(metadata) - if err != nil { - return nil, err - } - } else { - var ok bool - proxyAdapter, ok = Proxies()[proxyName] - if !ok { - opts = append(opts, dialer.WithInterface(proxyName)) - } - } - } - - if metadata.NetWork == C.TCP { - if proxyAdapter == nil { - opts = append(opts, dialer.WithResolver(r)) - return dialer.DialContext(ctx, network, addr, opts...) - } - - if proxyAdapter.IsL3Protocol(metadata) { // L3 proxy should resolve domain before to avoid loopback - if !metadata.Resolved() { - dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) - if err != nil { - return nil, err - } - metadata.DstIP = dstIP - } - metadata.Host = "" // clear host to avoid double resolve in proxy - } - - conn, err := proxyAdapter.DialContext(ctx, metadata) - if err != nil { - logMetadataErr(metadata, rule, proxyAdapter, err) - return nil, err - } - logMetadata(metadata, rule, conn) - - conn = statistic.NewTCPTracker(conn, statistic.DefaultManager, metadata, rule, 0, 0, false) - - return conn, nil - } else { - if proxyAdapter == nil { - return dialer.DialContext(ctx, network, metadata.AddrPort().String(), opts...) - } - - if !proxyAdapter.SupportUDP() { - return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) - } - - packetConn, err := proxyAdapter.ListenPacketContext(ctx, metadata) - if err != nil { - logMetadataErr(metadata, rule, proxyAdapter, err) - return nil, err - } - logMetadata(metadata, rule, packetConn) - - packetConn = statistic.NewUDPTracker(packetConn, statistic.DefaultManager, metadata, rule, 0, 0, false) - - return N.NewBindPacketConn(packetConn, metadata.UDPAddr()), nil - } - -} - -func (d *DNSDialer) ListenPacket(ctx context.Context, network, addr string) (net.PacketConn, error) { - r := d.r - proxyAdapter := d.proxyAdapter - proxyName := d.proxyName - var opts []dialer.Option - metadata := &C.Metadata{ - NetWork: C.UDP, - Type: C.INNER, - } - err := metadata.SetRemoteAddress(addr) - if err != nil { - return nil, err - } - if !metadata.Resolved() { - // udp must resolve host first - dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) - if err != nil { - return nil, err - } - metadata.DstIP = dstIP - } - - var rule C.Rule - if proxyAdapter == nil { - if proxyName == DnsRespectRules { - proxyAdapter, rule, err = resolveMetadata(metadata) - if err != nil { - return nil, err - } - } else { - var ok bool - proxyAdapter, ok = Proxies()[proxyName] - if !ok { - opts = append(opts, dialer.WithInterface(proxyName)) - } - } - } - - if proxyAdapter == nil { - return dialer.NewDialer(opts...).ListenPacket(ctx, network, "", metadata.AddrPort()) - } - - if !proxyAdapter.SupportUDP() { - return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) - } - - packetConn, err := proxyAdapter.ListenPacketContext(ctx, metadata) - if err != nil { - logMetadataErr(metadata, rule, proxyAdapter, err) - return nil, err - } - logMetadata(metadata, rule, packetConn) - - packetConn = statistic.NewUDPTracker(packetConn, statistic.DefaultManager, metadata, rule, 0, 0, false) - - return packetConn, nil -} diff --git a/tunnel/mode.go b/tunnel/mode.go deleted file mode 100644 index dd89c3a7b3..0000000000 --- a/tunnel/mode.go +++ /dev/null @@ -1,84 +0,0 @@ -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 -) - -// 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 -} - -// 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 -} - -// UnmarshalText unserialize Mode -func (m *TunnelMode) UnmarshalText(data []byte) error { - mode, exist := ModeMapping[strings.ToLower(string(data))] - if !exist { - return errors.New("invalid mode") - } - *m = mode - return nil -} - -// MarshalYAML serialize TunnelMode with yaml -func (m TunnelMode) MarshalYAML() (any, error) { - return m.String(), nil -} - -// MarshalJSON serialize Mode -func (m TunnelMode) MarshalJSON() ([]byte, error) { - return json.Marshal(m.String()) -} - -// MarshalText serialize Mode -func (m TunnelMode) MarshalText() ([]byte, error) { - return []byte(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 deleted file mode 100644 index 3f2770c249..0000000000 --- a/tunnel/statistic/manager.go +++ /dev/null @@ -1,127 +0,0 @@ -package statistic - -import ( - "os" - "time" - - "github.com/metacubex/mihomo/common/atomic" - - "github.com/puzpuzpuz/xsync/v3" - "github.com/shirou/gopsutil/v4/process" -) - -var DefaultManager *Manager - -func init() { - DefaultManager = &Manager{ - connections: xsync.NewMapOf[string, Tracker](), - uploadTemp: atomic.NewInt64(0), - downloadTemp: atomic.NewInt64(0), - uploadBlip: atomic.NewInt64(0), - downloadBlip: atomic.NewInt64(0), - uploadTotal: atomic.NewInt64(0), - downloadTotal: atomic.NewInt64(0), - process: &process.Process{Pid: int32(os.Getpid())}, - } - - go DefaultManager.handle() -} - -type Manager struct { - connections *xsync.MapOf[string, Tracker] - uploadTemp atomic.Int64 - downloadTemp atomic.Int64 - uploadBlip atomic.Int64 - downloadBlip atomic.Int64 - uploadTotal atomic.Int64 - downloadTotal atomic.Int64 - process *process.Process - memory uint64 -} - -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) Get(id string) (c Tracker) { - if value, ok := m.connections.Load(id); ok { - c = value - } - return -} - -func (m *Manager) Range(f func(c Tracker) bool) { - m.connections.Range(func(key string, value Tracker) bool { - return f(value) - }) -} - -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) Memory() uint64 { - m.updateMemory() - return m.memory -} - -func (m *Manager) Snapshot() *Snapshot { - var connections []*TrackerInfo - m.Range(func(c Tracker) bool { - connections = append(connections, c.Info()) - return true - }) - return &Snapshot{ - UploadTotal: m.uploadTotal.Load(), - DownloadTotal: m.downloadTotal.Load(), - Connections: connections, - Memory: m.memory, - } -} - -func (m *Manager) updateMemory() { - stat, err := m.process.MemoryInfo() - if err != nil { - return - } - m.memory = stat.RSS -} - -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.Swap(0)) - m.downloadBlip.Store(m.downloadTemp.Swap(0)) - } -} - -type Snapshot struct { - DownloadTotal int64 `json:"downloadTotal"` - UploadTotal int64 `json:"uploadTotal"` - Connections []*TrackerInfo `json:"connections"` - Memory uint64 `json:"memory"` -} diff --git a/tunnel/statistic/patch_android.go b/tunnel/statistic/patch_android.go deleted file mode 100644 index f1eee346eb..0000000000 --- a/tunnel/statistic/patch_android.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build android && cmfa - -package statistic - -func (m *Manager) Total() (up, down int64) { - return m.uploadTotal.Load(), m.downloadTotal.Load() -} diff --git a/tunnel/statistic/tracker.go b/tunnel/statistic/tracker.go deleted file mode 100644 index ca592d9774..0000000000 --- a/tunnel/statistic/tracker.go +++ /dev/null @@ -1,256 +0,0 @@ -package statistic - -import ( - "io" - "net" - "net/netip" - "time" - - "github.com/metacubex/mihomo/common/atomic" - "github.com/metacubex/mihomo/common/buf" - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - C "github.com/metacubex/mihomo/constant" - - "github.com/gofrs/uuid/v5" -) - -type Tracker interface { - ID() string - Close() error - Info() *TrackerInfo - C.Connection -} - -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 - - pushToManager bool `json:"-"` -} - -func (tt *tcpTracker) ID() string { - return tt.UUID.String() -} - -func (tt *tcpTracker) Info() *TrackerInfo { - return tt.TrackerInfo -} - -func (tt *tcpTracker) Read(b []byte) (int, error) { - n, err := tt.Conn.Read(b) - download := int64(n) - if tt.pushToManager { - tt.manager.PushDownloaded(download) - } - tt.DownloadTotal.Add(download) - return n, err -} - -func (tt *tcpTracker) ReadBuffer(buffer *buf.Buffer) (err error) { - err = tt.Conn.ReadBuffer(buffer) - download := int64(buffer.Len()) - if tt.pushToManager { - tt.manager.PushDownloaded(download) - } - tt.DownloadTotal.Add(download) - return -} - -func (tt *tcpTracker) UnwrapReader() (io.Reader, []N.CountFunc) { - return tt.Conn, []N.CountFunc{func(download int64) { - if tt.pushToManager { - tt.manager.PushDownloaded(download) - } - tt.DownloadTotal.Add(download) - }} -} - -func (tt *tcpTracker) Write(b []byte) (int, error) { - n, err := tt.Conn.Write(b) - upload := int64(n) - if tt.pushToManager { - tt.manager.PushUploaded(upload) - } - tt.UploadTotal.Add(upload) - return n, err -} - -func (tt *tcpTracker) WriteBuffer(buffer *buf.Buffer) (err error) { - upload := int64(buffer.Len()) - err = tt.Conn.WriteBuffer(buffer) - if tt.pushToManager { - tt.manager.PushUploaded(upload) - } - tt.UploadTotal.Add(upload) - return -} - -func (tt *tcpTracker) UnwrapWriter() (io.Writer, []N.CountFunc) { - return tt.Conn, []N.CountFunc{func(upload int64) { - if tt.pushToManager { - tt.manager.PushUploaded(upload) - } - tt.UploadTotal.Add(upload) - }} -} - -func (tt *tcpTracker) Close() error { - tt.manager.Leave(tt) - return tt.Conn.Close() -} - -func (tt *tcpTracker) Upstream() any { - return tt.Conn -} - -func parseRemoteDestination(addr net.Addr, conn C.Connection) string { - if addr != nil { - if addrPort, err := netip.ParseAddrPort(addr.String()); err == nil && addrPort.Addr().IsValid() { - return addrPort.Addr().String() - } - } - if conn != nil { - return conn.RemoteDestination() - } - return "" -} - -func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule, uploadTotal int64, downloadTotal int64, pushToManager bool) *tcpTracker { - metadata.RemoteDst = parseRemoteDestination(conn.RemoteAddr(), conn) - - t := &tcpTracker{ - Conn: conn, - manager: manager, - TrackerInfo: &TrackerInfo{ - UUID: utils.NewUUIDV4(), - Start: time.Now(), - Metadata: metadata, - Chain: conn.Chains(), - Rule: "", - UploadTotal: atomic.NewInt64(uploadTotal), - DownloadTotal: atomic.NewInt64(downloadTotal), - }, - pushToManager: pushToManager, - } - - if pushToManager { - if uploadTotal > 0 { - manager.PushUploaded(uploadTotal) - } - if downloadTotal > 0 { - manager.PushDownloaded(downloadTotal) - } - } - - 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 - - pushToManager bool `json:"-"` -} - -func (ut *udpTracker) ID() string { - return ut.UUID.String() -} - -func (ut *udpTracker) Info() *TrackerInfo { - return ut.TrackerInfo -} - -func (ut *udpTracker) ReadFrom(b []byte) (int, net.Addr, error) { - n, addr, err := ut.PacketConn.ReadFrom(b) - download := int64(n) - if ut.pushToManager { - ut.manager.PushDownloaded(download) - } - ut.DownloadTotal.Add(download) - return n, addr, err -} - -func (ut *udpTracker) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { - data, put, addr, err = ut.PacketConn.WaitReadFrom() - download := int64(len(data)) - if ut.pushToManager { - ut.manager.PushDownloaded(download) - } - ut.DownloadTotal.Add(download) - return -} - -func (ut *udpTracker) WriteTo(b []byte, addr net.Addr) (int, error) { - n, err := ut.PacketConn.WriteTo(b, addr) - upload := int64(n) - if ut.pushToManager { - 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 (ut *udpTracker) Upstream() any { - return ut.PacketConn -} - -func NewUDPTracker(conn C.PacketConn, manager *Manager, metadata *C.Metadata, rule C.Rule, uploadTotal int64, downloadTotal int64, pushToManager bool) *udpTracker { - metadata.RemoteDst = parseRemoteDestination(nil, conn) - - ut := &udpTracker{ - PacketConn: conn, - manager: manager, - TrackerInfo: &TrackerInfo{ - UUID: utils.NewUUIDV4(), - Start: time.Now(), - Metadata: metadata, - Chain: conn.Chains(), - Rule: "", - UploadTotal: atomic.NewInt64(uploadTotal), - DownloadTotal: atomic.NewInt64(downloadTotal), - }, - pushToManager: pushToManager, - } - - if pushToManager { - if uploadTotal > 0 { - manager.PushUploaded(uploadTotal) - } - if downloadTotal > 0 { - manager.PushDownloaded(downloadTotal) - } - } - - if rule != nil { - ut.TrackerInfo.Rule = rule.RuleType().String() - ut.TrackerInfo.RulePayload = rule.Payload() - } - - manager.Join(ut) - return ut -} diff --git a/tunnel/status.go b/tunnel/status.go deleted file mode 100644 index 388d597b38..0000000000 --- a/tunnel/status.go +++ /dev/null @@ -1,107 +0,0 @@ -package tunnel - -import ( - "encoding/json" - "errors" - "strings" - "sync/atomic" -) - -type TunnelStatus int - -// StatusMapping is a mapping for Status enum -var StatusMapping = map[string]TunnelStatus{ - Suspend.String(): Suspend, - Inner.String(): Inner, - Running.String(): Running, -} - -const ( - Suspend TunnelStatus = iota - Inner - Running -) - -// UnmarshalYAML unserialize Status with yaml -func (s *TunnelStatus) UnmarshalYAML(unmarshal func(any) error) error { - var tp string - unmarshal(&tp) - status, exist := StatusMapping[strings.ToLower(tp)] - if !exist { - return errors.New("invalid status") - } - *s = status - return nil -} - -// UnmarshalJSON unserialize Status -func (s *TunnelStatus) UnmarshalJSON(data []byte) error { - var tp string - json.Unmarshal(data, &tp) - status, exist := StatusMapping[strings.ToLower(tp)] - if !exist { - return errors.New("invalid status") - } - *s = status - return nil -} - -// UnmarshalText unserialize Status -func (s *TunnelStatus) UnmarshalText(data []byte) error { - status, exist := StatusMapping[strings.ToLower(string(data))] - if !exist { - return errors.New("invalid status") - } - *s = status - return nil -} - -// MarshalYAML serialize TunnelMode with yaml -func (s TunnelStatus) MarshalYAML() (any, error) { - return s.String(), nil -} - -// MarshalJSON serialize Status -func (s TunnelStatus) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} - -// MarshalText serialize Status -func (s TunnelStatus) MarshalText() ([]byte, error) { - return []byte(s.String()), nil -} - -func (s TunnelStatus) String() string { - switch s { - case Suspend: - return "suspend" - case Inner: - return "inner" - case Running: - return "running" - default: - return "Unknown" - } -} - -type AtomicStatus struct { - value atomic.Int32 -} - -func (a *AtomicStatus) Store(s TunnelStatus) { - a.value.Store(int32(s)) -} - -func (a *AtomicStatus) Load() TunnelStatus { - return TunnelStatus(a.value.Load()) -} - -func (a *AtomicStatus) String() string { - return a.Load().String() -} - -func newAtomicStatus(s TunnelStatus) *AtomicStatus { - a := &AtomicStatus{} - a.Store(s) - return a -} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go deleted file mode 100644 index b9507930c1..0000000000 --- a/tunnel/tunnel.go +++ /dev/null @@ -1,729 +0,0 @@ -package tunnel - -import ( - "context" - "errors" - "fmt" - "net" - "net/netip" - "path/filepath" - "runtime" - "strings" - "sync" - "time" - - N "github.com/metacubex/mihomo/common/net" - "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/loopback" - "github.com/metacubex/mihomo/component/nat" - P "github.com/metacubex/mihomo/component/process" - "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/component/slowdown" - "github.com/metacubex/mihomo/component/sniffer" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" - "github.com/metacubex/mihomo/constant/provider" - icontext "github.com/metacubex/mihomo/context" - "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/tunnel/statistic" -) - -const ( - queueCapacity = 64 // chan capacity tcpQueue and udpQueue - senderCapacity = 128 // chan capacity of PacketSender -) - -var ( - status = newAtomicStatus(Suspend) - udpInit sync.Once - udpQueues []chan C.PacketAdapter - natTable = nat.New() - rules []C.Rule - listeners = make(map[string]C.InboundListener) - subRules map[string][]C.Rule - proxies = make(map[string]C.Proxy) - providers map[string]provider.ProxyProvider - ruleProviders map[string]provider.RuleProvider - configMux sync.RWMutex - - // for compatibility, lazy init - tcpQueue chan C.ConnContext - tcpInOnce sync.Once - udpQueue chan C.PacketAdapter - udpInOnce sync.Once - - // Outbound Rule - mode = Rule - - // default timeout for UDP session - udpTimeout = 60 * time.Second - - findProcessMode P.FindProcessMode - - fakeIPRange netip.Prefix - - snifferDispatcher *sniffer.Dispatcher - sniffingEnable = false - - ruleUpdateCallback = utils.NewCallback[provider.RuleProvider]() -) - -type tunnel struct{} - -var Tunnel = tunnel{} -var _ C.Tunnel = Tunnel -var _ provider.Tunnel = Tunnel - -func (t tunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) { - connCtx := icontext.NewConnContext(conn, metadata) - handleTCPConn(connCtx) -} - -func initUDP() { - numUDPWorkers := 4 - if num := runtime.GOMAXPROCS(0); num > numUDPWorkers { - numUDPWorkers = num - } - - udpQueues = make([]chan C.PacketAdapter, numUDPWorkers) - for i := 0; i < numUDPWorkers; i++ { - queue := make(chan C.PacketAdapter, queueCapacity) - udpQueues[i] = queue - go processUDP(queue) - } -} - -func (t tunnel) HandleUDPPacket(packet C.UDPPacket, metadata *C.Metadata) { - udpInit.Do(initUDP) - - packetAdapter := C.NewPacketAdapter(packet, metadata) - key := packetAdapter.Key() - - hash := utils.MapHash(key) - queueNo := uint(hash) % uint(len(udpQueues)) - - select { - case udpQueues[queueNo] <- packetAdapter: - default: - packet.Drop() - } -} - -func (t tunnel) NatTable() C.NatTable { - return natTable -} - -func (t tunnel) Providers() map[string]provider.ProxyProvider { - return providers -} - -func (t tunnel) RuleProviders() map[string]provider.RuleProvider { - return ruleProviders -} - -func (t tunnel) RuleUpdateCallback() *utils.Callback[provider.RuleProvider] { - return ruleUpdateCallback -} - -func OnSuspend() { - status.Store(Suspend) -} - -func OnInnerLoading() { - status.Store(Inner) -} - -func OnRunning() { - status.Store(Running) -} - -func Status() TunnelStatus { - return status.Load() -} - -func SetFakeIPRange(p netip.Prefix) { - fakeIPRange = p -} - -func FakeIPRange() netip.Prefix { - return fakeIPRange -} - -func SetSniffing(b bool) { - if snifferDispatcher.Enable() { - configMux.Lock() - sniffingEnable = b - configMux.Unlock() - } -} - -func IsSniffing() bool { - return sniffingEnable -} - -// TCPIn return fan-in queue -// Deprecated: using Tunnel instead -func TCPIn() chan<- C.ConnContext { - tcpInOnce.Do(func() { - tcpQueue = make(chan C.ConnContext, queueCapacity) - go func() { - for connCtx := range tcpQueue { - go handleTCPConn(connCtx) - } - }() - }) - return tcpQueue -} - -// UDPIn return fan-in udp queue -// Deprecated: using Tunnel instead -func UDPIn() chan<- C.PacketAdapter { - udpInOnce.Do(func() { - udpQueue = make(chan C.PacketAdapter, queueCapacity) - go func() { - for packet := range udpQueue { - Tunnel.HandleUDPPacket(packet, packet.Metadata()) - } - }() - }) - return udpQueue -} - -// NatTable return nat table -func NatTable() C.NatTable { - return natTable -} - -// Rules return all rules -func Rules() []C.Rule { - return rules -} - -func Listeners() map[string]C.InboundListener { - return listeners -} - -// UpdateRules handle update rules -func UpdateRules(newRules []C.Rule, newSubRule map[string][]C.Rule, rp map[string]provider.RuleProvider) { - configMux.Lock() - rules = newRules - ruleProviders = rp - subRules = newSubRule - configMux.Unlock() -} - -// Proxies return all proxies -func Proxies() map[string]C.Proxy { - return proxies -} - -func ProxiesWithProviders() map[string]C.Proxy { - allProxies := make(map[string]C.Proxy) - for name, proxy := range proxies { - allProxies[name] = proxy - } - for _, p := range providers { - for _, proxy := range p.Proxies() { - name := proxy.Name() - allProxies[name] = proxy - } - } - return allProxies -} - -// Providers return all compatible providers -func Providers() map[string]provider.ProxyProvider { - return providers -} - -// RuleProviders return all loaded rule providers -func RuleProviders() map[string]provider.RuleProvider { - return ruleProviders -} - -// UpdateProxies handle update proxies -func UpdateProxies(newProxies map[string]C.Proxy, newProviders map[string]provider.ProxyProvider) { - configMux.Lock() - proxies = newProxies - providers = newProviders - configMux.Unlock() -} - -func UpdateListeners(newListeners map[string]C.InboundListener) { - configMux.Lock() - defer configMux.Unlock() - listeners = newListeners -} - -func UpdateSniffer(dispatcher *sniffer.Dispatcher) { - configMux.Lock() - snifferDispatcher = dispatcher - sniffingEnable = dispatcher.Enable() - configMux.Unlock() -} - -// Mode return current mode -func Mode() TunnelMode { - return mode -} - -// SetMode change the mode of tunnel -func SetMode(m TunnelMode) { - mode = m -} - -func FindProcessMode() P.FindProcessMode { - return findProcessMode -} - -// SetFindProcessMode replace SetAlwaysFindProcess -// always find process info if legacyAlways = true or mode.Always() = true, may be increase many memory -func SetFindProcessMode(mode P.FindProcessMode) { - findProcessMode = mode -} - -func isHandle(t C.Type) bool { - status := status.Load() - return status == Running || (status == Inner && t == C.INNER) -} - -func needLookupIP(metadata *C.Metadata) bool { - return resolver.MappingEnabled() && metadata.Host == "" && metadata.DstIP.IsValid() -} - -func preHandleMetadata(metadata *C.Metadata) error { - // handle IP string on host - if ip, err := netip.ParseAddr(metadata.Host); err == 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 = netip.Addr{} - metadata.DNSMode = C.DNSFakeIP - } else if node, ok := resolver.DefaultHosts.Search(host, false); ok { - // redir-host should lookup the hosts - metadata.DstIP, _ = node.RandIP() - } else if node != nil && node.IsDomain { - metadata.Host = node.Domain - } - } else if resolver.IsFakeIP(metadata.DstIP) { - return fmt.Errorf("fake DNS record %s missing", metadata.DstIP) - } - } else if node, ok := resolver.DefaultHosts.Search(metadata.Host, true); ok { - // try use domain mapping - metadata.Host = node.Domain - } - - return nil -} - -func resolveMetadata(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"] - // Rule - default: - proxy, rule, err = match(metadata) - } - return -} - -// processUDP starts a loop to handle udp packet -func processUDP(queue chan C.PacketAdapter) { - for conn := range queue { - handleUDPConn(conn) - } -} - -func handleUDPConn(packet C.PacketAdapter) { - if !isHandle(packet.Metadata().Type) { - packet.Drop() - return - } - - 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 = metadata.DstIP - } - - if err := preHandleMetadata(metadata); err != nil { - packet.Drop() - log.Debugln("[Metadata PreHandle] error: %s", err) - return - } - - key := packet.Key() - sender, loaded := natTable.GetOrCreate(key, func() C.PacketSender { - sender := newPacketSender() - if sniffingEnable && snifferDispatcher.Enable() { - return snifferDispatcher.UDPSniff(packet, sender) - } - return sender - }) - if !loaded { - dial := func() (C.PacketConn, C.WriteBackProxy, error) { - if err := sender.ResolveUDP(metadata); err != nil { - log.Warnln("[UDP] Resolve Ip error: %s", err) - return nil, nil, err - } - - proxy, rule, err := resolveMetadata(metadata) - if err != nil { - log.Warnln("[UDP] Parse metadata failed: %s", err.Error()) - return nil, nil, err - } - - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout) - defer cancel() - rawPc, err := retry(ctx, func(ctx context.Context) (C.PacketConn, error) { - return proxy.ListenPacketContext(ctx, metadata.Pure()) - }, func(err error) { - logMetadataErr(metadata, rule, proxy, err) - }) - if err != nil { - return nil, nil, err - } - logMetadata(metadata, rule, rawPc) - - pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule, 0, 0, true) - - if rawPc.Chains().Last() == "REJECT-DROP" { - _ = pc.Close() - return nil, nil, errors.New("rejected drop packet") - } - - oAddrPort := metadata.AddrPort() - writeBackProxy := nat.NewWriteBackProxy(packet) - - go handleUDPToLocal(writeBackProxy, pc, sender, key, oAddrPort, fAddr) - return pc, writeBackProxy, nil - } - - go func() { - pc, proxy, err := dial() - if err != nil { - sender.Close() - natTable.Delete(key) - return - } - sender.Process(pc, proxy) - }() - } - sender.Send(packet) // nonblocking -} - -func handleTCPConn(connCtx C.ConnContext) { - if !isHandle(connCtx.Metadata().Type) { - _ = connCtx.Conn().Close() - return - } - - defer func(conn net.Conn) { - _ = conn.Close() - }(connCtx.Conn()) - - metadata := connCtx.Metadata() - if !metadata.Valid() { - log.Warnln("[Metadata] not valid: %#v", metadata) - return - } - - preHandleFailed := false - if err := preHandleMetadata(metadata); err != nil { - log.Debugln("[Metadata PreHandle] error: %s", err) - preHandleFailed = true - } - - conn := connCtx.Conn() - conn.ResetPeeked() // reset before sniffer - if sniffingEnable && snifferDispatcher.Enable() { - // Try to sniff a domain when `preHandleMetadata` failed, this is usually - // caused by a "Fake DNS record missing" error when enhanced-mode is fake-ip. - if snifferDispatcher.TCPSniff(conn, metadata) { - // we now have a domain name - preHandleFailed = false - } - } - - // If both trials have failed, we can do nothing but give up - if preHandleFailed { - log.Debugln("[Metadata PreHandle] failed to sniff a domain for connection %s --> %s, give up", - metadata.SourceDetail(), metadata.RemoteAddress()) - return - } - - peekMutex := sync.Mutex{} - if !conn.Peeked() { - peekMutex.Lock() - go func() { - defer peekMutex.Unlock() - _ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) - _, _ = conn.Peek(1) - _ = conn.SetReadDeadline(time.Time{}) - }() - } - - proxy, rule, err := resolveMetadata(metadata) - if err != nil { - log.Warnln("[Metadata] parse failed: %s", err.Error()) - return - } - - dialMetadata := metadata - if len(metadata.Host) > 0 { - if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok { - if dstIp, _ := node.RandIP(); !FakeIPRange().Contains(dstIp) { - dialMetadata.DstIP = dstIp - dialMetadata.DNSMode = C.DNSHosts - dialMetadata = dialMetadata.Pure() - } - } - } - - var peekBytes []byte - var peekLen int - - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) - defer cancel() - remoteConn, err := retry(ctx, func(ctx context.Context) (remoteConn C.Conn, err error) { - remoteConn, err = proxy.DialContext(ctx, dialMetadata) - if err != nil { - return - } - - if N.NeedHandshake(remoteConn) { - defer func() { - for _, chain := range remoteConn.Chains() { - if chain == "REJECT" { - err = nil - return - } - } - if err != nil { - remoteConn = nil - } - }() - peekMutex.Lock() - defer peekMutex.Unlock() - peekBytes, _ = conn.Peek(conn.Buffered()) - _, err = remoteConn.Write(peekBytes) - if err != nil { - return - } - if peekLen = len(peekBytes); peekLen > 0 { - _, _ = conn.Discard(peekLen) - } - } - return - }, func(err error) { - logMetadataErr(metadata, rule, proxy, err) - }) - if err != nil { - return - } - logMetadata(metadata, rule, remoteConn) - - remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule, int64(peekLen), 0, true) - defer func(remoteConn C.Conn) { - _ = remoteConn.Close() - }(remoteConn) - - _ = conn.SetReadDeadline(time.Now()) // stop unfinished peek - peekMutex.Lock() - defer peekMutex.Unlock() - _ = conn.SetReadDeadline(time.Time{}) // reset - handleSocket(conn, remoteConn) -} - -func logMetadataErr(metadata *C.Metadata, rule C.Rule, proxy C.ProxyAdapter, err error) { - if rule == nil { - log.Warnln("[%s] dial %s %s --> %s error: %s", strings.ToUpper(metadata.NetWork.String()), proxy.Name(), metadata.SourceDetail(), metadata.RemoteAddress(), err.Error()) - } else { - log.Warnln("[%s] dial %s (match %s/%s) %s --> %s error: %s", strings.ToUpper(metadata.NetWork.String()), proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.SourceDetail(), metadata.RemoteAddress(), err.Error()) - } -} - -func logMetadata(metadata *C.Metadata, rule C.Rule, remoteConn C.Connection) { - switch { - case metadata.SpecialProxy != "": - log.Infoln("[%s] %s --> %s using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), metadata.SpecialProxy) - case rule != nil: - if rule.Payload() != "" { - log.Infoln("[%s] %s --> %s match %s using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), fmt.Sprintf("%s(%s)", rule.RuleType().String(), rule.Payload()), remoteConn.Chains().String()) - } else { - log.Infoln("[%s] %s --> %s match %s using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), rule.RuleType().String(), remoteConn.Chains().String()) - } - case mode == Global: - log.Infoln("[%s] %s --> %s using GLOBAL", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress()) - case mode == Direct: - log.Infoln("[%s] %s --> %s using DIRECT", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress()) - default: - log.Infoln("[%s] %s --> %s doesn't match any rule using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), remoteConn.Chains().Last()) - } -} - -func shouldResolveIP(rule C.Rule, metadata *C.Metadata) bool { - return rule.ShouldResolveIP() && metadata.Host != "" && !metadata.DstIP.IsValid() -} - -func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { - configMux.RLock() - defer configMux.RUnlock() - var ( - resolved bool - attemptProcessLookup = metadata.Type != C.INNER - ) - - if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok { - metadata.DstIP, _ = node.RandIP() - resolved = true - } - - for _, rule := range getRules(metadata) { - if !resolved && shouldResolveIP(rule, metadata) { - func() { - ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) - defer cancel() - ip, err := resolver.ResolveIP(ctx, 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 attemptProcessLookup && !findProcessMode.Off() && (findProcessMode.Always() || rule.ShouldFindProcess()) { - attemptProcessLookup = false - if !features.CMFA { - // normal check for process - uid, path, err := P.FindProcessName(metadata.NetWork.String(), metadata.SrcIP, int(metadata.SrcPort)) - if err != nil { - log.Debugln("[Process] find process error for %s: %v", metadata.String(), err) - } else { - metadata.Process = filepath.Base(path) - metadata.ProcessPath = path - metadata.Uid = uid - - if pkg, err := P.FindPackageName(metadata); err == nil { // for android (not CMFA) package names - metadata.Process = pkg - } - } - } else { - // check package names - pkg, err := P.FindPackageName(metadata) - if err != nil { - log.Debugln("[Process] find process error for %s: %v", metadata.String(), err) - } else { - metadata.Process = pkg - } - } - } - - if matched, ada := rule.Match(metadata); matched { - adapter, ok := proxies[ada] - if !ok { - continue - } - - // parse multi-layer nesting - passed := false - for adapter := adapter; adapter != nil; adapter = adapter.Unwrap(metadata, false) { - if adapter.Type() == C.Pass { - passed = true - break - } - } - if passed { - log.Debugln("%s match Pass rule", adapter.Name()) - continue - } - - if metadata.NetWork == C.UDP && !adapter.SupportUDP() { - log.Debugln("%s UDP is not supported", adapter.Name()) - continue - } - - return adapter, rule, nil - } - } - - return proxies["DIRECT"], nil, nil -} - -func getRules(metadata *C.Metadata) []C.Rule { - if sr, ok := subRules[metadata.SpecialRules]; ok { - log.Debugln("[Rule] use %s rules", metadata.SpecialRules) - return sr - } else { - log.Debugln("[Rule] use default rules") - return rules - } -} - -func shouldStopRetry(err error) bool { - if errors.Is(err, resolver.ErrIPNotFound) { - return true - } - if errors.Is(err, resolver.ErrIPVersion) { - return true - } - if errors.Is(err, resolver.ErrIPv6Disabled) { - return true - } - if errors.Is(err, loopback.ErrReject) { - return true - } - return false -} - -func retry[T any](ctx context.Context, ft func(context.Context) (T, error), fe func(err error)) (t T, err error) { - s := slowdown.New() - for i := 0; i < 10; i++ { - t, err = ft(ctx) - if err != nil { - if fe != nil { - fe(err) - } - if shouldStopRetry(err) { - return - } - if s.Wait(ctx) == nil { - continue - } else { - return - } - } else { - break - } - } - return -}