diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..d68bd252 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,83 @@ +name: Bug report +description: Create a report to help us improve +title: "[Bug] " +labels: ["bug"] +body: + - type: checkboxes + id: ensure + attributes: + label: Verify steps + description: " +在提交之前,请确认 +Please verify that you've followed these steps +" + 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. +" + required: false + - label: " +我已经在 [Issue Tracker](……/) 中找过我要提出的问题 +I have searched on the [issue tracker](……/) for a related issue. +" + required: true + - label: " +我已经使用 Alpha 分支版本测试过,问题依旧存在 +I have tested using the dev branch, and the issue still exists. +" + 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: " +这是 Mihomo 核心的问题,并非我所使用的 Mihomo 衍生版本(如 OpenMihomo、KoolMihomo 等)的特定问题 +This is an issue of the Mihomo core *per se*, not to the derivatives of Mihomo, like OpenMihomo or KoolMihomo. +" + required: true + - type: input + attributes: + label: Mihomo version + description: "use `mihomo -v`" + validations: + required: true + - type: dropdown + id: os + attributes: + label: What OS are you seeing the problem on? + multiple: true + options: + - macOS + - Windows + - Linux + - OpenBSD/FreeBSD + - type: textarea + attributes: + render: yaml + label: "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) +" + 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`. +" + - type: textarea + attributes: + label: Description + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..75d37b63 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: mihomo Community Support + url: https://github.com/MetaCubeX/mihomo/discussions + about: Please ask and answer questions about mihomo here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..7987526c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,37 @@ +name: Feature request +description: Suggest an idea 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 +" + 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 + - type: textarea + attributes: + label: Description + description: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Mihomo Core 的行为是什麽? + validations: + required: true + - type: textarea + attributes: + label: Possible Solution + description: " +此项非必须,但是如果你有想法的话欢迎提出。 +Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change +" \ No newline at end of file diff --git a/.github/rename-cgo.sh b/.github/rename-cgo.sh index 54841712..2bfdb3c6 100644 --- a/.github/rename-cgo.sh +++ b/.github/rename-cgo.sh @@ -5,16 +5,25 @@ for FILENAME in $FILENAMES do if [[ $FILENAME =~ "darwin-10.16-arm64" ]];then echo "rename darwin-10.16-arm64 $FILENAME" - mv $FILENAME clash.meta-darwin-arm64-cgo + mv $FILENAME mihomo-darwin-arm64-cgo elif [[ $FILENAME =~ "darwin-10.16-amd64" ]];then echo "rename darwin-10.16-amd64 $FILENAME" - mv $FILENAME clash.meta-darwin-amd64-cgo + mv $FILENAME mihomo-darwin-amd64-cgo elif [[ $FILENAME =~ "windows-4.0-386" ]];then echo "rename windows 386 $FILENAME" - mv $FILENAME clash.meta-windows-386-cgo.exe + mv $FILENAME mihomo-windows-386-cgo.exe elif [[ $FILENAME =~ "windows-4.0-amd64" ]];then echo "rename windows amd64 $FILENAME" - mv $FILENAME clash.meta-windows-amd64-cgo.exe + 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 diff --git a/.github/rename-go120.sh b/.github/rename-go120.sh new file mode 100644 index 00000000..eddb1769 --- /dev/null +++ b/.github/rename-go120.sh @@ -0,0 +1,12 @@ +#!/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/android-branch-auto-sync.yml b/.github/workflows/android-branch-auto-sync.yml new file mode 100644 index 00000000..fd7c9d66 --- /dev/null +++ b/.github/workflows/android-branch-auto-sync.yml @@ -0,0 +1,69 @@ +name: Android Branch Auto Sync +on: + workflow_dispatch: + push: + paths-ignore: + - "docs/**" + - "README.md" + - ".github/ISSUE_TEMPLATE/**" + branches: + - Alpha + - android-open + tags: + - "v*" + pull_request_target: + branches: + - Alpha + - android-open + +jobs: + update-dependencies: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config --global user.name 'GitHub Action' + git config --global user.email 'action@github.com' + + - name: Sync android-real with Alpha rebase android-open + run: | + git fetch origin + git checkout origin/Alpha -b android-real + git merge --squash origin/android-open + git commit -m "Android: patch" + + - name: Check for conflicts + run: | + CONFLICTS=$(git diff --name-only --diff-filter=U) + if [ ! -z "$CONFLICTS" ]; then + echo "There are conflicts in the following files:" + echo $CONFLICTS + exit 1 + fi + + - name: Push changes + run: | + git push origin android-real --force + + # Send "core-updated" to MetaCubeX/MihomoForAndroid to trigger update-dependencies + trigger-MFA-update: + needs: update-dependencies + 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/MihomoForAndroid/dispatches \ + -H "Accept: application/vnd.github.everest-preview+json" \ + -H "Authorization: token ${{ steps.generate-token.outputs.token }}" \ + -d '{"event_type": "core-updated"}' \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66b694de..a644f97a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,17 +5,19 @@ on: paths-ignore: - "docs/**" - "README.md" + - ".github/ISSUE_TEMPLATE/**" branches: - Alpha - - Beta - - Meta tags: - "v*" pull_request_target: branches: - Alpha - - Beta - - Meta + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + env: REGISTRY: docker.io jobs: @@ -46,26 +48,33 @@ jobs: target: "linux-mips-softfloat linux-mips-hardfloat linux-mipsle-softfloat linux-mipsle-hardfloat", id: "4", } + - { type: "WithoutCGO", target: "linux-386 linux-riscv64", id: "5" } - { type: "WithoutCGO", target: "freebsd-386 freebsd-amd64 freebsd-arm64", - id: "5", - } - - { - type: "WithoutCGO", - target: "windows-amd64-compatible windows-amd64 windows-386", id: "6", } - { type: "WithoutCGO", - target: "windows-arm64 windows-arm32v7", + target: "windows-amd64-compatible windows-amd64 windows-386", id: "7", } - { type: "WithoutCGO", - target: "darwin-amd64 darwin-arm64 android-arm64", + target: "windows-arm64 windows-arm32v7", id: "8", } + - { + type: "WithoutCGO", + target: "darwin-amd64 darwin-arm64 android-arm64", + id: "9", + } + # only for test + - { type: "WithoutCGO-GO120", target: "linux-amd64 linux-amd64-compatible",id: "1" } + # 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. + - { type: "WithoutCGO-GO120", target: "windows-amd64-compatible windows-amd64 windows-386",id: "2" } + # 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. + - { type: "WithoutCGO-GO120", target: "darwin-amd64 darwin-arm64 android-arm64",id: "3" } - { type: "WithCGO", target: "windows/*", id: "1" } - { type: "WithCGO", target: "linux/386", id: "2" } - { type: "WithCGO", target: "linux/amd64", id: "3" } @@ -108,34 +117,43 @@ jobs: - name: Set ENV run: | - echo "NAME=clash.meta" >> $GITHUB_ENV + sudo timedatectl set-timezone "Asia/Shanghai" + echo "NAME=mihomo" >> $GITHUB_ENV echo "REPO=${{ github.repository }}" >> $GITHUB_ENV echo "ShortSHA=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV - echo "BUILDTIME=$(date -u)" >> $GITHUB_ENV + echo "BUILDTIME=$(date)" >> $GITHUB_ENV echo "BRANCH=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV shell: bash - name: Set ENV run: | echo "TAGS=with_gvisor,with_lwip" >> $GITHUB_ENV - echo "LDFLAGS=-X 'github.com/Dreamacro/clash/constant.Version=${VERSION}' -X 'github.com/Dreamacro/clash/constant.BuildTime=${BUILDTIME}' -w -s -buildid=" >> $GITHUB_ENV + echo "LDFLAGS=-X 'github.com/metacubex/mihomo/constant.Version=${VERSION}' -X 'github.com/metacubex/mihomo/constant.BuildTime=${BUILDTIME}' -w -s -buildid=" >> $GITHUB_ENV shell: bash - name: Setup Go - uses: actions/setup-go@v3 + if: ${{ matrix.job.type!='WithoutCGO-GO120' }} + uses: actions/setup-go@v4 + with: + go-version: "1.21" + check-latest: true + + - name: Setup Go + if: ${{ matrix.job.type=='WithoutCGO-GO120' }} + uses: actions/setup-go@v4 with: go-version: "1.20" check-latest: true - name: Test - if: ${{ matrix.job.id=='1' && matrix.job.type=='WithoutCGO' }} + if: ${{ matrix.job.id=='1' && matrix.job.type!='WithCGO' }} run: | go test ./... - name: Build WithoutCGO - if: ${{ matrix.job.type=='WithoutCGO' }} + if: ${{ matrix.job.type!='WithCGO' }} env: - NAME: Clash.Meta + NAME: mihomo BINDIR: bin run: make -j$(($(nproc) + 1)) ${{ matrix.job.target }} @@ -143,7 +161,7 @@ jobs: if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }} id: setup-ndk with: - ndk-version: r25b + ndk-version: r26 add-to-path: false local-cache: true @@ -181,6 +199,17 @@ jobs: ls -la cd .. + - name: Rename + if: ${{ matrix.job.type=='WithoutCGO-GO120' }} + run: | + cd bin + ls -la + cp ../.github/rename-go120.sh ./ + bash ./rename-go120.sh + rm ./rename-go120.sh + ls -la + cd .. + - name: Zip if: ${{ success() }} run: | @@ -193,6 +222,10 @@ jobs: ls -la cd .. + - name: Save version + run: echo ${VERSION} > bin/version.txt + shell: bash + - uses: actions/upload-artifact@v3 if: ${{ success() }} with: @@ -201,8 +234,8 @@ jobs: Upload-Prerelease: permissions: write-all - if: ${{ github.ref_type=='branch' }} - needs: [ Build ] + if: ${{ github.ref_type=='branch' && github.event_name != 'pull_request' }} + needs: [Build] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v3 @@ -215,12 +248,17 @@ jobs: working-directory: bin - name: Delete current release assets - uses: andreaswilli/delete-release-assets-action@v2.0.0 + 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.0.6 with: @@ -228,20 +266,30 @@ jobs: 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) + [查看文档 / Docs](https://metacubex.github.io/Meta-Docs/) + EOF + - name: Upload Prerelease uses: softprops/action-gh-release@v1 if: ${{ success() }} with: - tag: ${{ github.ref_name }} tag_name: Prerelease-${{ github.ref_name }} - files: bin/* + files: | + bin/* prerelease: true generate_release_notes: true + body_path: release.txt Upload-Release: permissions: write-all if: ${{ github.ref_type=='tag' }} - needs: [ Build ] + needs: [Build] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v3 @@ -257,14 +305,14 @@ jobs: uses: softprops/action-gh-release@v1 if: ${{ success() }} with: - tag: ${{ github.ref_name }} tag_name: ${{ github.ref_name }} files: bin/* generate_release_notes: true Docker: + if: ${{ github.event_name != 'pull_request' }} permissions: write-all - needs: [ Build ] + needs: [Build] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -282,10 +330,10 @@ jobs: working-directory: bin - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Setup Docker buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 with: version: latest @@ -293,7 +341,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v4 with: images: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_ACCOUNT }}/${{secrets.DOCKERHUB_REPO}} - name: Show files @@ -302,7 +350,7 @@ jobs: ls bin/ - name: Log into registry if: github.event_name != 'pull_request' - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.DOCKER_HUB_USER }} @@ -312,7 +360,7 @@ jobs: # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile @@ -321,5 +369,7 @@ jobs: linux/386 linux/amd64 linux/arm64/v8 + linux/arm/v7 + # linux/riscv64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.golangci.yaml b/.golangci.yaml index f5b67397..1de71ad8 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -11,7 +11,7 @@ linters-settings: custom-order: true sections: - standard - - prefix(github.com/Dreamacro/clash) + - prefix(github.com/metacubex/mihomo) - default staticcheck: go: '1.19' diff --git a/Dockerfile b/Dockerfile index 9c2e44c7..c9cd56b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,27 @@ FROM alpine:latest as builder +ARG TARGETPLATFORM +RUN echo "I'm building for $TARGETPLATFORM" RUN apk add --no-cache gzip && \ - mkdir /clash-config && \ - wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \ - wget -O /clash-config/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \ - wget -O /clash-config/geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat + 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 /clash/file-name.sh -WORKDIR /clash +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.*"|awk NR==1` && \ - mv bin/$FILE_NAME clash.gz && gzip -d clash.gz && echo "$FILE_NAME" > /clash-config/test + FILE_NAME=`ls bin/ | egrep "$FILE_NAME.*"|awk NR==1` && echo $FILE_NAME && \ + mv bin/$FILE_NAME mihomo.gz && gzip -d mihomo.gz && echo "$FILE_NAME" > /mihomo-config/test FROM alpine:latest -LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/Clash.Meta" +LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/mihomo" RUN apk add --no-cache ca-certificates tzdata iptables -VOLUME ["/root/.config/clash/"] +VOLUME ["/root/.config/mihomo/"] -COPY --from=builder /clash-config/ /root/.config/clash/ -COPY --from=builder /clash/clash /clash -RUN chmod +x /clash -ENTRYPOINT [ "/clash" ] +COPY --from=builder /mihomo-config/ /root/.config/mihomo/ +COPY --from=builder /mihomo/mihomo /mihomo +RUN chmod +x /mihomo +ENTRYPOINT [ "/mihomo" ] diff --git a/Makefile b/Makefile index f7a637a9..f6ffcae5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -NAME=clash.meta +NAME=mihomo BINDIR=bin BRANCH=$(shell git branch --show-current) ifeq ($(BRANCH),Alpha) @@ -12,8 +12,8 @@ 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/Dreamacro/clash/constant.Version=$(VERSION)" \ - -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ +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 = \ @@ -31,6 +31,8 @@ PLATFORM_LIST = \ linux-mips-hardfloat \ linux-mipsle-softfloat \ linux-mipsle-hardfloat \ + linux-riscv64 \ + linux-loong64 \ android-arm64 \ freebsd-386 \ freebsd-amd64 \ @@ -101,6 +103,12 @@ linux-mips64: linux-mips64le: GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ +linux-riscv64: + GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-loong64: + GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + android-arm64: GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ diff --git a/README.md b/README.md index cdfa3505..8c82536c 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,17 @@
Meta Kernel
-

Another Clash Kernel.

+

Another Mihomo Kernel.

- - + + - - - + + + - +

@@ -21,262 +21,52 @@ ## Features - Local HTTP/HTTPS/SOCKS server with authentication support -- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections +- 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 hardcoding in config -- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`. +- 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 -## Wiki +## Dashboard -Documentation and configuring examples are available on [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki) and [Clash.Meta Wiki](https://docs.metacubex.one/). +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). -## Build +## Configration example -You should install [golang](https://go.dev) first. +Configuration example is located at [/docs/config.yaml](https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml). -Then get the source code of Clash.Meta: +## 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/Clash.Meta.git -cd Clash.Meta && go mod download +git clone https://github.com/MetaCubeX/mihomo.git +cd mihomo && go mod download +go build ``` -If you can't visit github,you should set proxy first: +Set go proxy if a connection to GitHub is not possible: ```shell go env -w GOPROXY=https://goproxy.io,direct ``` -Now you can build it: - -```shell -go build -``` - -If you need gvisor for tun stack, build with: +Build with gvisor tun stack: ```shell go build -tags with_gvisor ``` - - - - ### IPTABLES configuration Work on Linux OS which supported `iptables` @@ -290,71 +80,10 @@ iptables: inbound-interface: eth0 # detect the inbound interface, default is 'lo' ``` -### General installation guide for Linux - -- Create user given name `clash-meta` - -- Download and decompress pre-built binaries from [releases](https://github.com/MetaCubeX/Clash.Meta/releases) - -- Rename executable file to `Clash-Meta` and move to `/usr/local/bin/` - -- Create folder `/etc/Clash-Meta/` as working directory - -Run Meta Kernel by user `clash-meta` as a daemon. - -Create the systemd configuration file at `/etc/systemd/system/Clash-Meta.service`: - -``` -[Unit] -Description=Clash-Meta Daemon, Another Clash Kernel. -After=network.target NetworkManager.service systemd-networkd.service iwd.service - -[Service] -Type=simple -User=clash-meta -Group=clash-meta -LimitNPROC=500 -LimitNOFILE=1000000 -CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE -AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE -Restart=always -ExecStartPre=/usr/bin/sleep 1s -ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta - -[Install] -WantedBy=multi-user.target -``` - -Launch clashd on system startup with: - -```shell -$ systemctl enable Clash-Meta -``` - -Launch clashd immediately with: - -```shell -$ systemctl start Clash-Meta -``` - -### Display Process name - -Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`. - -To display process name in GUI please use [Razord-meta](https://github.com/MetaCubeX/Razord-meta). - -### Dashboard - -We also made a custom fork of yacd provide better support for this project, check it out at [Yacd-meta](https://github.com/MetaCubeX/Yacd-meta) - -## Development - -If you want to build an application that uses clash as a library, check out the -the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library) - ## Debugging -Check [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki/How-to-use-debug-api) to get an instruction on using debug API. +Check [wiki](https://wiki.metacubex.one/api/#debug) to get an instruction on using debug +API. ## Credits @@ -369,4 +98,4 @@ Check [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki/How-to-use-debug-api) This software is released under the GPL-3.0 license. -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FMetaCubeX%2Fmihomo.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FMetaCubeX%2Fmihomo?ref=badge_large) diff --git a/adapter/adapter.go b/adapter/adapter.go index ffb5ced0..74b11bd9 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -3,25 +3,42 @@ package adapter import ( "context" "encoding/json" + "errors" "fmt" - "github.com/Dreamacro/clash/common/queue" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" "net" "net/http" "net/netip" "net/url" + "strconv" "time" - "go.uber.org/atomic" + "github.com/metacubex/mihomo/common/atomic" + "github.com/metacubex/mihomo/common/queue" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/dialer" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + + "github.com/puzpuzpuz/xsync/v2" ) var UnifiedDelay = atomic.NewBool(false) +const ( + defaultHistoriesNum = 10 +) + +type extraProxyState struct { + history *queue.Queue[C.DelayHistory] + alive atomic.Bool +} + type Proxy struct { C.ProxyAdapter history *queue.Queue[C.DelayHistory] - alive *atomic.Bool + alive atomic.Bool + url string + extra *xsync.MapOf[string, *extraProxyState] } // Alive implements C.Proxy @@ -29,6 +46,15 @@ func (p *Proxy) Alive() bool { return p.alive.Load() } +// 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) @@ -62,9 +88,51 @@ func (p *Proxy) DelayHistory() []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() + } + + if queueM == nil { + queueM = p.history.Copy() + } + + histories := []C.DelayHistory{} + for _, item := range queueM { + histories = append(histories, item) + } + return histories +} + +func (p *Proxy) ExtraDelayHistory() map[string][]C.DelayHistory { + extraHistory := map[string][]C.DelayHistory{} + + p.extra.Range(func(k string, v *extraProxyState) bool { + + testUrl := k + state := v + + histories := []C.DelayHistory{} + queueM := state.history.Copy() + + for _, item := range queueM { + histories = append(histories, item) + } + + extraHistory[testUrl] = histories + + return true + }) + return extraHistory +} + // LastDelay return last history record. if proxy is not alive, return the max value of uint16. // implements C.Proxy func (p *Proxy) LastDelay() (delay uint16) { @@ -80,6 +148,28 @@ func (p *Proxy) LastDelay() (delay uint16) { return history.Delay } +// LastDelayForTestUrl implements C.Proxy +func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) { + var max uint16 = 0xffff + + alive := p.alive.Load() + history := p.history.Last() + + if state, ok := p.extra.Load(url); ok { + alive = state.alive.Load() + history = state.history.Last() + } + + if !alive { + return max + } + + if history.Delay == 0 { + return max + } + return history.Delay +} + // MarshalJSON implements C.ProxyAdapter func (p *Proxy) MarshalJSON() ([]byte, error) { inner, err := p.ProxyAdapter.MarshalJSON() @@ -90,6 +180,8 @@ func (p *Proxy) MarshalJSON() ([]byte, error) { mapping := map[string]any{} _ = json.Unmarshal(inner, &mapping) mapping["history"] = p.DelayHistory() + mapping["extra"] = p.ExtraDelayHistory() + mapping["alive"] = p.Alive() mapping["name"] = p.Name() mapping["udp"] = p.SupportUDP() mapping["xudp"] = p.SupportXUDP() @@ -99,16 +191,53 @@ func (p *Proxy) MarshalJSON() ([]byte, error) { // URLTest get the delay for the specified URL // implements C.Proxy -func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) { +func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], store C.DelayHistoryStoreType) (t uint16, err error) { defer func() { - p.alive.Store(err == nil) - record := C.DelayHistory{Time: time.Now()} - if err == nil { - record.Delay = t - } - p.history.Put(record) - if p.history.Len() > 10 { - p.history.Pop() + alive := err == nil + store = p.determineFinalStoreType(store, url) + + switch store { + case C.OriginalHistory: + p.alive.Store(alive) + record := C.DelayHistory{Time: time.Now()} + if alive { + record.Delay = t + } + p.history.Put(record) + if p.history.Len() > defaultHistoriesNum { + p.history.Pop() + } + + // test URL configured by the proxy provider + if len(p.url) == 0 { + p.url = url + } + case C.ExtraHistory: + record := C.DelayHistory{Time: time.Now()} + if alive { + record.Delay = t + } + p.history.Put(record) + if p.history.Len() > defaultHistoriesNum { + p.history.Pop() + } + + state, ok := p.extra.Load(url) + if !ok { + state = &extraProxyState{ + history: queue.New[C.DelayHistory](defaultHistoriesNum), + alive: atomic.NewBool(true), + } + p.extra.Store(url, state) + } + + state.alive.Store(alive) + state.history.Put(record) + if state.history.Len() > defaultHistoriesNum { + state.history.Pop() + } + default: + log.Debugln("health check result will be discarded, url: %s alive: %t, delay: %d", url, alive, t) } }() @@ -172,12 +301,22 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) { } } + if expectedStatus != nil && !expectedStatus.Check(uint16(resp.StatusCode)) { + // maybe another value should be returned for differentiation + err = errors.New("response status is inconsistent with the expected status") + } + t = uint16(time.Since(start) / time.Millisecond) return } func NewProxy(adapter C.ProxyAdapter) *Proxy { - return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)} + return &Proxy{ + ProxyAdapter: adapter, + history: queue.New[C.DelayHistory](defaultHistoriesNum), + alive: atomic.NewBool(true), + url: "", + extra: xsync.NewMapOf[*extraProxyState]()} } func urlToMetadata(rawURL string) (addr C.Metadata, err error) { @@ -198,11 +337,36 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) { return } } + uintPort, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return + } addr = C.Metadata{ Host: u.Hostname(), DstIP: netip.Addr{}, - DstPort: port, + DstPort: uint16(uintPort), } return } + +func (p *Proxy) determineFinalStoreType(store C.DelayHistoryStoreType, url string) C.DelayHistoryStoreType { + if store != C.DropHistory { + return store + } + + if len(p.url) == 0 || url == p.url { + return C.OriginalHistory + } + + if p.extra.Size() < 2*C.DefaultMaxHealthCheckUrlNum { + return C.ExtraHistory + } + + _, ok := p.extra.Load(url) + if ok { + return C.ExtraHistory + } + + return store +} diff --git a/adapter/inbound/addition.go b/adapter/inbound/addition.go index 5966e784..a9896c8c 100644 --- a/adapter/inbound/addition.go +++ b/adapter/inbound/addition.go @@ -1,13 +1,17 @@ package inbound import ( - C "github.com/Dreamacro/clash/constant" + "net" + + C "github.com/metacubex/mihomo/constant" ) type Addition func(metadata *C.Metadata) -func (a Addition) Apply(metadata *C.Metadata) { - a(metadata) +func ApplyAdditions(metadata *C.Metadata, additions ...Addition) { + for _, addition := range additions { + addition(metadata) + } } func WithInName(name string) Addition { @@ -16,6 +20,12 @@ func WithInName(name string) Addition { } } +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 @@ -27,3 +37,29 @@ func WithSpecialProxy(specialProxy string) Addition { 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 + } + } +} diff --git a/adapter/inbound/auth.go b/adapter/inbound/auth.go new file mode 100644 index 00000000..984c9bd6 --- /dev/null +++ b/adapter/inbound/auth.go @@ -0,0 +1,45 @@ +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 { + if addr.IsValid() { + for _, prefix := range skipAuthPrefixes { + if prefix.Contains(addr.Unmap()) { + return true + } + } + } + return false +} diff --git a/adapter/inbound/http.go b/adapter/inbound/http.go index b1b881ce..137e17d3 100644 --- a/adapter/inbound/http.go +++ b/adapter/inbound/http.go @@ -3,26 +3,16 @@ package inbound import ( "net" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/context" - "github.com/Dreamacro/clash/transport/socks5" + 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, source net.Addr, conn net.Conn, additions ...Addition) *context.ConnContext { +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 - for _, addition := range additions { - addition.Apply(metadata) - } - if ip, port, err := parseAddr(source); err == nil { - metadata.SrcIP = ip - metadata.SrcPort = port - } - if ip, port, err := parseAddr(conn.LocalAddr()); err == nil { - metadata.InIP = ip - metadata.InPort = port - } - return context.NewConnContext(conn, metadata) + ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(conn.LocalAddr())) + ApplyAdditions(metadata, additions...) + return conn, metadata } diff --git a/adapter/inbound/https.go b/adapter/inbound/https.go index 485e72bb..55f6731a 100644 --- a/adapter/inbound/https.go +++ b/adapter/inbound/https.go @@ -4,24 +4,14 @@ import ( "net" "net/http" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/context" + C "github.com/metacubex/mihomo/constant" ) // NewHTTPS receive CONNECT request and return ConnContext -func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) *context.ConnContext { +func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) { metadata := parseHTTPAddr(request) metadata.Type = C.HTTPS - for _, addition := range additions { - addition.Apply(metadata) - } - if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil { - metadata.SrcIP = ip - metadata.SrcPort = port - } - if ip, port, err := parseAddr(conn.LocalAddr()); err == nil { - metadata.InIP = ip - metadata.InPort = port - } - return context.NewConnContext(conn, metadata) + ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) + ApplyAdditions(metadata, additions...) + return conn, metadata } diff --git a/adapter/inbound/listen.go b/adapter/inbound/listen.go index fa82db92..8b7b5fb2 100644 --- a/adapter/inbound/listen.go +++ b/adapter/inbound/listen.go @@ -17,6 +17,10 @@ func SetTfo(open bool) { lc.DisableTFO = !open } +func SetMPTCP(open bool) { + setMultiPathTCP(&lc.ListenConfig, open) +} + func ListenContext(ctx context.Context, network, address string) (net.Listener, error) { return lc.Listen(ctx, network, address) } diff --git a/adapter/inbound/mptcp_go120.go b/adapter/inbound/mptcp_go120.go new file mode 100644 index 00000000..f9b22533 --- /dev/null +++ b/adapter/inbound/mptcp_go120.go @@ -0,0 +1,10 @@ +//go:build !go1.21 + +package inbound + +import "net" + +const multipathTCPAvailable = false + +func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) { +} diff --git a/adapter/inbound/mptcp_go121.go b/adapter/inbound/mptcp_go121.go new file mode 100644 index 00000000..6b35d1a8 --- /dev/null +++ b/adapter/inbound/mptcp_go121.go @@ -0,0 +1,11 @@ +//go:build go1.21 + +package inbound + +import "net" + +const multipathTCPAvailable = true + +func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) { + listenConfig.SetMultipathTCP(open) +} diff --git a/adapter/inbound/packet.go b/adapter/inbound/packet.go index 44e5e1a7..7e245f98 100644 --- a/adapter/inbound/packet.go +++ b/adapter/inbound/packet.go @@ -1,42 +1,20 @@ package inbound import ( - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" ) -// PacketAdapter is a UDP Packet adapter for socks/redir/tun -type PacketAdapter struct { - C.UDPPacket - metadata *C.Metadata -} - -// Metadata returns destination metadata -func (s *PacketAdapter) Metadata() *C.Metadata { - return s.metadata -} - // NewPacket is PacketAdapter generator -func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) C.PacketAdapter { +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 - for _, addition := range additions { - addition.Apply(metadata) - } - if ip, port, err := parseAddr(packet.LocalAddr()); err == nil { - metadata.SrcIP = ip - metadata.SrcPort = port - } + ApplyAdditions(metadata, WithSrcAddr(packet.LocalAddr())) if p, ok := packet.(C.UDPPacketInAddr); ok { - if ip, port, err := parseAddr(p.InAddr()); err == nil { - metadata.InIP = ip - metadata.InPort = port - } + ApplyAdditions(metadata, WithInAddr(p.InAddr())) } + ApplyAdditions(metadata, additions...) - return &PacketAdapter{ - packet, - metadata, - } + return packet, metadata } diff --git a/adapter/inbound/socket.go b/adapter/inbound/socket.go index 590f64d7..8cd301f7 100644 --- a/adapter/inbound/socket.go +++ b/adapter/inbound/socket.go @@ -2,49 +2,17 @@ package inbound import ( "net" - "net/netip" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/context" - "github.com/Dreamacro/clash/transport/socks5" + 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) *context.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 - for _, addition := range additions { - addition.Apply(metadata) - } - - if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil { - metadata.SrcIP = ip - metadata.SrcPort = port - } - if ip, port, err := parseAddr(conn.LocalAddr()); err == nil { - metadata.InIP = ip - metadata.InPort = port - } - - return context.NewConnContext(conn, metadata) -} - -func NewInner(conn net.Conn, dst string, host string) *context.ConnContext { - metadata := &C.Metadata{} - metadata.NetWork = C.TCP - metadata.Type = C.INNER - metadata.DNSMode = C.DNSMapping - metadata.Host = host - metadata.Process = C.ClashName - if h, port, err := net.SplitHostPort(dst); err == nil { - metadata.DstPort = port - if host == "" { - if ip, err := netip.ParseAddr(h); err == nil { - metadata.DstIP = ip - } - } - } - - return context.NewConnContext(conn, metadata) + 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 index 88e989f9..743337fc 100644 --- a/adapter/inbound/util.go +++ b/adapter/inbound/util.go @@ -1,16 +1,15 @@ package inbound import ( - "errors" "net" "net/http" "net/netip" "strconv" "strings" - "github.com/Dreamacro/clash/common/nnip" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/common/nnip" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" ) func parseSocksAddr(target socks5.Addr) *C.Metadata { @@ -20,14 +19,14 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata { case socks5.AtypDomainName: // trim for FQDN metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".") - metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) + metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) case socks5.AtypIPv4: metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len])) - metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) + metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) case socks5.AtypIPv6: ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len]) metadata.DstIP = ip6.Unmap() - metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) + metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) } return metadata @@ -43,11 +42,16 @@ func parseHTTPAddr(request *http.Request) *C.Metadata { // 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: port, + DstPort: uint16Port, } ip, err := netip.ParseAddr(host) @@ -57,24 +61,3 @@ func parseHTTPAddr(request *http.Request) *C.Metadata { return metadata } - -func parseAddr(addr net.Addr) (netip.Addr, string, error) { - // Filter when net.Addr interface is nil - if addr == nil { - return netip.Addr{}, "", errors.New("nil addr") - } - if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok { - ip, port, err := parseAddr(rawAddr.RawAddr()) - if err == nil { - return ip, port, err - } - } - addrStr := addr.String() - host, port, err := net.SplitHostPort(addrStr) - if err != nil { - return netip.Addr{}, "", err - } - - ip, err := netip.ParseAddr(host) - return ip, port, err -} diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go index 24de7d94..ae8c4651 100644 --- a/adapter/outbound/base.go +++ b/adapter/outbound/base.go @@ -3,15 +3,14 @@ package outbound import ( "context" "encoding/json" - "errors" "net" "strings" + "syscall" - N "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" - - "github.com/gofrs/uuid" + 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" ) type Base struct { @@ -22,6 +21,7 @@ type Base struct { udp bool xudp bool tfo bool + mpTcp bool rmark int id string prefer C.DNSPrefer @@ -35,12 +35,7 @@ func (b *Base) Name() string { // Id implements C.ProxyAdapter func (b *Base) Id() string { if b.id == "" { - id, err := uuid.NewV6() - if err != nil { - b.id = b.name - } else { - b.id = id.String() - } + b.id = utils.NewUUIDV6().String() } return b.id @@ -51,33 +46,33 @@ func (b *Base) Type() C.AdapterType { return b.tp } -// StreamConn implements C.ProxyAdapter -func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { - return c, errors.New("no support") +// 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, opts ...dialer.Option) (C.Conn, error) { - return nil, errors.New("no support") + 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, errors.New("no support") + return nil, C.ErrNotSupport } // ListenPacketContext implements C.ProxyAdapter func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { - return nil, errors.New("no support") + 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, errors.New("no support") + return nil, C.ErrNotSupport } // SupportWithDialer implements C.ProxyAdapter -func (b *Base) SupportWithDialer() bool { - return false +func (b *Base) SupportWithDialer() C.NetWork { + return C.InvalidNet } // SupportUOT implements C.ProxyAdapter @@ -100,6 +95,11 @@ func (b *Base) SupportTFO() bool { return b.tfo } +// 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{ @@ -140,13 +140,24 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option { default: } + if b.tfo { + opts = append(opts, dialer.WithTFO(true)) + } + + if b.mpTcp { + opts = append(opts, dialer.WithMPTCP(true)) + } + return opts } type BasicOption struct { + TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"` + MPTCP bool `proxy:"mptcp,omitempty" group:"mptcp,omitempty"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` IPVersion string `proxy:"ip-version,omitempty" group:"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 { @@ -156,6 +167,7 @@ type BaseOption struct { UDP bool XUDP bool TFO bool + MPTCP bool Interface string RoutingMark int Prefer C.DNSPrefer @@ -169,6 +181,7 @@ func NewBase(opt BaseOption) *Base { udp: opt.UDP, xudp: opt.XUDP, tfo: opt.TFO, + mpTcp: opt.MPTCP, iface: opt.Interface, rmark: opt.RoutingMark, prefer: opt.Prefer, @@ -199,13 +212,26 @@ func (c *conn) Upstream() any { return c.ExtendedConn } +func (c *conn) WriterReplaceable() bool { + return true +} + +func (c *conn) ReaderReplaceable() bool { + return true +} + 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 { - net.PacketConn + N.EnhancePacketConn chain C.Chain + adapterName string + connID string actualRemoteDestination string } @@ -223,8 +249,29 @@ 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 newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { - return &packetConn{pc, []string{a.Name()}, parseRemoteDestination(a.Addr())} + 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 { diff --git a/adapter/outbound/direct.go b/adapter/outbound/direct.go index cf1b2648..b9b9fefc 100644 --- a/adapter/outbound/direct.go +++ b/adapter/outbound/direct.go @@ -2,17 +2,24 @@ package outbound import ( "context" - "net" + "errors" + "net/netip" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" - C "github.com/Dreamacro/clash/constant" + 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" ) type Direct struct { *Base } +type DirectOption struct { + BasicOption + Name string `proxy:"name"` +} + // DialContext implements C.ProxyAdapter func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { opts = append(opts, dialer.WithResolver(resolver.DefaultResolver)) @@ -20,22 +27,40 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ... if err != nil { return nil, err } - tcpKeepAlive(c) + N.TCPKeepAlive(c) return NewConn(c, d), nil } // ListenPacketContext implements C.ProxyAdapter func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { - opts = append(opts, dialer.WithResolver(resolver.DefaultResolver)) - pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", metadata.DstIP), "", d.Base.DialOptions(opts...)...) + // 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.DefaultResolver) + if err != nil { + return nil, errors.New("can't resolve ip") + } + metadata.DstIP = ip + } + pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort)) if err != nil { return nil, err } - return newPacketConn(&directPacketConn{pc}, d), nil + return newPacketConn(pc, d), nil } -type directPacketConn struct { - net.PacketConn +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), + }, + } } func NewDirect() *Direct { diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go index 720dc3e1..b837e49a 100644 --- a/adapter/outbound/http.go +++ b/adapter/outbound/http.go @@ -7,15 +7,17 @@ import ( "encoding/base64" "errors" "fmt" + "io" "net" "net/http" - "net/url" "strconv" - "github.com/Dreamacro/clash/component/dialer" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" + 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 { @@ -40,12 +42,10 @@ type HttpOption struct { Headers map[string]string `proxy:"headers,omitempty"` } -// StreamConn implements C.ProxyAdapter -func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { +// 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) - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() err := cc.HandshakeContext(ctx) c = cc if err != nil { @@ -66,17 +66,23 @@ func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...di // 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) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - c, err = h.StreamConn(c, metadata) + c, err = h.StreamConnContext(ctx, c, metadata) if err != nil { return nil, err } @@ -85,40 +91,42 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad } // SupportWithDialer implements C.ProxyAdapter -func (h *Http) SupportWithDialer() bool { - return true +func (h *Http) SupportWithDialer() C.NetWork { + return C.TCP } func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { addr := metadata.RemoteAddress() - req := &http.Request{ - Method: http.MethodConnect, - URL: &url.URL{ - Host: addr, - }, - Host: addr, - Header: http.Header{ - "Proxy-Connection": []string{"Keep-Alive"}, - }, + 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", } - //增加headers - if len(h.option.Headers) != 0 { - for key, value := range h.option.Headers { - req.Header.Add(key, value) - } + for key, value := range h.option.Headers { + tempHeaders[key] = value } if h.user != "" && h.pass != "" { auth := h.user + ":" + h.pass - req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) + tempHeaders["Proxy-Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) } - if err := req.Write(rw); err != nil { + for key, value := range tempHeaders { + HeaderString += key + ": " + value + "\r\n" + } + + HeaderString += "\r\n" + + _, err := rw.Write([]byte(HeaderString)) + + if err != nil { return err } - resp, err := http.ReadResponse(bufio.NewReader(rw), req) + resp, err := http.ReadResponse(bufio.NewReader(rw), nil) + if err != nil { return err } @@ -149,19 +157,13 @@ func NewHttp(option HttpOption) (*Http, error) { if option.SNI != "" { sni = option.SNI } - if len(option.Fingerprint) == 0 { - tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{ - InsecureSkipVerify: option.SkipCertVerify, - ServerName: sni, - }) - } else { - var err error - if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(&tls.Config{ - InsecureSkipVerify: option.SkipCertVerify, - ServerName: sni, - }, option.Fingerprint); err != nil { - return nil, err - } + var err error + tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ + InsecureSkipVerify: option.SkipCertVerify, + ServerName: sni, + }, option.Fingerprint) + if err != nil { + return nil, err } } @@ -170,6 +172,8 @@ func NewHttp(option HttpOption) (*Http, error) { 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), diff --git a/adapter/outbound/hysteria.go b/adapter/outbound/hysteria.go index bd75cc3c..dacffd10 100644 --- a/adapter/outbound/hysteria.go +++ b/adapter/outbound/hysteria.go @@ -2,16 +2,11 @@ package outbound import ( "context" - "crypto/sha256" "crypto/tls" "encoding/base64" - "encoding/hex" - "encoding/pem" "fmt" "net" "net/netip" - "os" - "regexp" "strconv" "time" @@ -19,15 +14,17 @@ import ( "github.com/metacubex/quic-go/congestion" M "github.com/sagernet/sing/common/metadata" - "github.com/Dreamacro/clash/component/dialer" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion" - "github.com/Dreamacro/clash/transport/hysteria/core" - "github.com/Dreamacro/clash/transport/hysteria/obfs" - "github.com/Dreamacro/clash/transport/hysteria/pmtud_fix" - "github.com/Dreamacro/clash/transport/hysteria/transport" + "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/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" ) const ( @@ -41,26 +38,15 @@ const ( DefaultHopInterval = 10 ) -var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`) - type Hysteria struct { *Base + option *HysteriaOption client *core.Client } func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { - hdc := hyDialerWithContext{ - ctx: context.Background(), - hyDialer: func(network string) (net.PacketConn, error) { - return dialer.ListenPacket(ctx, network, "", h.Base.DialOptions(opts...)...) - }, - remoteAddr: func(addr string) (net.Addr, error) { - return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer) - }, - } - - tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), &hdc) + tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx, opts...)) if err != nil { return nil, err } @@ -69,20 +55,32 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts . } func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { - hdc := hyDialerWithContext{ + udpConn, err := h.client.DialUDP(h.genHdc(ctx, opts...)) + if err != nil { + return nil, err + } + return newPacketConn(&hyPacketConn{udpConn}, h), nil +} + +func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer { + return &hyDialerWithContext{ ctx: context.Background(), hyDialer: func(network string) (net.PacketConn, error) { - return dialer.ListenPacket(ctx, network, "", h.Base.DialOptions(opts...)...) + var err error + var cDialer C.Dialer = dialer.NewDialer(h.Base.DialOptions(opts...)...) + if len(h.option.DialerProxy) > 0 { + cDialer, err = proxydialer.NewByName(h.option.DialerProxy, cDialer) + if err != nil { + return nil, err + } + } + rAddrPort, _ := netip.ParseAddrPort(h.Addr()) + return cDialer.ListenPacket(ctx, network, "", rAddrPort) }, remoteAddr: func(addr string) (net.Addr, error) { return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer) }, } - udpConn, err := h.client.DialUDP(&hdc) - if err != nil { - return nil, err - } - return newPacketConn(&hyPacketConn{udpConn}, h), nil } type HysteriaOption struct { @@ -115,12 +113,12 @@ type HysteriaOption struct { func (c *HysteriaOption) Speed() (uint64, uint64, error) { var up, down uint64 - up = stringToBps(c.Up) + up = StringToBps(c.Up) if up == 0 { return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up) } - down = stringToBps(c.Down) + down = StringToBps(c.Down) if down == 0 { return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down) } @@ -148,37 +146,10 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { MinVersion: tls.VersionTLS13, } - var bs []byte var err error - if len(option.CustomCA) > 0 { - bs, err = os.ReadFile(option.CustomCA) - if err != nil { - return nil, fmt.Errorf("hysteria %s load ca error: %w", addr, err) - } - } else if option.CustomCAString != "" { - bs = []byte(option.CustomCAString) - } - - if len(bs) > 0 { - block, _ := pem.Decode(bs) - if block == nil { - return nil, fmt.Errorf("CA cert is not PEM") - } - - fpBytes := sha256.Sum256(block.Bytes) - if len(option.Fingerprint) == 0 { - option.Fingerprint = hex.EncodeToString(fpBytes[:]) - } - } - - if len(option.Fingerprint) != 0 { - var err error - tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) - if err != nil { - return nil, err - } - } else { - tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) + tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) + if err != nil { + return nil, err } if len(option.ALPN) > 0 { @@ -258,46 +229,11 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), }, + option: &option, client: client, }, nil } -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 - switch m[2] { - case "K": - n = 1 << 10 - case "M": - n = 1 << 20 - case "G": - n = 1 << 30 - case "T": - n = 1 << 40 - default: - n = 1 - } - v, _ := strconv.ParseUint(m[1], 10, 64) - n = v * n - if m[3] == "b" { - // Bits, need to convert to bytes - n = n >> 3 - } - return n -} - type hyPacketConn struct { core.UDPConn } @@ -312,6 +248,16 @@ func (c *hyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { 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 { diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go new file mode 100644 index 00000000..ddd5ccea --- /dev/null +++ b/adapter/outbound/hysteria2.go @@ -0,0 +1,157 @@ +package outbound + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "runtime" + "strconv" + + CN "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" + tuicCommon "github.com/metacubex/mihomo/transport/tuic/common" + + "github.com/metacubex/sing-quic/hysteria2" + + M "github.com/sagernet/sing/common/metadata" +) + +func init() { + hysteria2.SetCongestionController = tuicCommon.SetCongestionController +} + +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"` + 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"` +} + +func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { + options := h.Base.DialOptions(opts...) + h.dialer.SetDialer(dialer.NewDialer(options...)) + c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) + if err != nil { + return nil, err + } + return NewConn(CN.NewRefConn(c, h), h), nil +} + +func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { + options := h.Base.DialOptions(opts...) + h.dialer.SetDialer(dialer.NewDialer(options...)) + 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.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), h), h), nil +} + +func closeHysteria2(h *Hysteria2) { + if h.client != nil { + _ = h.client.CloseWithError(errors.New("proxy removed")) + } +} + +func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + 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 + } + + singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()) + + clientOptions := hysteria2.ClientOptions{ + Context: context.TODO(), + Dialer: singDialer, + ServerAddress: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)), + SendBPS: StringToBps(option.Up), + ReceiveBPS: StringToBps(option.Down), + SalamanderPassword: salamanderPassword, + Password: option.Password, + TLSConfig: tlsConfig, + UDPDisabled: false, + CWND: option.CWND, + } + + client, err := hysteria2.NewClient(clientOptions) + if err != nil { + return nil, err + } + + 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, + client: client, + dialer: singDialer, + } + runtime.SetFinalizer(outbound, closeHysteria2) + + return outbound, nil +} diff --git a/adapter/outbound/reality.go b/adapter/outbound/reality.go new file mode 100644 index 00000000..766138da --- /dev/null +++ b/adapter/outbound/reality.go @@ -0,0 +1,35 @@ +package outbound + +import ( + "encoding/base64" + "encoding/hex" + "errors" + + tlsC "github.com/metacubex/mihomo/component/tls" + + "golang.org/x/crypto/curve25519" +) + +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) + + n, err := base64.RawURLEncoding.Decode(config.PublicKey[:], []byte(o.PublicKey)) + if err != nil || n != curve25519.ScalarSize { + return nil, errors.New("invalid REALITY public key") + } + + 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 index 43833238..5625f932 100644 --- a/adapter/outbound/reject.go +++ b/adapter/outbound/reject.go @@ -6,22 +6,37 @@ import ( "net" "time" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/common/buf" + "github.com/metacubex/mihomo/component/dialer" + C "github.com/metacubex/mihomo/constant" ) type Reject struct { *Base } +type RejectOption struct { + Name string `proxy:"name"` +} + // DialContext implements C.ProxyAdapter func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { - return NewConn(&nopConn{}, r), nil + return NewConn(nopConn{}, r), nil } // ListenPacketContext implements C.ProxyAdapter func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { - return newPacketConn(&nopPacketConn{}, r), nil + return newPacketConn(nopPacketConn{}, r), nil +} + +func NewRejectWithOption(option RejectOption) *Reject { + return &Reject{ + Base: &Base{ + name: option.Name, + tp: C.Direct, + udp: true, + }, + } } func NewReject() *Reject { @@ -48,27 +63,40 @@ func NewPass() *Reject { type nopConn struct{} -func (rw *nopConn) Read(b []byte) (int, error) { +func (rw nopConn) Read(b []byte) (int, error) { return 0, io.EOF } -func (rw *nopConn) Write(b []byte) (int, error) { +func (rw nopConn) ReadBuffer(buffer *buf.Buffer) error { + return io.EOF +} + +func (rw nopConn) Write(b []byte) (int, error) { return 0, io.EOF } -func (rw *nopConn) Close() error { return nil } -func (rw *nopConn) LocalAddr() net.Addr { return nil } -func (rw *nopConn) RemoteAddr() net.Addr { return nil } -func (rw *nopConn) SetDeadline(time.Time) error { return nil } -func (rw *nopConn) SetReadDeadline(time.Time) error { return nil } -func (rw *nopConn) SetWriteDeadline(time.Time) error { return nil } +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) Close() error { return nil } -func (npc *nopPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} } -func (npc *nopPacketConn) SetDeadline(time.Time) error { return nil } -func (npc *nopPacketConn) SetReadDeadline(time.Time) error { return nil } -func (npc *nopPacketConn) SetWriteDeadline(time.Time) error { return nil } +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 } diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 54566666..ffc72abb 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -2,24 +2,24 @@ package outbound import ( "context" - "crypto/tls" "errors" "fmt" "net" "strconv" - "github.com/Dreamacro/clash/common/structure" - "github.com/Dreamacro/clash/component/dialer" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/shadowtls" - obfs "github.com/Dreamacro/clash/transport/simple-obfs" - "github.com/Dreamacro/clash/transport/socks5" - v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" + 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" + "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-shadowsocks" - "github.com/metacubex/sing-shadowsocks/shadowimpl" - "github.com/sagernet/sing/common/bufio" + restlsC "github.com/3andne/restls-client-go" + shadowsocks "github.com/metacubex/sing-shadowsocks2" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/uot" ) @@ -33,21 +33,23 @@ type ShadowSocks struct { obfsMode string obfsOption *simpleObfsOption v2rayOption *v2rayObfs.Option - shadowTLSOption *shadowTLSOption - tlsConfig *tls.Config + shadowTLSOption *shadowtls.ShadowTLSOption + restlsConfig *restlsC.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"` + 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 { @@ -56,14 +58,15 @@ type simpleObfsOption struct { } 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"` + 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"` } type shadowTLSOption struct { @@ -71,10 +74,19 @@ type shadowTLSOption struct { Host string `obfs:"host"` Fingerprint string `obfs:"fingerprint,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` + Version int `obfs:"version,omitempty"` } -// StreamConn implements C.ProxyAdapter -func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { +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, error) { + useEarly := false switch ss.obfsMode { case "tls": c = obfs.NewTLSObfs(c, ss.obfsOption.Host) @@ -83,17 +95,39 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port) case "websocket": var err error - c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption) + c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption) if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } case shadowtls.Mode: - c = shadowtls.NewShadowTLS(c, ss.shadowTLSOption.Password, ss.tlsConfig) + var err error + c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption) + if err != nil { + return nil, err + } + useEarly = true + case restls.Mode: + var err error + 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 metadata.NetWork == C.UDP && ss.option.UDPOverTCP { - return ss.method.DialConn(c, M.ParseSocksaddr(uot.UOTMagicAddress+":443")) + 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)) } - return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) } // DialContext implements C.ProxyAdapter @@ -103,17 +137,23 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, op // 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) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - c, err = ss.StreamConn(c, metadata) + c, err = ss.StreamConnContext(ctx, c, metadata) return NewConn(c, ss), err } @@ -124,12 +164,18 @@ func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Meta // ListenPacketWithDialer implements C.ProxyAdapter func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { + if len(ss.option.DialerProxy) > 0 { + dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer) + if err != nil { + return nil, err + } + } if ss.option.UDPOverTCP { tcpConn, err := ss.DialContextWithDialer(ctx, dialer, metadata) if err != nil { return nil, err } - return newPacketConn(uot.NewClientConn(tcpConn), ss), nil + return ss.ListenPacketOnStreamConn(ctx, tcpConn, metadata) } addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer) if err != nil { @@ -140,21 +186,35 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial if err != nil { return nil, err } - pc = ss.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: pc, Addr: addr}) + pc = ss.method.DialPacketConn(N.NewBindPacketConn(pc, addr)) return newPacketConn(pc, ss), nil } // SupportWithDialer implements C.ProxyAdapter -func (ss *ShadowSocks) SupportWithDialer() bool { - return true +func (ss *ShadowSocks) SupportWithDialer() C.NetWork { + return C.ALLNet } // ListenPacketOnStreamConn implements C.ProxyAdapter -func (ss *ShadowSocks) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { +func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { if ss.option.UDPOverTCP { - return newPacketConn(uot.NewClientConn(c), ss), nil + // 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(uot.NewConn(c, uot.Request{Destination: destination}), ss), nil + } else { + return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), ss), nil + } } - return nil, errors.New("no support") + return nil, C.ErrNotSupport } // SupportUOT implements C.ProxyAdapter @@ -164,15 +224,17 @@ func (ss *ShadowSocks) SupportUOT() bool { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - method, err := shadowimpl.FetchMethod(option.Cipher, option.Password) + method, err := shadowsocks.CreateMethod(context.Background(), option.Cipher, shadowsocks.MethodOptions{ + Password: option.Password, + }) if err != nil { return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) } var v2rayOption *v2rayObfs.Option var obfsOption *simpleObfsOption - var shadowTLSOpt *shadowTLSOption - var tlsConfig *tls.Config + var shadowTLSOpt *shadowtls.ShadowTLSOption + var restlsConfig *restlsC.Config obfsMode := "" decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) @@ -198,10 +260,11 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { } obfsMode = opts.Mode v2rayOption = &v2rayObfs.Option{ - Host: opts.Host, - Path: opts.Path, - Headers: opts.Headers, - Mux: opts.Mux, + Host: opts.Host, + Path: opts.Path, + Headers: opts.Headers, + Mux: opts.Mux, + V2rayHttpUpgrade: opts.V2rayHttpUpgrade, } if opts.TLS { @@ -210,25 +273,40 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { } } else if option.Plugin == shadowtls.Mode { obfsMode = shadowtls.Mode - shadowTLSOpt = &shadowTLSOption{} - if err := decoder.Decode(option.PluginOpts, shadowTLSOpt); err != nil { + 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) } - tlsConfig = &tls.Config{ - NextProtos: shadowtls.DefaultALPN, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: shadowTLSOpt.SkipCertVerify, - ServerName: shadowTLSOpt.Host, + shadowTLSOpt = &shadowtls.ShadowTLSOption{ + Password: opt.Password, + Host: opt.Host, + Fingerprint: opt.Fingerprint, + ClientFingerprint: option.ClientFingerprint, + SkipCertVerify: opt.SkipCertVerify, + Version: opt.Version, + } + } 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) } - if len(shadowTLSOpt.Fingerprint) == 0 { - tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } else { - if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, shadowTLSOpt.Fingerprint); err != nil { - return nil, err - } + restlsConfig, err = restlsC.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{ @@ -237,6 +315,8 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { 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), @@ -248,39 +328,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { v2rayOption: v2rayOption, obfsOption: obfsOption, shadowTLSOption: shadowTLSOpt, - tlsConfig: tlsConfig, + restlsConfig: restlsConfig, }, nil } - -type ssPacketConn struct { - net.PacketConn - rAddr net.Addr -} - -func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { - packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) - if err != nil { - return - } - return spc.PacketConn.WriteTo(packet[3:], spc.rAddr) -} - -func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { - n, _, e := spc.PacketConn.ReadFrom(b) - if e != nil { - return 0, nil, e - } - - addr := socks5.SplitAddr(b[:n]) - if addr == nil { - return 0, nil, errors.New("parse addr error") - } - - udpAddr := addr.UDPAddr() - if udpAddr == nil { - return 0, nil, errors.New("parse addr error") - } - - copy(b, b[len(addr):]) - return n - len(addr), udpAddr, e -} diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go index e84de879..07d78047 100644 --- a/adapter/outbound/shadowsocksr.go +++ b/adapter/outbound/shadowsocksr.go @@ -2,21 +2,26 @@ package outbound import ( "context" + "errors" "fmt" "net" "strconv" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/shadowsocks/core" - "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" - "github.com/Dreamacro/clash/transport/shadowsocks/shadowstream" - "github.com/Dreamacro/clash/transport/ssr/obfs" - "github.com/Dreamacro/clash/transport/ssr/protocol" + 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 @@ -36,8 +41,8 @@ type ShadowSocksROption struct { UDP bool `proxy:"udp,omitempty"` } -// StreamConn implements C.ProxyAdapter -func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { +// StreamConnContext implements C.ProxyAdapter +func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { c = ssr.obfs.StreamConn(c) c = ssr.cipher.StreamConn(c) var ( @@ -65,17 +70,23 @@ func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.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) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - c, err = ssr.StreamConn(c, metadata) + c, err = ssr.StreamConnContext(ctx, c, metadata) return NewConn(c, ssr), err } @@ -86,6 +97,12 @@ func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Me // 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 := resolveUDPAddrWithPrefer(ctx, "udp", ssr.addr, ssr.prefer) if err != nil { return nil, err @@ -96,19 +113,19 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di return nil, err } - pc = ssr.cipher.PacketConn(pc) - pc = ssr.protocol.PacketConn(pc) - return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil + 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() bool { - return true +func (ssr *ShadowSocksR) SupportWithDialer() C.NetWork { + return C.ALLNet } func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { // SSR protocol compatibility - // https://github.com/Dreamacro/clash/pull/2056 + // https://github.com/metacubex/mihomo/pull/2056 if option.Cipher == "none" { option.Cipher = "dummy" } @@ -163,12 +180,74 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { 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 new file mode 100644 index 00000000..dff1e8eb --- /dev/null +++ b/adapter/outbound/singmux.go @@ -0,0 +1,118 @@ +package outbound + +import ( + "context" + "errors" + "runtime" + + 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" + + mux "github.com/sagernet/sing-mux" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" +) + +type SingMux struct { + C.ProxyAdapter + base ProxyBase + 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"` +} + +type ProxyBase interface { + DialOptions(opts ...dialer.Option) []dialer.Option +} + +func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { + options := s.base.DialOptions(opts...) + s.dialer.SetDialer(dialer.NewDialer(options...)) + c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) + if err != nil { + return nil, err + } + return NewConn(CN.NewRefConn(c, s), s.ProxyAdapter), err +} + +func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { + if s.onlyTcp { + return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...) + } + options := s.base.DialOptions(opts...) + s.dialer.SetDialer(dialer.NewDialer(options...)) + + // 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.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), s), s.ProxyAdapter), 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 closeSingMux(s *SingMux) { + _ = s.client.Close() +} + +func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.ProxyAdapter, error) { + singDialer := proxydialer.NewSingDialer(proxy, dialer.NewDialer(), option.Statistic) + client, err := mux.NewClient(mux.Options{ + Dialer: singDialer, + Protocol: option.Protocol, + MaxConnections: option.MaxConnections, + MinStreams: option.MinStreams, + MaxStreams: option.MaxStreams, + Padding: option.Padding, + }) + if err != nil { + return nil, err + } + outbound := &SingMux{ + ProxyAdapter: proxy, + base: base, + client: client, + dialer: singDialer, + onlyTcp: option.OnlyTcp, + } + runtime.SetFinalizer(outbound, closeSingMux) + return outbound, nil +} diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go index 1331b526..76ed4be9 100644 --- a/adapter/outbound/snell.go +++ b/adapter/outbound/snell.go @@ -6,15 +6,18 @@ import ( "net" "strconv" - "github.com/Dreamacro/clash/common/structure" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" - obfs "github.com/Dreamacro/clash/transport/simple-obfs" - "github.com/Dreamacro/clash/transport/snell" + 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 @@ -50,15 +53,14 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell { return snell.StreamConn(c, option.psk, option.version) } -// StreamConn implements C.ProxyAdapter -func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { +// StreamConnContext implements C.ProxyAdapter +func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) if metadata.NetWork == C.UDP { err := snell.WriteUDPHeader(c, s.version) return c, err } - port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) - err := snell.WriteHeader(c, metadata.String(), uint(port), s.version) + err := snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version) return c, err } @@ -70,8 +72,7 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d return nil, err } - port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) - if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil { + if err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version); err != nil { c.Close() return nil, err } @@ -83,17 +84,23 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d // 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) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - c, err = s.StreamConn(c, metadata) + c, err = s.StreamConnContext(ctx, c, metadata) return NewConn(c, s), err } @@ -104,11 +111,18 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o // 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 } - tcpKeepAlive(c) + N.TCPKeepAlive(c) c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) err = snell.WriteUDPHeader(c, s.version) @@ -121,8 +135,8 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met } // SupportWithDialer implements C.ProxyAdapter -func (s *Snell) SupportWithDialer() bool { - return true +func (s *Snell) SupportWithDialer() C.NetWork { + return C.ALLNet } // SupportUOT implements C.ProxyAdapter @@ -167,10 +181,13 @@ func NewSnell(option SnellOption) (*Snell, error) { 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, @@ -178,12 +195,20 @@ func NewSnell(option SnellOption) (*Snell, error) { if option.Version == snell.Version2 { s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) { - c, err := dialer.DialContext(ctx, "tcp", addr, s.Base.DialOptions()...) + var err error + var cDialer C.Dialer = dialer.NewDialer(s.Base.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 } - tcpKeepAlive(c) + N.TCPKeepAlive(c) return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil }) } diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go index d40a6bff..c17ee6a7 100644 --- a/adapter/outbound/socks5.go +++ b/adapter/outbound/socks5.go @@ -7,16 +7,20 @@ import ( "fmt" "io" "net" + "net/netip" "strconv" - "github.com/Dreamacro/clash/component/dialer" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + 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 @@ -37,12 +41,10 @@ type Socks5Option struct { Fingerprint string `proxy:"fingerprint,omitempty"` } -// StreamConn implements C.ProxyAdapter -func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { +// 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) - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() err := cc.HandshakeContext(ctx) c = cc if err != nil { @@ -70,17 +72,23 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts .. // 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) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - c, err = ss.StreamConn(c, metadata) + c, err = ss.StreamConnContext(ctx, c, metadata) if err != nil { return nil, err } @@ -89,13 +97,20 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me } // SupportWithDialer implements C.ProxyAdapter -func (ss *Socks5) SupportWithDialer() bool { - return true +func (ss *Socks5) SupportWithDialer() C.NetWork { + return C.TCP } // ListenPacketContext implements C.ProxyAdapter func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { - c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...) + var cDialer C.Dialer = dialer.NewDialer(ss.Base.DialOptions(opts...)...) + 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 @@ -113,7 +128,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, safeConnClose(c, err) }(c) - tcpKeepAlive(c) + N.TCPKeepAlive(c) var user *socks5.User if ss.user != "" { user = &socks5.User{ @@ -122,7 +137,8 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, } } - bindAddr, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user) + udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) + bindAddr, err := socks5.ClientHandshake(c, udpAssocateAddr, socks5.CmdUDPAssociate, user) if err != nil { err = fmt.Errorf("client hanshake error: %w", err) return @@ -142,7 +158,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, bindUDPAddr.IP = serverAddr.IP } - pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", bindUDPAddr.AddrPort().Addr()), "", ss.Base.DialOptions(opts...)...) + pc, err := cDialer.ListenPacket(ctx, "udp", "", bindUDPAddr.AddrPort()) if err != nil { return } @@ -166,13 +182,10 @@ func NewSocks5(option Socks5Option) (*Socks5, error) { ServerName: option.Server, } - if len(option.Fingerprint) == 0 { - tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } else { - var err error - if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil { - return nil, err - } + var err error + tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + if err != nil { + return nil, err } } @@ -182,10 +195,13 @@ func NewSocks5(option Socks5Option) (*Socks5, error) { 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, diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index 2a8cfe47..cd1dd28c 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -8,13 +8,14 @@ import ( "net/http" "strconv" - N "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/component/dialer" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/gun" - "github.com/Dreamacro/clash/transport/trojan" - "github.com/Dreamacro/clash/transport/vless" + 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/trojan" ) type Trojan struct { @@ -26,34 +27,36 @@ type Trojan struct { gunTLSConfig *tls.Config gunConfig *gun.Config transport *gun.TransportWrap + + realityConfig *tlsC.RealityConfig } 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"` - GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` - WSOpts WSOptions `proxy:"ws-opts,omitempty"` - Flow string `proxy:"flow,omitempty"` - FlowShow bool `proxy:"flow-show,omitempty"` - ClientFingerprint string `proxy:"client-fingerprint,omitempty"` + 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"` + ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } -func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { +func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error) { if t.option.Network == "ws" { host, port, _ := net.SplitHostPort(t.addr) wsOpts := &trojan.WebsocketOption{ - Host: host, - Port: port, - Path: t.option.WSOpts.Path, + Host: host, + Port: port, + Path: t.option.WSOpts.Path, + V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade, } if t.option.SNI != "" { @@ -68,14 +71,14 @@ func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { wsOpts.Headers = header } - return t.instance.StreamWebsocketConn(c, wsOpts) + return t.instance.StreamWebsocketConn(ctx, c, wsOpts) } - return t.instance.StreamConn(c) + return t.instance.StreamConn(ctx, c) } -// StreamConn implements C.ProxyAdapter -func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { +// StreamConnContext implements C.ProxyAdapter +func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { var err error if tlsC.HaveGlobalFingerprint() && len(t.option.ClientFingerprint) == 0 { @@ -83,26 +86,21 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) } if t.transport != nil { - c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig) + c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig) } else { - c, err = t.plainStream(c) + c, err = t.plainStream(ctx, c) } if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } - c, err = t.instance.PresetXTLSConn(c) - if err != nil { - return nil, err - } - if metadata.NetWork == C.UDP { err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) return c, err } err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)) - return N.NewExtendedConn(c), err + return c, err } // DialContext implements C.ProxyAdapter @@ -114,12 +112,6 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ... return nil, err } - c, err = t.instance.PresetXTLSConn(c) - if err != nil { - c.Close() - return nil, err - } - if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil { c.Close() return nil, err @@ -132,17 +124,23 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ... // 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) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - c, err = t.StreamConn(c, metadata) + c, err = t.StreamConnContext(ctx, c, metadata) if err != nil { return nil, err } @@ -176,6 +174,12 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.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) @@ -183,8 +187,8 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me defer func(c net.Conn) { safeConnClose(c, err) }(c) - tcpKeepAlive(c) - c, err = t.plainStream(c) + N.TCPKeepAlive(c) + c, err = t.plainStream(ctx, c) if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } @@ -199,8 +203,8 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me } // SupportWithDialer implements C.ProxyAdapter -func (t *Trojan) SupportWithDialer() bool { - return true +func (t *Trojan) SupportWithDialer() C.NetWork { + return C.ALLNet } // ListenPacketOnStreamConn implements C.ProxyAdapter @@ -222,24 +226,10 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { ALPN: option.ALPN, ServerName: option.Server, SkipCertVerify: option.SkipCertVerify, - FlowShow: option.FlowShow, Fingerprint: option.Fingerprint, ClientFingerprint: option.ClientFingerprint, } - switch option.Network { - case "", "tcp": - if len(option.Flow) >= 16 { - option.Flow = option.Flow[:16] - switch option.Flow { - case vless.XRO, vless.XRD, vless.XRS: - tOption.Flow = option.Flow - default: - return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) - } - } - } - if option.SNI != "" { tOption.ServerName = option.SNI } @@ -250,6 +240,8 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { 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), @@ -258,13 +250,28 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { option: &option, } + var err error + t.realityConfig, err = option.RealityOpts.Parse() + if err != nil { + return nil, err + } + tOption.Reality = t.realityConfig + if option.Network == "grpc" { dialFn := func(network, addr string) (net.Conn, error) { - c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...) + var err error + var cDialer C.Dialer = dialer.NewDialer(t.Base.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(context.Background(), "tcp", t.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) return c, nil } @@ -275,16 +282,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { ServerName: tOption.ServerName, } - if len(option.Fingerprint) == 0 { - tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } else { - var err error - if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil { - return nil, err - } + var err error + tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + if err != nil { + return nil, err } - t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint) + t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig) t.gunTLSConfig = tlsConfig t.gunConfig = &gun.Config{ diff --git a/adapter/outbound/tuic.go b/adapter/outbound/tuic.go index 0ca13670..666e72fa 100644 --- a/adapter/outbound/tuic.go +++ b/adapter/outbound/tuic.go @@ -2,27 +2,30 @@ package outbound import ( "context" - "crypto/sha256" "crypto/tls" - "encoding/hex" - "encoding/pem" + "errors" "fmt" "math" "net" - "os" "strconv" "time" - "github.com/metacubex/quic-go" + "github.com/metacubex/mihomo/component/ca" + "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/tuic" - "github.com/Dreamacro/clash/component/dialer" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/tuic" + "github.com/gofrs/uuid/v5" + "github.com/metacubex/quic-go" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/uot" ) type Tuic struct { *Base + option *TuicOption client *tuic.PoolClient } @@ -31,7 +34,9 @@ type TuicOption struct { Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` - Token string `proxy:"token"` + 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"` @@ -42,15 +47,21 @@ type TuicOption struct { 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"` - 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"` + 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 @@ -74,6 +85,32 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, op // 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 @@ -82,71 +119,52 @@ func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, meta } // SupportWithDialer implements C.ProxyAdapter -func (t *Tuic) SupportWithDialer() bool { - return true +func (t *Tuic) SupportWithDialer() C.NetWork { + return C.ALLNet } -func (t *Tuic) dial(ctx context.Context, opts ...dialer.Option) (pc net.PacketConn, addr net.Addr, err error) { - return t.dialWithDialer(ctx, dialer.NewDialer(opts...)) -} - -func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (pc net.PacketConn, addr net.Addr, err error) { +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 := resolveUDPAddrWithPrefer(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 } 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 bs []byte var err error - if len(option.CustomCA) > 0 { - bs, err = os.ReadFile(option.CustomCA) - if err != nil { - return nil, fmt.Errorf("tuic %s load ca error: %w", addr, err) - } - } else if option.CustomCAString != "" { - bs = []byte(option.CustomCAString) + tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) + if err != nil { + return nil, err } - if len(bs) > 0 { - block, _ := pem.Decode(bs) - if block == nil { - return nil, fmt.Errorf("CA cert is not PEM") - } - - fpBytes := sha256.Sum256(block.Bytes) - if len(option.Fingerprint) == 0 { - option.Fingerprint = hex.EncodeToString(fpBytes[:]) - } - } - - if len(option.Fingerprint) != 0 { - var err error - tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) - if err != nil { - return nil, err - } - } else { - tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } - - if len(option.ALPN) > 0 { + 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"} @@ -160,8 +178,9 @@ func NewTuic(option TuicOption) (*Tuic, error) { option.HeartbeatInterval = 10000 } + udpRelayMode := tuic.QUIC if option.UdpRelayMode != "quic" { - option.UdpRelayMode = "native" + udpRelayMode = tuic.NATIVE } if option.MaxUdpRelayPacketSize == 0 { @@ -172,6 +191,24 @@ func NewTuic(option TuicOption) (*Tuic, error) { 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)) @@ -184,6 +221,7 @@ func NewTuic(option TuicOption) (*Tuic, error) { MaxIncomingUniStreams: quicMaxOpenStreams, KeepAlivePeriod: time.Duration(option.HeartbeatInterval) * time.Millisecond, DisablePathMTUDiscovery: option.DisableMTUDiscovery, + MaxDatagramFrameSize: int64(option.MaxDatagramFrameSize), EnableDatagrams: true, } if option.ReceiveWindowConn == 0 { @@ -198,12 +236,18 @@ func NewTuic(option TuicOption) (*Tuic, error) { if len(option.Ip) > 0 { addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port)) } - host := option.Server if option.DisableSni { - host = "" 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) } - tkn := tuic.GenTKN(option.Token) t := &Tuic{ Base: &Base{ @@ -213,8 +257,10 @@ func NewTuic(option TuicOption) (*Tuic, error) { udp: true, tfo: option.FastOpen, iface: option.Interface, + rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), }, + option: &option, } clientMaxOpenStreams := int64(option.MaxOpenStreams) @@ -227,21 +273,44 @@ func NewTuic(option TuicOption) (*Tuic, error) { if clientMaxOpenStreams < 1 { clientMaxOpenStreams = 1 } - clientOption := &tuic.ClientOption{ - TlsConfig: tlsConfig, - QuicConfig: quicConfig, - Host: host, - Token: tkn, - UdpRelayMode: option.UdpRelayMode, - CongestionController: option.CongestionController, - ReduceRtt: option.ReduceRtt, - RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond, - MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize, - FastOpen: option.FastOpen, - MaxOpenStreams: clientMaxOpenStreams, - } - t.client = tuic.NewPoolClient(clientOption) + if len(option.Token) > 0 { + tkn := tuic.GenTKN(option.Token) + clientOption := &tuic.ClientOptionV4{ + TlsConfig: 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: 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 index 68d6b355..ce9e5f65 100644 --- a/adapter/outbound/util.go +++ b/adapter/outbound/util.go @@ -4,31 +4,23 @@ import ( "bytes" "context" "crypto/tls" - xtls "github.com/xtls/go" + "fmt" "net" "net/netip" + "regexp" "strconv" "sync" - "time" - "github.com/Dreamacro/clash/component/resolver" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/component/resolver" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" ) var ( - globalClientSessionCache tls.ClientSessionCache - globalClientXSessionCache xtls.ClientSessionCache - once sync.Once + globalClientSessionCache tls.ClientSessionCache + once sync.Once ) -func tcpKeepAlive(c net.Conn) { - if tcp, ok := c.(*net.TCPConn); ok { - _ = tcp.SetKeepAlive(true) - _ = tcp.SetKeepAlivePeriod(30 * time.Second) - } -} - func getClientSessionCache() tls.ClientSessionCache { once.Do(func() { globalClientSessionCache = tls.NewLRUClientSessionCache(128) @@ -36,18 +28,11 @@ func getClientSessionCache() tls.ClientSessionCache { return globalClientSessionCache } -func getClientXSessionCache() xtls.ClientSessionCache { - once.Do(func() { - globalClientXSessionCache = xtls.NewLRUClientSessionCache(128) - }) - return globalClientXSessionCache -} - func serializesSocksAddr(metadata *C.Metadata) []byte { var buf [][]byte addrType := metadata.AddrType() aType := uint8(addrType) - p, _ := strconv.ParseUint(metadata.DstPort, 10, 16) + p := uint(metadata.DstPort) port := []byte{uint8(p >> 8), uint8(p & 0xff)} switch addrType { case socks5.AtypDomainName: @@ -138,3 +123,41 @@ func safeConnClose(c net.Conn, err error) { _ = 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 + switch m[2] { + case "K": + n = 1 << 10 + case "M": + n = 1 << 20 + case "G": + n = 1 << 30 + case "T": + n = 1 << 40 + default: + n = 1 + } + v, _ := strconv.ParseUint(m[1], 10, 64) + n = v * n + if m[3] == "b" { + // Bits, need to convert to bytes + n = n >> 3 + } + return n +} diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index e46e245d..dbe8b1a4 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -12,18 +12,23 @@ import ( "strconv" "sync" - "github.com/Dreamacro/clash/common/convert" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/gun" - "github.com/Dreamacro/clash/transport/socks5" - "github.com/Dreamacro/clash/transport/vless" - "github.com/Dreamacro/clash/transport/vmess" + "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/log" + "github.com/metacubex/mihomo/transport/gun" + "github.com/metacubex/mihomo/transport/socks5" + "github.com/metacubex/mihomo/transport/vless" + "github.com/metacubex/mihomo/transport/vmess" - vmessSing "github.com/sagernet/sing-vmess" - "github.com/sagernet/sing-vmess/packetaddr" + vmessSing "github.com/metacubex/sing-vmess" + "github.com/metacubex/sing-vmess/packetaddr" M "github.com/sagernet/sing/common/metadata" ) @@ -41,6 +46,8 @@ type Vless struct { gunTLSConfig *tls.Config gunConfig *gun.Config transport *gun.TransportWrap + + realityConfig *tlsC.RealityConfig } type VlessOption struct { @@ -50,13 +57,14 @@ type VlessOption struct { Port int `proxy:"port"` UUID string `proxy:"uuid"` Flow string `proxy:"flow,omitempty"` - FlowShow bool `proxy:"flow-show,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"` @@ -69,7 +77,7 @@ type VlessOption struct { ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } -func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { +func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { var err error if tlsC.HaveGlobalFingerprint() && len(v.option.ClientFingerprint) == 0 { @@ -78,7 +86,6 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { switch v.option.Network { case "ws": - host, port, _ := net.SplitHostPort(v.addr) wsOpts := &vmess.WebsocketConfig{ Host: host, @@ -86,6 +93,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { Path: v.option.WSOpts.Path, MaxEarlyData: v.option.WSOpts.MaxEarlyData, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, + V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, ClientFingerprint: v.option.ClientFingerprint, Headers: http.Header{}, } @@ -104,13 +112,9 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { NextProtos: []string{"http/1.1"}, } - if len(v.option.Fingerprint) == 0 { - wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } else { - wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) - if err != nil { - return nil, err - } + wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) + if err != nil { + return nil, err } if v.option.ServerName != "" { @@ -124,10 +128,10 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { convert.SetUserAgent(wsOpts.Headers) } } - c, err = vmess.StreamWebsocketConn(c, wsOpts) + c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) case "http": // readability first, so just copy default TLS logic - c, err = v.streamTLSOrXTLSConn(c, false) + c, err = v.streamTLSConn(ctx, c, false) if err != nil { return nil, err } @@ -142,7 +146,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { c = vmess.StreamHTTPConn(c, httpOpts) case "h2": - c, err = v.streamTLSOrXTLSConn(c, true) + c, err = v.streamTLSConn(ctx, c, true) if err != nil { return nil, err } @@ -154,42 +158,59 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { c, err = vmess.StreamH2Conn(c, h2Opts) case "grpc": - c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) + c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) default: // default tcp network - // handle TLS And XTLS - c, err = v.streamTLSOrXTLSConn(c, false) + // handle TLS + c, err = v.streamTLSConn(ctx, c, false) } if err != nil { return nil, err } - return v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP)) + return v.streamConn(c, metadata) } -func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) { - host, _, _ := net.SplitHostPort(v.addr) - - if v.isXTLSEnabled() && !isH2 { - xtlsOpts := vless.XTLSConfig{ - Host: host, - SkipCertVerify: v.option.SkipCertVerify, - Fingerprint: v.option.Fingerprint, +func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { + 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, + } } - - if v.option.ServerName != "" { - xtlsOpts.Host = v.option.ServerName + 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 +} - return vless.StreamXTLSConn(conn, &xtlsOpts) +func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) { + if v.option.TLS { + host, _, _ := net.SplitHostPort(v.addr) - } else if v.option.TLS { 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 { @@ -200,16 +221,12 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) tlsOpts.Host = v.option.ServerName } - return vmess.StreamTLSConn(conn, &tlsOpts) + return vmess.StreamTLSConn(ctx, conn, &tlsOpts) } return conn, nil } -func (v *Vless) isXTLSEnabled() bool { - return v.client.Addons != nil -} - // DialContext implements C.ProxyAdapter func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { // gun transport @@ -234,16 +251,22 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d // 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()) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - c, err = v.StreamConn(c, metadata) + c, err = v.StreamConnContext(ctx, c, metadata) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } @@ -260,7 +283,6 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o } metadata.DstIP = ip } - var c net.Conn // gun transport if v.transport != nil && len(opts) == 0 { @@ -272,27 +294,25 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o safeConnClose(c, err) }(c) - if v.option.PacketAddr { - packetAddrMetadata := *metadata // make a copy - packetAddrMetadata.Host = packetaddr.SeqPacketMagicAddress - packetAddrMetadata.DstPort = "443" - - c, err = v.client.StreamConn(c, parseVlessAddr(&packetAddrMetadata, false)) - } else { - c, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP)) - } - + c, err = v.streamConn(c, metadata) if err != nil { return nil, fmt.Errorf("new vless client error: %v", err) } - return v.ListenPacketOnStreamConn(c, metadata) + return v.ListenPacketOnStreamConn(ctx, c, metadata) } return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), 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) @@ -301,49 +321,56 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met } 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()) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - if v.option.PacketAddr { - packetAddrMetadata := *metadata // make a copy - packetAddrMetadata.Host = packetaddr.SeqPacketMagicAddress - packetAddrMetadata.DstPort = "443" - - c, err = v.StreamConn(c, &packetAddrMetadata) - } else { - c, err = v.StreamConn(c, metadata) - } - + c, err = v.StreamConnContext(ctx, c, metadata) if err != nil { return nil, fmt.Errorf("new vless client error: %v", err) } - return v.ListenPacketOnStreamConn(c, metadata) + return v.ListenPacketOnStreamConn(ctx, c, metadata) } // SupportWithDialer implements C.ProxyAdapter -func (v *Vless) SupportWithDialer() bool { - return true +func (v *Vless) SupportWithDialer() C.NetWork { + return C.ALLNet } // ListenPacketOnStreamConn implements C.ProxyAdapter -func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { +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 { - return newPacketConn(&threadSafePacketConn{ - PacketConn: vmessSing.NewXUDPConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), - }, v), nil + 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(&threadSafePacketConn{ - PacketConn: packetaddr.NewConn(&vlessPacketConn{ + return newPacketConn(N.NewThreadSafePacketConn( + packetaddr.NewConn(&vlessPacketConn{ Conn: c, rAddr: metadata.UDPAddr(), - }, M.ParseSocksaddr(metadata.RemoteAddress())), - }, v), nil + }, M.SocksaddrFromNet(metadata.UDPAddr())), + ), v), nil } return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil } @@ -372,12 +399,11 @@ func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr { copy(addr[1:], metadata.Host) } - port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) return &vless.DstAddr{ UDP: metadata.NetWork == C.UDP, AddrType: addrType, Addr: addr, - Port: uint16(port), + Port: metadata.DstPort, Mux: metadata.NetWork == C.UDP && xudp, } } @@ -479,10 +505,13 @@ func NewVless(option VlessOption) (*Vless, error) { if option.Network != "ws" && len(option.Flow) >= 16 { option.Flow = option.Flow[:16] switch option.Flow { - case vless.XRO, vless.XRD, vless.XRS: + case vless.XRV: + log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV) addons = &vless.Addons{ Flow: option.Flow, } + case vless.XRO, vless.XRD, vless.XRS: + log.Fatalln("Legacy XTLS protocol %s is deprecated and no longer supported", option.Flow) default: return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) } @@ -497,8 +526,11 @@ func NewVless(option VlessOption) (*Vless, error) { option.XUDP = true } } + if option.XUDP { + option.PacketAddr = false + } - client, err := vless.NewClient(option.UUID, addons, option.FlowShow) + client, err := vless.NewClient(option.UUID, addons) if err != nil { return nil, err } @@ -510,6 +542,8 @@ func NewVless(option VlessOption) (*Vless, error) { 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), @@ -518,6 +552,11 @@ func NewVless(option VlessOption) (*Vless, error) { 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 { @@ -525,11 +564,19 @@ func NewVless(option VlessOption) (*Vless, error) { } case "grpc": dialFn := func(network, addr string) (net.Conn, error) { - c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...) + var err error + var cDialer C.Dialer = dialer.NewDialer(v.Base.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(context.Background(), "tcp", v.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) return c, nil } @@ -538,22 +585,25 @@ func NewVless(option VlessOption) (*Vless, error) { Host: v.option.ServerName, ClientFingerprint: v.option.ClientFingerprint, } - tlsConfig := tlsC.GetGlobalTLSConfig(&tls.Config{ - InsecureSkipVerify: v.option.SkipCertVerify, - ServerName: v.option.ServerName, - }) - - if v.option.ServerName == "" { - host, _, _ := net.SplitHostPort(v.addr) - tlsConfig.ServerName = host - gunConfig.Host = host + if option.ServerName == "" { + gunConfig.Host = v.addr + } + var tlsConfig *tls.Config + if option.TLS { + tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{ + InsecureSkipVerify: v.option.SkipCertVerify, + ServerName: v.option.ServerName, + }) + 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.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 index 5da8c8b1..8811fb0d 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -11,15 +11,20 @@ import ( "strings" "sync" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/gun" - clashVMess "github.com/Dreamacro/clash/transport/vmess" + 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/sagernet/sing-vmess" - "github.com/sagernet/sing-vmess/packetaddr" + vmess "github.com/metacubex/sing-vmess" + "github.com/metacubex/sing-vmess/packetaddr" M "github.com/sagernet/sing/common/metadata" ) @@ -34,32 +39,36 @@ type Vmess struct { 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"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - ServerName string `proxy:"servername,omitempty"` - HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` - HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` - GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` - WSOpts WSOptions `proxy:"ws-opts,omitempty"` - 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"` + 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 { @@ -82,10 +91,11 @@ type WSOptions struct { 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"` } -// StreamConn implements C.ProxyAdapter -func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { +// StreamConnContext implements C.ProxyAdapter +func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { var err error if tlsC.HaveGlobalFingerprint() && (len(v.option.ClientFingerprint) == 0) { @@ -94,14 +104,14 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { switch v.option.Network { case "ws": - host, port, _ := net.SplitHostPort(v.addr) - wsOpts := &clashVMess.WebsocketConfig{ + 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, ClientFingerprint: v.option.ClientFingerprint, Headers: http.Header{}, } @@ -120,12 +130,9 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { NextProtos: []string{"http/1.1"}, } - if len(v.option.Fingerprint) == 0 { - wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } else { - if wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint); err != nil { - return nil, err - } + wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) + if err != nil { + return nil, err } if v.option.ServerName != "" { @@ -134,92 +141,137 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { wsOpts.TLSConfig.ServerName = host } } - c, err = clashVMess.StreamWebsocketConn(c, wsOpts) + 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 := &clashVMess.TLSConfig{ + 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 = clashVMess.StreamTLSConn(c, tlsOpts) + c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts) if err != nil { return nil, err } } host, _, _ := net.SplitHostPort(v.addr) - httpOpts := &clashVMess.HTTPConfig{ + httpOpts := &mihomoVMess.HTTPConfig{ Host: host, Method: v.option.HTTPOpts.Method, Path: v.option.HTTPOpts.Path, Headers: v.option.HTTPOpts.Headers, } - c = clashVMess.StreamHTTPConn(c, httpOpts) + c = mihomoVMess.StreamHTTPConn(c, httpOpts) case "h2": host, _, _ := net.SplitHostPort(v.addr) - tlsOpts := clashVMess.TLSConfig{ + tlsOpts := mihomoVMess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, NextProtos: []string{"h2"}, ClientFingerprint: v.option.ClientFingerprint, + Reality: v.realityConfig, } if v.option.ServerName != "" { tlsOpts.Host = v.option.ServerName } - c, err = clashVMess.StreamTLSConn(c, &tlsOpts) + c, err = mihomoVMess.StreamTLSConn(ctx, c, &tlsOpts) if err != nil { return nil, err } - h2Opts := &clashVMess.H2Config{ + h2Opts := &mihomoVMess.H2Config{ Hosts: v.option.HTTP2Opts.Host, Path: v.option.HTTP2Opts.Path, } - c, err = clashVMess.StreamH2Conn(c, h2Opts) + c, err = mihomoVMess.StreamH2Conn(c, h2Opts) case "grpc": - c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) + c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) default: // handle TLS if v.option.TLS { host, _, _ := net.SplitHostPort(v.addr) - tlsOpts := &clashVMess.TLSConfig{ + 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 = clashVMess.StreamTLSConn(c, tlsOpts) + c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts) } } if err != nil { return nil, err } + return v.streamConn(c, metadata) +} + +func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { if metadata.NetWork == C.UDP { if v.option.XUDP { - return v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + var globalID [8]byte + if metadata.SourceValid() { + globalID = utils.GlobalID(metadata.SourceAddress()) + } + if N.NeedHandshake(c) { + 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 N.NeedHandshake(c) { + 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 { - return v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + if N.NeedHandshake(c) { + conn = v.client.DialEarlyPacketConn(c, + M.SocksaddrFromNet(metadata.UDPAddr())) + } else { + conn, err = v.client.DialPacketConn(c, + M.SocksaddrFromNet(metadata.UDPAddr())) + } } } else { - return v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + if N.NeedHandshake(c) { + 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 @@ -234,7 +286,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d safeConnClose(c, err) }(c) - c, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + c, err = v.client.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) if err != nil { return nil, err } @@ -246,16 +298,22 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d // 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()) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - c, err = v.StreamConn(c, metadata) + c, err = v.StreamConnContext(ctx, c, metadata) return NewConn(c, v), err } @@ -269,14 +327,6 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o } metadata.DstIP = ip } - - if v.option.PacketAddr { - _metadata := *metadata // make a copy - metadata = &_metadata - metadata.Host = packetaddr.SeqPacketMagicAddress - metadata.DstPort = "443" - } - var c net.Conn // gun transport if v.transport != nil && len(opts) == 0 { @@ -288,22 +338,24 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o safeConnClose(c, err) }(c) - if v.option.XUDP { - c, err = v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) - } else { - c, err = v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) - } - + c, err = v.streamConn(c, metadata) if err != nil { return nil, fmt.Errorf("new vmess client error: %v", err) } - return v.ListenPacketOnStreamConn(c, metadata) + return v.ListenPacketOnStreamConn(ctx, c, metadata) } return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), 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) @@ -317,29 +369,36 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) - c, err = v.StreamConn(c, metadata) + c, err = v.StreamConnContext(ctx, c, metadata) if err != nil { return nil, fmt.Errorf("new vmess client error: %v", err) } - return v.ListenPacketOnStreamConn(c, metadata) + return v.ListenPacketOnStreamConn(ctx, c, metadata) } // SupportWithDialer implements C.ProxyAdapter -func (v *Vmess) SupportWithDialer() bool { - return true +func (v *Vmess) SupportWithDialer() C.NetWork { + return C.ALLNet } // ListenPacketOnStreamConn implements C.ProxyAdapter -func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { - if v.option.PacketAddr { - return newPacketConn(&threadSafePacketConn{PacketConn: packetaddr.NewBindConn(c)}, v), nil - } else if pc, ok := c.(net.PacketConn); ok { - return newPacketConn(&threadSafePacketConn{PacketConn: pc}, v), nil +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 } @@ -358,6 +417,7 @@ func NewVmess(option VmessOption) (*Vmess, error) { 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 @@ -373,13 +433,6 @@ func NewVmess(option VmessOption) (*Vmess, error) { option.PacketAddr = false } - switch option.Network { - case "h2", "grpc": - if !option.TLS { - option.TLS = true - } - } - v := &Vmess{ Base: &Base{ name: option.Name, @@ -387,6 +440,8 @@ func NewVmess(option VmessOption) (*Vmess, error) { 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), @@ -402,11 +457,19 @@ func NewVmess(option VmessOption) (*Vmess, error) { } case "grpc": dialFn := func(network, addr string) (net.Conn, error) { - c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...) + var err error + var cDialer C.Dialer = dialer.NewDialer(v.Base.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(context.Background(), "tcp", v.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } - tcpKeepAlive(c) + N.TCPKeepAlive(c) return c, nil } @@ -415,37 +478,35 @@ func NewVmess(option VmessOption) (*Vmess, error) { Host: v.option.ServerName, ClientFingerprint: v.option.ClientFingerprint, } - tlsConfig := &tls.Config{ - InsecureSkipVerify: v.option.SkipCertVerify, - ServerName: v.option.ServerName, + if option.ServerName == "" { + gunConfig.Host = v.addr } - - if v.option.ServerName == "" { - host, _, _ := net.SplitHostPort(v.addr) - tlsConfig.ServerName = host - gunConfig.Host = host + var tlsConfig *tls.Config + if option.TLS { + tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{ + InsecureSkipVerify: v.option.SkipCertVerify, + ServerName: v.option.ServerName, + }) + 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.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) } + + v.realityConfig, err = v.option.RealityOpts.Parse() + if err != nil { + return nil, err + } + return v, nil } -type threadSafePacketConn struct { - net.PacketConn - access sync.Mutex -} - -func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { - c.access.Lock() - defer c.access.Unlock() - return c.PacketConn.WriteTo(b, addr) -} - type vmessPacketConn struct { net.Conn rAddr net.Addr @@ -455,9 +516,9 @@ type vmessPacketConn struct { // WriteTo implments C.PacketConn.WriteTo // Since VMess doesn't support full cone NAT by design, we verify if addr matches uc.rAddr, and drop the packet if not. func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { - allowedAddr := uc.rAddr.(*net.UDPAddr) - destAddr := addr.(*net.UDPAddr) - if !(allowedAddr.IP.Equal(destAddr.IP) && allowedAddr.Port == destAddr.Port) { + allowedAddr := uc.rAddr + destAddr := addr + if allowedAddr.String() != destAddr.String() { return 0, ErrUDPRemoteAddrMismatch } uc.access.Lock() diff --git a/adapter/outbound/wireguard.go b/adapter/outbound/wireguard.go index d1a5ea6e..9af1751b 100644 --- a/adapter/outbound/wireguard.go +++ b/adapter/outbound/wireguard.go @@ -13,11 +13,13 @@ import ( "strings" "sync" - CN "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/listener/sing" + 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/dns" + "github.com/metacubex/mihomo/log" wireguard "github.com/metacubex/sing-wireguard" @@ -25,7 +27,6 @@ import ( "github.com/sagernet/sing/common/debug" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" "github.com/sagernet/wireguard-go/device" ) @@ -34,66 +35,69 @@ type WireGuard struct { bind *wireguard.ClientBind device *device.Device tunDevice wireguard.Device - dialer *wgDialer + dialer proxydialer.SingDialer startOnce sync.Once startErr error + resolver *dns.Resolver + refP *refProxyAdapter } type WireGuardOption struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - Ip string `proxy:"ip,omitempty"` - Ipv6 string `proxy:"ipv6,omitempty"` - PrivateKey string `proxy:"private-key"` - PublicKey string `proxy:"public-key"` - PreSharedKey string `proxy:"pre-shared-key,omitempty"` - Reserved []uint8 `proxy:"reserved,omitempty"` - Workers int `proxy:"workers,omitempty"` - MTU int `proxy:"mtu,omitempty"` - UDP bool `proxy:"udp,omitempty"` - PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"` + WireGuardPeerOption + Name string `proxy:"name"` + 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"` + + Peers []WireGuardPeerOption `proxy:"peers,omitempty"` + + RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"` + Dns []string `proxy:"dns,omitempty"` } -type wgDialer struct { - options []dialer.Option +type WireGuardPeerOption struct { + Server string `proxy:"server"` + Port int `proxy:"port"` + Ip string `proxy:"ip,omitempty"` + Ipv6 string `proxy:"ipv6,omitempty"` + PublicKey string `proxy:"public-key,omitempty"` + PreSharedKey string `proxy:"pre-shared-key,omitempty"` + Reserved []uint8 `proxy:"reserved,omitempty"` + AllowedIPs []string `proxy:"allowed-ips,omitempty"` } -func (d *wgDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return dialer.DialContext(ctx, network, destination.String(), d.options...) +type wgSingErrorHandler struct { + name string } -func (d *wgDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", destination.Addr), "", d.options...) -} +var _ E.Handler = (*wgSingErrorHandler)(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), - }, - dialer: &wgDialer{}, +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 } - runtime.SetFinalizer(outbound, closeWireGuard) + log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", w.name, err)) +} - 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)) - } - reserved[0] = uint8(option.Reserved[0]) - reserved[1] = uint8(option.Reserved[1]) - reserved[2] = uint8(option.Reserved[2]) - } - peerAddr := M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)) - outbound.bind = wireguard.NewClientBind(context.Background(), outbound.dialer, peerAddr, reserved) +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 WireGuardPeerOption) Prefixes() ([]netip.Prefix, error) { localPrefixes := make([]netip.Prefix, 0, 2) if len(option.Ip) > 0 { if !strings.Contains(option.Ip, "/") { @@ -118,7 +122,46 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { if len(localPrefixes) == 0 { return nil, E.New("missing local address") } - var privateKey, peerPublicKey, preSharedKey string + 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), + }, + dialer: proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()), + } + runtime.SetFinalizer(outbound, closeWireGuard) + + 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 + var connectAddr M.Socksaddr + if len(option.Peers) < 2 { + isConnect = true + if len(option.Peers) == 1 { + connectAddr = option.Peers[0].Addr() + } else { + connectAddr = option.Addr() + } + } + outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, outbound.dialer, isConnect, connectAddr, reserved) + + var localPrefixes []netip.Prefix + + var privateKey string { bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey) if err != nil { @@ -126,40 +169,92 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { } privateKey = hex.EncodeToString(bytes) } - { - bytes, err := base64.StdEncoding.DecodeString(option.PublicKey) - if err != nil { - return nil, E.Cause(err, "decode peer public key") - } - peerPublicKey = 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") - } - preSharedKey = hex.EncodeToString(bytes) - } ipcConf := "private_key=" + privateKey - ipcConf += "\npublic_key=" + peerPublicKey - ipcConf += "\nendpoint=" + peerAddr.String() - if preSharedKey != "" { - ipcConf += "\npreshared_key=" + preSharedKey - } - var has4, has6 bool - for _, address := range localPrefixes { - if address.Addr().Is4() { - has4 = true - } else { - has6 = true + if peersLen := len(option.Peers); peersLen > 0 { + localPrefixes = make([]netip.Prefix, 0, peersLen*2) + for i, peer := range option.Peers { + var peerPublicKey, preSharedKey string + { + bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode public key for peer ", i) + } + peerPublicKey = 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) + } + preSharedKey = hex.EncodeToString(bytes) + } + destination := peer.Addr() + ipcConf += "\npublic_key=" + peerPublicKey + ipcConf += "\nendpoint=" + destination.String() + if preSharedKey != "" { + ipcConf += "\npreshared_key=" + preSharedKey + } + if len(peer.AllowedIPs) == 0 { + return nil, E.New("missing allowed_ips for peer ", i) + } + for _, allowedIP := range peer.AllowedIPs { + ipcConf += "\nallowed_ip=" + allowedIP + } + 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)) + } + copy(reserved[:], option.Reserved) + outbound.bind.SetReservedForEndpoint(destination, reserved) + } + prefixes, err := peer.Prefixes() + if err != nil { + return nil, err + } + localPrefixes = append(localPrefixes, prefixes...) + } + } else { + var peerPublicKey, preSharedKey string + { + bytes, err := base64.StdEncoding.DecodeString(option.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode peer public key") + } + peerPublicKey = 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") + } + preSharedKey = hex.EncodeToString(bytes) + } + ipcConf += "\npublic_key=" + peerPublicKey + ipcConf += "\nendpoint=" + connectAddr.String() + if preSharedKey != "" { + ipcConf += "\npreshared_key=" + preSharedKey + } + var err error + localPrefixes, err = option.Prefixes() + if err != nil { + return nil, err + } + var has4, has6 bool + for _, address := range localPrefixes { + if address.Addr().Is4() { + has4 = true + } else { + has6 = true + } + } + if has4 { + ipcConf += "\nallowed_ip=0.0.0.0/0" + } + if has6 { + ipcConf += "\nallowed_ip=::/0" } } - if has4 { - ipcConf += "\nallowed_ip=0.0.0.0/0" - } - if has6 { - ipcConf += "\nallowed_ip=::/0" - } + if option.PersistentKeepalive != 0 { ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", option.PersistentKeepalive) } @@ -167,27 +262,55 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { if mtu == 0 { mtu = 1408 } + if len(localPrefixes) == 0 { + return nil, E.New("missing local address") + } var err error outbound.tunDevice, err = wireguard.NewStackDevice(localPrefixes, uint32(mtu)) if err != nil { return nil, E.Cause(err, "create WireGuard device") } - outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, &device.Logger{ + outbound.device = device.NewDevice(context.Background(), outbound.tunDevice, outbound.bind, &device.Logger{ Verbosef: func(format string, args ...interface{}) { - sing.Logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) + log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) }, Errorf: func(format string, args ...interface{}) { - sing.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) + log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) }, }, option.Workers) if debug.Enabled { - sing.Logger.Trace("created wireguard ipc conf: \n", ipcConf) + log.SingLogger.Trace(fmt.Sprintf("[WG](%s) created wireguard ipc conf: \n %s", option.Name, ipcConf)) } err = outbound.device.IpcSet(ipcConf) if err != nil { return nil, E.Cause(err, "setup wireguard") } //err = outbound.tunDevice.Start() + + var has6 bool + for _, address := range localPrefixes { + if !address.Addr().Unmap().Is4() { + has6 = true + break + } + } + + refP := &refProxyAdapter{} + outbound.refP = refP + 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 = refP + } + outbound.resolver = dns.NewResolver(dns.Config{ + Main: nss, + IPv6: has6, + }) + } + return outbound, nil } @@ -199,7 +322,8 @@ func closeWireGuard(w *WireGuard) { } func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { - w.dialer.options = opts + options := w.Base.DialOptions(opts...) + w.dialer.SetDialer(dialer.NewDialer(options...)) var conn net.Conn w.startOnce.Do(func() { w.startErr = w.tunDevice.Start() @@ -207,16 +331,18 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts if w.startErr != nil { return nil, w.startErr } - if !metadata.Resolved() { - var addrs []netip.Addr - addrs, err = resolver.LookupIP(ctx, metadata.Host) - if err != nil { - return nil, err + if !metadata.Resolved() || w.resolver != nil { + r := resolver.DefaultResolver + if w.resolver != nil { + w.refP.SetProxyAdapter(w) + defer w.refP.ClearProxyAdapter() + r = w.resolver } - conn, err = N.DialSerial(ctx, w.tunDevice, "tcp", M.ParseSocksaddr(metadata.RemoteAddress()), addrs) + 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 { - port, _ := strconv.Atoi(metadata.DstPort) - conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, uint16(port))) + conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) } if err != nil { return nil, err @@ -228,7 +354,8 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts } func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { - w.dialer.options = opts + options := w.Base.DialOptions(opts...) + w.dialer.SetDialer(dialer.NewDialer(options...)) var pc net.PacketConn w.startOnce.Do(func() { w.startErr = w.tunDevice.Start() @@ -239,15 +366,20 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat if err != nil { return nil, err } - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) + if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" { + r := resolver.DefaultResolver + if w.resolver != nil { + w.refP.SetProxyAdapter(w) + defer w.refP.ClearProxyAdapter() + 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 } - port, _ := strconv.Atoi(metadata.DstPort) - pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, uint16(port))) + pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) if err != nil { return nil, err } @@ -256,3 +388,144 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat } return newPacketConn(CN.NewRefPacketConn(pc, w), w), nil } + +// IsL3Protocol implements C.ProxyAdapter +func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool { + return true +} + +type refProxyAdapter struct { + proxyAdapter C.ProxyAdapter + count int + mutex sync.Mutex +} + +func (r *refProxyAdapter) SetProxyAdapter(proxyAdapter C.ProxyAdapter) { + r.mutex.Lock() + defer r.mutex.Unlock() + r.proxyAdapter = proxyAdapter + r.count++ +} + +func (r *refProxyAdapter) ClearProxyAdapter() { + r.mutex.Lock() + defer r.mutex.Unlock() + r.count-- + if r.count == 0 { + r.proxyAdapter = nil + } +} + +func (r *refProxyAdapter) Name() string { + if r.proxyAdapter != nil { + return r.proxyAdapter.Name() + } + return "" +} + +func (r *refProxyAdapter) Type() C.AdapterType { + if r.proxyAdapter != nil { + return r.proxyAdapter.Type() + } + return C.AdapterType(0) +} + +func (r *refProxyAdapter) Addr() string { + if r.proxyAdapter != nil { + return r.proxyAdapter.Addr() + } + return "" +} + +func (r *refProxyAdapter) SupportUDP() bool { + if r.proxyAdapter != nil { + return r.proxyAdapter.SupportUDP() + } + return false +} + +func (r *refProxyAdapter) SupportXUDP() bool { + if r.proxyAdapter != nil { + return r.proxyAdapter.SupportXUDP() + } + return false +} + +func (r *refProxyAdapter) SupportTFO() bool { + if r.proxyAdapter != nil { + return r.proxyAdapter.SupportTFO() + } + return false +} + +func (r *refProxyAdapter) MarshalJSON() ([]byte, error) { + if r.proxyAdapter != nil { + return r.proxyAdapter.MarshalJSON() + } + return nil, C.ErrNotSupport +} + +func (r *refProxyAdapter) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { + if r.proxyAdapter != nil { + return r.proxyAdapter.StreamConnContext(ctx, c, metadata) + } + return nil, C.ErrNotSupport +} + +func (r *refProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { + if r.proxyAdapter != nil { + return r.proxyAdapter.DialContext(ctx, metadata, opts...) + } + return nil, C.ErrNotSupport +} + +func (r *refProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { + if r.proxyAdapter != nil { + return r.proxyAdapter.ListenPacketContext(ctx, metadata, opts...) + } + return nil, C.ErrNotSupport +} + +func (r *refProxyAdapter) SupportUOT() bool { + if r.proxyAdapter != nil { + return r.proxyAdapter.SupportUOT() + } + return false +} + +func (r *refProxyAdapter) SupportWithDialer() C.NetWork { + if r.proxyAdapter != nil { + return r.proxyAdapter.SupportWithDialer() + } + return C.InvalidNet +} + +func (r *refProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) { + if r.proxyAdapter != nil { + return r.proxyAdapter.DialContextWithDialer(ctx, dialer, metadata) + } + return nil, C.ErrNotSupport +} + +func (r *refProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) { + if r.proxyAdapter != nil { + return r.proxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata) + } + return nil, C.ErrNotSupport +} + +func (r *refProxyAdapter) IsL3Protocol(metadata *C.Metadata) bool { + if r.proxyAdapter != nil { + return r.proxyAdapter.IsL3Protocol(metadata) + } + return false +} + +func (r *refProxyAdapter) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { + if r.proxyAdapter != nil { + return r.proxyAdapter.Unwrap(metadata, touch) + } + return nil +} + +var _ C.ProxyAdapter = (*refProxyAdapter)(nil) diff --git a/adapter/outboundgroup/fallback.go b/adapter/outboundgroup/fallback.go index 34365d0e..d0dd98b1 100644 --- a/adapter/outboundgroup/fallback.go +++ b/adapter/outboundgroup/fallback.go @@ -6,17 +6,21 @@ import ( "errors" "time" - "github.com/Dreamacro/clash/adapter/outbound" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/provider" + "github.com/metacubex/mihomo/adapter/outbound" + "github.com/metacubex/mihomo/common/callback" + 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/constant/provider" ) type Fallback struct { *GroupBase - disableUDP bool - testUrl string - selected string + disableUDP bool + testUrl string + selected string + expectedStatus string } func (f *Fallback) Now() string { @@ -30,11 +34,20 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts . c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...) if err == nil { c.AppendToChains(f) - f.onDialSuccess() } else { f.onDialFailed(proxy.Type(), err) } + if N.NeedHandshake(c) { + c = callback.NewFirstWriteCallBackConn(c, func(err error) { + if err == nil { + f.onDialSuccess() + } else { + f.onDialFailed(proxy.Type(), err) + } + }) + } + return c, err } @@ -59,6 +72,11 @@ func (f *Fallback) SupportUDP() bool { 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{} @@ -66,9 +84,11 @@ func (f *Fallback) MarshalJSON() ([]byte, error) { all = append(all, proxy.Name()) } return json.Marshal(map[string]any{ - "type": f.Type().String(), - "now": f.Now(), - "all": all, + "type": f.Type().String(), + "now": f.Now(), + "all": all, + "testUrl": f.testUrl, + "expected": f.expectedStatus, }) } @@ -82,12 +102,14 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy { proxies := f.GetProxies(touch) for _, proxy := range proxies { if len(f.selected) == 0 { - if proxy.Alive() { + // if proxy.Alive() { + if proxy.AliveForTestUrl(f.testUrl) { return proxy } } else { if proxy.Name() == f.selected { - if proxy.Alive() { + // if proxy.Alive() { + if proxy.AliveForTestUrl(f.testUrl) { return proxy } else { f.selected = "" @@ -113,15 +135,21 @@ func (f *Fallback) Set(name string) error { } f.selected = name - if !p.Alive() { + // if !p.Alive() { + if !p.AliveForTestUrl(f.testUrl) { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000)) defer cancel() - _, _ = p.URLTest(ctx, f.testUrl) + expectedStatus, _ := utils.NewIntRanges[uint16](f.expectedStatus) + _, _ = p.URLTest(ctx, f.testUrl, expectedStatus, C.ExtraHistory) } return nil } +func (f *Fallback) ForceSet(name string) { + f.selected = name +} + func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { return &Fallback{ GroupBase: NewGroupBase(GroupBaseOption{ @@ -136,7 +164,8 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) option.ExcludeType, providers, }), - disableUDP: option.DisableUDP, - testUrl: option.URL, + disableUDP: option.DisableUDP, + testUrl: option.URL, + expectedStatus: option.ExpectedStatus, } } diff --git a/adapter/outboundgroup/groupbase.go b/adapter/outboundgroup/groupbase.go index 0a421793..d4a812f6 100644 --- a/adapter/outboundgroup/groupbase.go +++ b/adapter/outboundgroup/groupbase.go @@ -7,15 +7,16 @@ import ( "sync" "time" - "github.com/Dreamacro/clash/adapter/outbound" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/provider" - types "github.com/Dreamacro/clash/constant/provider" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" + "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" - "go.uber.org/atomic" ) type GroupBase struct { @@ -27,7 +28,7 @@ type GroupBase struct { failedTestMux sync.Mutex failedTimes int failedTime time.Time - failedTesting *atomic.Bool + failedTesting atomic.Bool proxies [][]C.Proxy versions []atomic.Uint32 } @@ -130,10 +131,6 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { } } - if len(proxies) == 0 { - return append(proxies, tunnel.Proxies()["COMPATIBLE"]) - } - if len(gb.providers) > 1 && len(gb.filterRegs) > 1 { var newProxies []C.Proxy proxiesSet := map[string]struct{}{} @@ -189,10 +186,14 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { proxies = newProxies } + if len(proxies) == 0 { + return append(proxies, tunnel.Proxies()["COMPATIBLE"]) + } + return proxies } -func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16, error) { +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{} @@ -201,7 +202,7 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16 proxy := proxy wg.Add(1) go func() { - delay, err := proxy.URLTest(ctx, url) + delay, err := proxy.URLTest(ctx, url, expectedStatus, C.DropHistory) if err == nil { lock.Lock() mp[proxy.Name()] = delay diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go index 48bd4994..885aeaee 100644 --- a/adapter/outboundgroup/loadbalance.go +++ b/adapter/outboundgroup/loadbalance.go @@ -6,24 +6,29 @@ import ( "errors" "fmt" "net" + "sync" "time" - "github.com/Dreamacro/clash/adapter/outbound" - "github.com/Dreamacro/clash/common/cache" - "github.com/Dreamacro/clash/common/murmur3" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/provider" + "github.com/metacubex/mihomo/adapter/outbound" + "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/common/callback" + 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/constant/provider" "golang.org/x/net/publicsuffix" ) -type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy +type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy type LoadBalance struct { *GroupBase - disableUDP bool - strategyFn strategyFn + disableUDP bool + strategyFn strategyFn + testUrl string + expectedStatus string } var errStrategy = errors.New("unsupported strategy") @@ -83,17 +88,24 @@ func jumpHash(key uint64, buckets int32) int32 { // DialContext implements C.ProxyAdapter func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) { proxy := lb.Unwrap(metadata, true) - - defer func() { - if err == nil { - c.AppendToChains(lb) - lb.onDialSuccess() - } else { - lb.onDialFailed(proxy.Type(), err) - } - }() - c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...) + + if err == nil { + c.AppendToChains(lb) + } else { + lb.onDialFailed(proxy.Type(), err) + } + + if N.NeedHandshake(c) { + c = callback.NewFirstWriteCallBackConn(c, func(err error) { + if err == nil { + lb.onDialSuccess() + } else { + lb.onDialFailed(proxy.Type(), err) + } + }) + } + return } @@ -114,23 +126,33 @@ func (lb *LoadBalance) SupportUDP() bool { return !lb.disableUDP } -func strategyRoundRobin() strategyFn { - flag := true +// 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 - return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { + 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) - for i := 0; i < length; i++ { - flag = !flag - if flag { - idx = (idx - 1) % length - } else { - idx = (idx + 2) % length - } - if idx < 0 { - idx = idx + length - } - proxy := proxies[idx] - if proxy.Alive() { + + if touch { + defer func() { + idx = (idx + i) % length + }() + } + + for ; i < length; i++ { + id := (idx + i) % length + proxy := proxies[id] + // if proxy.Alive() { + if proxy.AliveForTestUrl(url) { + i++ return proxy } } @@ -139,22 +161,24 @@ func strategyRoundRobin() strategyFn { } } -func strategyConsistentHashing() strategyFn { +func strategyConsistentHashing(url string) strategyFn { maxRetry := 5 - return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { - key := uint64(murmur3.Sum32([]byte(getKey(metadata)))) + 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.Alive() { + // if proxy.Alive() { + 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.Alive() { + // if proxy.Alive() { + if proxy.AliveForTestUrl(url) { return proxy } } @@ -163,14 +187,14 @@ func strategyConsistentHashing() strategyFn { } } -func strategyStickySessions() strategyFn { +func strategyStickySessions(url string) strategyFn { ttl := time.Minute * 10 maxRetry := 5 lruCache := cache.New[uint64, int]( cache.WithAge[uint64, int](int64(ttl.Seconds())), cache.WithSize[uint64, int](1000)) - return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { - key := uint64(murmur3.Sum32([]byte(getKeyWithSrcAndDst(metadata)))) + 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 { @@ -180,7 +204,8 @@ func strategyStickySessions() strategyFn { nowIdx := idx for i := 1; i < maxRetry; i++ { proxy := proxies[nowIdx] - if proxy.Alive() { + // if proxy.Alive() { + if proxy.AliveForTestUrl(url) { if nowIdx != idx { lruCache.Delete(key) lruCache.Set(key, nowIdx) @@ -201,7 +226,7 @@ func strategyStickySessions() strategyFn { // Unwrap implements C.ProxyAdapter func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { proxies := lb.GetProxies(touch) - return lb.strategyFn(proxies, metadata) + return lb.strategyFn(proxies, metadata, touch) } // MarshalJSON implements C.ProxyAdapter @@ -211,8 +236,10 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) { all = append(all, proxy.Name()) } return json.Marshal(map[string]any{ - "type": lb.Type().String(), - "all": all, + "type": lb.Type().String(), + "all": all, + "testUrl": lb.testUrl, + "expectedStatus": lb.expectedStatus, }) } @@ -220,11 +247,11 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide var strategyFn strategyFn switch strategy { case "consistent-hashing": - strategyFn = strategyConsistentHashing() + strategyFn = strategyConsistentHashing(option.URL) case "round-robin": - strategyFn = strategyRoundRobin() + strategyFn = strategyRoundRobin(option.URL) case "sticky-sessions": - strategyFn = strategyStickySessions() + strategyFn = strategyStickySessions(option.URL) default: return nil, fmt.Errorf("%w: %s", errStrategy, strategy) } @@ -241,7 +268,9 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide option.ExcludeType, providers, }), - strategyFn: strategyFn, - disableUDP: option.DisableUDP, + strategyFn: strategyFn, + disableUDP: option.DisableUDP, + testUrl: option.URL, + expectedStatus: option.ExpectedStatus, }, nil } diff --git a/adapter/outboundgroup/parser.go b/adapter/outboundgroup/parser.go index 05976c89..8f3335d8 100644 --- a/adapter/outboundgroup/parser.go +++ b/adapter/outboundgroup/parser.go @@ -3,35 +3,37 @@ package outboundgroup import ( "errors" "fmt" + "strings" - "github.com/Dreamacro/clash/adapter/outbound" - "github.com/Dreamacro/clash/adapter/provider" - "github.com/Dreamacro/clash/common/structure" - C "github.com/Dreamacro/clash/constant" - types "github.com/Dreamacro/clash/constant/provider" + "github.com/metacubex/mihomo/adapter/outbound" + "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" ) var ( errFormat = errors.New("format error") - errType = errors.New("unsupport type") + errType = errors.New("unsupported type") errMissProxy = errors.New("`use` or `proxies` missing") - errMissHealthCheck = errors.New("`url` or `interval` missing") errDuplicateProvider = errors.New("duplicate provider name") ) type GroupCommonOption struct { outbound.BasicOption - Name string `group:"name"` - Type string `group:"type"` - Proxies []string `group:"proxies,omitempty"` - Use []string `group:"use,omitempty"` - URL string `group:"url,omitempty"` - Interval int `group:"interval,omitempty"` - Lazy bool `group:"lazy,omitempty"` - DisableUDP bool `group:"disable-udp,omitempty"` - Filter string `group:"filter,omitempty"` - ExcludeFilter string `group:"exclude-filter,omitempty"` - ExcludeType string `group:"exclude-type,omitempty"` + Name string `group:"name"` + Type string `group:"type"` + Proxies []string `group:"proxies,omitempty"` + Use []string `group:"use,omitempty"` + URL string `group:"url,omitempty"` + Interval int `group:"interval,omitempty"` + Lazy bool `group:"lazy,omitempty"` + DisableUDP bool `group:"disable-udp,omitempty"` + Filter string `group:"filter,omitempty"` + ExcludeFilter string `group:"exclude-filter,omitempty"` + ExcludeType string `group:"exclude-type,omitempty"` + ExpectedStatus string `group:"expected-status,omitempty"` } func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) { @@ -53,30 +55,36 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide providers := []types.ProxyProvider{} if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { - return nil, errMissProxy + return nil, fmt.Errorf("%s: %w", groupName, errMissProxy) } + expectedStatus, err := utils.NewIntRanges[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 + testUrl := groupOption.URL + if len(groupOption.Proxies) != 0 { ps, err := getProxies(proxyMap, groupOption.Proxies) if err != nil { - return nil, err + return nil, fmt.Errorf("%s: %w", groupName, err) } if _, ok := providersMap[groupName]; ok { - return nil, errDuplicateProvider + return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider) } - // select don't need health check - if groupOption.Type == "select" || groupOption.Type == "relay" { - hc := provider.NewHealthCheck(ps, "", 0, true) - pd, err := provider.NewCompatibleProvider(groupName, ps, hc) - if err != nil { - return nil, err - } + var url string + var interval uint - providers = append(providers, pd) - providersMap[groupName] = pd - } else { + // select don't need health check + if groupOption.Type != "select" && groupOption.Type != "relay" { if groupOption.URL == "" { groupOption.URL = "https://cp.cloudflare.com/generate_204" } @@ -85,22 +93,29 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide groupOption.Interval = 300 } - hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy) - pd, err := provider.NewCompatibleProvider(groupName, ps, hc) - if err != nil { - return nil, err - } - - providers = append(providers, pd) - providersMap[groupName] = pd + url = groupOption.URL + interval = uint(groupOption.Interval) } + + hc := provider.NewHealthCheck(ps, url, interval, true, expectedStatus) + pd, err := provider.NewCompatibleProvider(groupName, ps, hc) + if err != nil { + return nil, fmt.Errorf("%s: %w", groupName, err) + } + + providers = append(providers, pd) + providersMap[groupName] = pd } if len(groupOption.Use) != 0 { list, err := getProviders(providersMap, groupOption.Use) if err != nil { - return nil, err + return nil, fmt.Errorf("%s: %w", groupName, err) } + + // different proxy groups use different test URL + addTestUrlToProviders(list, testUrl, expectedStatus, groupOption.Filter, uint(groupOption.Interval)) + providers = append(providers, list...) } else { groupOption.Filter = "" @@ -154,3 +169,13 @@ func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]type } 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/relay.go b/adapter/outboundgroup/relay.go index b466b1ff..2b1be8a5 100644 --- a/adapter/outboundgroup/relay.go +++ b/adapter/outboundgroup/relay.go @@ -3,51 +3,17 @@ package outboundgroup import ( "context" "encoding/json" - "net" - "net/netip" - "strings" - - "github.com/Dreamacro/clash/adapter/outbound" - N "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/provider" + "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" ) type Relay struct { *GroupBase } -type proxyDialer struct { - proxy C.Proxy - dialer C.Dialer -} - -func (p proxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - currentMeta, err := addrToMetadata(address) - if err != nil { - return nil, err - } - if strings.Contains(network, "udp") { // should not support this operation - currentMeta.NetWork = C.UDP - pc, err := p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta) - if err != nil { - return nil, err - } - return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil - } - return p.proxy.DialContextWithDialer(ctx, p.dialer, currentMeta) -} - -func (p proxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { - currentMeta, err := addrToMetadata(rAddrPort.String()) - if err != nil { - return nil, err - } - currentMeta.NetWork = C.UDP - return p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta) -} - // DialContext implements C.ProxyAdapter func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { proxies, chainProxies := r.proxies(metadata, true) @@ -61,10 +27,7 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d var d C.Dialer d = dialer.NewDialer(r.Base.DialOptions(opts...)...) for _, proxy := range proxies[:len(proxies)-1] { - d = proxyDialer{ - proxy: proxy, - dialer: d, - } + d = proxydialer.New(proxy, d, false) } last := proxies[len(proxies)-1] conn, err := last.DialContextWithDialer(ctx, d, metadata) @@ -95,10 +58,7 @@ func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o var d C.Dialer d = dialer.NewDialer(r.Base.DialOptions(opts...)...) for _, proxy := range proxies[:len(proxies)-1] { - d = proxyDialer{ - proxy: proxy, - dialer: d, - } + d = proxydialer.New(proxy, d, false) } last := proxies[len(proxies)-1] pc, err := last.ListenPacketWithDialer(ctx, d, metadata) @@ -129,7 +89,10 @@ func (r *Relay) SupportUDP() bool { if proxy.SupportUOT() { return true } - if !proxy.SupportWithDialer() { + switch proxy.SupportWithDialer() { + case C.ALLNet: + case C.UDP: + default: // C.TCP and C.InvalidNet return false } } @@ -176,7 +139,7 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy) } func (r *Relay) Addr() string { - proxies, _ := r.proxies(nil, true) + proxies, _ := r.proxies(nil, false) return proxies[len(proxies)-1].Addr() } diff --git a/adapter/outboundgroup/selector.go b/adapter/outboundgroup/selector.go index 6356d10e..4d06c544 100644 --- a/adapter/outboundgroup/selector.go +++ b/adapter/outboundgroup/selector.go @@ -5,10 +5,10 @@ import ( "encoding/json" "errors" - "github.com/Dreamacro/clash/adapter/outbound" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/provider" + "github.com/metacubex/mihomo/adapter/outbound" + "github.com/metacubex/mihomo/component/dialer" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/provider" ) type Selector struct { @@ -44,6 +44,11 @@ func (s *Selector) SupportUDP() bool { 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{} @@ -73,6 +78,10 @@ func (s *Selector) Set(name string) error { 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) diff --git a/adapter/outboundgroup/urltest.go b/adapter/outboundgroup/urltest.go index 27cef9c6..8c861768 100644 --- a/adapter/outboundgroup/urltest.go +++ b/adapter/outboundgroup/urltest.go @@ -3,13 +3,16 @@ package outboundgroup import ( "context" "encoding/json" + "errors" "time" - "github.com/Dreamacro/clash/adapter/outbound" - "github.com/Dreamacro/clash/common/singledo" - "github.com/Dreamacro/clash/component/dialer" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/provider" + "github.com/metacubex/mihomo/adapter/outbound" + "github.com/metacubex/mihomo/common/callback" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/singledo" + "github.com/metacubex/mihomo/component/dialer" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/provider" ) type urlTestOption func(*URLTest) @@ -22,26 +25,59 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption { type URLTest struct { *GroupBase - tolerance uint16 - disableUDP bool - fastNode C.Proxy - fastSingle *singledo.Single[C.Proxy] + selected string + testUrl string + expectedStatus string + tolerance uint16 + disableUDP bool + 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.selected = name + u.fast(false) + return nil +} + +func (u *URLTest) ForceSet(name string) { + u.selected = name +} + // DialContext implements C.ProxyAdapter func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) { proxy := u.fast(true) c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...) if err == nil { c.AppendToChains(u) - u.onDialSuccess() } else { u.onDialFailed(proxy.Type(), err) } + + if N.NeedHandshake(c) { + c = callback.NewFirstWriteCallBackConn(c, func(err error) { + if err == nil { + u.onDialSuccess() + } else { + u.onDialFailed(proxy.Type(), err) + } + }) + } + return c, err } @@ -61,10 +97,24 @@ func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { } func (u *URLTest) fast(touch bool) C.Proxy { + + proxies := u.GetProxies(touch) + if u.selected != "" { + for _, proxy := range proxies { + if !proxy.Alive() { + continue + } + if proxy.Name() == u.selected { + u.fastNode = proxy + return proxy + } + } + } + elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) { - proxies := u.GetProxies(touch) fast := proxies[0] - min := fast.LastDelay() + // min := fast.LastDelay() + min := fast.LastDelayForTestUrl(u.testUrl) fastNotExist := true for _, proxy := range proxies[1:] { @@ -72,22 +122,24 @@ func (u *URLTest) fast(touch bool) C.Proxy { fastNotExist = false } - if !proxy.Alive() { + // if !proxy.Alive() { + if !proxy.AliveForTestUrl(u.testUrl) { continue } - delay := proxy.LastDelay() + // delay := proxy.LastDelay() + delay := proxy.LastDelayForTestUrl(u.testUrl) if delay < min { fast = proxy min = delay } - } + } // tolerance - if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance { + // if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.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 @@ -102,10 +154,14 @@ 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{} @@ -113,9 +169,11 @@ func (u *URLTest) MarshalJSON() ([]byte, error) { all = append(all, proxy.Name()) } return json.Marshal(map[string]any{ - "type": u.Type().String(), - "now": u.Now(), - "all": all, + "type": u.Type().String(), + "now": u.Now(), + "all": all, + "testUrl": u.testUrl, + "expected": u.expectedStatus, }) } @@ -147,8 +205,10 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o option.ExcludeType, providers, }), - fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), - disableUDP: option.DisableUDP, + fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), + disableUDP: option.DisableUDP, + testUrl: option.URL, + expectedStatus: option.ExpectedStatus, } for _, option := range options { diff --git a/adapter/outboundgroup/util.go b/adapter/outboundgroup/util.go index 578011f8..84216377 100644 --- a/adapter/outboundgroup/util.go +++ b/adapter/outboundgroup/util.go @@ -1,44 +1,6 @@ package outboundgroup -import ( - "fmt" - "net" - "net/netip" - "time" - - C "github.com/Dreamacro/clash/constant" -) - -func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) { - host, port, err := net.SplitHostPort(rawAddress) - if err != nil { - err = fmt.Errorf("addrToMetadata failed: %w", err) - return - } - - if ip, err := netip.ParseAddr(host); err != nil { - addr = &C.Metadata{ - Host: host, - DstPort: port, - } - } else { - addr = &C.Metadata{ - Host: "", - DstIP: ip.Unmap(), - DstPort: port, - } - } - - return -} - -func tcpKeepAlive(c net.Conn) { - if tcp, ok := c.(*net.TCPConn); ok { - _ = tcp.SetKeepAlive(true) - _ = tcp.SetKeepAlivePeriod(30 * time.Second) - } -} - type SelectAble interface { Set(string) error + ForceSet(name string) } diff --git a/adapter/parser.go b/adapter/parser.go index d9c18694..1d363c1f 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -3,11 +3,11 @@ package adapter import ( "fmt" - tlsC "github.com/Dreamacro/clash/component/tls" + tlsC "github.com/metacubex/mihomo/component/tls" - "github.com/Dreamacro/clash/adapter/outbound" - "github.com/Dreamacro/clash/common/structure" - C "github.com/Dreamacro/clash/constant" + "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) { @@ -23,7 +23,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { ) switch proxyType { case "ss": - ssOption := &outbound.ShadowSocksOption{} + ssOption := &outbound.ShadowSocksOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} err = decoder.Decode(mapping, ssOption) if err != nil { break @@ -56,10 +56,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { Method: "GET", Path: []string{"/"}, }, - } - - if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { - vmessOption.ClientFingerprint = GlobalUtlsClient + ClientFingerprint: tlsC.GetGlobalFingerprint(), } err = decoder.Decode(mapping, vmessOption) @@ -68,12 +65,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { } proxy, err = outbound.NewVmess(*vmessOption) case "vless": - vlessOption := &outbound.VlessOption{} - - if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { - vlessOption.ClientFingerprint = GlobalUtlsClient - } - + vlessOption := &outbound.VlessOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} err = decoder.Decode(mapping, vlessOption) if err != nil { break @@ -87,12 +79,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { } proxy, err = outbound.NewSnell(*snellOption) case "trojan": - trojanOption := &outbound.TrojanOption{} - - if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { - trojanOption.ClientFingerprint = GlobalUtlsClient - } - + trojanOption := &outbound.TrojanOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} err = decoder.Decode(mapping, trojanOption) if err != nil { break @@ -105,6 +92,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { 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) @@ -119,6 +113,20 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { 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 "reject": + rejectOption := &outbound.RejectOption{} + err = decoder.Decode(mapping, rejectOption) + if err != nil { + break + } + proxy = outbound.NewRejectWithOption(*rejectOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } @@ -127,5 +135,19 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { 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, proxy.(outbound.ProxyBase)) + if err != nil { + return nil, err + } + } + } + return NewProxy(proxy), nil } diff --git a/adapter/provider/healthcheck.go b/adapter/provider/healthcheck.go index 16b9ad61..e7f021e1 100644 --- a/adapter/provider/healthcheck.go +++ b/adapter/provider/healthcheck.go @@ -2,15 +2,18 @@ package provider import ( "context" + "strings" + "sync" "time" - "github.com/Dreamacro/clash/common/batch" - "github.com/Dreamacro/clash/common/singledo" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/atomic" + "github.com/metacubex/mihomo/common/batch" + "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/gofrs/uuid" - "go.uber.org/atomic" + "github.com/dlclark/regexp2" ) const ( @@ -22,96 +25,214 @@ type HealthCheckOption struct { Interval uint } +type extraOption struct { + expectedStatus utils.IntRanges[uint16] + filters map[string]struct{} +} + type HealthCheck struct { - url string - proxies []C.Proxy - interval uint - lazy bool - lastTouch *atomic.Int64 - done chan struct{} - singleDo *singledo.Single[struct{}] + url string + extra map[string]*extraOption + mu sync.Mutex + started atomic.Bool + proxies []C.Proxy + interval time.Duration + lazy bool + expectedStatus utils.IntRanges[uint16] + lastTouch atomic.TypedValue[time.Time] + done chan struct{} + singleDo *singledo.Single[struct{}] } func (hc *HealthCheck) process() { - ticker := time.NewTicker(time.Duration(hc.interval) * time.Second) - - go func() { - time.Sleep(30 * time.Second) - hc.lazyCheck() - }() + if hc.started.Load() { + log.Warnln("Skip start health check timer due to it's started") + return + } + ticker := time.NewTicker(hc.interval) + hc.start() for { select { case <-ticker.C: - hc.lazyCheck() + 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.done: ticker.Stop() + hc.stop() return } } } -func (hc *HealthCheck) lazyCheck() bool { - now := time.Now().Unix() - if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) { - hc.check() - return true - } else { - log.Debugln("Skip once health check because we are lazy") - return false - } -} - 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 + } + + // due to the time-consuming nature of health checks, a maximum of defaultMaxTestURLNum URLs can be set for testing + if len(hc.extra) > C.DefaultMaxHealthCheckUrlNum { + log.Debugln("skip add url: %s to health check because it has reached the maximum limit: %d", url, C.DefaultMaxHealthCheckUrlNum) + return + } + + option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus} + splitAndAddFiltersToExtra(filter, option) + hc.extra[url] = option + + if hc.auto() && !hc.started.Load() { + go hc.process() + } +} + +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().Unix()) + hc.lastTouch.Store(time.Now()) +} + +func (hc *HealthCheck) start() { + hc.started.Store(true) +} + +func (hc *HealthCheck) stop() { + hc.started.Store(false) } func (hc *HealthCheck) check() { _, _, _ = hc.singleDo.Do(func() (struct{}, error) { - id := "" - if uid, err := uuid.NewV4(); err == nil { - id = uid.String() - } + id := utils.NewUUIDV4().String() log.Debugln("Start New Health Checking {%s}", id) b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10)) - for _, proxy := range hc.proxies { - p := proxy - b.Go(p.Name(), func() (bool, error) { - ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) - defer cancel() - log.Debugln("Health Checking %s {%s}", p.Name(), id) - _, _ = p.URLTest(ctx, hc.url) - log.Debugln("Health Checked %s : %t %d ms {%s}", p.Name(), p.Alive(), p.LastDelay(), id) - return false, nil - }) - } + // 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 *batch.Batch[bool], 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 store = C.OriginalHistory + var expectedStatus utils.IntRanges[uint16] + if option != nil { + if url != hc.url { + store = C.ExtraHistory + } + + 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, "|"), 0) + } + } + + for _, proxy := range hc.proxies { + // skip proxies that do not require health check + if filterReg != nil { + if match, _ := filterReg.FindStringMatch(proxy.Name()); match == nil { + continue + } + } + + p := proxy + b.Go(p.Name(), func() (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) + defer cancel() + log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid) + _, _ = p.URLTest(ctx, url, expectedStatus, store) + 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 false, nil + }) + } +} + func (hc *HealthCheck) close() { hc.done <- struct{}{} } -func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck { +func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck { + if len(url) == 0 { + interval = 0 + expectedStatus = nil + } + return &HealthCheck{ - proxies: proxies, - url: url, - interval: interval, - lazy: lazy, - lastTouch: atomic.NewInt64(0), - done: make(chan struct{}, 1), - singleDo: singledo.NewSingle[struct{}](time.Second), + proxies: proxies, + url: url, + extra: map[string]*extraOption{}, + interval: time.Duration(interval) * time.Second, + lazy: lazy, + expectedStatus: expectedStatus, + done: make(chan struct{}, 1), + singleDo: singledo.NewSingle[struct{}](time.Second), } } diff --git a/adapter/provider/parser.go b/adapter/provider/parser.go index fc5ed936..321380ed 100644 --- a/adapter/provider/parser.go +++ b/adapter/provider/parser.go @@ -5,29 +5,35 @@ import ( "fmt" "time" - "github.com/Dreamacro/clash/common/structure" - "github.com/Dreamacro/clash/component/resource" - C "github.com/Dreamacro/clash/constant" - types "github.com/Dreamacro/clash/constant/provider" + "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" ) -var errVehicleType = errors.New("unsupport vehicle type") +var ( + errVehicleType = errors.New("unsupport vehicle type") + errSubPath = errors.New("path is not subpath of home directory") +) type healthCheckSchema struct { - Enable bool `provider:"enable"` - URL string `provider:"url"` - Interval int `provider:"interval"` - Lazy bool `provider:"lazy,omitempty"` + Enable bool `provider:"enable"` + URL string `provider:"url"` + Interval int `provider:"interval"` + Lazy bool `provider:"lazy,omitempty"` + ExpectedStatus string `provider:"expected-status,omitempty"` } type proxyProviderSchema struct { Type string `provider:"type"` - Path string `provider:"path"` + Path string `provider:"path,omitempty"` URL string `provider:"url,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"` HealthCheck healthCheckSchema `provider:"health-check,omitempty"` } @@ -43,20 +49,33 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide return nil, err } + expectedStatus, err := utils.NewIntRanges[uint16](schema.HealthCheck.ExpectedStatus) + if err != nil { + return nil, err + } + var hcInterval uint if schema.HealthCheck.Enable { hcInterval = uint(schema.HealthCheck.Interval) } - hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy) - - path := C.Path.Resolve(schema.Path) + hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy, expectedStatus) var vehicle types.Vehicle switch schema.Type { case "file": + path := C.Path.Resolve(schema.Path) vehicle = resource.NewFileVehicle(path) case "http": - vehicle = resource.NewHTTPVehicle(schema.URL, path) + if schema.Path != "" { + path := C.Path.Resolve(schema.Path) + if !C.Path.IsSafePath(path) { + return nil, fmt.Errorf("%w: %s", errSubPath, path) + } + vehicle = resource.NewHTTPVehicle(schema.URL, path) + } else { + path := C.Path.GetPathByHash("proxies", schema.URL) + vehicle = resource.NewHTTPVehicle(schema.URL, path) + } default: return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) } @@ -65,6 +84,7 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide filter := schema.Filter excludeFilter := schema.ExcludeFilter excludeType := schema.ExcludeType + dialerProxy := schema.DialerProxy - return NewProxySetProvider(name, interval, filter, excludeFilter, excludeType, vehicle, hc) + return NewProxySetProvider(name, interval, filter, excludeFilter, excludeType, dialerProxy, vehicle, hc) } diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go index e8bd7ed1..ffaec91b 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -10,13 +10,15 @@ import ( "strings" "time" - "github.com/Dreamacro/clash/adapter" - "github.com/Dreamacro/clash/common/convert" - clashHttp "github.com/Dreamacro/clash/component/http" - "github.com/Dreamacro/clash/component/resource" - C "github.com/Dreamacro/clash/constant" - types "github.com/Dreamacro/clash/constant/provider" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/adapter" + "github.com/metacubex/mihomo/common/convert" + "github.com/metacubex/mihomo/common/utils" + mihomoHttp "github.com/metacubex/mihomo/component/http" + "github.com/metacubex/mihomo/component/resource" + C "github.com/metacubex/mihomo/constant" + types "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/tunnel/statistic" "github.com/dlclark/regexp2" "gopkg.in/yaml.v3" @@ -49,6 +51,7 @@ func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { "type": pp.Type().String(), "vehicleType": pp.VehicleType().String(), "proxies": pp.Proxies(), + "testUrl": pp.healthCheck.url, "updatedAt": pp.UpdatedAt, "subscriptionInfo": pp.subscriptionInfo, }) @@ -80,6 +83,8 @@ func (pp *proxySetProvider) Initial() error { return err } pp.OnUpdate(elm) + pp.getSubscriptionInfo() + pp.closeAllConnections() return nil } @@ -95,11 +100,15 @@ func (pp *proxySetProvider) Touch() { pp.healthCheck.touch() } +func (pp *proxySetProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { + pp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval) +} + func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { pp.proxies = proxies pp.healthCheck.setProxy(proxies) if pp.healthCheck.auto() { - defer func() { go pp.healthCheck.lazyCheck() }() + go pp.healthCheck.check() } } @@ -110,8 +119,8 @@ func (pp *proxySetProvider) getSubscriptionInfo() { go func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) defer cancel() - resp, err := clashHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), - http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil) + resp, err := mihomoHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), + http.MethodGet, http.Header{"User-Agent": {"mihomo"}}, nil) if err != nil { return } @@ -119,7 +128,7 @@ func (pp *proxySetProvider) getSubscriptionInfo() { userInfoStr := strings.TrimSpace(resp.Header.Get("subscription-userinfo")) if userInfoStr == "" { - resp2, err := clashHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), + resp2, err := mihomoHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), http.MethodGet, http.Header{"User-Agent": {"Quantumultx"}}, nil) if err != nil { return @@ -137,12 +146,24 @@ func (pp *proxySetProvider) getSubscriptionInfo() { }() } +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 stopProxyProvider(pd *ProxySetProvider) { pd.healthCheck.close() _ = pd.Fetcher.Destroy() } -func NewProxySetProvider(name string, interval time.Duration, filter string, excludeFilter string, excludeType string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { +func NewProxySetProvider(name string, interval time.Duration, filter string, excludeFilter string, excludeType string, dialerProxy string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { excludeFilterReg, err := regexp2.Compile(excludeFilter, 0) if err != nil { return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) @@ -170,10 +191,8 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, exc healthCheck: hc, } - fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, excludeTypeArray, filterRegs, excludeFilterReg), proxiesOnUpdate(pd)) + fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, excludeTypeArray, filterRegs, excludeFilterReg, dialerProxy), proxiesOnUpdate(pd)) pd.Fetcher = fetcher - - pd.getSubscriptionInfo() wrapper := &ProxySetProvider{pd} runtime.SetFinalizer(wrapper, stopProxyProvider) return wrapper, nil @@ -197,6 +216,7 @@ func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { "type": cp.Type().String(), "vehicleType": cp.VehicleType().String(), "proxies": cp.Proxies(), + "testUrl": cp.healthCheck.url, }) } @@ -236,6 +256,10 @@ func (cp *compatibleProvider) Touch() { cp.healthCheck.touch() } +func (cp *compatibleProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { + cp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval) +} + func stopCompatibleProvider(pd *CompatibleProvider) { pd.healthCheck.close() } @@ -268,14 +292,14 @@ func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) { } } -func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray []string, filterRegs []*regexp2.Regexp, excludeFilterReg *regexp2.Regexp) resource.Parser[[]C.Proxy] { +func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray []string, filterRegs []*regexp2.Regexp, excludeFilterReg *regexp2.Regexp, dialerProxy string) resource.Parser[[]C.Proxy] { return func(buf []byte) ([]C.Proxy, error) { schema := &ProxySchema{} if err := yaml.Unmarshal(buf, schema); err != nil { proxies, err1 := convert.ConvertsV2Ray(buf) if err1 != nil { - return nil, fmt.Errorf("%s, %w", err.Error(), err1) + return nil, fmt.Errorf("%w, %w", err, err1) } schema.Proxies = proxies } @@ -331,6 +355,9 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray if _, ok := proxiesSet[name]; ok { continue } + if len(dialerProxy) > 0 { + mapping["dialer-proxy"] = dialerProxy + } proxy, err := adapter.ParseProxy(mapping) if err != nil { return nil, fmt.Errorf("proxy %d error: %w", idx, err) diff --git a/adapter/provider/subscription_info.go b/adapter/provider/subscription_info.go index fc6992e2..8b90601c 100644 --- a/adapter/provider/subscription_info.go +++ b/adapter/provider/subscription_info.go @@ -1,7 +1,6 @@ package provider import ( - "github.com/dlclark/regexp2" "strconv" "strings" ) @@ -13,45 +12,24 @@ type SubscriptionInfo struct { Expire int64 } -func NewSubscriptionInfo(str string) (si *SubscriptionInfo, err error) { - si = &SubscriptionInfo{} - str = strings.ToLower(str) - reTraffic := regexp2.MustCompile("upload=(\\d+); download=(\\d+); total=(\\d+)", 0) - reExpire := regexp2.MustCompile("expire=(\\d+)", 0) - - match, err := reTraffic.FindStringMatch(str) - if err != nil || match == nil { - return nil, err - } - group := match.Groups() - si.Upload, err = str2uint64(group[1].String()) - if err != nil { - return nil, err - } - - si.Download, err = str2uint64(group[2].String()) - if err != nil { - return nil, err - } - - si.Total, err = str2uint64(group[3].String()) - if err != nil { - return nil, err - } - - match, _ = reExpire.FindStringMatch(str) - if match != nil { - group = match.Groups() - si.Expire, err = str2uint64(group[1].String()) +func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo, err error) { + userinfo = strings.ToLower(userinfo) + userinfo = strings.ReplaceAll(userinfo, " ", "") + si = new(SubscriptionInfo) + for _, field := range strings.Split(userinfo, ";") { + switch name, value, _ := strings.Cut(field, "="); name { + case "upload": + si.Upload, err = strconv.ParseInt(value, 10, 64) + case "download": + si.Download, err = strconv.ParseInt(value, 10, 64) + case "total": + si.Total, err = strconv.ParseInt(value, 10, 64) + case "expire": + si.Expire, err = strconv.ParseInt(value, 10, 64) + } if err != nil { - return nil, err + return } } - return } - -func str2uint64(str string) (int64, error) { - i, err := strconv.ParseInt(str, 10, 64) - return i, err -} diff --git a/common/atomic/type.go b/common/atomic/type.go new file mode 100644 index 00000000..71695c63 --- /dev/null +++ b/common/atomic/type.go @@ -0,0 +1,198 @@ +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 new file mode 100644 index 00000000..708fcf90 --- /dev/null +++ b/common/atomic/value.go @@ -0,0 +1,64 @@ +package atomic + +import ( + "encoding/json" + "sync/atomic" +) + +func DefaultValue[T any]() T { + var defaultValue T + return defaultValue +} + +type TypedValue[T any] struct { + value atomic.Value + _ noCopy +} + +func (t *TypedValue[T]) Load() T { + value := t.value.Load() + if value == nil { + return DefaultValue[T]() + } + return value.(T) +} + +func (t *TypedValue[T]) Store(value T) { + t.value.Store(value) +} + +func (t *TypedValue[T]) Swap(new T) T { + old := t.value.Swap(new) + if old == nil { + return DefaultValue[T]() + } + return old.(T) +} + +func (t *TypedValue[T]) CompareAndSwap(old, new T) bool { + return t.value.CompareAndSwap(old, 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/buf/sing.go b/common/buf/sing.go index b5e015f5..0907a95c 100644 --- a/common/buf/sing.go +++ b/common/buf/sing.go @@ -5,15 +5,17 @@ import ( "github.com/sagernet/sing/common/buf" ) +const BufferSize = buf.BufferSize + type Buffer = buf.Buffer -var StackNewSize = buf.StackNewSize -var KeepAlive = common.KeepAlive +var New = buf.New +var NewPacket = buf.NewPacket +var NewSize = buf.NewSize +var With = buf.With +var As = buf.As -//go:norace -func Dup[T any](obj T) T { - return common.Dup(obj) -} - -var Must = common.Must -var Error = common.Error +var ( + Must = common.Must + Error = common.Error +) diff --git a/common/cache/lrucache.go b/common/cache/lrucache.go index 73600e71..d71cc3b9 100644 --- a/common/cache/lrucache.go +++ b/common/cache/lrucache.go @@ -6,7 +6,9 @@ import ( "sync" "time" - "github.com/Dreamacro/clash/common/generics/list" + "github.com/metacubex/mihomo/common/generics/list" + + "github.com/samber/lo" ) // Option is part of Functional Options Pattern @@ -82,9 +84,27 @@ func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { // Get returns the 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 getZero[V](), false + 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 @@ -96,9 +116,12 @@ func (c *LruCache[K, V]) Get(key K) (V, bool) { // 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 getZero[V](), time.Time{}, false + return lo.Empty[V](), time.Time{}, false } return el.value, time.Unix(el.expires, 0), true @@ -115,11 +138,18 @@ func (c *LruCache[K, V]) Exist(key K) bool { // Set stores the 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)) + c.setWithExpire(key, value, time.Unix(expires, 0)) } // SetWithExpire stores the any representation of a response for a given key and given expires. @@ -128,6 +158,10 @@ 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 @@ -165,9 +199,6 @@ func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) { } func (c *LruCache[K, V]) get(key K) *entry[K, V] { - c.mu.Lock() - defer c.mu.Unlock() - le, ok := c.cache[key] if !ok { return nil @@ -191,12 +222,11 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] { // Delete removes the value associated with a key. func (c *LruCache[K, V]) Delete(key K) { c.mu.Lock() + defer c.mu.Unlock() if le, ok := c.cache[key]; ok { c.deleteElement(le) } - - c.mu.Unlock() } func (c *LruCache[K, V]) maybeDeleteOldest() { @@ -219,10 +249,10 @@ func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) { func (c *LruCache[K, V]) Clear() error { c.mu.Lock() + defer c.mu.Unlock() c.cache = make(map[K]*list.Element[*entry[K, V]]) - c.mu.Unlock() return nil } @@ -231,8 +261,3 @@ type entry[K comparable, V any] struct { value V expires int64 } - -func getZero[T any]() T { - var result T - return result -} diff --git a/common/callback/callback.go b/common/callback/callback.go new file mode 100644 index 00000000..9ae0f94a --- /dev/null +++ b/common/callback/callback.go @@ -0,0 +1,55 @@ +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/cmd/cmd_test.go b/common/cmd/cmd_test.go index 4bba6def..b124a22d 100644 --- a/common/cmd/cmd_test.go +++ b/common/cmd/cmd_test.go @@ -21,7 +21,7 @@ func TestSplitArgs(t *testing.T) { func TestExecCmd(t *testing.T) { if runtime.GOOS == "windows" { - _, err := ExecCmd("dir") + _, err := ExecCmd("cmd -c 'dir'") assert.Nil(t, err) return } diff --git a/common/convert/converter.go b/common/convert/converter.go index 7d896d53..55035bbe 100644 --- a/common/convert/converter.go +++ b/common/convert/converter.go @@ -5,13 +5,14 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/Dreamacro/clash/log" "net/url" "strconv" "strings" + + "github.com/metacubex/mihomo/log" ) -// ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config +// ConvertsV2Ray convert V2Ray subscribe proxies data to mihomo proxies config func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { data := DecodeBase64(buf) @@ -49,7 +50,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { hysteria["port"] = urlHysteria.Port() hysteria["sni"] = query.Get("peer") hysteria["obfs"] = query.Get("obfs") - hysteria["alpn"] = []string{query.Get("alpn")} + 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") @@ -65,6 +68,79 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure")) proxies = append(proxies, hysteria) + case "hysteria2": + 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"] = scheme + 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 + } case "trojan": urlTrojan, err := url.Parse(line) @@ -83,12 +159,14 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { trojan["port"] = urlTrojan.Port() trojan["password"] = urlTrojan.User.Username() trojan["udp"] = true - trojan["skip-cert-verify"] = false + trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure")) - sni := query.Get("sni") - if sni != "" { + 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 != "" { @@ -201,7 +279,8 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { vmess["servername"] = sni } - network := strings.ToLower(values["net"].(string)) + network, _ := values["net"].(string) + network = strings.ToLower(network) if values["type"] == "http" { network = "http" } else if network == "http" { @@ -209,9 +288,15 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { } vmess["network"] = network - tls := strings.ToLower(values["tls"].(string)) - if strings.HasSuffix(tls, "tls") { - vmess["tls"] = true + 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 { @@ -327,6 +412,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { } } proxies = append(proxies, ss) + case "ssr": dcBuf, err := encRaw.DecodeString(body) if err != nil { diff --git a/common/convert/converter_test.go b/common/convert/converter_test.go new file mode 100644 index 00000000..83b41c4c --- /dev/null +++ b/common/convert/converter_test.go @@ -0,0 +1,35 @@ +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 index 03a48ecd..a715b556 100644 --- a/common/convert/util.go +++ b/common/convert/util.go @@ -2,12 +2,14 @@ package convert import ( "encoding/base64" - "github.com/metacubex/sing-shadowsocks/shadowimpl" - "math/rand" "net/http" "strings" + "time" - "github.com/gofrs/uuid" + "github.com/metacubex/mihomo/common/utils" + + "github.com/metacubex/sing-shadowsocks/shadowimpl" + "github.com/zhangyunhao116/fastrand" ) var hostsSuffix = []string{ @@ -292,8 +294,7 @@ var ( ) func RandHost() string { - id, _ := uuid.NewV4() - base := strings.ToLower(base64.RawURLEncoding.EncodeToString(id.Bytes())) + base := strings.ToLower(base64.RawURLEncoding.EncodeToString(utils.NewUUIDV4().Bytes())) base = strings.ReplaceAll(base, "-", "") base = strings.ReplaceAll(base, "_", "") buf := []byte(base) @@ -301,11 +302,11 @@ func RandHost() string { prefix += string(buf[6:8]) + "-" prefix += string(buf[len(buf)-8:]) - return prefix + hostsSuffix[rand.Intn(hostsLen)] + return prefix + hostsSuffix[fastrand.Intn(hostsLen)] } func RandUserAgent() string { - return userAgents[rand.Intn(uaLen)] + return userAgents[fastrand.Intn(uaLen)] } func SetUserAgent(header http.Header) { @@ -317,6 +318,6 @@ func SetUserAgent(header http.Header) { } func VerifyMethod(cipher, password string) (err error) { - _, err = shadowimpl.FetchMethod(cipher, password) + _, err = shadowimpl.FetchMethod(cipher, password, time.Now) return } diff --git a/common/convert/v.go b/common/convert/v.go index 606d8aff..2d8cf732 100644 --- a/common/convert/v.go +++ b/common/convert/v.go @@ -24,20 +24,27 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m proxy["port"] = url.Port() proxy["uuid"] = url.User.Username() proxy["udp"] = true - proxy["skip-cert-verify"] = false - proxy["tls"] = false tls := strings.ToLower(query.Get("security")) - if strings.HasSuffix(tls, "tls") { + 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": diff --git a/common/net/addr.go b/common/net/addr.go new file mode 100644 index 00000000..4efaefcd --- /dev/null +++ b/common/net/addr.go @@ -0,0 +1,36 @@ +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 index 1e20a8c0..231c24c2 100644 --- a/common/net/bind.go +++ b/common/net/bind.go @@ -3,34 +3,43 @@ package net import "net" type bindPacketConn struct { - net.PacketConn + EnhancePacketConn rAddr net.Addr } -func (wpc *bindPacketConn) Read(b []byte) (n int, err error) { - n, _, err = wpc.PacketConn.ReadFrom(b) +func (c *bindPacketConn) Read(b []byte) (n int, err error) { + n, _, err = c.EnhancePacketConn.ReadFrom(b) return n, err } -func (wpc *bindPacketConn) Write(b []byte) (n int, err error) { - return wpc.PacketConn.WriteTo(b, wpc.rAddr) +func (c *bindPacketConn) WaitRead() (data []byte, put func(), err error) { + data, put, _, err = c.EnhancePacketConn.WaitReadFrom() + return } -func (wpc *bindPacketConn) RemoteAddr() net.Addr { - return wpc.rAddr +func (c *bindPacketConn) Write(b []byte) (n int, err error) { + return c.EnhancePacketConn.WriteTo(b, c.rAddr) } -func (wpc *bindPacketConn) LocalAddr() net.Addr { - if wpc.PacketConn.LocalAddr() == nil { +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 wpc.PacketConn.LocalAddr() + return c.EnhancePacketConn.LocalAddr() } } +func (c *bindPacketConn) Upstream() any { + return c.EnhancePacketConn +} + func NewBindPacketConn(pc net.PacketConn, rAddr net.Addr) net.Conn { return &bindPacketConn{ - PacketConn: pc, - rAddr: rAddr, + EnhancePacketConn: NewEnhancePacketConn(pc), + rAddr: rAddr, } } diff --git a/common/net/bufconn.go b/common/net/bufconn.go index ba0ca026..37c8ba25 100644 --- a/common/net/bufconn.go +++ b/common/net/bufconn.go @@ -4,7 +4,7 @@ import ( "bufio" "net" - "github.com/Dreamacro/clash/common/buf" + "github.com/metacubex/mihomo/common/buf" ) var _ ExtendedConn = (*BufferedConn)(nil) @@ -12,13 +12,24 @@ 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)} + 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. @@ -26,11 +37,24 @@ 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) } @@ -48,20 +72,35 @@ func (c *BufferedConn) Buffered() int { } func (c *BufferedConn) ReadBuffer(buffer *buf.Buffer) (err error) { - if c.r.Buffered() > 0 { + 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) + c.r = nil // drop bufio.Reader to let gc can clean up its internal buf + return buf.As(b) + } + return nil +} + func (c *BufferedConn) Upstream() any { return c.ExtendedConn } func (c *BufferedConn) ReaderReplaceable() bool { - if c.r.Buffered() > 0 { + if c.r != nil && c.r.Buffered() > 0 { return false } return true } + +func (c *BufferedConn) WriterReplaceable() bool { + return true +} diff --git a/common/net/cached.go b/common/net/cached.go new file mode 100644 index 00000000..fb605b74 --- /dev/null +++ b/common/net/cached.go @@ -0,0 +1,49 @@ +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 new file mode 100644 index 00000000..917028d1 --- /dev/null +++ b/common/net/context.go @@ -0,0 +1,31 @@ +package net + +import ( + "context" + "net" +) + +// SetupContextForConn is a helper function that starts connection I/O interrupter goroutine. +func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) { + var ( + quit = make(chan struct{}) + interrupt = make(chan error, 1) + ) + go func() { + select { + case <-quit: + interrupt <- nil + case <-ctx.Done(): + // Close the connection, discarding the error + _ = conn.Close() + interrupt <- ctx.Err() + } + }() + return func(inputErr *error) { + close(quit) + if ctxErr := <-interrupt; ctxErr != nil && inputErr != nil { + // Return context error to user. + inputErr = &ctxErr + } + } +} diff --git a/common/net/deadline/packet.go b/common/net/deadline/packet.go new file mode 100644 index 00000000..67043198 --- /dev/null +++ b/common/net/deadline/packet.go @@ -0,0 +1,154 @@ +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 new file mode 100644 index 00000000..3e314fb8 --- /dev/null +++ b/common/net/deadline/packet_enhance.go @@ -0,0 +1,83 @@ +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 new file mode 100644 index 00000000..65db1b8f --- /dev/null +++ b/common/net/deadline/packet_sing.go @@ -0,0 +1,177 @@ +package deadline + +import ( + "os" + "runtime" + + "github.com/metacubex/mihomo/common/net/packet" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/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(newBuffer func() *buf.Buffer) { + c.packetReadWaiter.InitializeReadWaiter(newBuffer) +} + +func (c *singPacketReadWaiter) WaitReadPacket() (destination M.Socksaddr, err error) { +FOR: + for { + select { + case result := <-c.netPacketConn.resultCh: + if result != nil { + if result, ok := result.(*singWaitReadResult); ok { + 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 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) + destination, err = c.packetReadWaiter.WaitReadPacket() + return + } + + <-c.netPacketConn.resultCh + go c.pipeWaitReadPacket() + + return c.WaitReadPacket() +} + +func (c *singPacketReadWaiter) pipeWaitReadPacket() { + destination, err := c.packetReadWaiter.WaitReadPacket() + result := &singWaitReadResult{} + 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 new file mode 100644 index 00000000..2cccfb42 --- /dev/null +++ b/common/net/deadline/pipe.go @@ -0,0 +1,84 @@ +// 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/packet.go b/common/net/packet.go new file mode 100644 index 00000000..fd03b4f8 --- /dev/null +++ b/common/net/packet.go @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..0cdbccae --- /dev/null +++ b/common/net/packet/packet.go @@ -0,0 +1,77 @@ +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 new file mode 100644 index 00000000..2073e35d --- /dev/null +++ b/common/net/packet/packet_posix.go @@ -0,0 +1,65 @@ +//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 + addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: strconv.FormatInt(int64(from.ZoneId), 10)} + } + } + // 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 new file mode 100644 index 00000000..cfcf5ed0 --- /dev/null +++ b/common/net/packet/packet_sing.go @@ -0,0 +1,79 @@ +package packet + +import ( + "net" + + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/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 + newBuffer := func() *buf.Buffer { + buff = buf.NewPacket() // do not use stack buffer + return buff + } + if c.packetReadWaiter != nil { + c.packetReadWaiter.InitializeReadWaiter(newBuffer) + defer c.packetReadWaiter.InitializeReadWaiter(nil) + dest, err = c.packetReadWaiter.WaitReadPacket() + } else { + dest, err = c.SingPacketConn.ReadPacket(newBuffer()) + } + if dest.IsFqdn() { + addr = dest + } else { + addr = dest.UDPAddr() + } + if err != nil { + if buff != 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 new file mode 100644 index 00000000..cb4c518b --- /dev/null +++ b/common/net/packet/packet_windows.go @@ -0,0 +1,15 @@ +//go:build windows + +package packet + +import ( + "net" +) + +type enhanceUDPConn struct { + *net.UDPConn +} + +func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { + return waitReadFrom(c.UDPConn) +} diff --git a/common/net/packet/ref.go b/common/net/packet/ref.go new file mode 100644 index 00000000..a562b2e2 --- /dev/null +++ b/common/net/packet/ref.go @@ -0,0 +1,75 @@ +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 new file mode 100644 index 00000000..2ca955fa --- /dev/null +++ b/common/net/packet/ref_sing.go @@ -0,0 +1,26 @@ +package packet + +import ( + "runtime" + + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/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 new file mode 100644 index 00000000..14d64233 --- /dev/null +++ b/common/net/packet/thread.go @@ -0,0 +1,36 @@ +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 new file mode 100644 index 00000000..0869a512 --- /dev/null +++ b/common/net/packet/thread_sing.go @@ -0,0 +1,24 @@ +package packet + +import ( + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/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 index 324e6474..6d0dde98 100644 --- a/common/net/refconn.go +++ b/common/net/refconn.go @@ -4,10 +4,12 @@ import ( "net" "runtime" "time" + + "github.com/metacubex/mihomo/common/buf" ) type refConn struct { - conn net.Conn + conn ExtendedConn ref any } @@ -55,50 +57,26 @@ 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) net.Conn { - return &refConn{conn: conn, ref: ref} -} - -type refPacketConn struct { - pc net.PacketConn - ref any -} - -func (pc *refPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - defer runtime.KeepAlive(pc.ref) - return pc.pc.ReadFrom(p) -} - -func (pc *refPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - defer runtime.KeepAlive(pc.ref) - return pc.pc.WriteTo(p, addr) -} - -func (pc *refPacketConn) Close() error { - defer runtime.KeepAlive(pc.ref) - return pc.pc.Close() -} - -func (pc *refPacketConn) LocalAddr() net.Addr { - defer runtime.KeepAlive(pc.ref) - return pc.pc.LocalAddr() -} - -func (pc *refPacketConn) SetDeadline(t time.Time) error { - defer runtime.KeepAlive(pc.ref) - return pc.pc.SetDeadline(t) -} - -func (pc *refPacketConn) SetReadDeadline(t time.Time) error { - defer runtime.KeepAlive(pc.ref) - return pc.pc.SetReadDeadline(t) -} - -func (pc *refPacketConn) SetWriteDeadline(t time.Time) error { - defer runtime.KeepAlive(pc.ref) - return pc.pc.SetWriteDeadline(t) -} - -func NewRefPacketConn(pc net.PacketConn, ref any) net.PacketConn { - return &refPacketConn{pc: pc, ref: ref} + return &refConn{conn: NewExtendedConn(conn), ref: ref} } diff --git a/common/net/relay.go b/common/net/relay.go index 6191e76b..f2a1b146 100644 --- a/common/net/relay.go +++ b/common/net/relay.go @@ -12,7 +12,7 @@ package net // // go func() { // // Wrapping to avoid using *net.TCPConn.(ReadFrom) -// // See also https://github.com/Dreamacro/clash/pull/1209 +// // See also https://github.com/metacubex/mihomo/pull/1209 // _, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn}) // leftConn.SetReadDeadline(time.Now()) // ch <- err diff --git a/common/net/sing.go b/common/net/sing.go index 342f2e95..c92008ba 100644 --- a/common/net/sing.go +++ b/common/net/sing.go @@ -3,8 +3,11 @@ package net import ( "context" "net" + "runtime" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/bufio/deadline" "github.com/sagernet/sing/common/network" ) @@ -16,7 +19,22 @@ type ExtendedConn = network.ExtendedConn type ExtendedWriter = network.ExtendedWriter type ExtendedReader = network.ExtendedReader +func NewDeadlineConn(conn net.Conn) ExtendedConn { + return deadline.NewFallbackConn(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 + // 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 index a84e7e4c..0499e54c 100644 --- a/common/net/tcpip.go +++ b/common/net/tcpip.go @@ -4,8 +4,11 @@ import ( "fmt" "net" "strings" + "time" ) +var KeepAliveInterval = 15 * time.Second + func SplitNetworkType(s string) (string, string, error) { var ( shecme string @@ -44,3 +47,10 @@ func SplitHostPort(s string) (host, port string, hasPort bool, err error) { host, port, err = net.SplitHostPort(temp) return } + +func TCPKeepAlive(c net.Conn) { + if tcp, ok := c.(*net.TCPConn); ok { + _ = tcp.SetKeepAlive(true) + _ = tcp.SetKeepAlivePeriod(KeepAliveInterval) + } +} diff --git a/common/net/tls.go b/common/net/tls.go index 5e1c81f2..b2865503 100644 --- a/common/net/tls.go +++ b/common/net/tls.go @@ -10,8 +10,12 @@ import ( "math/big" ) -func ParseCert(certificate, privateKey string) (tls.Certificate, error) { - if certificate == "" || privateKey == "" { +type Path interface { + Resolve(path string) string +} + +func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) { + if certificate == "" && privateKey == "" { return newRandomTLSKeyPair() } cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) @@ -19,6 +23,8 @@ func ParseCert(certificate, privateKey string) (tls.Certificate, error) { return cert, nil } + certificate = path.Resolve(certificate) + privateKey = path.Resolve(privateKey) 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()) diff --git a/common/observable/observable_test.go b/common/observable/observable_test.go index 5459e0e2..d263cb94 100644 --- a/common/observable/observable_test.go +++ b/common/observable/observable_test.go @@ -5,8 +5,9 @@ import ( "testing" "time" + "github.com/metacubex/mihomo/common/atomic" + "github.com/stretchr/testify/assert" - "go.uber.org/atomic" ) func iterator[T any](item []T) chan T { @@ -44,7 +45,7 @@ func TestObservable_MultiSubscribe(t *testing.T) { wg.Add(2) waitCh := func(ch <-chan int) { for range ch { - count.Inc() + count.Add(1) } wg.Done() } diff --git a/common/picker/picker.go b/common/picker/picker.go index 97004460..3a7688ca 100644 --- a/common/picker/picker.go +++ b/common/picker/picker.go @@ -47,6 +47,7 @@ func (p *Picker[T]) Wait() T { p.wg.Wait() if p.cancel != nil { p.cancel() + p.cancel = nil } return p.result } @@ -69,6 +70,7 @@ func (p *Picker[T]) Go(f func() (T, error)) { p.result = ret if p.cancel != nil { p.cancel() + p.cancel = nil } }) } else { @@ -78,3 +80,13 @@ func (p *Picker[T]) Go(f func() (T, error)) { } }() } + +// 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 index 17b823cb..4c1c9ebe 100644 --- a/common/picker/picker_test.go +++ b/common/picker/picker_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/samber/lo" "github.com/stretchr/testify/assert" ) @@ -15,7 +16,7 @@ func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, err case <-timer.C: return input, nil case <-ctx.Done(): - return getZero[T](), ctx.Err() + return lo.Empty[T](), ctx.Err() } } } @@ -35,11 +36,6 @@ func TestPicker_Timeout(t *testing.T) { picker.Go(sleepAndSend(ctx, 20, 1)) number := picker.Wait() - assert.Equal(t, number, getZero[int]()) + assert.Equal(t, number, lo.Empty[int]()) assert.NotNil(t, picker.Error()) } - -func getZero[T any]() T { - var result T - return result -} diff --git a/common/pool/alloc.go b/common/pool/alloc.go index 25f79897..5722b047 100644 --- a/common/pool/alloc.go +++ b/common/pool/alloc.go @@ -32,23 +32,32 @@ func NewAllocator() *Allocator { // Get a []byte from pool with most appropriate cap func (alloc *Allocator) Get(size int) []byte { - if size <= 0 || size > 65536 { + switch { + case size < 0: + panic("alloc.Get: len out of range") + case size == 0: return nil - } + case size > 65536: + return make([]byte, size) + default: + bits := msb(size) + if size == 1< 65536 { + return nil + } + bits := msb(cap(buf)) - if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1< end { - return &Range[T]{ + return Range[T]{ start: end, end: start, } } - return &Range[T]{ + return Range[T]{ start: start, end: end, } } -func (r *Range[T]) Contains(t T) bool { +func (r Range[T]) Contains(t T) bool { return t >= r.start && t <= r.end } -func (r *Range[T]) LeftContains(t T) bool { +func (r Range[T]) LeftContains(t T) bool { return t >= r.start && t < r.end } -func (r *Range[T]) RightContains(t T) bool { +func (r Range[T]) RightContains(t T) bool { return t > r.start && t <= r.end } -func (r *Range[T]) Start() T { +func (r Range[T]) Start() T { return r.start } -func (r *Range[T]) End() T { +func (r Range[T]) End() T { return r.end } diff --git a/common/utils/ranges.go b/common/utils/ranges.go new file mode 100644 index 00000000..705bbdee --- /dev/null +++ b/common/utils/ranges.go @@ -0,0 +1,77 @@ +package utils + +import ( + "errors" + "fmt" + "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) (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 + } + + 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) +} + +func NewIntRangesFromList[T constraints.Integer](list []string) (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 := strconv.ParseInt(strings.Trim(status[0], "[ ]"), 10, 64) + if err != nil { + return nil, errIntRanges + } + + switch statusLen { + case 1: + ranges = append(ranges, NewRange(T(start), T(start))) + case 2: + end, err := strconv.ParseUint(strings.Trim(status[1], "[ ]"), 10, 64) + if err != nil { + return nil, errIntRanges + } + + ranges = append(ranges, NewRange(T(start), T(end))) + } + } + + return ranges, nil +} + +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 +} diff --git a/common/utils/slice.go b/common/utils/slice.go new file mode 100644 index 00000000..1b0fa494 --- /dev/null +++ b/common/utils/slice.go @@ -0,0 +1,34 @@ +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 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 new file mode 100644 index 00000000..e427d299 --- /dev/null +++ b/common/utils/string_unsafe.go @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..5d5ae596 --- /dev/null +++ b/common/utils/strings.go @@ -0,0 +1,9 @@ +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 index 66e176ed..f559b471 100644 --- a/common/utils/uuid.go +++ b/common/utils/uuid.go @@ -1,16 +1,51 @@ package utils import ( - "github.com/gofrs/uuid" + "github.com/gofrs/uuid/v5" + "github.com/zhangyunhao116/fastrand" ) -var uuidNamespace, _ = uuid.FromString("00000000-0000-0000-0000-000000000000") +type fastRandReader struct{} + +func (r fastRandReader) Read(p []byte) (int, error) { + return fastrand.Read(p) +} + +var UnsafeUUIDGenerator = uuid.NewGenWithOptions(uuid.WithRandomReader(fastRandReader{})) + +func NewUUIDV1() uuid.UUID { + u, _ := UnsafeUUIDGenerator.NewV1() // fastrand.Read 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() // fastrand.Read 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() // fastrand.Read wouldn't cause error, so ignore err is safe + return u +} + +func NewUUIDV7() uuid.UUID { + u, _ := UnsafeUUIDGenerator.NewV7() // fastrand.Read 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 uuid.NewV5(uuidNamespace, str), nil + return NewUUIDV5(uuid.Nil, str), nil } return u, nil } diff --git a/common/utils/uuid_test.go b/common/utils/uuid_test.go index ba00ea08..3e0507d8 100644 --- a/common/utils/uuid_test.go +++ b/common/utils/uuid_test.go @@ -1,7 +1,7 @@ package utils import ( - "github.com/gofrs/uuid" + "github.com/gofrs/uuid/v5" "reflect" "testing" ) diff --git a/component/auth/auth.go b/component/auth/auth.go index 9d30b927..b52fa135 100644 --- a/component/auth/auth.go +++ b/component/auth/auth.go @@ -1,9 +1,5 @@ package auth -import ( - "sync" -) - type Authenticator interface { Verify(user string, pass string) bool Users() []string @@ -15,12 +11,12 @@ type AuthUser struct { } type inMemoryAuthenticator struct { - storage *sync.Map + storage map[string]string usernames []string } func (au *inMemoryAuthenticator) Verify(user string, pass string) bool { - realPass, ok := au.storage.Load(user) + realPass, ok := au.storage[user] return ok && realPass == pass } @@ -30,17 +26,13 @@ func NewAuthenticator(users []AuthUser) Authenticator { if len(users) == 0 { return nil } - - au := &inMemoryAuthenticator{storage: &sync.Map{}} - for _, user := range users { - au.storage.Store(user.User, user.Pass) + 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) } - usernames := make([]string, 0, len(users)) - au.storage.Range(func(key, value any) bool { - usernames = append(usernames, key.(string)) - return true - }) - au.usernames = usernames - return au } diff --git a/component/ca/config.go b/component/ca/config.go new file mode 100644 index 00000000..03fb007c --- /dev/null +++ b/component/ca/config.go @@ -0,0 +1,143 @@ +package ca + +import ( + "bytes" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "os" + "strings" + "sync" +) + +var trustCerts []*x509.Certificate +var globalCertPool *x509.CertPool +var mutex sync.RWMutex +var errNotMatch = errors.New("certificate fingerprints do not match") + +func AddCertificate(certificate string) error { + mutex.Lock() + defer mutex.Unlock() + if certificate == "" { + return fmt.Errorf("certificate is empty") + } + if cert, err := x509.ParseCertificate([]byte(certificate)); err == nil { + trustCerts = append(trustCerts, cert) + return nil + } else { + return fmt.Errorf("add certificate failed") + } +} + +func initializeCertPool() { + var err error + globalCertPool, err = x509.SystemCertPool() + if err != nil { + globalCertPool = x509.NewCertPool() + } + for _, cert := range trustCerts { + globalCertPool.AddCert(cert) + } +} + +func ResetCertificate() { + mutex.Lock() + defer mutex.Unlock() + trustCerts = nil + initializeCertPool() +} + +func getCertPool() *x509.CertPool { + if len(trustCerts) == 0 { + return nil + } + if globalCertPool == nil { + mutex.Lock() + defer mutex.Unlock() + if globalCertPool != nil { + return globalCertPool + } + initializeCertPool() + } + return globalCertPool +} + +func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + // ssl pining + for i := range rawCerts { + rawCert := rawCerts[i] + cert, err := x509.ParseCertificate(rawCert) + if err == nil { + hash := sha256.Sum256(cert.Raw) + if bytes.Equal(fingerprint[:], hash[:]) { + return nil + } + } + } + return errNotMatch + } +} + +func convertFingerprint(fingerprint string) (*[32]byte, error) { + fingerprint = strings.TrimSpace(strings.Replace(fingerprint, ":", "", -1)) + fpByte, err := hex.DecodeString(fingerprint) + if err != nil { + return nil, err + } + + if len(fpByte) != 32 { + return nil, fmt.Errorf("fingerprint string length error,need sha256 fingerprint") + } + return (*[32]byte)(fpByte), nil +} + +// GetTLSConfig specified fingerprint, customCA and customCAString +func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, customCAString string) (*tls.Config, error) { + if tlsConfig == nil { + tlsConfig = &tls.Config{} + } + var certificate []byte + var err error + if len(customCA) > 0 { + certificate, err = os.ReadFile(customCA) + 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) + } + tlsConfig.RootCAs = certPool + } else { + tlsConfig.RootCAs = getCertPool() + } + if len(fingerprint) > 0 { + var fingerprintBytes *[32]byte + fingerprintBytes, err = convertFingerprint(fingerprint) + if err != nil { + return nil, err + } + tlsConfig = GetGlobalTLSConfig(tlsConfig) + tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes) + 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/dhcp/conn.go b/component/dhcp/conn.go index 90a9e25b..ff26275b 100644 --- a/component/dhcp/conn.go +++ b/component/dhcp/conn.go @@ -5,7 +5,7 @@ import ( "net" "runtime" - "github.com/Dreamacro/clash/component/dialer" + "github.com/metacubex/mihomo/component/dialer" ) func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, error) { @@ -14,5 +14,15 @@ func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, er listenAddr = "255.255.255.255:68" } - return dialer.ListenPacket(ctx, "udp4", listenAddr, dialer.WithInterface(ifaceName), dialer.WithAddrReuse(true)) + options := []dialer.Option{ + dialer.WithInterface(ifaceName), + dialer.WithAddrReuse(true), + } + + // fallback bind on windows, because syscall bind can not receive broadcast + if runtime.GOOS == "windows" { + options = append(options, dialer.WithFallbackBind(true)) + } + + return dialer.ListenPacket(ctx, "udp4", listenAddr, options...) } diff --git a/component/dhcp/dhcp.go b/component/dhcp/dhcp.go index be2c578a..04ad2eda 100644 --- a/component/dhcp/dhcp.go +++ b/component/dhcp/dhcp.go @@ -6,8 +6,8 @@ import ( "net" "net/netip" - "github.com/Dreamacro/clash/common/nnip" - "github.com/Dreamacro/clash/component/iface" + "github.com/metacubex/mihomo/common/nnip" + "github.com/metacubex/mihomo/component/iface" "github.com/insomniacslk/dhcp/dhcpv4" ) diff --git a/component/dialer/bind.go b/component/dialer/bind.go new file mode 100644 index 00000000..72df8c72 --- /dev/null +++ b/component/dialer/bind.go @@ -0,0 +1,101 @@ +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 + } + + 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() || destination.Is4In6() { + 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) (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 index 8705a708..f83b86f8 100644 --- a/component/dialer/bind_darwin.go +++ b/component/dialer/bind_darwin.go @@ -6,7 +6,7 @@ import ( "net/netip" "syscall" - "github.com/Dreamacro/clash/component/iface" + "github.com/metacubex/mihomo/component/iface" "golang.org/x/sys/unix" ) diff --git a/component/dialer/bind_others.go b/component/dialer/bind_others.go index 5cb2fd62..44181610 100644 --- a/component/dialer/bind_others.go +++ b/component/dialer/bind_others.go @@ -5,99 +5,16 @@ package dialer import ( "net" "net/netip" - "strconv" - "strings" - - "github.com/Dreamacro/clash/component/iface" ) -func lookupLocalAddr(ifaceName string, network string, destination netip.Addr, port int) (net.Addr, error) { - ifaceObj, err := iface.ResolveInterface(ifaceName) - if err != nil { - return nil, err - } - - var addr *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 bindIfaceToDialer(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 := lookupLocalAddr(ifaceName, network, destination, int(local)) - if err != nil { - return err - } - - dialer.LocalAddr = addr - - return nil + return fallbackBindIfaceToDialer(ifaceName, dialer, network, destination) } -func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) { - _, port, err := net.SplitHostPort(address) - if err != nil { - port = "0" - } - - local, _ := strconv.ParseUint(port, 10, 16) - - addr, err := lookupLocalAddr(ifaceName, network, netip.Addr{}, int(local)) - if err != nil { - return "", err - } - - return addr.String(), nil +func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, network, address string) (string, error) { + return fallbackBindIfaceToListenConfig(ifaceName, lc, network, address) } func ParseNetwork(network string, addr netip.Addr) string { - // fix bindIfaceToListenConfig() force bind to an ipv4 address - if !strings.HasSuffix(network, "4") && - !strings.HasSuffix(network, "6") && - addr.Unmap().Is6() { - network += "6" - } - return network + return fallbackParseNetwork(network, addr) } diff --git a/component/dialer/bind_windows.go b/component/dialer/bind_windows.go index b680e90f..120f1657 100644 --- a/component/dialer/bind_windows.go +++ b/component/dialer/bind_windows.go @@ -3,12 +3,13 @@ package dialer import ( "context" "encoding/binary" + "fmt" "net" "net/netip" "syscall" "unsafe" - "github.com/Dreamacro/clash/component/iface" + "github.com/metacubex/mihomo/component/iface" ) const ( @@ -20,11 +21,19 @@ func bind4(handle syscall.Handle, ifaceIdx int) error { var bytes [4]byte binary.BigEndian.PutUint32(bytes[:], uint32(ifaceIdx)) idx := *(*uint32)(unsafe.Pointer(&bytes[0])) - return syscall.SetsockoptInt(handle, syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx)) + 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 { - return syscall.SetsockoptInt(handle, syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, ifaceIdx) + 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) controlFn { @@ -37,14 +46,25 @@ func bindControl(ifaceIdx int) controlFn { var innerErr error err = c.Control(func(fd uintptr) { handle := syscall.Handle(fd) + bind6err := bind6(handle, ifaceIdx) + bind4err := bind4(handle, ifaceIdx) switch network { - case "tcp6", "udp6": - innerErr = bind6(handle, ifaceIdx) - _ = bind4(handle, ifaceIdx) - default: - innerErr = bind4(handle, ifaceIdx) - // try bind ipv6, if failed, ignore. it's a workaround for windows disable interface ipv6 - _ = bind6(handle, ifaceIdx) + 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 { + // 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 + } } }) diff --git a/component/dialer/control.go b/component/dialer/control.go index 26b1db76..18ed84e4 100644 --- a/component/dialer/control.go +++ b/component/dialer/control.go @@ -20,3 +20,20 @@ func addControlToListenConfig(lc *net.ListenConfig, fn controlFn) { 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/control_go119.go b/component/dialer/control_go119.go deleted file mode 100644 index ec980586..00000000 --- a/component/dialer/control_go119.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build !go1.20 - -package dialer - -import ( - "context" - "net" - "syscall" -) - -func addControlToDialer(d *net.Dialer, fn controlFn) { - ld := *d - d.Control = func(network, address string, c syscall.RawConn) (err error) { - switch { - case ld.Control != nil: - if err = ld.Control(network, address, c); err != nil { - return - } - } - return fn(context.Background(), network, address, c) - } -} diff --git a/component/dialer/control_go120.go b/component/dialer/control_go120.go deleted file mode 100644 index 65e33f9c..00000000 --- a/component/dialer/control_go120.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build go1.20 - -package dialer - -import ( - "context" - "net" - "syscall" -) - -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 index e31936e9..0e0e3cef 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -6,22 +6,22 @@ import ( "fmt" "net" "net/netip" + "os" "strings" "sync" + "time" - "github.com/Dreamacro/clash/component/resolver" - - "go.uber.org/atomic" + "github.com/metacubex/mihomo/component/resolver" ) +type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) + var ( - dialMux sync.Mutex - actualSingleDialContext = singleDialContext - actualDualStackDialContext = dualStackDialContext - tcpConcurrent = false - DisableIPv6 = false - ErrorInvalidedNetworkStack = errors.New("invalided network stack") - ErrorDisableIPv6 = errors.New("IPv6 is disabled, dialer cancel") + dialMux sync.Mutex + actualSingleStackDialContext = serialSingleStackDialContext + actualDualStackDialContext = serialDualStackDialContext + tcpConcurrent = false + fallbackTimeout = 300 * time.Millisecond ) func applyOptions(options ...Option) *option { @@ -54,33 +54,31 @@ func DialContext(ctx context.Context, network, address string, options ...Option 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 actualSingleDialContext(ctx, network, address, opt) + return actualSingleStackDialContext(ctx, network, ips, port, opt) case "tcp", "udp": - return actualDualStackDialContext(ctx, network, address, opt) + return actualDualStackDialContext(ctx, network, ips, port, opt) default: return nil, ErrorInvalidedNetworkStack } } func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) { - cfg := &option{ - interfaceName: DefaultInterface.Load(), - routingMark: int(DefaultRoutingMark.Load()), - } - - for _, o := range DefaultOptions { - o(cfg) - } - - for _, o := range options { - o(cfg) - } + cfg := applyOptions(options...) lc := &net.ListenConfig{} if cfg.interfaceName != "" { - addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address) + bind := bindIfaceToListenConfig + if cfg.fallbackBind { + bind = fallbackBindIfaceToListenConfig + } + addr, err := bind(cfg.interfaceName, lc, network, address) if err != nil { return nil, err } @@ -96,341 +94,258 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio return lc.ListenPacket(ctx, network, address) } -func SetDial(concurrent bool) { +func SetTcpConcurrent(concurrent bool) { dialMux.Lock() + defer dialMux.Unlock() tcpConcurrent = concurrent if concurrent { - actualSingleDialContext = concurrentSingleDialContext + actualSingleStackDialContext = concurrentSingleStackDialContext actualDualStackDialContext = concurrentDualStackDialContext } else { - actualSingleDialContext = singleDialContext - actualDualStackDialContext = dualStackDialContext + actualSingleStackDialContext = serialSingleStackDialContext + actualDualStackDialContext = serialDualStackDialContext } - - dialMux.Unlock() } -func GetDial() bool { +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) { - dialer := &net.Dialer{} + 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) if opt.interfaceName != "" { - if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil { + bind := bindIfaceToDialer + if opt.fallbackBind { + bind = fallbackBindIfaceToDialer + } + if err := bind(opt.interfaceName, dialer, network, destination); err != nil { return nil, err } } if opt.routingMark != 0 { bindMarkToDialer(opt.routingMark, dialer, network, destination) } - - if DisableIPv6 && destination.Is6() { - return nil, ErrorDisableIPv6 + if opt.mpTcp { + setMultiPathTCP(dialer) } - - return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port)) + if opt.tfo { + return dialTFO(ctx, *dialer, network, address) + } + return dialer.DialContext(ctx, network, address) } -func dualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, err +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) - type dialResult struct { - net.Conn - error - resolved bool - ipv6 bool - done bool - } - results := make(chan dialResult) - var primary, fallback dialResult + var wg sync.WaitGroup - startRacer := func(ctx context.Context, network, host string, r resolver.Resolver, ipv6 bool) { - result := dialResult{ipv6: ipv6, done: true} + 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 { + if result.Conn != nil && result.error == nil { _ = result.Conn.Close() } } }() - - var ip netip.Addr - if ipv6 { - if r == nil { - ip, result.error = resolver.ResolveIPv6ProxyServerHost(ctx, host) - } else { - ip, result.error = resolver.ResolveIPv6WithResolver(ctx, host, r) - } - } else { - if r == nil { - ip, result.error = resolver.ResolveIPv4ProxyServerHost(ctx, host) - } else { - ip, result.error = resolver.ResolveIPv4WithResolver(ctx, host, r) - } - } - if result.error != nil { - return - } - result.resolved = true - - result.Conn, result.error = dialContext(ctx, network, ip, port, opt) + result.Conn, result.error = dialFn(ctx, network, ips, port, opt) } - go startRacer(ctx, network+"4", host, opt.resolver, false) - go startRacer(ctx, network+"6", host, opt.resolver, true) + if len(ipv4s) != 0 { + wg.Add(1) + go racer(ipv4s, preferIPVersion != 6) + } - count := 2 - for i := 0; i < count; i++ { + 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 res := <-results: + 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 { - return res.Conn, nil - } - - if !res.ipv6 { - primary = res - } else { + if res.isPrimary { + return res.Conn, nil + } fallback = res - } - - if primary.done && fallback.done { - if primary.resolved { - return nil, primary.error - } else if fallback.resolved { - return nil, fallback.error + } else { + if res.isPrimary { + errs = append([]error{fmt.Errorf("connect failed: %w", res.error)}, errs...) } else { - return nil, primary.error + errs = append(errs, fmt.Errorf("connect failed: %w", res.error)) } } - case <-ctx.Done(): - err = ctx.Err() - break } } - if err == nil { - err = fmt.Errorf("dual stack dial failed") - } else { - err = fmt.Errorf("dual stack dial failed:%w", err) + if fallback.error == nil && fallback.Conn != nil { + return fallback.Conn, nil } - return nil, err + return nil, errors.Join(errs...) } -func concurrentDualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, err +func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + if len(ips) == 0 { + return nil, ErrorNoIpAddress } - - var ips []netip.Addr - if opt.resolver != nil { - ips, err = resolver.LookupIPWithResolver(ctx, host, opt.resolver) - } else { - ips, err = resolver.LookupIPProxyServerHost(ctx, host) - } - - if err != nil { - return nil, err - } - - return concurrentDialContext(ctx, network, ips, port, opt) -} - -func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + results := make(chan dialResult) returned := make(chan struct{}) defer close(returned) - - type dialResult struct { - ip netip.Addr - net.Conn - error - isPrimary bool - done bool - } - - preferCount := atomic.NewInt32(0) - results := make(chan dialResult) - tcpRacer := func(ctx context.Context, ip netip.Addr) { - result := dialResult{ip: ip, done: true} - + 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 { + if result.Conn != nil && result.error == nil { _ = result.Conn.Close() } } }() - if strings.Contains(network, "tcp") { - network = "tcp" - } else { - network = "udp" - } - - if ip.Is6() { - network += "6" - if opt.prefer != 4 { - result.isPrimary = true - } - } - - if ip.Is4() { - network += "4" - if opt.prefer != 6 { - result.isPrimary = true - } - } - - if result.isPrimary { - preferCount.Add(1) - } - result.Conn, result.error = dialContext(ctx, network, ip, port, opt) } for _, ip := range ips { - go tcpRacer(ctx, ip) + go racer(ctx, ip) } - - connCount := len(ips) - var fallback dialResult - var primaryError error - var finalError error - for i := 0; i < connCount; i++ { - select { - case res := <-results: - if res.error == nil { - if res.isPrimary { - return res.Conn, nil - } else { - if !fallback.done || fallback.error != nil { - fallback = res - } - } - } else { - if res.isPrimary { - primaryError = res.error - preferCount.Add(-1) - if preferCount.Load() == 0 && fallback.done && fallback.error == nil { - return fallback.Conn, nil - } - } - } - case <-ctx.Done(): - if fallback.done && fallback.error == nil { - return fallback.Conn, nil - } - finalError = ctx.Err() - break + 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 fallback.done && fallback.error == nil { - return fallback.Conn, nil + if len(errs) > 0 { + return nil, errors.Join(errs...) } - - if primaryError != nil { - return nil, primaryError - } - - if fallback.error != nil { - return nil, fallback.error - } - - if finalError == nil { - finalError = fmt.Errorf("all ips %v tcp shake hands failed", ips) - } else { - finalError = fmt.Errorf("concurrent dial failed:%w", finalError) - } - - return nil, finalError + return nil, os.ErrDeadlineExceeded } -func singleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) { +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, err - } - - var ip netip.Addr - switch network { - case "tcp4", "udp4": - if opt.resolver == nil { - ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host) - } else { - ip, err = resolver.ResolveIPv4WithResolver(ctx, host, opt.resolver) - } - default: - if opt.resolver == nil { - ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host) - } else { - ip, err = resolver.ResolveIPv6WithResolver(ctx, host, opt.resolver) - } - } - if err != nil { - return nil, err - } - - return dialContext(ctx, network, ip, port, opt) -} - -func concurrentSingleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) { - switch network { - case "tcp4", "udp4": - return concurrentIPv4DialContext(ctx, network, address, opt) - default: - return concurrentIPv6DialContext(ctx, network, address, opt) - } -} - -func concurrentIPv4DialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, err + return nil, "-1", err } var ips []netip.Addr - if opt.resolver == nil { - ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host) - } else { - ips, err = resolver.LookupIPv4WithResolver(ctx, host, opt.resolver) + switch network { + case "tcp4", "udp4": + if preferResolver == nil { + ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host) + } else { + ips, err = resolver.LookupIPv4WithResolver(ctx, host, preferResolver) + } + case "tcp6", "udp6": + if preferResolver == nil { + ips, err = resolver.LookupIPv6ProxyServerHost(ctx, host) + } else { + ips, err = resolver.LookupIPv6WithResolver(ctx, host, preferResolver) + } + default: + if preferResolver == nil { + ips, err = resolver.LookupIPProxyServerHost(ctx, host) + } else { + ips, err = resolver.LookupIPWithResolver(ctx, host, preferResolver) + } } - if err != nil { - return nil, err + return nil, "-1", fmt.Errorf("dns resolve failed: %w", err) } - - return concurrentDialContext(ctx, network, ips, port, opt) -} - -func concurrentIPv6DialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, err + for i, ip := range ips { + if ip.Is4In6() { + ips[i] = ip.Unmap() + } } - - var ips []netip.Addr - if opt.resolver == nil { - ips, err = resolver.LookupIPv6ProxyServerHost(ctx, host) - } else { - ips, err = resolver.LookupIPv6WithResolver(ctx, host, opt.resolver) - } - - if err != nil { - return nil, err - } - - return concurrentDialContext(ctx, network, ips, port, opt) + return ips, port, nil } type Dialer struct { @@ -442,7 +357,12 @@ func (d Dialer) DialContext(ctx context.Context, network, address string) (net.C } func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { - return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, WithOption(d.Opt)) + opt := WithOption(d.Opt) + if rAddrPort.Addr().Unmap().IsLoopback() { + // avoid "The requested address is not valid in its context." + opt = WithInterface("") + } + return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, opt) } func NewDialer(options ...Option) Dialer { diff --git a/component/dialer/error.go b/component/dialer/error.go new file mode 100644 index 00000000..035baa03 --- /dev/null +++ b/component/dialer/error.go @@ -0,0 +1,10 @@ +package dialer + +import ( + "errors" +) + +var ( + ErrorNoIpAddress = errors.New("no ip address") + ErrorInvalidedNetworkStack = errors.New("invalided network stack") +) diff --git a/component/dialer/mark_nonlinux.go b/component/dialer/mark_nonlinux.go index ea448276..64e58784 100644 --- a/component/dialer/mark_nonlinux.go +++ b/component/dialer/mark_nonlinux.go @@ -7,7 +7,7 @@ import ( "net/netip" "sync" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/log" ) var printMarkWarnOnce sync.Once diff --git a/component/dialer/mptcp_go120.go b/component/dialer/mptcp_go120.go new file mode 100644 index 00000000..6e564673 --- /dev/null +++ b/component/dialer/mptcp_go120.go @@ -0,0 +1,12 @@ +//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 new file mode 100644 index 00000000..360826c8 --- /dev/null +++ b/component/dialer/mptcp_go121.go @@ -0,0 +1,11 @@ +//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 index 27adc845..c0c21891 100644 --- a/component/dialer/options.go +++ b/component/dialer/options.go @@ -1,24 +1,34 @@ package dialer import ( - "github.com/Dreamacro/clash/component/resolver" + "context" + "net" - "go.uber.org/atomic" + "github.com/metacubex/mihomo/common/atomic" + "github.com/metacubex/mihomo/component/resolver" ) var ( DefaultOptions []Option - DefaultInterface = atomic.NewString("") + DefaultInterface = atomic.NewTypedValue[string]("") DefaultRoutingMark = atomic.NewInt32(0) ) +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) @@ -29,6 +39,12 @@ func WithInterface(name string) Option { } } +func WithFallbackBind(fallback bool) Option { + return func(opt *option) { + opt.fallbackBind = fallback + } +} + func WithAddrReuse(reuse bool) Option { return func(opt *option) { opt.addrReuse = reuse @@ -69,6 +85,24 @@ func WithOnlySingleStack(isIPv4 bool) Option { } } +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 diff --git a/component/dialer/tfo.go b/component/dialer/tfo.go new file mode 100644 index 00000000..4863d6ae --- /dev/null +++ b/component/dialer/tfo.go @@ -0,0 +1,136 @@ +package dialer + +import ( + "context" + "github.com/sagernet/tfo-go" + "io" + "net" + "time" +) + +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 nil + } + return c.Conn.LocalAddr() +} + +func (c *tfoConn) RemoteAddr() net.Addr { + if c.Conn == nil { + return nil + } + 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.WithCancel(ctx) + 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/ebpf/bpf/redir.c b/component/ebpf/bpf/redir.c index a24afec8..6ef5ee0c 100644 --- a/component/ebpf/bpf/redir.c +++ b/component/ebpf/bpf/redir.c @@ -173,7 +173,7 @@ static __always_inline bool is_lan_ip(__be32 addr) { return false; } -SEC("tc_clash_auto_redir_ingress") +SEC("tc_mihomo_auto_redir_ingress") int tc_redir_ingress_func(struct __sk_buff *skb) { void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end; @@ -264,7 +264,7 @@ int tc_redir_ingress_func(struct __sk_buff *skb) { return TC_ACT_OK; } -SEC("tc_clash_auto_redir_egress") +SEC("tc_mihomo_auto_redir_egress") int tc_redir_egress_func(struct __sk_buff *skb) { void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end; @@ -276,10 +276,10 @@ int tc_redir_egress_func(struct __sk_buff *skb) { if (eth->h_proto != bpf_htons(ETH_P_IP)) return TC_ACT_OK; - __u32 key = 0, *redir_ip, *redir_port; // *clash_mark + __u32 key = 0, *redir_ip, *redir_port; // *mihomo_mark -// clash_mark = bpf_map_lookup_elem(&redir_params_map, &key); -// if (clash_mark && *clash_mark != 0 && *clash_mark == skb->mark) +// mihomo_mark = bpf_map_lookup_elem(&redir_params_map, &key); +// if (mihomo_mark && *mihomo_mark != 0 && *mihomo_mark == skb->mark) // return TC_ACT_OK; struct iphdr *iph = (struct iphdr *)(eth + 1); diff --git a/component/ebpf/bpf/tc.c b/component/ebpf/bpf/tc.c index 4eebf41c..3513bf04 100644 --- a/component/ebpf/bpf/tc.c +++ b/component/ebpf/bpf/tc.c @@ -38,7 +38,7 @@ static __always_inline bool is_lan_ip(__be32 addr) { return false; } -SEC("tc_clash_redirect_to_tun") +SEC("tc_mihomo_redirect_to_tun") int tc_tun_func(struct __sk_buff *skb) { void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end; @@ -50,13 +50,13 @@ int tc_tun_func(struct __sk_buff *skb) { if (eth->h_proto == bpf_htons(ETH_P_ARP)) return TC_ACT_OK; - __u32 key = 0, *clash_mark, *tun_ifindex; + __u32 key = 0, *mihomo_mark, *tun_ifindex; - clash_mark = bpf_map_lookup_elem(&tc_params_map, &key); - if (!clash_mark) + mihomo_mark = bpf_map_lookup_elem(&tc_params_map, &key); + if (!mihomo_mark) return TC_ACT_OK; - if (skb->mark == *clash_mark) + if (skb->mark == *mihomo_mark) return TC_ACT_OK; if (eth->h_proto == bpf_htons(ETH_P_IP)) { diff --git a/component/ebpf/ebpf.go b/component/ebpf/ebpf.go index 6257675c..b0f5a65f 100644 --- a/component/ebpf/ebpf.go +++ b/component/ebpf/ebpf.go @@ -3,8 +3,8 @@ package ebpf import ( "net/netip" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" ) type TcEBpfProgram struct { diff --git a/component/ebpf/ebpf_linux.go b/component/ebpf/ebpf_linux.go index 2ffd4bd5..304f32fe 100644 --- a/component/ebpf/ebpf_linux.go +++ b/component/ebpf/ebpf_linux.go @@ -6,11 +6,11 @@ import ( "fmt" "net/netip" - "github.com/Dreamacro/clash/common/cmd" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/ebpf/redir" - "github.com/Dreamacro/clash/component/ebpf/tc" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/common/cmd" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/ebpf/redir" + "github.com/metacubex/mihomo/component/ebpf/tc" + C "github.com/metacubex/mihomo/constant" "github.com/sagernet/netlink" ) @@ -47,7 +47,7 @@ func NewTcEBpfProgram(ifaceNames []string, tunName string) (*TcEBpfProgram, erro tunIndex := uint32(tunIface.Attrs().Index) - dialer.DefaultRoutingMark.Store(C.ClashTrafficMark) + dialer.DefaultRoutingMark.Store(C.MihomoTrafficMark) ifMark := uint32(dialer.DefaultRoutingMark.Load()) diff --git a/component/ebpf/redir/auto_redirect.go b/component/ebpf/redir/auto_redirect.go index 4fd8b785..57c99616 100644 --- a/component/ebpf/redir/auto_redirect.go +++ b/component/ebpf/redir/auto_redirect.go @@ -16,9 +16,9 @@ import ( "github.com/sagernet/netlink" "golang.org/x/sys/unix" - "github.com/Dreamacro/clash/component/ebpf/byteorder" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/component/ebpf/byteorder" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf ../bpf/redir.c @@ -131,7 +131,7 @@ func (e *EBpfRedirect) Start() error { filter := &netlink.BpfFilter{ FilterAttrs: filterAttrs, Fd: objs.bpfPrograms.TcRedirIngressFunc.FD(), - Name: "clash-redir-ingress-" + e.ifName, + Name: "mihomo-redir-ingress-" + e.ifName, DirectAction: true, } @@ -153,7 +153,7 @@ func (e *EBpfRedirect) Start() error { filterEgress := &netlink.BpfFilter{ FilterAttrs: filterAttrsEgress, Fd: objs.bpfPrograms.TcRedirEgressFunc.FD(), - Name: "clash-redir-egress-" + e.ifName, + Name: "mihomo-redir-egress-" + e.ifName, DirectAction: true, } diff --git a/component/ebpf/tc/redirect_to_tun.go b/component/ebpf/tc/redirect_to_tun.go index 1edc1781..d7be64af 100644 --- a/component/ebpf/tc/redirect_to_tun.go +++ b/component/ebpf/tc/redirect_to_tun.go @@ -14,8 +14,8 @@ import ( "github.com/sagernet/netlink" "golang.org/x/sys/unix" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf ../bpf/tc.c @@ -115,7 +115,7 @@ func (e *EBpfTC) Start() error { filter := &netlink.BpfFilter{ FilterAttrs: filterAttrs, Fd: objs.bpfPrograms.TcTunFunc.FD(), - Name: "clash-tc-" + e.ifName, + Name: "mihomo-tc-" + e.ifName, DirectAction: true, } diff --git a/component/fakeip/cachefile.go b/component/fakeip/cachefile.go index c31d751f..6f0cc48b 100644 --- a/component/fakeip/cachefile.go +++ b/component/fakeip/cachefile.go @@ -3,7 +3,7 @@ package fakeip import ( "net/netip" - "github.com/Dreamacro/clash/component/profile/cachefile" + "github.com/metacubex/mihomo/component/profile/cachefile" ) type cachefileStore struct { diff --git a/component/fakeip/memory.go b/component/fakeip/memory.go index 249c5e2a..f36bbb55 100644 --- a/component/fakeip/memory.go +++ b/component/fakeip/memory.go @@ -3,7 +3,7 @@ package fakeip import ( "net/netip" - "github.com/Dreamacro/clash/common/cache" + "github.com/metacubex/mihomo/common/cache" ) type memoryStore struct { diff --git a/component/fakeip/pool.go b/component/fakeip/pool.go index ee11fedd..2b06fc0b 100644 --- a/component/fakeip/pool.go +++ b/component/fakeip/pool.go @@ -6,9 +6,9 @@ import ( "strings" "sync" - "github.com/Dreamacro/clash/common/nnip" - "github.com/Dreamacro/clash/component/profile/cachefile" - "github.com/Dreamacro/clash/component/trie" + "github.com/metacubex/mihomo/common/nnip" + "github.com/metacubex/mihomo/component/profile/cachefile" + "github.com/metacubex/mihomo/component/trie" ) const ( @@ -36,7 +36,7 @@ type Pool struct { cycle bool mux sync.Mutex host *trie.DomainTrie[struct{}] - ipnet *netip.Prefix + ipnet netip.Prefix store store } @@ -91,7 +91,7 @@ func (p *Pool) Broadcast() netip.Addr { } // IPNet return raw ipnet -func (p *Pool) IPNet() *netip.Prefix { +func (p *Pool) IPNet() netip.Prefix { return p.ipnet } @@ -153,7 +153,7 @@ func (p *Pool) restoreState() { } type Options struct { - IPNet *netip.Prefix + IPNet netip.Prefix Host *trie.DomainTrie[struct{}] // Size sets the maximum number of entries in memory @@ -171,7 +171,7 @@ func New(options Options) (*Pool, error) { hostAddr = options.IPNet.Masked().Addr() gateway = hostAddr.Next() first = gateway.Next().Next().Next() // default start with 198.18.0.4 - last = nnip.UnMasked(*options.IPNet) + last = nnip.UnMasked(options.IPNet) ) if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) { diff --git a/component/fakeip/pool_test.go b/component/fakeip/pool_test.go index ae343f96..a7569ab0 100644 --- a/component/fakeip/pool_test.go +++ b/component/fakeip/pool_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" - "github.com/Dreamacro/clash/component/profile/cachefile" - "github.com/Dreamacro/clash/component/trie" + "github.com/metacubex/mihomo/component/profile/cachefile" + "github.com/metacubex/mihomo/component/trie" "github.com/stretchr/testify/assert" "go.etcd.io/bbolt" @@ -32,7 +32,7 @@ func createCachefileStore(options Options) (*Pool, string, error) { if err != nil { return nil, "", err } - f, err := os.CreateTemp("", "clash") + f, err := os.CreateTemp("", "mihomo") if err != nil { return nil, "", err } @@ -51,7 +51,7 @@ func createCachefileStore(options Options) (*Pool, string, error) { func TestPool_Basic(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.0/28") pools, tempfile, err := createPools(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 10, }) assert.Nil(t, err) @@ -79,7 +79,7 @@ func TestPool_Basic(t *testing.T) { func TestPool_BasicV6(t *testing.T) { ipnet := netip.MustParsePrefix("2001:4860:4860::8888/118") pools, tempfile, err := createPools(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 10, }) assert.Nil(t, err) @@ -107,7 +107,7 @@ func TestPool_BasicV6(t *testing.T) { func TestPool_Case_Insensitive(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/29") pools, tempfile, err := createPools(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 10, }) assert.Nil(t, err) @@ -128,7 +128,7 @@ func TestPool_Case_Insensitive(t *testing.T) { func TestPool_CycleUsed(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.16/28") pools, tempfile, err := createPools(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 10, }) assert.Nil(t, err) @@ -152,7 +152,7 @@ func TestPool_Skip(t *testing.T) { tree := trie.New[struct{}]() tree.Insert("example.com", struct{}{}) pools, tempfile, err := createPools(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 10, Host: tree, }) @@ -168,7 +168,7 @@ func TestPool_Skip(t *testing.T) { func TestPool_MaxCacheSize(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/24") pool, _ := New(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 2, }) @@ -183,7 +183,7 @@ func TestPool_MaxCacheSize(t *testing.T) { func TestPool_DoubleMapping(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/24") pool, _ := New(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 2, }) @@ -213,7 +213,7 @@ func TestPool_DoubleMapping(t *testing.T) { func TestPool_Clone(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/24") pool, _ := New(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 2, }) @@ -223,7 +223,7 @@ func TestPool_Clone(t *testing.T) { assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5})) newPool, _ := New(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 2, }) newPool.CloneFrom(pool) @@ -236,7 +236,7 @@ func TestPool_Clone(t *testing.T) { func TestPool_Error(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/31") _, err := New(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 10, }) @@ -246,7 +246,7 @@ func TestPool_Error(t *testing.T) { func TestPool_FlushFileCache(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/28") pools, tempfile, err := createPools(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 10, }) assert.Nil(t, err) @@ -278,7 +278,7 @@ func TestPool_FlushFileCache(t *testing.T) { func TestPool_FlushMemoryCache(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/28") pool, _ := New(Options{ - IPNet: &ipnet, + IPNet: ipnet, Size: 10, }) diff --git a/component/geodata/attr.go b/component/geodata/attr.go index e35a25ca..a9742aca 100644 --- a/component/geodata/attr.go +++ b/component/geodata/attr.go @@ -3,7 +3,7 @@ package geodata import ( "strings" - "github.com/Dreamacro/clash/component/geodata/router" + "github.com/metacubex/mihomo/component/geodata/router" ) type AttributeList struct { diff --git a/component/geodata/geodata.go b/component/geodata/geodata.go index ac0f820e..a6ef146a 100644 --- a/component/geodata/geodata.go +++ b/component/geodata/geodata.go @@ -1,13 +1,10 @@ package geodata import ( - "errors" "fmt" - C "github.com/Dreamacro/clash/constant" - "strings" - "github.com/Dreamacro/clash/component/geodata/router" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/component/geodata/router" + C "github.com/metacubex/mihomo/constant" ) type loader struct { @@ -15,47 +12,7 @@ type loader struct { } func (l *loader) LoadGeoSite(list string) ([]*router.Domain, error) { - return l.LoadGeoSiteWithAttr(C.GeositeName, list) -} - -func (l *loader) LoadGeoSiteWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) { - parts := strings.Split(siteWithAttr, "@") - if len(parts) == 0 { - return nil, errors.New("empty rule") - } - list := strings.TrimSpace(parts[0]) - attrVal := parts[1:] - - if len(list) == 0 { - return nil, fmt.Errorf("empty listname in rule: %s", siteWithAttr) - } - - domains, err := l.LoadSiteByPath(file, list) - if err != nil { - return nil, err - } - - attrs := parseAttrs(attrVal) - if attrs.IsEmpty() { - if strings.Contains(siteWithAttr, "@") { - log.Warnln("empty attribute list: %s", siteWithAttr) - } - return domains, nil - } - - 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", siteWithAttr) - } - - return filteredDomains, nil + return l.LoadSiteByPath(C.GeositeName, list) } func (l *loader) LoadGeoIP(country string) ([]*router.CIDR, error) { diff --git a/component/geodata/geodataproto.go b/component/geodata/geodataproto.go index ffefc484..0f1ce4d2 100644 --- a/component/geodata/geodataproto.go +++ b/component/geodata/geodataproto.go @@ -1,7 +1,7 @@ package geodata import ( - "github.com/Dreamacro/clash/component/geodata/router" + "github.com/metacubex/mihomo/component/geodata/router" ) type LoaderImplementation interface { @@ -14,6 +14,5 @@ type LoaderImplementation interface { type Loader interface { LoaderImplementation LoadGeoSite(list string) ([]*router.Domain, error) - LoadGeoSiteWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) LoadGeoIP(country string) ([]*router.CIDR, error) } diff --git a/component/geodata/init.go b/component/geodata/init.go index f7dd7a9e..0b193c94 100644 --- a/component/geodata/init.go +++ b/component/geodata/init.go @@ -1,13 +1,17 @@ package geodata import ( + "context" "fmt" - "github.com/Dreamacro/clash/component/mmdb" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" "io" "net/http" "os" + "time" + + 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 @@ -38,7 +42,9 @@ func InitGeoSite() error { } func downloadGeoSite(path string) (err error) { - resp, err := http.Get(C.GeoSiteUrl) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, C.GeoSiteUrl, http.MethodGet, http.Header{"User-Agent": {"mihomo"}}, nil) if err != nil { return } @@ -55,7 +61,9 @@ func downloadGeoSite(path string) (err error) { } func downloadGeoIP(path string) (err error) { - resp, err := http.Get(C.GeoIpUrl) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, C.GeoIpUrl, http.MethodGet, http.Header{"User-Agent": {"mihomo"}}, nil) if err != nil { return } diff --git a/component/geodata/memconservative/cache.go b/component/geodata/memconservative/cache.go index 28c2c238..ef76a42c 100644 --- a/component/geodata/memconservative/cache.go +++ b/component/geodata/memconservative/cache.go @@ -5,9 +5,9 @@ import ( "os" "strings" - "github.com/Dreamacro/clash/component/geodata/router" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/component/geodata/router" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" "google.golang.org/protobuf/proto" ) @@ -118,7 +118,7 @@ func (g GeoSiteCache) Unmarshal(filename, code string) (*router.GeoSite, error) case errFailedToReadBytes, errFailedToReadExpectedLenBytes, errInvalidGeodataFile, errInvalidGeodataVarintLength: - log.Warnln("failed to decode geoip file: %s%s", filename, ", fallback to the original ReadFile method") + 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 diff --git a/component/geodata/memconservative/memc.go b/component/geodata/memconservative/memc.go index 88d3b4e5..30d89f10 100644 --- a/component/geodata/memconservative/memc.go +++ b/component/geodata/memconservative/memc.go @@ -5,8 +5,8 @@ import ( "fmt" "runtime" - "github.com/Dreamacro/clash/component/geodata" - "github.com/Dreamacro/clash/component/geodata/router" + "github.com/metacubex/mihomo/component/geodata" + "github.com/metacubex/mihomo/component/geodata/router" ) type memConservativeLoader struct { diff --git a/component/geodata/router/condition.go b/component/geodata/router/condition.go index 4e0ad46c..156614ae 100644 --- a/component/geodata/router/condition.go +++ b/component/geodata/router/condition.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/Dreamacro/clash/component/geodata/strmatcher" + "github.com/metacubex/mihomo/component/geodata/strmatcher" ) var matcherTypeMap = map[Domain_Type]strmatcher.Type{ diff --git a/component/geodata/router/config.pb.go b/component/geodata/router/config.pb.go index 7c3af22a..59d90c7a 100644 --- a/component/geodata/router/config.pb.go +++ b/component/geodata/router/config.pb.go @@ -84,7 +84,7 @@ type Domain struct { unknownFields protoimpl.UnknownFields // Domain matching type. - Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=clash.component.geodata.router.Domain_Type" json:"type,omitempty"` + 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. @@ -585,22 +585,22 @@ func file_component_geodata_router_config_proto_rawDescGZIP() []byte { 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: clash.component.geodata.router.Domain.Type - (*Domain)(nil), // 1: clash.component.geodata.router.Domain - (*CIDR)(nil), // 2: clash.component.geodata.router.CIDR - (*GeoIP)(nil), // 3: clash.component.geodata.router.GeoIP - (*GeoIPList)(nil), // 4: clash.component.geodata.router.GeoIPList - (*GeoSite)(nil), // 5: clash.component.geodata.router.GeoSite - (*GeoSiteList)(nil), // 6: clash.component.geodata.router.GeoSiteList - (*Domain_Attribute)(nil), // 7: clash.component.geodata.router.Domain.Attribute + (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: clash.component.geodata.router.Domain.type:type_name -> clash.component.geodata.router.Domain.Type - 7, // 1: clash.component.geodata.router.Domain.attribute:type_name -> clash.component.geodata.router.Domain.Attribute - 2, // 2: clash.component.geodata.router.GeoIP.cidr:type_name -> clash.component.geodata.router.CIDR - 3, // 3: clash.component.geodata.router.GeoIPList.entry:type_name -> clash.component.geodata.router.GeoIP - 1, // 4: clash.component.geodata.router.GeoSite.domain:type_name -> clash.component.geodata.router.Domain - 5, // 5: clash.component.geodata.router.GeoSiteList.entry:type_name -> clash.component.geodata.router.GeoSite + 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 diff --git a/component/geodata/router/config.proto b/component/geodata/router/config.proto index 245faadf..98795740 100644 --- a/component/geodata/router/config.proto +++ b/component/geodata/router/config.proto @@ -1,9 +1,9 @@ syntax = "proto3"; -package clash.component.geodata.router; -option csharp_namespace = "Clash.Component.Geodata.Router"; -option go_package = "github.com/Dreamacro/clash/component/geodata/router"; -option java_package = "com.clash.component.geodata.router"; +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. diff --git a/component/geodata/standard/standard.go b/component/geodata/standard/standard.go index 355cbf34..bfaae5ec 100644 --- a/component/geodata/standard/standard.go +++ b/component/geodata/standard/standard.go @@ -6,9 +6,9 @@ import ( "os" "strings" - "github.com/Dreamacro/clash/component/geodata" - "github.com/Dreamacro/clash/component/geodata/router" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/component/geodata" + "github.com/metacubex/mihomo/component/geodata/router" + C "github.com/metacubex/mihomo/constant" "google.golang.org/protobuf/proto" ) diff --git a/component/geodata/strmatcher/ac_automaton_matcher.go b/component/geodata/strmatcher/ac_automaton_matcher.go index d134c68a..ca7dc48b 100644 --- a/component/geodata/strmatcher/ac_automaton_matcher.go +++ b/component/geodata/strmatcher/ac_automaton_matcher.go @@ -1,7 +1,7 @@ package strmatcher import ( - "github.com/Dreamacro/clash/common/generics/list" + "github.com/metacubex/mihomo/common/generics/list" ) const validCharCount = 53 diff --git a/component/geodata/strmatcher/mph_matcher.go b/component/geodata/strmatcher/mph_matcher.go index 3c10cb49..8d8b0508 100644 --- a/component/geodata/strmatcher/mph_matcher.go +++ b/component/geodata/strmatcher/mph_matcher.go @@ -234,26 +234,26 @@ tail: case s == 0: case s < 4: h ^= uint64(*(*byte)(p)) - h ^= uint64(*(*byte)(add(p, s>>1))) << 8 - h ^= uint64(*(*byte)(add(p, s-1))) << 16 + h ^= uint64(*(*byte)(unsafe.Add(p, s>>1))) << 8 + h ^= uint64(*(*byte)(unsafe.Add(p, s-1))) << 16 h = rotl31(h*m1) * m2 case s <= 8: h ^= uint64(readUnaligned32(p)) - h ^= uint64(readUnaligned32(add(p, s-4))) << 32 + h ^= uint64(readUnaligned32(unsafe.Add(p, s-4))) << 32 h = rotl31(h*m1) * m2 case s <= 16: h ^= readUnaligned64(p) h = rotl31(h*m1) * m2 - h ^= readUnaligned64(add(p, s-8)) + h ^= readUnaligned64(unsafe.Add(p, s-8)) h = rotl31(h*m1) * m2 case s <= 32: h ^= readUnaligned64(p) h = rotl31(h*m1) * m2 - h ^= readUnaligned64(add(p, 8)) + h ^= readUnaligned64(unsafe.Add(p, 8)) h = rotl31(h*m1) * m2 - h ^= readUnaligned64(add(p, s-16)) + h ^= readUnaligned64(unsafe.Add(p, s-16)) h = rotl31(h*m1) * m2 - h ^= readUnaligned64(add(p, s-8)) + h ^= readUnaligned64(unsafe.Add(p, s-8)) h = rotl31(h*m1) * m2 default: v1 := h @@ -263,16 +263,16 @@ tail: for s >= 32 { v1 ^= readUnaligned64(p) v1 = rotl31(v1*m1) * m2 - p = add(p, 8) + p = unsafe.Add(p, 8) v2 ^= readUnaligned64(p) v2 = rotl31(v2*m2) * m3 - p = add(p, 8) + p = unsafe.Add(p, 8) v3 ^= readUnaligned64(p) v3 = rotl31(v3*m3) * m4 - p = add(p, 8) + p = unsafe.Add(p, 8) v4 ^= readUnaligned64(p) v4 = rotl31(v4*m4) * m1 - p = add(p, 8) + p = unsafe.Add(p, 8) s -= 32 } h = v1 ^ v2 ^ v3 ^ v4 @@ -285,10 +285,6 @@ tail: return uintptr(h) } -func add(p unsafe.Pointer, x uintptr) unsafe.Pointer { - return unsafe.Pointer(uintptr(p) + x) -} - 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 diff --git a/component/geodata/utils.go b/component/geodata/utils.go index 9e7e50b1..4716ccbd 100644 --- a/component/geodata/utils.go +++ b/component/geodata/utils.go @@ -1,9 +1,14 @@ package geodata import ( + "errors" "fmt" - "github.com/Dreamacro/clash/component/geodata/router" - C "github.com/Dreamacro/clash/constant" + "golang.org/x/sync/singleflight" + "strings" + + "github.com/metacubex/mihomo/component/geodata/router" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" ) var geoLoaderName = "memconservative" @@ -34,6 +39,8 @@ func Verify(name string) error { } } +var loadGeoSiteMatcherSF = singleflight.Group{} + func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) { if len(countryCode) == 0 { return nil, 0, fmt.Errorf("country code could not be empty") @@ -44,16 +51,53 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) not = true countryCode = countryCode[1:] } + countryCode = strings.ToLower(countryCode) - geoLoader, err := GetGeoDataLoader(geoLoaderName) - if err != nil { - return nil, 0, err + parts := strings.Split(countryCode, "@") + if len(parts) == 0 { + return nil, 0, errors.New("empty rule") + } + listName := strings.TrimSpace(parts[0]) + attrVal := parts[1:] + + if len(listName) == 0 { + return nil, 0, fmt.Errorf("empty listname in rule: %s", countryCode) } - domains, err := geoLoader.LoadGeoSite(countryCode) + v, err, shared := loadGeoSiteMatcherSF.Do(listName, func() (interface{}, error) { + geoLoader, err := GetGeoDataLoader(geoLoaderName) + if err != nil { + return nil, err + } + return geoLoader.LoadGeoSite(listName) + }) if err != nil { + if !shared { + loadGeoSiteMatcherSF.Forget(listName) // don't store the error result + } return nil, 0, err } + domains := v.([]*router.Domain) + + attrs := parseAttrs(attrVal) + 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 @@ -68,25 +112,34 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) return matcher, len(domains), nil } +var loadGeoIPMatcherSF = singleflight.Group{} + func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) { if len(country) == 0 { return nil, 0, fmt.Errorf("country code could not be empty") } - geoLoader, err := GetGeoDataLoader(geoLoaderName) - if err != nil { - return nil, 0, err - } not := false if country[0] == '!' { not = true country = country[1:] } + country = strings.ToLower(country) - records, err := geoLoader.LoadGeoIP(country) + v, err, shared := loadGeoIPMatcherSF.Do(country, func() (interface{}, error) { + geoLoader, err := GetGeoDataLoader(geoLoaderName) + if err != nil { + return nil, err + } + return geoLoader.LoadGeoIP(country) + }) if err != nil { + if !shared { + loadGeoIPMatcherSF.Forget(country) // don't store the error result + } return nil, 0, err } + records := v.([]*router.CIDR) geoIP := &router.GeoIP{ CountryCode: country, @@ -98,6 +151,10 @@ func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) { if err != nil { return nil, 0, err } - return matcher, len(records), nil } + +func ClearCache() { + loadGeoSiteMatcherSF = singleflight.Group{} + loadGeoIPMatcherSF = singleflight.Group{} +} diff --git a/component/http/http.go b/component/http/http.go index 54a3daa9..455db681 100644 --- a/component/http/http.go +++ b/component/http/http.go @@ -2,21 +2,22 @@ package http import ( "context" - "github.com/Dreamacro/clash/component/tls" - "github.com/Dreamacro/clash/listener/inner" + "crypto/tls" "io" "net" "net/http" URL "net/url" + "runtime" "strings" "time" -) -const ( - UA = "clash.meta" + "github.com/metacubex/mihomo/component/ca" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/listener/inner" ) func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) { + UA := C.UA method = strings.ToUpper(method) urlRes, err := URL.Parse(url) if err != nil { @@ -47,15 +48,20 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st 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) { - conn := inner.HandleTcp(address, urlRes.Hostname()) - return conn, nil + if conn, err := inner.HandleTcp(address); err == nil { + return conn, nil + } else { + d := net.Dialer{} + return d.DialContext(ctx, network, address) + } }, - TLSClientConfig: tls.GetDefaultTLSConfig(), + TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}), } client := http.Client{Transport: transport} diff --git a/component/iface/iface.go b/component/iface/iface.go index 11c754f8..bf186165 100644 --- a/component/iface/iface.go +++ b/component/iface/iface.go @@ -4,15 +4,16 @@ import ( "errors" "net" "net/netip" + "strings" "time" - "github.com/Dreamacro/clash/common/singledo" + "github.com/metacubex/mihomo/common/singledo" ) type Interface struct { Index int Name string - Addrs []*netip.Prefix + Addrs []netip.Prefix HardwareAddr net.HardwareAddr } @@ -37,19 +38,28 @@ func ResolveInterface(name string) (*Interface, error) { if err != nil { continue } + // if not available device like Meta, dummy0, docker0, etc. + if (iface.Flags&net.FlagMulticast == 0) || (iface.Flags&net.FlagPointToPoint != 0) || (iface.Flags&net.FlagRunning == 0) { + continue + } - ipNets := make([]*netip.Prefix, 0, len(addrs)) + ipNets := make([]netip.Prefix, 0, len(addrs)) for _, addr := range addrs { ipNet := addr.(*net.IPNet) ip, _ := netip.AddrFromSlice(ipNet.IP) + //unavailable IPv6 Address + if ip.Is6() && strings.HasPrefix(ip.String(), "fe80") { + continue + } + ones, bits := ipNet.Mask.Size() if bits == 32 { ip = ip.Unmap() } pf := netip.PrefixFrom(ip, ones) - ipNets = append(ipNets, &pf) + ipNets = append(ipNets, pf) } r[iface.Name] = &Interface{ @@ -79,27 +89,27 @@ func FlushCache() { interfaces.Reset() } -func (iface *Interface) PickIPv4Addr(destination netip.Addr) (*netip.Prefix, error) { - return iface.pickIPAddr(destination, func(addr *netip.Prefix) bool { +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 { +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 +func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr netip.Prefix) bool) (netip.Prefix, error) { + var fallback netip.Prefix for _, addr := range iface.Addrs { if !accept(addr) { continue } - if fallback == nil && !addr.Addr().IsLinkLocalUnicast() { + if !fallback.IsValid() && !addr.Addr().IsLinkLocalUnicast() { fallback = addr if !destination.IsValid() { @@ -112,8 +122,8 @@ func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr *net } } - if fallback == nil { - return nil, ErrAddrNotFound + if !fallback.IsValid() { + return netip.Prefix{}, ErrAddrNotFound } return fallback, nil diff --git a/component/mmdb/mmdb.go b/component/mmdb/mmdb.go index 8f28d486..f83b9922 100644 --- a/component/mmdb/mmdb.go +++ b/component/mmdb/mmdb.go @@ -1,53 +1,85 @@ package mmdb import ( - "github.com/oschwald/geoip2-golang" + "context" "io" "net/http" "os" "sync" + "time" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + mihomoHttp "github.com/metacubex/mihomo/component/http" + 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 ( - mmdb *geoip2.Reader - once sync.Once + reader Reader + once sync.Once ) func LoadFromBytes(buffer []byte) { once.Do(func() { - var err error - mmdb, err = geoip2.FromBytes(buffer) + mmdb, err := maxminddb.FromBytes(buffer) if err != nil { log.Fatalln("Can't load mmdb: %s", err.Error()) } + reader = Reader{Reader: mmdb} + switch mmdb.Metadata.DatabaseType { + case "sing-geoip": + reader.databaseType = typeSing + case "Meta-geoip0": + reader.databaseType = typeMetaV0 + default: + reader.databaseType = typeMaxmind + } }) } func Verify() bool { - instance, err := geoip2.Open(C.Path.MMDB()) + instance, err := maxminddb.Open(C.Path.MMDB()) if err == nil { instance.Close() } return err == nil } -func Instance() *geoip2.Reader { +func Instance() Reader { once.Do(func() { - var err error - mmdb, err = geoip2.Open(C.Path.MMDB()) + mmdbPath := C.Path.MMDB() + log.Debugln("Load MMDB file: %s", mmdbPath) + mmdb, err := maxminddb.Open(mmdbPath) if err != nil { - log.Fatalln("Can't load mmdb: %s", err.Error()) + log.Fatalln("Can't load MMDB: %s", err.Error()) + } + reader = Reader{Reader: mmdb} + switch mmdb.Metadata.DatabaseType { + case "sing-geoip": + reader.databaseType = typeSing + case "Meta-geoip0": + reader.databaseType = typeMetaV0 + default: + reader.databaseType = typeMaxmind } }) - return mmdb + return reader } func DownloadMMDB(path string) (err error) { - resp, err := http.Get(C.MmdbUrl) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, C.MmdbUrl, http.MethodGet, http.Header{"User-Agent": {"mihomo"}}, nil) if err != nil { return } diff --git a/component/mmdb/reader.go b/component/mmdb/reader.go new file mode 100644 index 00000000..4db53d4f --- /dev/null +++ b/component/mmdb/reader.go @@ -0,0 +1,56 @@ +package mmdb + +import ( + "fmt" + "net" + + "github.com/oschwald/maxminddb-golang" + "github.com/sagernet/sing/common" +) + +type geoip2Country struct { + Country struct { + IsoCode string `maxminddb:"iso_code"` + } `maxminddb:"country"` +} + +type Reader struct { + *maxminddb.Reader + databaseType +} + +func (r Reader) 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{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 + return common.Map(record, func(it any) string { + return it.(string) + }) + } + return []string{} + + default: + panic(fmt.Sprint("unknown geoip database type:", r.databaseType)) + } +} diff --git a/component/nat/proxy.go b/component/nat/proxy.go new file mode 100644 index 00000000..66af3be2 --- /dev/null +++ b/component/nat/proxy.go @@ -0,0 +1,26 @@ +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 index 5dcd91ed..b2908c94 100644 --- a/component/nat/table.go +++ b/component/nat/table.go @@ -4,43 +4,54 @@ import ( "net" "sync" - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" + + "github.com/puzpuzpuz/xsync/v2" ) type Table struct { - mapping sync.Map + mapping *xsync.MapOf[string, *Entry] + lockMap *xsync.MapOf[string, *sync.Cond] } type Entry struct { PacketConn C.PacketConn - LocalUDPConnMap sync.Map + WriteBackProxy C.WriteBackProxy + LocalUDPConnMap *xsync.MapOf[string, *net.UDPConn] + LocalLockMap *xsync.MapOf[string, *sync.Cond] } -func (t *Table) Set(key string, e C.PacketConn) { +func (t *Table) Set(key string, e C.PacketConn, w C.WriteBackProxy) { t.mapping.Store(key, &Entry{ PacketConn: e, - LocalUDPConnMap: sync.Map{}, + WriteBackProxy: w, + LocalUDPConnMap: xsync.NewMapOf[*net.UDPConn](), + LocalLockMap: xsync.NewMapOf[*sync.Cond](), }) } -func (t *Table) Get(key string) C.PacketConn { +func (t *Table) Get(key string) (C.PacketConn, C.WriteBackProxy) { entry, exist := t.getEntry(key) if !exist { - return nil + return nil, nil } - return entry.PacketConn + return entry.PacketConn, entry.WriteBackProxy } func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) { - item, loaded := t.mapping.LoadOrStore(key, sync.NewCond(&sync.Mutex{})) - return item.(*sync.Cond), loaded + item, loaded := t.lockMap.LoadOrCompute(key, makeLock) + return item, loaded } func (t *Table) Delete(key string) { t.mapping.Delete(key) } -func (t *Table) GetLocalConn(lAddr, rAddr string) *net.UDPConn { +func (t *Table) DeleteLock(lockKey string) { + t.lockMap.Delete(lockKey) +} + +func (t *Table) GetForLocalConn(lAddr, rAddr string) *net.UDPConn { entry, exist := t.getEntry(lAddr) if !exist { return nil @@ -49,10 +60,10 @@ func (t *Table) GetLocalConn(lAddr, rAddr string) *net.UDPConn { if !exist { return nil } - return item.(*net.UDPConn) + return item } -func (t *Table) AddLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool { +func (t *Table) AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool { entry, exist := t.getEntry(lAddr) if !exist { return false @@ -61,7 +72,7 @@ func (t *Table) AddLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool { return true } -func (t *Table) RangeLocalConn(lAddr string, f func(key, value any) bool) { +func (t *Table) RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool) { entry, exist := t.getEntry(lAddr) if !exist { return @@ -74,11 +85,11 @@ func (t *Table) GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool if !loaded { return nil, false } - item, loaded := entry.LocalUDPConnMap.LoadOrStore(key, sync.NewCond(&sync.Mutex{})) - return item.(*sync.Cond), loaded + item, loaded := entry.LocalLockMap.LoadOrCompute(key, makeLock) + return item, loaded } -func (t *Table) DeleteLocalConnMap(lAddr, key string) { +func (t *Table) DeleteForLocalConn(lAddr, key string) { entry, loaded := t.getEntry(lAddr) if !loaded { return @@ -86,17 +97,26 @@ func (t *Table) DeleteLocalConnMap(lAddr, key string) { entry.LocalUDPConnMap.Delete(key) } -func (t *Table) getEntry(key string) (*Entry, bool) { - item, ok := t.mapping.Load(key) - // This should not happen usually since this function called after PacketConn created - if !ok { - return nil, false +func (t *Table) DeleteLockForLocalConn(lAddr, key string) { + entry, loaded := t.getEntry(lAddr) + if !loaded { + return } - entry, ok := item.(*Entry) - return entry, ok + 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{} + return &Table{ + mapping: xsync.NewMapOf[*Entry](), + lockMap: xsync.NewMapOf[*sync.Cond](), + } } diff --git a/component/process/process_freebsd_amd64.go b/component/process/process_freebsd_amd64.go index 709ade3b..1884afcc 100644 --- a/component/process/process_freebsd_amd64.go +++ b/component/process/process_freebsd_amd64.go @@ -10,8 +10,8 @@ import ( "syscall" "unsafe" - "github.com/Dreamacro/clash/common/nnip" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/nnip" + "github.com/metacubex/mihomo/log" ) // store process name for when dealing with multiple PROCESS-NAME rules diff --git a/component/process/process_windows.go b/component/process/process_windows.go index cce08e30..d43c78c6 100644 --- a/component/process/process_windows.go +++ b/component/process/process_windows.go @@ -7,8 +7,8 @@ import ( "syscall" "unsafe" - "github.com/Dreamacro/clash/common/nnip" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/nnip" + "github.com/metacubex/mihomo/log" "golang.org/x/sys/windows" ) @@ -67,7 +67,7 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string err := initWin32API() if err != nil { log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error()) - log.Warnln("All PROCESS-NAMES rules will be skiped") + log.Warnln("All PROCESS-NAMES rules will be skipped") return } }) diff --git a/component/profile/cachefile/cache.go b/component/profile/cachefile/cache.go index 3d2dd1de..68812824 100644 --- a/component/profile/cachefile/cache.go +++ b/component/profile/cachefile/cache.go @@ -5,9 +5,9 @@ import ( "sync" "time" - "github.com/Dreamacro/clash/component/profile" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/component/profile" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" "go.etcd.io/bbolt" ) diff --git a/component/profile/profile.go b/component/profile/profile.go index e3d9e78c..36db8cc3 100644 --- a/component/profile/profile.go +++ b/component/profile/profile.go @@ -1,7 +1,7 @@ package profile import ( - "go.uber.org/atomic" + "github.com/metacubex/mihomo/common/atomic" ) // StoreSelected is a global switch for storing selected proxy to cache diff --git a/component/proxydialer/proxydialer.go b/component/proxydialer/proxydialer.go new file mode 100644 index 00000000..71a658b8 --- /dev/null +++ b/component/proxydialer/proxydialer.go @@ -0,0 +1,93 @@ +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 d, ok := p.dialer.(dialer.Dialer); ok { // first using old function to let mux work + conn, err = p.proxy.DialContext(ctx, currentMeta, dialer.WithOption(d.Opt)) + } 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 d, ok := p.dialer.(dialer.Dialer); ok { // first using old function to let mux work + pc, err = p.proxy.ListenPacketContext(ctx, currentMeta, dialer.WithOption(d.Opt)) + } 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 new file mode 100644 index 00000000..71180c01 --- /dev/null +++ b/component/proxydialer/sing.go @@ -0,0 +1,82 @@ +package proxydialer + +import ( + "context" + "net" + + C "github.com/metacubex/mihomo/constant" + + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/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/resolver/host.go b/component/resolver/host.go new file mode 100644 index 00000000..69c29a3c --- /dev/null +++ b/component/resolver/host.go @@ -0,0 +1,125 @@ +package resolver + +import ( + "errors" + "net/netip" + "strings" + _ "unsafe" + + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/trie" + "github.com/zhangyunhao116/fastrand" +) + +type Hosts struct { + *trie.DomainTrie[HostValue] +} + +func NewHosts(hosts *trie.DomainTrie[HostValue]) Hosts { + return Hosts{ + hosts, + } +} + +// lookupStaticHost looks up the addresses and the canonical name for the given host from /etc/hosts. +// +//go:linkname lookupStaticHost net.lookupStaticHost +func lookupStaticHost(host string) ([]string, string) + +// 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 { + addr, _ := 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[fastrand.Intn(len(hv.IPs))], nil +} diff --git a/component/resolver/host_windows.go b/component/resolver/host_windows.go new file mode 100644 index 00000000..669f9547 --- /dev/null +++ b/component/resolver/host_windows.go @@ -0,0 +1,19 @@ +//go:build !go1.22 + +// a simple standard lib fix from: https://github.com/golang/go/commit/33d4a5105cf2b2d549922e909e9239a48b8cefcc + +package resolver + +import ( + "golang.org/x/sys/windows" + _ "unsafe" +) + +//go:linkname testHookHostsPath net.testHookHostsPath +var testHookHostsPath string + +func init() { + if dir, err := windows.GetSystemDirectory(); err == nil { + testHookHostsPath = dir + "/Drivers/etc/hosts" + } +} diff --git a/component/resolver/resolver.go b/component/resolver/resolver.go index fa1e7c02..8cbc62fa 100644 --- a/component/resolver/resolver.go +++ b/component/resolver/resolver.go @@ -4,15 +4,16 @@ import ( "context" "errors" "fmt" - "math/rand" "net" "net/netip" "strings" "time" - "github.com/Dreamacro/clash/component/trie" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/trie" "github.com/miekg/dns" + "github.com/zhangyunhao116/fastrand" ) var ( @@ -27,7 +28,7 @@ var ( DisableIPv6 = true // DefaultHosts aim to resolve hosts - DefaultHosts = trie.New[netip.Addr]() + DefaultHosts = NewHosts(trie.New[HostValue]()) // DefaultDNSTimeout defined the default dns request timeout DefaultDNSTimeout = time.Second * 5 @@ -43,17 +44,17 @@ 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) - ResolveIP(ctx context.Context, host string) (ip netip.Addr, err error) - ResolveIPv4(ctx context.Context, host string) (ip netip.Addr, err error) - ResolveIPv6(ctx context.Context, host string) (ip netip.Addr, err error) ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error) + Invalid() bool } // LookupIPv4WithResolver same as LookupIPv4, but with a resolver func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { - if node := DefaultHosts.Search(host); node != nil { - if ip := node.Data(); ip.Is4() { - return []netip.Addr{node.Data()}, nil + 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 } } @@ -65,14 +66,10 @@ func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]net return []netip.Addr{}, ErrIPVersion } - if r != nil { + if r != nil && r.Invalid() { return r.LookupIPv4(ctx, host) } - if DefaultResolver != nil { - return DefaultResolver.LookupIPv4(ctx, host) - } - ipAddrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", host) if err != nil { return nil, err @@ -96,7 +93,7 @@ func ResolveIPv4WithResolver(ctx context.Context, host string, r Resolver) (neti } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } - return ips[rand.Intn(len(ips))], nil + return ips[fastrand.Intn(len(ips))], nil } // ResolveIPv4 with a host, return ipv4 @@ -110,9 +107,11 @@ func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]net return nil, ErrIPv6Disabled } - if node := DefaultHosts.Search(host); node != nil { - if ip := node.Data(); ip.Is6() { - return []netip.Addr{ip}, nil + 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 } } @@ -123,12 +122,9 @@ func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]net return nil, ErrIPVersion } - if r != nil { + if r != nil && r.Invalid() { return r.LookupIPv6(ctx, host) } - if DefaultResolver != nil { - return DefaultResolver.LookupIPv6(ctx, host) - } ipAddrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip6", host) if err != nil { @@ -153,7 +149,7 @@ func ResolveIPv6WithResolver(ctx context.Context, host string, r Resolver) (neti } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } - return ips[rand.Intn(len(ips))], nil + return ips[fastrand.Intn(len(ips))], nil } func ResolveIPv6(ctx context.Context, host string) (netip.Addr, error) { @@ -162,17 +158,17 @@ func ResolveIPv6(ctx context.Context, host string) (netip.Addr, error) { // LookupIPWithResolver same as LookupIP, but with a resolver func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { - if node := DefaultHosts.Search(host); node != nil { - return []netip.Addr{node.Data()}, nil + if node, ok := DefaultHosts.Search(host, false); ok { + return node.IPs, nil } - if r != nil { + if r != nil && r.Invalid() { if DisableIPv6 { return r.LookupIPv4(ctx, host) } return r.LookupIP(ctx, host) } else if DisableIPv6 { - return LookupIPv4(ctx, host) + return LookupIPv4WithResolver(ctx, host, r) } if ip, err := netip.ParseAddr(host); err == nil { @@ -202,10 +198,14 @@ func ResolveIPWithResolver(ctx context.Context, host string, r Resolver) (netip. } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } - return ips[rand.Intn(len(ips))], nil + ipv4s, ipv6s := SortationAddr(ips) + if len(ipv4s) > 0 { + return ipv4s[fastrand.Intn(len(ipv4s))], nil + } + return ipv6s[fastrand.Intn(len(ipv6s))], nil } -// ResolveIP with a host, return ip +// 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) } @@ -266,3 +266,14 @@ func LookupIPProxyServerHost(ctx context.Context, host string) ([]netip.Addr, er } return LookupIP(ctx, host) } + +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/resource/fetcher.go b/component/resource/fetcher.go index 4b905c7f..31dc5c08 100644 --- a/component/resource/fetcher.go +++ b/component/resource/fetcher.go @@ -7,8 +7,10 @@ import ( "path/filepath" "time" - types "github.com/Dreamacro/clash/constant/provider" - "github.com/Dreamacro/clash/log" + types "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/log" + + "github.com/samber/lo" ) var ( @@ -65,7 +67,7 @@ func (f *Fetcher[V]) Initial() (V, error) { } if err != nil { - return getZero[V](), err + return lo.Empty[V](), err } var contents V @@ -85,18 +87,18 @@ func (f *Fetcher[V]) Initial() (V, error) { if err != nil { if !isLocal { - return getZero[V](), err + return lo.Empty[V](), err } // parse local file error, fallback to remote buf, err = f.vehicle.Read() if err != nil { - return getZero[V](), err + return lo.Empty[V](), err } contents, err = f.parser(buf) if err != nil { - return getZero[V](), err + return lo.Empty[V](), err } isLocal = false @@ -104,7 +106,7 @@ func (f *Fetcher[V]) Initial() (V, error) { if f.vehicle.Type() != types.File && !isLocal { if err := safeWrite(f.vehicle.Path(), buf); err != nil { - return getZero[V](), err + return lo.Empty[V](), err } } @@ -121,7 +123,7 @@ func (f *Fetcher[V]) Initial() (V, error) { func (f *Fetcher[V]) Update() (V, bool, error) { buf, err := f.vehicle.Read() if err != nil { - return getZero[V](), false, err + return lo.Empty[V](), false, err } now := time.Now() @@ -129,17 +131,17 @@ func (f *Fetcher[V]) Update() (V, bool, error) { if bytes.Equal(f.hash[:], hash[:]) { f.UpdatedAt = &now _ = os.Chtimes(f.vehicle.Path(), now, now) - return getZero[V](), true, nil + return lo.Empty[V](), true, nil } contents, err := f.parser(buf) if err != nil { - return getZero[V](), false, err + return lo.Empty[V](), false, err } if f.vehicle.Type() != types.File { if err := safeWrite(f.vehicle.Path(), buf); err != nil { - return getZero[V](), false, err + return lo.Empty[V](), false, err } } @@ -210,8 +212,3 @@ func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicl interval: interval, } } - -func getZero[V any]() V { - var result V - return result -} diff --git a/component/resource/vehicle.go b/component/resource/vehicle.go index 927a9604..b2e29418 100644 --- a/component/resource/vehicle.go +++ b/component/resource/vehicle.go @@ -2,12 +2,14 @@ package resource import ( "context" - clashHttp "github.com/Dreamacro/clash/component/http" - types "github.com/Dreamacro/clash/constant/provider" + "errors" "io" "net/http" "os" "time" + + mihomoHttp "github.com/metacubex/mihomo/component/http" + types "github.com/metacubex/mihomo/constant/provider" ) type FileVehicle struct { @@ -50,12 +52,14 @@ func (h *HTTPVehicle) Path() string { func (h *HTTPVehicle) Read() ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() - resp, err := clashHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil) + resp, err := mihomoHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil) if err != nil { return nil, err } - defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return nil, errors.New(resp.Status) + } buf, err := io.ReadAll(resp.Body) if err != nil { return nil, err diff --git a/component/sniffer/base_sniffer.go b/component/sniffer/base_sniffer.go index c2958cc6..55f51c50 100644 --- a/component/sniffer/base_sniffer.go +++ b/component/sniffer/base_sniffer.go @@ -3,18 +3,18 @@ package sniffer import ( "errors" - "github.com/Dreamacro/clash/common/utils" - "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/sniffer" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/sniffer" ) type SnifferConfig struct { OverrideDest bool - Ports []utils.Range[uint16] + Ports utils.IntRanges[uint16] } type BaseSniffer struct { - ports []utils.Range[uint16] + ports utils.IntRanges[uint16] supportNetworkType constant.NetWork } @@ -23,8 +23,8 @@ func (*BaseSniffer) Protocol() string { return "unknown" } -// SniffTCP implements sniffer.Sniffer -func (*BaseSniffer) SniffTCP(bytes []byte) (string, error) { +// SniffData implements sniffer.Sniffer +func (*BaseSniffer) SniffData(bytes []byte) (string, error) { return "", errors.New("TODO") } @@ -35,15 +35,10 @@ func (bs *BaseSniffer) SupportNetwork() constant.NetWork { // SupportPort implements sniffer.Sniffer func (bs *BaseSniffer) SupportPort(port uint16) bool { - for _, portRange := range bs.ports { - if portRange.Contains(port) { - return true - } - } - return false + return bs.ports.Check(port) } -func NewBaseSniffer(ports []utils.Range[uint16], networkType constant.NetWork) *BaseSniffer { +func NewBaseSniffer(ports utils.IntRanges[uint16], networkType constant.NetWork) *BaseSniffer { return &BaseSniffer{ ports: ports, supportNetworkType: networkType, diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go index f4511b97..29bea088 100644 --- a/component/sniffer/dispatcher.go +++ b/component/sniffer/dispatcher.go @@ -5,16 +5,15 @@ import ( "fmt" "net" "net/netip" - "strconv" "sync" "time" - "github.com/Dreamacro/clash/common/cache" - N "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/component/trie" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/sniffer" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/cache" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/trie" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/sniffer" + "github.com/metacubex/mihomo/log" ) var ( @@ -28,32 +27,53 @@ var Dispatcher *SnifferDispatcher type SnifferDispatcher struct { enable bool sniffers map[sniffer.Sniffer]SnifferConfig - forceDomain *trie.DomainTrie[struct{}] - skipSNI *trie.DomainTrie[struct{}] + forceDomain *trie.DomainSet + skipSNI *trie.DomainSet skipList *cache.LruCache[string, uint8] rwMux sync.RWMutex forceDnsMapping bool parsePureIp bool } -func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) { - bufConn, ok := conn.(*N.BufferedConn) - if !ok { - return +func (sd *SnifferDispatcher) shouldOverride(metadata *C.Metadata) bool { + return (metadata.Host == "" && sd.parsePureIp) || + sd.forceDomain.Has(metadata.Host) || + (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) +} + +func (sd *SnifferDispatcher) UDPSniff(packet C.PacketAdapter) bool { + metadata := packet.Metadata() + + if sd.shouldOverride(packet.Metadata()) { + for sniffer, config := range sd.sniffers { + if sniffer.SupportNetwork() == C.UDP || sniffer.SupportNetwork() == C.ALLNet { + inWhitelist := sniffer.SupportPort(metadata.DstPort) + overrideDest := config.OverrideDest + + if inWhitelist { + host, err := sniffer.SniffData(packet.Data()) + if err != nil { + continue + } + + sd.replaceDomain(metadata, host, overrideDest) + return true + } + } + } } - if (metadata.Host == "" && sd.parsePureIp) || sd.forceDomain.Search(metadata.Host) != nil || (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) { - port, err := strconv.ParseUint(metadata.DstPort, 10, 16) - if err != nil { - log.Debugln("[Sniffer] Dst port is error") - return - } + return false +} +// TCPSniff returns true if the connection is sniffed to have a domain +func (sd *SnifferDispatcher) 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(uint16(port)) + inWhitelist = sniffer.SupportPort(metadata.DstPort) if inWhitelist { overrideDest = config.OverrideDest break @@ -62,26 +82,26 @@ func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) { } if !inWhitelist { - return + return false } sd.rwMux.RLock() - dst := fmt.Sprintf("%s:%s", metadata.DstIP, metadata.DstPort) + dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) if count, ok := sd.skipList.Get(dst); ok && count > 5 { log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) defer sd.rwMux.RUnlock() - return + return false } sd.rwMux.RUnlock() - if host, err := sd.sniffDomain(bufConn, metadata); err != nil { + if host, err := sd.sniffDomain(conn, metadata); err != nil { sd.cacheSniffFailed(metadata) - log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%s] to [%s:%s]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) - return + log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) + return false } else { - if sd.skipSNI.Search(host) != nil { + if sd.skipSNI.Has(host) { log.Debugln("[Sniffer] Skip sni[%s]", host) - return + return false } sd.rwMux.RLock() @@ -89,20 +109,24 @@ func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) { sd.rwMux.RUnlock() sd.replaceDomain(metadata, host, overrideDest) + return true } } + return false } func (sd *SnifferDispatcher) replaceDomain(metadata *C.Metadata, host string, overrideDest bool) { + // show log early, since the following code may mutate `metadata.Host` + log.Debugln("[Sniffer] Sniff %s [%s]-->[%s] success, replace domain [%s]-->[%s]", + metadata.NetWork, + metadata.SourceDetail(), + metadata.RemoteAddress(), + metadata.Host, host) metadata.SniffHost = host if overrideDest { metadata.Host = host } metadata.DNSMode = C.DNSNormal - log.Debugln("[Sniffer] Sniff TCP [%s]-->[%s] success, replace domain [%s]-->[%s]", - metadata.SourceDetail(), - metadata.RemoteAddress(), - metadata.Host, host) } func (sd *SnifferDispatcher) Enable() bool { @@ -133,7 +157,7 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad continue } - host, err := s.SniffTCP(bytes) + host, err := s.SniffData(bytes) if err != nil { //log.Debugln("[Sniffer] [%s] Sniff data failed %s", s.Protocol(), metadata.DstIP) continue @@ -154,7 +178,7 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) { sd.rwMux.Lock() - dst := fmt.Sprintf("%s:%s", metadata.DstIP, metadata.DstPort) + dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) count, _ := sd.skipList.Get(dst) if count <= 5 { count++ @@ -171,8 +195,8 @@ func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) { return &dispatcher, nil } -func NewSnifferDispatcher(snifferConfig map[sniffer.Type]SnifferConfig, forceDomain *trie.DomainTrie[struct{}], - skipSNI *trie.DomainTrie[struct{}], +func NewSnifferDispatcher(snifferConfig map[sniffer.Type]SnifferConfig, + forceDomain *trie.DomainSet, skipSNI *trie.DomainSet, forceDnsMapping bool, parsePureIp bool) (*SnifferDispatcher, error) { dispatcher := SnifferDispatcher{ enable: true, @@ -202,6 +226,8 @@ func NewSniffer(name sniffer.Type, snifferConfig SnifferConfig) (sniffer.Sniffer 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 index bfa7ca6e..76bf1559 100644 --- a/component/sniffer/http_sniffer.go +++ b/component/sniffer/http_sniffer.go @@ -7,9 +7,9 @@ import ( "net" "strings" - "github.com/Dreamacro/clash/common/utils" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/sniffer" + "github.com/metacubex/mihomo/common/utils" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/sniffer" ) var ( @@ -34,11 +34,9 @@ type HTTPSniffer struct { var _ sniffer.Sniffer = (*HTTPSniffer)(nil) func NewHTTPSniffer(snifferConfig SnifferConfig) (*HTTPSniffer, error) { - ports := make([]utils.Range[uint16], 0) - if len(snifferConfig.Ports) == 0 { - ports = append(ports, *utils.NewRange[uint16](80, 80)) - } else { - ports = append(ports, snifferConfig.Ports...) + ports := snifferConfig.Ports + if len(ports) == 0 { + ports = utils.IntRanges[uint16]{utils.NewRange[uint16](80, 80)} } return &HTTPSniffer{ BaseSniffer: NewBaseSniffer(ports, C.TCP), @@ -60,7 +58,7 @@ func (http *HTTPSniffer) SupportNetwork() C.NetWork { return C.TCP } -func (http *HTTPSniffer) SniffTCP(bytes []byte) (string, error) { +func (http *HTTPSniffer) SniffData(bytes []byte) (string, error) { domain, err := SniffHTTP(bytes) if err == nil { return *domain, nil diff --git a/component/sniffer/quic_sniffer.go b/component/sniffer/quic_sniffer.go index de78cf82..0e3994f0 100644 --- a/component/sniffer/quic_sniffer.go +++ b/component/sniffer/quic_sniffer.go @@ -1,3 +1,287 @@ package sniffer -//TODO +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "encoding/binary" + "errors" + "io" + + "github.com/metacubex/mihomo/common/buf" + "github.com/metacubex/mihomo/common/utils" + C "github.com/metacubex/mihomo/constant" + + "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 +) + +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") +) + +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 (quic QuicSniffer) Protocol() string { + return "quic" +} + +func (quic QuicSniffer) SupportNetwork() C.NetWork { + return C.UDP +} + +func (quic QuicSniffer) SniffData(b []byte) (string, 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 + } + + if (typeByte&0x30)>>4 != 0x0 { + return "", errNotQuicInitial + } + + var destConnID []byte + if l, err := buffer.ReadByte(); err != nil { + return "", errNotQuic + } else if destConnID, err = buffer.ReadBytes(int(l)); err != nil { + return "", errNotQuic + } + + 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 packetNumber[0] != 0 && packetNumber[0] != 1 { + return "", errNotQuicInitial + } + + 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) + + cryptoLen := uint(0) + cryptoData := cache.Extend(buffer.Len()) + for i := 0; !buffer.IsEmpty(); i++ { + 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 + } + if cryptoLen < uint(offset+length) { + cryptoLen = uint(offset + length) + } + if _, err := buffer.Read(cryptoData[offset : offset+length]); err != nil { // Field: Crypto Data + return "", io.ErrUnexpectedEOF + } + 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 + } + } + + domain, err := ReadClientHello(cryptoData[:cryptoLen]) + if err != nil { + return "", err + } + + return *domain, 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 index e7ced43c..18cc9152 100644 --- a/component/sniffer/sniff_test.go +++ b/component/sniffer/sniff_test.go @@ -1,9 +1,40 @@ package sniffer import ( + "bytes" + "encoding/hex" + "github.com/stretchr/testify/assert" "testing" ) +func TestQuicHeaders(t *testing.T) { + cases := []struct { + input string + domain string + }{ + { + input: "cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b", + domain: "www.google.com", + }, + { + input: "c3000000011266f50524e8d0fe88cbf51e3ad71a13198235000044c82dc5d943fb34cc6d5c5e433610dc7a44f5951935c2c1d14ac641b02472340a892c4492dbfe3f8262109108fc36d96bdc1e9e46b5f1f6ef6104add2aafbfd8e79246eb3b4637541aaed7d195571724e642ab4d31c909f1db86e7d8516117ce8716bd1e3acb664c499086b0f3bc7258595420e7bb969f934457d195e832ffff4ffddf11123eeadacc48190e356c8f0f6abc381deb7e285e3b0613a795b19bddb9f002ffdf6fd70f0ff2072302b33d2421aac6540bb9f0e85c7237af0dd56225b2264d769160febab952e64bd5155f23e58c6113891143f946591032b41816aed3ac54f521f60605f86791de24c5765b664c1348cc53d5d631b4bbefe1915f2b21fefafb47badeb72d8ba1fd5c3cfeb0ba9d0112396f170e94cd33952c4fa87997b870931bf1a300e8e127f530815ff087815b4f9d004cbcd17013ac143847572a1655a5b36e054e8b9951d747c2c6ff25d7b2edb13a2a6b8074062332f2191f6830cf435a4ed9db5d9c4eb43a143bf3edf0c48f6f9435dafad4afb743a5a33990379df953ecd388e848aff0ebba9ccc052b8303c0bd1fee7e7553af1894e81b7772818bb69249540ccb8cfb47b1517abaf71c81c3bd271f1a5f1b66465f850f377c9db682b8e543c3d0c10fcd2dee263630889b7d1d521d1d27e866ea4ab5f43790d6a7f76ceefd5783678ca92cc131fa42fc4a01e2a81cad734ddf17a53e1bda8e0a21afc9e8c1118c9459b13519f5b3c3d9692c92234f01129d47ae8ec70625170847472801190b46d36f73b868f55f5a18a3cb05af6d38610e0829e4fbf13ddcc202341702e43dcf33be76ff4afe327e5783287c137aad075752940b41e7d9f5146e36d908897c6d7a9fdc343fde2d9c9d6e6a6b237669bd3e6abe0a732861a679eadfa29a876c6a646953c9361830811b012b26b31c9e7158f8de9c9a108346ddee3dd3886da6258364c1281bff8e055f6384e3a23e198b5e6b726fa7f811b3338072019d4b5fd05891770d11e3ed6ab5f7ed33db1c6220c5aa8fa1909949ac55d5435b75982e17aa80940fa574f0aba4dc340129cad491fdf1f5e05c4e83e36ad29ff38f15e1c9436c792024442f57f07583d671dd05446c84ea20b471303f6ae4e5e13f244d671e0ebe94d3d5c17d3f3f378cdd51fa8a6d2c977c78a2397dd1e251cd979803d617d45f575e5d9db0a28b3c4c25fe2af24af5bddac09786b6d6d8aa19cfbd5409bdbfed7d518ef5c863f3ee757bd9d37cddc546cc57d2e52b6ae58789f297a300f1d76c3842603eae4b1224de31a939a68875c86e697aeebf7ebc65568f43fc681bacab830ac4a2164d324e90067125bad702192d01cb3cb3d2689ae681967e86fd7ac93a25cf2e905c88ca5ad7d11962f021754cf3f61224517bd3411d5b5a83955bcea79d702466d073a6eaadc1202b3693e555b051a5b19457023a01e7f943742bb7f5f8aeba8d4e363973aebdccfb12479619cfb93e833be702a307e796dc7431a48abd9b755b392c510b98cd20ef778e2ac88d6a04f23ba8a253d7eb7c13e0c88c3a21f7e23857c58704d139703a47e0965bf2dc8810dc36894ac1f3da73c155e271c106a718b2d184e4e5637c820fe909984642960edfc9e62ac50af5dd3feee6bc560ced7bda676d4e290c9c5916fad52180bbc83d3483e95c79bac15c209936f21042dc2b6253eefdac06e7f4745044eaa0acedabf1d1c8cd9402738", + domain: "cloudflare-dns.com", + }, + } + q, err := NewQuicSniffer(SnifferConfig{}) + assert.NoError(t, err) + + for _, test := range cases { + pkt, err := hex.DecodeString(test.input) + assert.NoError(t, err) + oriPkt := bytes.Clone(pkt) + domain, err := q.SniffData(pkt) + assert.NoError(t, err) + assert.Equal(t, test.domain, domain) + assert.Equal(t, oriPkt, pkt) // ensure input data not changed + } +} + func TestTLSHeaders(t *testing.T) { cases := []struct { input []byte @@ -142,6 +173,7 @@ func TestTLSHeaders(t *testing.T) { } for _, test := range cases { + input := bytes.Clone(test.input) domain, err := SniffTLS(test.input) if test.err { if err == nil { @@ -155,5 +187,6 @@ func TestTLSHeaders(t *testing.T) { 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 index 0867d0f0..974df79a 100644 --- a/component/sniffer/tls_sniffer.go +++ b/component/sniffer/tls_sniffer.go @@ -5,9 +5,9 @@ import ( "errors" "strings" - "github.com/Dreamacro/clash/common/utils" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/sniffer" + "github.com/metacubex/mihomo/common/utils" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/sniffer" ) var ( @@ -22,11 +22,9 @@ type TLSSniffer struct { } func NewTLSSniffer(snifferConfig SnifferConfig) (*TLSSniffer, error) { - ports := make([]utils.Range[uint16], 0) - if len(snifferConfig.Ports) == 0 { - ports = append(ports, *utils.NewRange[uint16](443, 443)) - } else { - ports = append(ports, snifferConfig.Ports...) + ports := snifferConfig.Ports + if len(ports) == 0 { + ports = utils.IntRanges[uint16]{utils.NewRange[uint16](443, 443)} } return &TLSSniffer{ BaseSniffer: NewBaseSniffer(ports, C.TCP), @@ -41,7 +39,7 @@ func (tls *TLSSniffer) SupportNetwork() C.NetWork { return C.TCP } -func (tls *TLSSniffer) SniffTCP(bytes []byte) (string, error) { +func (tls *TLSSniffer) SniffData(bytes []byte) (string, error) { domain, err := SniffTLS(bytes) if err == nil { return *domain, nil diff --git a/component/tls/config.go b/component/tls/config.go deleted file mode 100644 index 39d1b1fd..00000000 --- a/component/tls/config.go +++ /dev/null @@ -1,135 +0,0 @@ -package tls - -import ( - "bytes" - "crypto/sha256" - "crypto/tls" - "crypto/x509" - "encoding/hex" - "errors" - "fmt" - "strings" - "sync" - - CN "github.com/Dreamacro/clash/common/net" - - xtls "github.com/xtls/go" -) - -var tlsCertificates = make([]tls.Certificate, 0) - -var mutex sync.RWMutex -var errNotMacth error = errors.New("certificate fingerprints do not match") - -func AddCertificate(privateKey, certificate string) error { - mutex.Lock() - defer mutex.Unlock() - if cert, err := CN.ParseCert(certificate, privateKey); err != nil { - return err - } else { - tlsCertificates = append(tlsCertificates, cert) - } - return nil -} - -func GetCertificates() []tls.Certificate { - mutex.RLock() - defer mutex.RUnlock() - return tlsCertificates -} - -func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - // ssl pining - for i := range rawCerts { - rawCert := rawCerts[i] - cert, err := x509.ParseCertificate(rawCert) - if err == nil { - hash := sha256.Sum256(cert.Raw) - if bytes.Equal(fingerprint[:], hash[:]) { - return nil - } - } - } - return errNotMacth - } -} - -func convertFingerprint(fingerprint string) (*[32]byte, error) { - fingerprint = strings.TrimSpace(strings.Replace(fingerprint, ":", "", -1)) - fpByte, err := hex.DecodeString(fingerprint) - if err != nil { - return nil, err - } - - if len(fpByte) != 32 { - return nil, fmt.Errorf("fingerprint string length error,need sha256 fingerprint") - } - return (*[32]byte)(fpByte), nil -} - -func GetDefaultTLSConfig() *tls.Config { - return GetGlobalTLSConfig(nil) -} - -// GetSpecifiedFingerprintTLSConfig specified fingerprint -func GetSpecifiedFingerprintTLSConfig(tlsConfig *tls.Config, fingerprint string) (*tls.Config, error) { - if fingerprintBytes, err := convertFingerprint(fingerprint); err != nil { - return nil, err - } else { - tlsConfig = GetGlobalTLSConfig(tlsConfig) - tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes) - tlsConfig.InsecureSkipVerify = true - return tlsConfig, nil - } -} - -func GetGlobalTLSConfig(tlsConfig *tls.Config) *tls.Config { - if tlsConfig == nil { - return &tls.Config{ - Certificates: tlsCertificates, - } - } - tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCertificates...) - return tlsConfig -} - -// GetSpecifiedFingerprintXTLSConfig specified fingerprint -func GetSpecifiedFingerprintXTLSConfig(tlsConfig *xtls.Config, fingerprint string) (*xtls.Config, error) { - if fingerprintBytes, err := convertFingerprint(fingerprint); err != nil { - return nil, err - } else { - tlsConfig = GetGlobalXTLSConfig(tlsConfig) - tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes) - tlsConfig.InsecureSkipVerify = true - return tlsConfig, nil - } -} - -func GetGlobalXTLSConfig(tlsConfig *xtls.Config) *xtls.Config { - xtlsCerts := make([]xtls.Certificate, len(tlsCertificates)) - for _, cert := range tlsCertificates { - tlsSsaList := make([]xtls.SignatureScheme, len(cert.SupportedSignatureAlgorithms)) - for _, ssa := range cert.SupportedSignatureAlgorithms { - tlsSsa := xtls.SignatureScheme(ssa) - tlsSsaList = append(tlsSsaList, tlsSsa) - } - xtlsCert := xtls.Certificate{ - Certificate: cert.Certificate, - PrivateKey: cert.PrivateKey, - OCSPStaple: cert.OCSPStaple, - SignedCertificateTimestamps: cert.SignedCertificateTimestamps, - Leaf: cert.Leaf, - SupportedSignatureAlgorithms: tlsSsaList, - } - xtlsCerts = append(xtlsCerts, xtlsCert) - } - if tlsConfig == nil { - return &xtls.Config{ - Certificates: xtlsCerts, - } - } - - tlsConfig.Certificates = xtlsCerts - return tlsConfig -} diff --git a/component/tls/reality.go b/component/tls/reality.go new file mode 100644 index 00000000..250dc4d0 --- /dev/null +++ b/component/tls/reality.go @@ -0,0 +1,183 @@ +package tls + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/ed25519" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "crypto/tls" + "crypto/x509" + "encoding/binary" + "errors" + "net" + "net/http" + "reflect" + "strings" + "time" + "unsafe" + + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/ntp" + + utls "github.com/sagernet/utls" + "github.com/zhangyunhao116/fastrand" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/hkdf" + "golang.org/x/net/http2" +) + +const RealityMaxShortIDLen = 8 + +type RealityConfig struct { + PublicKey [curve25519.ScalarSize]byte + ShortID [RealityMaxShortIDLen]byte +} + +//go:linkname aesgcmPreferred crypto/tls.aesgcmPreferred +func aesgcmPreferred(ciphers []uint16) bool + +func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) { + retry := 0 + for fingerprint, exists := GetFingerprint(ClientFingerprint); exists; 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 + } + + 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]) + + ecdheParams := uConn.HandshakeState.State13.EcdheParams + if ecdheParams == nil { + // WTF??? + if retry > 2 { + return nil, errors.New("nil ecdheParams") + } + continue // retry + } + authKey := ecdheParams.SharedKey(realityConfig.PublicKey[:]) + 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 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 + } + return nil, errors.New("unknown uTLS fingerprint") +} + +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, _ := http.NewRequest("GET", "https://"+serverName, nil) + request.Header.Set("User-Agent", fingerprint.Client) + request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", fastrand.Intn(32)+30)}) + response, err := client.Do(request) + if err != nil { + return + } + //_, _ = io.Copy(io.Discard, response.Body) + time.Sleep(time.Duration(5+fastrand.Int63n(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)) + 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 index f965fc64..787f6fad 100644 --- a/component/tls/utls.go +++ b/component/tls/utls.go @@ -4,10 +4,10 @@ import ( "crypto/tls" "net" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/log" "github.com/mroth/weightedrand/v2" - utls "github.com/refraction-networking/utls" + utls "github.com/sagernet/utls" ) type UConn struct { @@ -21,7 +21,7 @@ type UClientHelloID struct { var initRandomFingerprint UClientHelloID var initUtlsClient string -func UClient(c net.Conn, config *tls.Config, fingerprint UClientHelloID) net.Conn { +func UClient(c net.Conn, config *tls.Config, fingerprint UClientHelloID) *UConn { utlsConn := utls.UClient(c, copyConfig(config), utls.ClientHelloID{ Client: fingerprint.Client, Version: fingerprint.Version, @@ -45,8 +45,13 @@ func GetFingerprint(ClientFingerprint string) (UClientHelloID, bool) { } fingerprint, ok := Fingerprints[ClientFingerprint] - log.Debugln("use specified fingerprint:%s", fingerprint.Client) - return fingerprint, ok + if ok { + log.Debugln("use specified fingerprint:%s", fingerprint.Client) + return fingerprint, ok + } else { + log.Warnln("wrong ClientFingerprint:%s", ClientFingerprint) + return UClientHelloID{}, false + } } func RollFingerprint() (UClientHelloID, bool) { @@ -67,7 +72,22 @@ var Fingerprints = map[string]UClientHelloID{ "firefox": {&utls.HelloFirefox_Auto}, "safari": {&utls.HelloSafari_Auto}, "ios": {&utls.HelloIOS_Auto}, - "randomized": {&utls.HelloRandomized}, + "android": {&utls.HelloAndroid_11_OkHttp}, + "edge": {&utls.HelloEdge_Auto}, + "360": {&utls.Hello360_Auto}, + "qq": {&utls.HelloQQ_Auto}, + "random": {nil}, + "randomized": {nil}, +} + +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"] = UClientHelloID{&randomized} } func copyConfig(c *tls.Config) *utls.Config { @@ -79,10 +99,9 @@ func copyConfig(c *tls.Config) *utls.Config { } } -// WebsocketHandshake basically calls UConn.Handshake inside it but it will only send -// http/1.1 in its ALPN. +// 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 (c *UConn) WebsocketHandshake() error { +func (c *UConn) BuildWebsocketHandshakeState() 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 { @@ -100,11 +119,11 @@ func (c *UConn) WebsocketHandshake() error { if !hasALPNExtension { // Append extension if doesn't exists c.Extensions = append(c.Extensions, &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}) } - // Rebuild the client hello and do the handshake + // Rebuild the client hello if err := c.BuildHandshakeState(); err != nil { return err } - return c.Handshake() + return nil } func SetGlobalUtlsClient(Client string) { @@ -112,10 +131,7 @@ func SetGlobalUtlsClient(Client string) { } func HaveGlobalFingerprint() bool { - if len(initUtlsClient) != 0 && initUtlsClient != "none" { - return true - } - return false + return len(initUtlsClient) != 0 && initUtlsClient != "none" } func GetGlobalFingerprint() string { diff --git a/component/trie/domain.go b/component/trie/domain.go index d9463c6e..3decbb02 100644 --- a/component/trie/domain.go +++ b/component/trie/domain.go @@ -25,7 +25,7 @@ func ValidAndSplitDomain(domain string) ([]string, bool) { if domain != "" && domain[len(domain)-1] == '.' { return nil, false } - + domain = strings.ToLower(domain) parts := strings.Split(domain, domainStep) if len(parts) == 1 { if parts[0] == "" { @@ -123,6 +123,33 @@ func (t *DomainTrie[T]) Optimize() { t.root.optimize() } +func (t *DomainTrie[T]) Foreach(print func(domain string, data T)) { + for key, data := range t.root.getChildren() { + recursion([]string{key}, data, print) + if data != nil && data.inited { + print(joinDomain([]string{key}), data.data) + } + } +} + +func recursion[T any](items []string, node *Node[T], fn func(domain string, data T)) { + for key, data := range node.getChildren() { + newItems := append([]string{key}, items...) + if data != nil && data.inited { + domain := joinDomain(newItems) + if domain[0] == domainStepByte { + domain = complexWildcard + domain + } + fn(domain, data.Data()) + } + recursion(newItems, data, fn) + } +} + +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 new file mode 100644 index 00000000..860d1235 --- /dev/null +++ b/component/trie/domain_set.go @@ -0,0 +1,175 @@ +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) { + reserveDomains = append(reserveDomains, utils.Reverse(domain)) + }) + // 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 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_test.go b/component/trie/domain_set_test.go new file mode 100644 index 00000000..77106d5f --- /dev/null +++ b/component/trie/domain_set_test.go @@ -0,0 +1,85 @@ +package trie_test + +import ( + "testing" + + "github.com/metacubex/mihomo/component/trie" + "github.com/stretchr/testify/assert" +) + +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{}{})) + } + 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")) +} + +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{}{})) + } + 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")) +} + +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{}{})) + } + 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")) +} diff --git a/component/trie/domain_test.go b/component/trie/domain_test.go index c54b3d3b..4c5d8002 100644 --- a/component/trie/domain_test.go +++ b/component/trie/domain_test.go @@ -1,16 +1,17 @@ -package trie +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 := New[netip.Addr]() + tree := trie.New[netip.Addr]() domains := []string{ "example.com", "google.com", @@ -18,7 +19,7 @@ func TestTrie_Basic(t *testing.T) { } for _, domain := range domains { - tree.Insert(domain, localIP) + assert.NoError(t, tree.Insert(domain, localIP)) } node := tree.Search("example.com") @@ -31,7 +32,7 @@ func TestTrie_Basic(t *testing.T) { } func TestTrie_Wildcard(t *testing.T) { - tree := New[netip.Addr]() + tree := trie.New[netip.Addr]() domains := []string{ "*.example.com", "sub.*.example.com", @@ -47,7 +48,7 @@ func TestTrie_Wildcard(t *testing.T) { } for _, domain := range domains { - tree.Insert(domain, localIP) + assert.NoError(t, tree.Insert(domain, localIP)) } assert.NotNil(t, tree.Search("sub.example.com")) @@ -64,7 +65,7 @@ func TestTrie_Wildcard(t *testing.T) { } func TestTrie_Priority(t *testing.T) { - tree := New[int]() + tree := trie.New[int]() domains := []string{ ".dev", "example.dev", @@ -79,7 +80,7 @@ func TestTrie_Priority(t *testing.T) { } for idx, domain := range domains { - tree.Insert(domain, idx+1) + assert.NoError(t, tree.Insert(domain, idx+1)) } assertFn("test.dev", 1) @@ -90,8 +91,8 @@ func TestTrie_Priority(t *testing.T) { } func TestTrie_Boundary(t *testing.T) { - tree := New[netip.Addr]() - tree.Insert("*.dev", localIP) + 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)) @@ -99,9 +100,29 @@ func TestTrie_Boundary(t *testing.T) { } func TestTrie_WildcardBoundary(t *testing.T) { - tree := New[netip.Addr]() - tree.Insert("+.*", localIP) - tree.Insert("stun.*.*.*", localIP) + 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) { + count++ + }) + assert.Equal(t, 7, count) +} diff --git a/component/trie/ipcidr_trie.go b/component/trie/ipcidr_trie.go index a3a63f95..a2ccfa16 100644 --- a/component/trie/ipcidr_trie.go +++ b/component/trie/ipcidr_trie.go @@ -1,8 +1,9 @@ package trie import ( - "github.com/Dreamacro/clash/log" "net" + + "github.com/metacubex/mihomo/log" ) type IPV6 bool @@ -47,11 +48,10 @@ func (trie *IpCidrTrie) AddIpCidrForString(ipCidr string) error { } func (trie *IpCidrTrie) IsContain(ip net.IP) bool { - ip, isIpv4 := checkAndConverterIp(ip) if ip == nil { return false } - + isIpv4 := len(ip) == net.IPv4len var groupValues []uint32 var ipCidrNode *IpCidrNode @@ -71,7 +71,13 @@ func (trie *IpCidrTrie) IsContain(ip net.IP) bool { } func (trie *IpCidrTrie) IsContainForString(ipString string) bool { - return trie.IsContain(net.ParseIP(ipString)) + 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) { @@ -82,9 +88,8 @@ func ipCidrToSubIpCidr(ipNet *net.IPNet) ([]net.IP, int, bool, error) { isIpv4 bool err error ) - - ip, isIpv4 := checkAndConverterIp(ipNet.IP) - ipList, newMaskSize, err = subIpCidr(ip, maskSize, isIpv4) + isIpv4 = len(ipNet.IP) == net.IPv4len + ipList, newMaskSize, err = subIpCidr(ipNet.IP, maskSize, isIpv4) return ipList, newMaskSize, isIpv4, err } @@ -238,18 +243,3 @@ func search(root *IpCidrNode, groupValues []uint32) *IpCidrNode { return nil } - -// return net.IP To4 or To16 and is ipv4 -func checkAndConverterIp(ip net.IP) (net.IP, bool) { - ipResult := ip.To4() - if ipResult == nil { - ipResult = ip.To16() - if ipResult == nil { - return nil, false - } - - return ipResult, false - } - - return ipResult, true -} diff --git a/component/trie/node.go b/component/trie/node.go index e19b40ac..3aa2bc7d 100644 --- a/component/trie/node.go +++ b/component/trie/node.go @@ -116,6 +116,18 @@ func (n *Node[T]) setData(data T) { 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 } diff --git a/component/trie/trie_test.go b/component/trie/trie_test.go index dca77c05..e1b20103 100644 --- a/component/trie/trie_test.go +++ b/component/trie/trie_test.go @@ -3,8 +3,9 @@ package trie import ( "net" "testing" + + "github.com/stretchr/testify/assert" ) -import "github.com/stretchr/testify/assert" func TestIpv4AddSuccess(t *testing.T) { trie := NewIpCidrTrie() @@ -96,5 +97,11 @@ func TestIpv6Search(t *testing.T) { 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/config/config.go b/config/config.go index d71fc63f..76ff9d68 100644 --- a/config/config.go +++ b/config/config.go @@ -4,41 +4,40 @@ import ( "container/list" "errors" "fmt" - "net" "net/netip" "net/url" "os" - "reflect" - "runtime" - "strconv" + "path" + "regexp" "strings" "time" - "github.com/Dreamacro/clash/adapter" - "github.com/Dreamacro/clash/adapter/outbound" - "github.com/Dreamacro/clash/adapter/outboundgroup" - "github.com/Dreamacro/clash/adapter/provider" - "github.com/Dreamacro/clash/common/utils" - "github.com/Dreamacro/clash/component/auth" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/fakeip" - "github.com/Dreamacro/clash/component/geodata" - "github.com/Dreamacro/clash/component/geodata/router" - P "github.com/Dreamacro/clash/component/process" - SNIFF "github.com/Dreamacro/clash/component/sniffer" - tlsC "github.com/Dreamacro/clash/component/tls" - "github.com/Dreamacro/clash/component/trie" - C "github.com/Dreamacro/clash/constant" - providerTypes "github.com/Dreamacro/clash/constant/provider" - snifferTypes "github.com/Dreamacro/clash/constant/sniffer" - "github.com/Dreamacro/clash/dns" - L "github.com/Dreamacro/clash/listener" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/log" - R "github.com/Dreamacro/clash/rules" - RP "github.com/Dreamacro/clash/rules/provider" - T "github.com/Dreamacro/clash/tunnel" + "github.com/metacubex/mihomo/adapter" + "github.com/metacubex/mihomo/adapter/outbound" + "github.com/metacubex/mihomo/adapter/outboundgroup" + "github.com/metacubex/mihomo/adapter/provider" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/auth" + "github.com/metacubex/mihomo/component/fakeip" + "github.com/metacubex/mihomo/component/geodata" + "github.com/metacubex/mihomo/component/geodata/router" + P "github.com/metacubex/mihomo/component/process" + "github.com/metacubex/mihomo/component/resolver" + SNIFF "github.com/metacubex/mihomo/component/sniffer" + tlsC "github.com/metacubex/mihomo/component/tls" + "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" + RP "github.com/metacubex/mihomo/rules/provider" + T "github.com/metacubex/mihomo/tunnel" "gopkg.in/yaml.v3" ) @@ -53,6 +52,7 @@ type General struct { IPv6 bool `json:"ipv6"` Interface string `json:"interface-name"` RoutingMark int `json:"-"` + GeoXUrl GeoXUrl `json:"geox-url"` GeodataMode bool `json:"geodata-mode"` GeodataLoader string `json:"geodata-loader"` TCPConcurrent bool `json:"tcp-concurrent"` @@ -60,23 +60,26 @@ type General struct { Sniffing bool `json:"sniffing"` EBpf EBpf `json:"-"` GlobalClientFingerprint string `json:"global-client-fingerprint"` + GlobalUA string `json:"global-ua"` } // 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"` - AllowLan bool `json:"allow-lan"` - BindAddress string `json:"bind-address"` - InboundTfo bool `json:"inbound-tfo"` + 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"` + AllowLan bool `json:"allow-lan"` + BindAddress string `json:"bind-address"` + InboundTfo bool `json:"inbound-tfo"` + InboundMPTCP bool `json:"inbound-mptcp"` } // Controller config @@ -87,11 +90,22 @@ type Controller struct { Secret string `json:"-"` } +// NTP config +type NTP struct { + Enable bool `yaml:"enable"` + Server string `yaml:"server"` + Port int `yaml:"port"` + Interval int `yaml:"interval"` + DialerProxy string `yaml:"dialer-proxy"` + WriteToSystem bool `yaml:"write-to-system"` +} + // DNS config type DNS struct { Enable bool `yaml:"enable"` PreferH3 bool `yaml:"prefer-h3"` IPv6 bool `yaml:"ipv6"` + IPv6Timeout uint `yaml:"ipv6-timeout"` NameServer []dns.NameServer `yaml:"nameserver"` Fallback []dns.NameServer `yaml:"fallback"` FallbackFilter FallbackFilter `yaml:"fallback-filter"` @@ -99,7 +113,7 @@ type DNS struct { EnhancedMode C.DNSMode `yaml:"enhanced-mode"` DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` FakeIPRange *fakeip.Pool - Hosts *trie.DomainTrie[netip.Addr] + Hosts *trie.DomainTrie[resolver.HostValue] NameServerPolicy map[string][]dns.NameServer ProxyServerNameserver []dns.NameServer } @@ -108,7 +122,7 @@ type DNS struct { type FallbackFilter struct { GeoIP bool `yaml:"geoip"` GeoIPCode string `yaml:"geoip-code"` - IPCIDR []*netip.Prefix `yaml:"ipcidr"` + IPCIDR []netip.Prefix `yaml:"ipcidr"` Domain []string `yaml:"domain"` GeoSite []*router.DomainMatcher `yaml:"geosite"` } @@ -120,13 +134,9 @@ type Profile struct { } type TLS struct { - RawCert `yaml:",inline"` - CustomTrustCert []RawCert `yaml:"custom-certifactes"` -} - -type RawCert struct { - Certificate string `yaml:"certificate"` - PrivateKey string `yaml:"private-key"` + Certificate string `yaml:"certificate"` + PrivateKey string `yaml:"private-key"` + CustomTrustCert []string `yaml:"custom-certifactes"` } // IPTables config @@ -139,25 +149,27 @@ type IPTables struct { type Sniffer struct { Enable bool Sniffers map[snifferTypes.Type]SNIFF.SnifferConfig - Reverses *trie.DomainTrie[struct{}] - ForceDomain *trie.DomainTrie[struct{}] - SkipDomain *trie.DomainTrie[struct{}] + ForceDomain *trie.DomainSet + SkipDomain *trie.DomainSet ForceDnsMapping bool ParsePureIp bool } // Experimental config type Experimental struct { - Fingerprints []string `yaml:"fingerprints"` + Fingerprints []string `yaml:"fingerprints"` + QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"` + QUICGoDisableECN bool `yaml:"quic-go-disable-ecn"` } -// Config is clash config manager +// Config is mihomo config manager type Config struct { General *General IPTables *IPTables + NTP *NTP DNS *DNS Experimental *Experimental - Hosts *trie.DomainTrie[netip.Addr] + Hosts *trie.DomainTrie[resolver.HostValue] Profile *Profile Rules []C.Rule SubRules map[string][]C.Rule @@ -171,10 +183,20 @@ type Config struct { TLS *TLS } +type RawNTP struct { + Enable bool `yaml:"enable"` + Server string `yaml:"server"` + ServerPort int `yaml:"server-port"` + Interval int `yaml:"interval"` + DialerProxy string `yaml:"dialer-proxy"` + WriteToSystem bool `yaml:"write-to-system"` +} + type RawDNS struct { Enable bool `yaml:"enable"` PreferH3 bool `yaml:"prefer-h3"` IPv6 bool `yaml:"ipv6"` + IPv6Timeout uint `yaml:"ipv6-timeout"` UseHosts bool `yaml:"use-hosts"` NameServer []string `yaml:"nameserver"` Fallback []string `yaml:"fallback"` @@ -206,33 +228,38 @@ type RawTun struct { RedirectToTun []string `yaml:"-" json:"-"` MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` - //Inet4Address []LC.ListenPrefix `yaml:"inet4-address" json:"inet4_address,omitempty"` - Inet6Address []LC.ListenPrefix `yaml:"inet6-address" json:"inet6_address,omitempty"` - StrictRoute bool `yaml:"strict-route" json:"strict_route,omitempty"` - Inet4RouteAddress []LC.ListenPrefix `yaml:"inet4_route_address" json:"inet4_route_address,omitempty"` - Inet6RouteAddress []LC.ListenPrefix `yaml:"inet6_route_address" json:"inet6_route_address,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"` + //Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4_address,omitempty"` + Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6_address,omitempty"` + StrictRoute bool `yaml:"strict-route" json:"strict_route,omitempty"` + 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"` + 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"` } type RawTuicServer struct { - Enable bool `yaml:"enable" json:"enable"` - Listen string `yaml:"listen" json:"listen"` - Token []string `yaml:"token" json:"token"` - 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"` + 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 RawConfig struct { @@ -244,7 +271,9 @@ type RawConfig struct { ShadowSocksConfig string `yaml:"ss-config"` VmessConfig string `yaml:"vmess-config"` InboundTfo bool `yaml:"inbound-tfo"` + InboundMPTCP bool `yaml:"inbound-mptcp"` Authentication []string `yaml:"authentication"` + SkipAuthPrefixes []netip.Prefix `yaml:"skip-auth-prefixes"` AllowLan bool `yaml:"allow-lan"` BindAddress string `yaml:"bind-address"` Mode T.TunnelMode `yaml:"mode"` @@ -254,6 +283,8 @@ type RawConfig struct { ExternalController string `yaml:"external-controller"` ExternalControllerTLS string `yaml:"external-controller-tls"` ExternalUI string `yaml:"external-ui"` + ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"` + ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"` Secret string `yaml:"secret"` Interface string `yaml:"interface-name"` RoutingMark int `yaml:"routing-mark"` @@ -263,11 +294,14 @@ type RawConfig struct { 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"` + GlobalUA string `yaml:"global-ua"` + KeepAliveInterval int `yaml:"keep-alive-interval"` Sniffer RawSniffer `yaml:"sniffer"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` RuleProvider map[string]map[string]any `yaml:"rule-providers"` - Hosts map[string]string `yaml:"hosts"` + Hosts map[string]any `yaml:"hosts"` + NTP RawNTP `yaml:"ntp"` DNS RawDNS `yaml:"dns"` Tun RawTun `yaml:"tun"` TuicServer RawTuicServer `yaml:"tuic-server"` @@ -275,7 +309,7 @@ type RawConfig struct { IPTables IPTables `yaml:"iptables"` Experimental Experimental `yaml:"experimental"` Profile Profile `yaml:"profile"` - GeoXUrl RawGeoXUrl `yaml:"geox-url"` + GeoXUrl GeoXUrl `yaml:"geox-url"` Proxy []map[string]any `yaml:"proxies"` ProxyGroup []map[string]any `yaml:"proxy-groups"` Rule []string `yaml:"rules"` @@ -284,7 +318,7 @@ type RawConfig struct { Listeners []map[string]any `yaml:"listeners"` } -type RawGeoXUrl struct { +type GeoXUrl struct { GeoIp string `yaml:"geoip" json:"geoip"` Mmdb string `yaml:"mmdb" json:"mmdb"` GeoSite string `yaml:"geosite" json:"geosite"` @@ -341,12 +375,13 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { UnifiedDelay: false, Authentication: []string{}, LogLevel: log.INFO, - Hosts: map[string]string{}, + Hosts: map[string]any{}, Rule: []string{}, Proxy: []map[string]any{}, ProxyGroup: []map[string]any{}, TCPConcurrent: false, FindProcessMode: P.FindProcessStrict, + GlobalUA: "mihomo", Tun: RawTun{ Enable: false, Device: "", @@ -354,11 +389,12 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query AutoRoute: true, AutoDetectInterface: true, - Inet6Address: []LC.ListenPrefix{LC.ListenPrefix(netip.MustParsePrefix("fdfe:dcba:9876::1/126"))}, + Inet6Address: []netip.Prefix{netip.MustParsePrefix("fdfe:dcba:9876::1/126")}, }, TuicServer: RawTuicServer{ Enable: false, Token: nil, + Users: nil, Certificate: "", PrivateKey: "", Listen: "", @@ -377,10 +413,18 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { InboundInterface: "lo", Bypass: []string{}, }, + NTP: RawNTP{ + Enable: false, + WriteToSystem: false, + Server: "time.apple.com", + ServerPort: 123, + Interval: 30, + }, DNS: RawDNS{ Enable: false, IPv6: false, UseHosts: true, + IPv6Timeout: 100, EnhancedMode: C.DNSMapping, FakeIPRange: "198.18.0.1/16", FallbackFilter: RawFallbackFilter{ @@ -418,11 +462,12 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { Profile: Profile{ StoreSelected: true, }, - GeoXUrl: RawGeoXUrl{ - GeoIp: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat", - Mmdb: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb", - GeoSite: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat", + GeoXUrl: GeoXUrl{ + Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", + 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", }, + ExternalUIURL: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip", } if err := yaml.Unmarshal(buf, rawCfg); err != nil { @@ -447,7 +492,11 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.General = general - dialer.DefaultInterface.Store(config.General.Interface) + if len(config.General.GlobalClientFingerprint) != 0 { + log.Debugln("GlobalClientFingerprint: %s", config.General.GlobalClientFingerprint) + tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint) + } + proxies, providers, err := parseProxies(rawCfg) if err != nil { return nil, err @@ -486,7 +535,10 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.Hosts = hosts - dnsCfg, err := parseDNS(rawCfg, hosts, rules) + ntpCfg := paresNTP(rawCfg) + config.NTP = ntpCfg + + dnsCfg, err := parseDNS(rawCfg, hosts, rules, ruleProviders) if err != nil { return nil, err } @@ -522,24 +574,44 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm - if len(config.General.GlobalClientFingerprint) != 0 { - log.Debugln("GlobalClientFingerprint:%s", config.General.GlobalClientFingerprint) - tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint) - } - return config, nil } func parseGeneral(cfg *RawConfig) (*General, error) { - externalUI := cfg.ExternalUI geodata.SetLoader(cfg.GeodataLoader) + C.GeoIpUrl = cfg.GeoXUrl.GeoIp + C.GeoSiteUrl = cfg.GeoXUrl.GeoSite + C.MmdbUrl = cfg.GeoXUrl.Mmdb + C.GeodataMode = cfg.GeodataMode + C.UA = cfg.GlobalUA + if cfg.KeepAliveInterval != 0 { + N.KeepAliveInterval = time.Duration(cfg.KeepAliveInterval) * time.Second + } + + ExternalUIPath = cfg.ExternalUI // checkout externalUI exist - if externalUI != "" { - externalUI = C.Path.Resolve(externalUI) - if _, err := os.Stat(externalUI); os.IsNotExist(err) { - return nil, fmt.Errorf("external-ui: %s not exist", externalUI) + if ExternalUIPath != "" { + ExternalUIPath = C.Path.Resolve(ExternalUIPath) + if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) { + defaultUIpath := path.Join(C.Path.HomeDir(), "ui") + log.Warnln("external-ui: %s does not exist, creating folder in %s", ExternalUIPath, defaultUIpath) + if err := os.MkdirAll(defaultUIpath, os.ModePerm); err != nil { + return nil, err + } + ExternalUIPath = defaultUIpath + cfg.ExternalUI = defaultUIpath } } + // checkout UIpath/name exist + if cfg.ExternalUIName != "" { + ExternalUIName = cfg.ExternalUIName + } else { + ExternalUIFolder = ExternalUIPath + } + if cfg.ExternalUIURL != "" { + ExternalUIURL = cfg.ExternalUIURL + } + cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun return &General{ Inbound: Inbound{ @@ -551,8 +623,10 @@ func parseGeneral(cfg *RawConfig) (*General, error) { ShadowSocksConfig: cfg.ShadowSocksConfig, VmessConfig: cfg.VmessConfig, AllowLan: cfg.AllowLan, + SkipAuthPrefixes: cfg.SkipAuthPrefixes, BindAddress: cfg.BindAddress, InboundTfo: cfg.InboundTfo, + InboundMPTCP: cfg.InboundMPTCP, }, Controller: Controller{ ExternalController: cfg.ExternalController, @@ -566,12 +640,14 @@ func parseGeneral(cfg *RawConfig) (*General, error) { IPv6: cfg.IPv6, Interface: cfg.Interface, RoutingMark: cfg.RoutingMark, + GeoXUrl: cfg.GeoXUrl, GeodataMode: cfg.GeodataMode, GeodataLoader: cfg.GeodataLoader, TCPConcurrent: cfg.TCPConcurrent, FindProcessMode: cfg.FindProcessMode, EBpf: cfg.EBpf, GlobalClientFingerprint: cfg.GlobalClientFingerprint, + GlobalUA: cfg.GlobalUA, }, nil } @@ -658,7 +734,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ } ps = append(ps, proxies[v]) } - hc := provider.NewHealthCheck(ps, "", 0, true) + hc := provider.NewHealthCheck(ps, "", 0, true, nil) pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) providersMap[provider.ReservedName] = pd @@ -713,6 +789,9 @@ func parseRuleProviders(cfg *RawConfig) (ruleProviders map[string]providerTypes. func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (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") @@ -823,26 +902,50 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, subRules map[s rules = append(rules, parsed) } - runtime.GC() - return rules, nil } -func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) { - tree := trie.New[netip.Addr]() +func parseHosts(cfg *RawConfig) (*trie.DomainTrie[resolver.HostValue], error) { + tree := trie.New[resolver.HostValue]() // add default hosts - if err := tree.Insert("localhost", netip.AddrFrom4([4]byte{127, 0, 0, 1})); err != nil { + 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, ipStr := range cfg.Hosts { - ip, err := netip.ParseAddr(ipStr) - if err != nil { - return nil, fmt.Errorf("%s is not a valid IP", ipStr) + for domain, anyValue := range cfg.Hosts { + if str, ok := anyValue.(string); ok && str == "mihomo" { + if addrs, err := net.InterfaceAddrs(); err != nil { + log.Errorln("insert mihomo 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 + } } - _ = tree.Insert(domain, ip) + 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() @@ -875,7 +978,7 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) } - proxyAdapter := u.Fragment + proxyName := u.Fragment var addr, dnsNetType string params := map[string]string{} @@ -892,8 +995,8 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) case "https": addr, err = hostWithDefaultPort(u.Host, "443") if err == nil { - proxyAdapter = "" - clearURL := url.URL{Scheme: "https", Host: addr, Path: u.Path} + proxyName = "" + clearURL := url.URL{Scheme: "https", Host: addr, Path: u.Path, User: u.User} addr = clearURL.String() dnsNetType = "https" // DNS over HTTPS if len(u.Fragment) != 0 { @@ -902,7 +1005,7 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) if len(arr) == 0 { continue } else if len(arr) == 1 { - proxyAdapter = arr[0] + proxyName = arr[0] } else if len(arr) == 2 { params[arr[0]] = arr[1] } else { @@ -917,6 +1020,21 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) case "quic": addr, err = hostWithDefaultPort(u.Host, "853") dnsNetType = "quic" // DNS over QUIC + case "system": + dnsNetType = "system" // System DNS + 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) } @@ -928,23 +1046,32 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) nameservers = append( nameservers, dns.NameServer{ - Net: dnsNetType, - Addr: addr, - ProxyAdapter: proxyAdapter, - Interface: dialer.DefaultInterface, - Params: params, - PreferH3: preferH3, + Net: dnsNetType, + Addr: addr, + ProxyName: proxyName, + Params: params, + PreferH3: preferH3, }, ) } return nameservers, nil } +func init() { + dns.ParseNameServer = func(servers []string) ([]dns.NameServer, error) { // using by wireguard + return parseNameServer(servers, 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 @@ -958,49 +1085,80 @@ func parsePureDNSServer(server string) string { } } } -func parseNameServerPolicy(nsPolicy map[string]any, preferH3 bool) (map[string][]dns.NameServer, error) { +func parseNameServerPolicy(nsPolicy map[string]any, ruleProviders map[string]providerTypes.RuleProvider, preferH3 bool) (map[string][]dns.NameServer, error) { policy := map[string][]dns.NameServer{} + updatedPolicy := make(map[string]interface{}) + re := regexp.MustCompile(`[a-zA-Z0-9\-]+\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?`) - for domain, server := range nsPolicy { - var ( - nameservers []dns.NameServer - err error - ) - - switch reflect.TypeOf(server).Kind() { - case reflect.Slice, reflect.Array: - origin := reflect.ValueOf(server) - servers := make([]string, 0) - for i := 0; i < origin.Len(); i++ { - servers = append(servers, fmt.Sprintf("%v", origin.Index(i))) + for k, v := range nsPolicy { + if strings.Contains(k, ",") { + if strings.Contains(k, "geosite:") { + subkeys := strings.Split(k, ":") + subkeys = subkeys[1:] + subkeys = strings.Split(subkeys[0], ",") + for _, subkey := range subkeys { + newKey := "geosite:" + subkey + updatedPolicy[newKey] = v + } + } else if strings.Contains(k, "rule-set:") { + subkeys := strings.Split(k, ":") + subkeys = subkeys[1:] + subkeys = strings.Split(subkeys[0], ",") + for _, subkey := range subkeys { + newKey := "rule-set:" + subkey + updatedPolicy[newKey] = v + } + } else if re.MatchString(k) { + subkeys := strings.Split(k, ",") + for _, subkey := range subkeys { + updatedPolicy[subkey] = v + } } - nameservers, err = parseNameServer(servers, preferH3) - case reflect.String: - nameservers, err = parseNameServer([]string{fmt.Sprintf("%v", server)}, preferH3) - default: - return nil, errors.New("server format error, must be string or array") + } else { + updatedPolicy[k] = v } + } + + for domain, server := range updatedPolicy { + servers, err := utils.ToStringSlice(server) + if err != nil { + return nil, err + } + nameservers, err := parseNameServer(servers, preferH3) if err != nil { return nil, err } if _, valid := trie.ValidAndSplitDomain(domain); !valid { return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain) } + if strings.HasPrefix(domain, "rule-set:") { + domainSetName := domain[9:] + if provider, ok := ruleProviders[domainSetName]; !ok { + return nil, fmt.Errorf("not found rule-set: %s", domainSetName) + } else { + switch provider.Behavior() { + case providerTypes.IPCIDR: + return nil, fmt.Errorf("rule provider type error, except domain,actual %s", provider.Behavior()) + case providerTypes.Classical: + log.Warnln("%s provider is %s, only matching it contain domain rule", provider.Name(), provider.Behavior()) + } + } + } policy[domain] = nameservers } return policy, nil } -func parseFallbackIPCIDR(ips []string) ([]*netip.Prefix, error) { - var ipNets []*netip.Prefix +func parseFallbackIPCIDR(ips []string) ([]netip.Prefix, error) { + var ipNets []netip.Prefix for idx, ip := range ips { ipnet, err := netip.ParsePrefix(ip) if err != nil { return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error()) } - ipNets = append(ipNets, &ipnet) + ipNets = append(ipNets, ipnet) } return ipNets, nil @@ -1038,11 +1196,23 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM log.Infoln("Start initial GeoSite dns fallback filter `%s`, records: %d", country, recordsCount) } } - runtime.GC() return sites, nil } -func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.Rule) (*DNS, error) { +func paresNTP(rawCfg *RawConfig) *NTP { + cfg := rawCfg.NTP + ntpCfg := &NTP{ + Enable: cfg.Enable, + Server: cfg.Server, + Port: cfg.ServerPort, + Interval: cfg.Interval, + DialerProxy: cfg.DialerProxy, + WriteToSystem: cfg.WriteToSystem, + } + return ntpCfg +} + +func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rules []C.Rule, 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") @@ -1052,10 +1222,11 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R Enable: cfg.Enable, Listen: cfg.Listen, PreferH3: cfg.PreferH3, + IPv6Timeout: cfg.IPv6Timeout, IPv6: cfg.IPv6, EnhancedMode: cfg.EnhancedMode, FallbackFilter: FallbackFilter{ - IPCIDR: []*netip.Prefix{}, + IPCIDR: []netip.Prefix{}, GeoSite: []*router.DomainMatcher{}, }, } @@ -1068,7 +1239,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R return nil, err } - if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy, cfg.PreferH3); err != nil { + if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy, ruleProviders, cfg.PreferH3); err != nil { return nil, err } @@ -1084,6 +1255,9 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R } // 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) @@ -1126,7 +1300,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R } pool, err := fakeip.New(fakeip.Options{ - IPNet: &fakeIPRange, + IPNet: fakeIPRange, Size: 1000, Host: host, Persistence: rawCfg.Profile.StoreFakeIP, @@ -1189,21 +1363,24 @@ func parseTun(rawTun RawTun, general *General) error { AutoDetectInterface: rawTun.AutoDetectInterface, RedirectToTun: rawTun.RedirectToTun, - MTU: rawTun.MTU, - Inet4Address: []LC.ListenPrefix{LC.ListenPrefix(tunAddressPrefix)}, - Inet6Address: rawTun.Inet6Address, - StrictRoute: rawTun.StrictRoute, - Inet4RouteAddress: rawTun.Inet4RouteAddress, - Inet6RouteAddress: rawTun.Inet6RouteAddress, - IncludeUID: rawTun.IncludeUID, - IncludeUIDRange: rawTun.IncludeUIDRange, - ExcludeUID: rawTun.ExcludeUID, - ExcludeUIDRange: rawTun.ExcludeUIDRange, - IncludeAndroidUser: rawTun.IncludeAndroidUser, - IncludePackage: rawTun.IncludePackage, - ExcludePackage: rawTun.ExcludePackage, - EndpointIndependentNat: rawTun.EndpointIndependentNat, - UDPTimeout: rawTun.UDPTimeout, + MTU: rawTun.MTU, + Inet4Address: []netip.Prefix{tunAddressPrefix}, + Inet6Address: rawTun.Inet6Address, + StrictRoute: rawTun.StrictRoute, + Inet4RouteAddress: rawTun.Inet4RouteAddress, + Inet6RouteAddress: rawTun.Inet6RouteAddress, + Inet4RouteExcludeAddress: rawTun.Inet4RouteExcludeAddress, + Inet6RouteExcludeAddress: rawTun.Inet6RouteExcludeAddress, + IncludeUID: rawTun.IncludeUID, + IncludeUIDRange: rawTun.IncludeUIDRange, + ExcludeUID: rawTun.ExcludeUID, + ExcludeUIDRange: rawTun.ExcludeUIDRange, + IncludeAndroidUser: rawTun.IncludeAndroidUser, + IncludePackage: rawTun.IncludePackage, + ExcludePackage: rawTun.ExcludePackage, + EndpointIndependentNat: rawTun.EndpointIndependentNat, + UDPTimeout: rawTun.UDPTimeout, + FileDescriptor: rawTun.FileDescriptor, } return nil @@ -1214,6 +1391,7 @@ func parseTuicServer(rawTuic RawTuicServer, general *General) error { Enable: rawTuic.Enable, Listen: rawTuic.Listen, Token: rawTuic.Token, + Users: rawTuic.Users, Certificate: rawTuic.Certificate, PrivateKey: rawTuic.PrivateKey, CongestionController: rawTuic.CongestionController, @@ -1221,6 +1399,7 @@ func parseTuicServer(rawTuic RawTuicServer, general *General) error { AuthenticationTimeout: rawTuic.AuthenticationTimeout, ALPN: rawTuic.ALPN, MaxUdpRelayPacketSize: rawTuic.MaxUdpRelayPacketSize, + CWND: rawTuic.CWND, } return nil } @@ -1236,7 +1415,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { if len(snifferRaw.Sniff) != 0 { for sniffType, sniffConfig := range snifferRaw.Sniff { find := false - ports, err := parsePortRange(sniffConfig.Ports) + ports, err := utils.NewIntRangesFromList[uint16](sniffConfig.Ports) if err != nil { return nil, err } @@ -1259,9 +1438,11 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { } } } else { - // Deprecated: Use Sniff instead - log.Warnln("Deprecated: Use Sniff instead") - globalPorts, err := parsePortRange(snifferRaw.Ports) + if sniffer.Enable { + // Deprecated: Use Sniff instead + log.Warnln("Deprecated: Use Sniff instead") + } + globalPorts, err := utils.NewIntRangesFromList[uint16](snifferRaw.Ports) if err != nil { return nil, err } @@ -1285,48 +1466,24 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { } sniffer.Sniffers = loadSniffer - sniffer.ForceDomain = trie.New[struct{}]() - for _, domain := range snifferRaw.ForceDomain { - err := sniffer.ForceDomain.Insert(domain, struct{}{}) - if err != nil { - return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err) - } - } - sniffer.ForceDomain.Optimize() - sniffer.SkipDomain = trie.New[struct{}]() - for _, domain := range snifferRaw.SkipDomain { - err := sniffer.SkipDomain.Insert(domain, struct{}{}) + forceDomainTrie := trie.New[struct{}]() + for _, domain := range snifferRaw.ForceDomain { + err := forceDomainTrie.Insert(domain, struct{}{}) if err != nil { return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err) } } - sniffer.SkipDomain.Optimize() + sniffer.ForceDomain = forceDomainTrie.NewDomainSet() + + skipDomainTrie := trie.New[struct{}]() + for _, domain := range snifferRaw.SkipDomain { + err := skipDomainTrie.Insert(domain, struct{}{}) + if err != nil { + return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err) + } + } + sniffer.SkipDomain = skipDomainTrie.NewDomainSet() return sniffer, nil } - -func parsePortRange(portRanges []string) ([]utils.Range[uint16], error) { - ports := make([]utils.Range[uint16], 0) - for _, portRange := range portRanges { - portRaws := strings.Split(portRange, "-") - p, err := strconv.ParseUint(portRaws[0], 10, 16) - if err != nil { - return nil, fmt.Errorf("%s format error", portRange) - } - - start := uint16(p) - if len(portRaws) > 1 { - p, err = strconv.ParseUint(portRaws[1], 10, 16) - if err != nil { - return nil, fmt.Errorf("%s format error", portRange) - } - - end := uint16(p) - ports = append(ports, *utils.NewRange(start, end)) - } else { - ports = append(ports, *utils.NewRange(start, start)) - } - } - return ports, nil -} diff --git a/config/initial.go b/config/initial.go index 0921040d..61d12895 100644 --- a/config/initial.go +++ b/config/initial.go @@ -2,11 +2,10 @@ package config import ( "fmt" - "github.com/Dreamacro/clash/component/geodata" "os" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" ) // Init prepare necessary files @@ -28,23 +27,6 @@ func Init(dir string) error { f.Write([]byte(`mixed-port: 7890`)) f.Close() } - buf, _ := os.ReadFile(C.Path.Config()) - rawCfg, err := UnmarshalRawConfig(buf) - if err != nil { - log.Errorln(err.Error()) - fmt.Printf("configuration file %s test failed\n", C.Path.Config()) - os.Exit(1) - } - if !C.GeodataMode { - C.GeodataMode = rawCfg.GeodataMode - } - C.GeoIpUrl = rawCfg.GeoXUrl.GeoIp - C.GeoSiteUrl = rawCfg.GeoXUrl.GeoSite - C.MmdbUrl = rawCfg.GeoXUrl.Mmdb - // initial GeoIP - if err := geodata.InitGeoIP(); err != nil { - return fmt.Errorf("can't initial GeoIP: %w", err) - } return nil } diff --git a/config/updateGeo.go b/config/update_geo.go similarity index 72% rename from config/updateGeo.go rename to config/update_geo.go index a5f7b17b..718c2d07 100644 --- a/config/updateGeo.go +++ b/config/update_geo.go @@ -2,14 +2,13 @@ package config import ( "fmt" - "github.com/Dreamacro/clash/component/geodata" - _ "github.com/Dreamacro/clash/component/geodata/standard" - C "github.com/Dreamacro/clash/constant" - "github.com/oschwald/geoip2-golang" - "io" - "net/http" - "os" "runtime" + + "github.com/metacubex/mihomo/component/geodata" + _ "github.com/metacubex/mihomo/component/geodata/standard" + C "github.com/metacubex/mihomo/constant" + + "github.com/oschwald/maxminddb-golang" ) func UpdateGeoDatabases() error { @@ -39,7 +38,7 @@ func UpdateGeoDatabases() error { return fmt.Errorf("can't download MMDB database file: %w", err) } - instance, err := geoip2.FromBytes(data) + instance, err := maxminddb.FromBytes(data) if err != nil { return fmt.Errorf("invalid MMDB database file: %s", err) } @@ -63,19 +62,7 @@ func UpdateGeoDatabases() error { return fmt.Errorf("can't save GeoSite database file: %w", err) } + geodata.ClearCache() + return nil } - -func downloadForBytes(url string) ([]byte, error) { - resp, err := http.Get(url) - 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) -} diff --git a/config/update_ui.go b/config/update_ui.go new file mode 100644 index 00000000..e5596597 --- /dev/null +++ b/config/update_ui.go @@ -0,0 +1,145 @@ +package config + +import ( + "archive/zip" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + "sync" + + C "github.com/metacubex/mihomo/constant" +) + +var ( + ExternalUIURL string + ExternalUIPath string + ExternalUIFolder string + ExternalUIName string +) +var ( + ErrIncompleteConf = errors.New("ExternalUI configure incomplete") +) +var xdMutex sync.Mutex + +func UpdateUI() error { + xdMutex.Lock() + defer xdMutex.Unlock() + + err := prepare() + if err != nil { + return err + } + + data, err := downloadForBytes(ExternalUIURL) + if err != nil { + return fmt.Errorf("can't download file: %w", err) + } + + saved := path.Join(C.Path.HomeDir(), "download.zip") + if saveFile(data, saved) != nil { + return fmt.Errorf("can't save zip file: %w", err) + } + defer os.Remove(saved) + + err = cleanup(ExternalUIFolder) + if err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("cleanup exist file error: %w", err) + } + } + + unzipFolder, err := unzip(saved, C.Path.HomeDir()) + if err != nil { + return fmt.Errorf("can't extract zip file: %w", err) + } + + err = os.Rename(unzipFolder, ExternalUIFolder) + if err != nil { + return fmt.Errorf("can't rename folder: %w", err) + } + return nil +} + +func prepare() error { + if ExternalUIPath == "" || ExternalUIURL == "" { + return ErrIncompleteConf + } + + if ExternalUIName != "" { + ExternalUIFolder = filepath.Clean(path.Join(ExternalUIPath, ExternalUIName)) + if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) { + if err := os.MkdirAll(ExternalUIPath, os.ModePerm); err != nil { + return err + } + } + } else { + ExternalUIFolder = ExternalUIPath + } + + return nil +} + +func unzip(src, dest string) (string, error) { + r, err := zip.OpenReader(src) + if err != nil { + return "", err + } + defer r.Close() + var extractedFolder string + for _, f := range r.File { + fpath := filepath.Join(dest, 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 + } + if extractedFolder == "" { + extractedFolder = filepath.Dir(fpath) + } + } + return extractedFolder, nil +} + +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/config/utils.go b/config/utils.go index 2c470618..5a4fecbf 100644 --- a/config/utils.go +++ b/config/utils.go @@ -1,14 +1,37 @@ package config import ( + "context" "fmt" + "io" "net" + "net/http" + "net/netip" + "os" "strings" + "time" - "github.com/Dreamacro/clash/adapter/outboundgroup" - "github.com/Dreamacro/clash/common/structure" + "github.com/metacubex/mihomo/adapter/outboundgroup" + "github.com/metacubex/mihomo/common/structure" + mihomoHttp "github.com/metacubex/mihomo/component/http" ) +func downloadForBytes(url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {"mihomo"}}, 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) +} + func trimArr(arr []string) (r []string) { for _, e := range arr { r = append(r, strings.Trim(e, " ")) @@ -149,20 +172,11 @@ func proxyGroupsDagSort(groupsConfig []map[string]any) error { } func verifyIP6() bool { - addrs, err := net.InterfaceAddrs() - if err != nil { - return false - } - for _, addr := range addrs { - ipNet, isIpNet := addr.(*net.IPNet) - if isIpNet && !ipNet.IP.IsLoopback() { - if ipNet.IP.To16() != nil { - s := ipNet.IP.String() - for i := 0; i < len(s); i++ { - switch s[i] { - case ':': - return true - } + 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 } } } diff --git a/constant/adapters.go b/constant/adapters.go index bf5f7fdb..5cf6e07c 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -2,13 +2,16 @@ package constant import ( "context" + "errors" "fmt" "net" "net/netip" "sync" "time" - "github.com/Dreamacro/clash/component/dialer" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/dialer" ) // Adapter Type @@ -33,16 +36,20 @@ const ( Vless Trojan Hysteria + Hysteria2 WireGuard Tuic ) const ( - DefaultTCPTimeout = 5 * time.Second - DefaultUDPTimeout = DefaultTCPTimeout - DefaultTLSTimeout = DefaultTCPTimeout + DefaultTCPTimeout = 5 * time.Second + DefaultUDPTimeout = DefaultTCPTimeout + DefaultTLSTimeout = DefaultTCPTimeout + DefaultMaxHealthCheckUrlNum = 16 ) +var ErrNotSupport = errors.New("no support") + type Connection interface { Chains() Chain AppendToChains(adapter ProxyAdapter) @@ -72,12 +79,12 @@ func (c Chain) Last() string { } type Conn interface { - net.Conn + N.ExtendedConn Connection } type PacketConn interface { - net.PacketConn + N.EnhancePacketConn Connection // Deprecate WriteWithMetadata because of remote resolve DNS cause TURN failed // WriteWithMetadata(p []byte, metadata *Metadata) (n int, err error) @@ -102,11 +109,11 @@ type ProxyAdapter interface { // // Examples: // conn, _ := net.DialContext(context.Background(), "tcp", "host:port") - // conn, _ = adapter.StreamConn(conn, metadata) + // conn, _ = adapter.StreamConnContext(context.Background(), conn, metadata) // // It returns a C.Conn with protocol which start with // a new session (if any) - StreamConn(c net.Conn, metadata *Metadata) (net.Conn, error) + 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) @@ -116,16 +123,19 @@ type ProxyAdapter interface { // SupportUOT return UDP over TCP support SupportUOT() bool - SupportWithDialer() 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 } type Group interface { - URLTest(ctx context.Context, url string) (mp map[string]uint16, err error) + URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error) GetProxies(touch bool) []Proxy Touch() } @@ -135,12 +145,23 @@ type DelayHistory struct { Delay uint16 `json:"delay"` } +type DelayHistoryStoreType int + +const ( + OriginalHistory DelayHistoryStoreType = iota + ExtraHistory + DropHistory +) + type Proxy interface { ProxyAdapter Alive() bool + AliveForTestUrl(url string) bool DelayHistory() []DelayHistory + ExtraDelayHistory() map[string][]DelayHistory LastDelay() uint16 - URLTest(ctx context.Context, url string) (uint16, error) + LastDelayForTestUrl(url string) uint16 + URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], store DelayHistoryStoreType) (uint16, error) // Deprecated: use DialContext instead. Dial(metadata *Metadata) (Conn, error) @@ -180,6 +201,8 @@ func (at AdapterType) String() string { return "Trojan" case Hysteria: return "Hysteria" + case Hysteria2: + return "Hysteria2" case WireGuard: return "WireGuard" case Tuic: @@ -210,7 +233,7 @@ type UDPPacket interface { // - variable source IP/Port is important to STUN // - if addr is not provided, WriteBack will write out UDP packet with SourceIP/Port equals to original Target, // this is important when using Fake-IP. - WriteBack(b []byte, addr net.Addr) (n int, err error) + WriteBack // Drop call after packet is used, could recycle buffer in this function. Drop() @@ -229,22 +252,52 @@ type PacketAdapter interface { Metadata() *Metadata } -type NatTable interface { - Set(key string, e PacketConn) +type packetAdapter struct { + UDPPacket + metadata *Metadata +} - Get(key string) PacketConn +// Metadata returns destination metadata +func (s *packetAdapter) Metadata() *Metadata { + return s.metadata +} + +func NewPacketAdapter(packet UDPPacket, metadata *Metadata) PacketAdapter { + return &packetAdapter{ + packet, + metadata, + } +} + +type WriteBack interface { + WriteBack(b []byte, addr net.Addr) (n int, err error) +} + +type WriteBackProxy interface { + WriteBack + UpdateWriteBack(wb WriteBack) +} + +type NatTable interface { + Set(key string, e PacketConn, w WriteBackProxy) + + Get(key string) (PacketConn, WriteBackProxy) GetOrCreateLock(key string) (*sync.Cond, bool) Delete(key string) - GetLocalConn(lAddr, rAddr string) *net.UDPConn + DeleteLock(key string) - AddLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool + GetForLocalConn(lAddr, rAddr string) *net.UDPConn - RangeLocalConn(lAddr string, f func(key, value any) bool) + AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool - GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool) + RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool) - DeleteLocalConnMap(lAddr, key string) + 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 index e641ed14..11ad7011 100644 --- a/constant/context.go +++ b/constant/context.go @@ -3,7 +3,9 @@ package constant import ( "net" - "github.com/gofrs/uuid" + N "github.com/metacubex/mihomo/common/net" + + "github.com/gofrs/uuid/v5" ) type PlainContext interface { @@ -13,7 +15,7 @@ type PlainContext interface { type ConnContext interface { PlainContext Metadata() *Metadata - Conn() net.Conn + Conn() *N.BufferedConn } type PacketConnContext interface { diff --git a/constant/ebpf.go b/constant/ebpf.go index b722dce1..e3bb62fe 100644 --- a/constant/ebpf.go +++ b/constant/ebpf.go @@ -3,14 +3,14 @@ package constant import ( "net/netip" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/transport/socks5" ) const ( - BpfFSPath = "/sys/fs/bpf/clash" + BpfFSPath = "/sys/fs/bpf/mihomo" - TcpAutoRedirPort = 't'<<8 | 'r'<<0 - ClashTrafficMark = 'c'<<24 | 'l'<<16 | 't'<<8 | 'm'<<0 + TcpAutoRedirPort = 't'<<8 | 'r'<<0 + MihomoTrafficMark = 'c'<<24 | 'l'<<16 | 't'<<8 | 'm'<<0 ) type EBpf interface { diff --git a/constant/features/low_memory.go b/constant/features/low_memory.go new file mode 100644 index 00000000..0d252113 --- /dev/null +++ b/constant/features/low_memory.go @@ -0,0 +1,6 @@ +//go:build with_low_memory +package features + +func init() { + TAGS = append(TAGS, "with_low_memory") +} diff --git a/constant/features/no_doq.go b/constant/features/no_doq.go deleted file mode 100644 index c915272f..00000000 --- a/constant/features/no_doq.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build no_doq - -package features - -func init() { - TAGS = append(TAGS, "no_doq") -} diff --git a/constant/features/no_fake_tcp.go b/constant/features/no_fake_tcp.go new file mode 100644 index 00000000..f536a066 --- /dev/null +++ b/constant/features/no_fake_tcp.go @@ -0,0 +1,7 @@ +//go:build no_fake_tcp + +package features + +func init() { + TAGS = append(TAGS, "no_fake_tcp") +} diff --git a/constant/features/no_gvisor.go b/constant/features/no_gvisor.go deleted file mode 100644 index d0d5391a..00000000 --- a/constant/features/no_gvisor.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build no_gvisor - -package features - -func init() { - TAGS = append(TAGS, "no_gvisor") -} diff --git a/constant/features/with_gvisor.go b/constant/features/with_gvisor.go new file mode 100644 index 00000000..1b3417b3 --- /dev/null +++ b/constant/features/with_gvisor.go @@ -0,0 +1,7 @@ +//go:build with_gvisor + +package features + +func init() { + TAGS = append(TAGS, "with_gvisor") +} diff --git a/constant/http.go b/constant/http.go new file mode 100644 index 00000000..8e321f6b --- /dev/null +++ b/constant/http.go @@ -0,0 +1,5 @@ +package constant + +var ( + UA string +) diff --git a/constant/listener.go b/constant/listener.go index 6f9f169b..f69b4a9b 100644 --- a/constant/listener.go +++ b/constant/listener.go @@ -16,7 +16,7 @@ type MultiAddrListener interface { type InboundListener interface { Name() string - Listen(tcpIn chan<- ConnContext, udpIn chan<- PacketAdapter, natTable NatTable) error + Listen(tunnel Tunnel) error Close() error Address() string RawAddress() string diff --git a/constant/metadata.go b/constant/metadata.go index 599a6055..4b547a81 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -7,7 +7,7 @@ import ( "net/netip" "strconv" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/transport/socks5" ) // Socks addr type @@ -15,7 +15,10 @@ const ( TCP NetWork = iota UDP ALLNet + InvalidNet = 0xff +) +const ( HTTP Type = iota HTTPS SOCKS4 @@ -27,18 +30,23 @@ const ( TUNNEL TUN TUIC + HYSTERIA2 INNER ) type NetWork int func (n NetWork) String() string { - if n == TCP { + switch n { + case TCP: return "tcp" - } else if n == UDP { + case UDP: return "udp" + case ALLNet: + return "all" + default: + return "invalid" } - return "all" } func (n NetWork) MarshalJSON() ([]byte, error) { @@ -71,6 +79,8 @@ func (t Type) String() string { return "Tun" case TUIC: return "Tuic" + case HYSTERIA2: + return "Hysteria2" case INNER: return "Inner" default: @@ -103,6 +113,8 @@ func ParseType(t string) (*Type, error) { res = TUN case "TUIC": res = TUIC + case "HYSTERIA2": + res = HYSTERIA2 case "INNER": res = INNER default: @@ -121,11 +133,12 @@ type Metadata struct { Type Type `json:"type"` SrcIP netip.Addr `json:"sourceIP"` DstIP netip.Addr `json:"destinationIP"` - SrcPort string `json:"sourcePort"` - DstPort string `json:"destinationPort"` + 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 string `json:"inboundPort"` + 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"` @@ -139,16 +152,16 @@ type Metadata struct { } func (m *Metadata) RemoteAddress() string { - return net.JoinHostPort(m.String(), m.DstPort) + return net.JoinHostPort(m.String(), strconv.FormatUint(uint64(m.DstPort), 10)) } func (m *Metadata) SourceAddress() string { - return net.JoinHostPort(m.SrcIP.String(), m.SrcPort) + return net.JoinHostPort(m.SrcIP.String(), strconv.FormatUint(uint64(m.SrcPort), 10)) } func (m *Metadata) SourceDetail() string { if m.Type == INNER { - return fmt.Sprintf("%s", ClashName) + return fmt.Sprintf("%s", MihomoName) } switch { @@ -163,6 +176,10 @@ func (m *Metadata) SourceDetail() string { } } +func (m *Metadata) SourceValid() bool { + return m.SrcPort != 0 && m.SrcIP.IsValid() +} + func (m *Metadata) AddrType() int { switch true { case m.Host != "" || !m.DstIP.IsValid(): @@ -198,15 +215,15 @@ func (m *Metadata) Pure() *Metadata { 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 } - port, _ := strconv.ParseUint(m.DstPort, 10, 16) - return &net.UDPAddr{ - IP: m.DstIP.AsSlice(), - Port: int(port), - } + return net.UDPAddrFromAddrPort(m.AddrPort()) } func (m *Metadata) String() string { @@ -222,3 +239,54 @@ func (m *Metadata) String() string { 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 +} diff --git a/constant/path.go b/constant/path.go index 29ac9872..a920fbbc 100644 --- a/constant/path.go +++ b/constant/path.go @@ -1,13 +1,16 @@ package constant import ( + "crypto/md5" + "encoding/hex" "os" P "path" "path/filepath" + "strconv" "strings" ) -const Name = "clash" +const Name = "mihomo" var ( GeositeName = "GeoSite.dat" @@ -15,19 +18,30 @@ var ( ) // 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) - return &path{homeDir: homeDir, configFile: "config.yaml"} + + if _, err = os.Stat(homeDir); err != nil { + if configHome, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok { + homeDir = P.Join(configHome, Name) + } + } + + return &path{homeDir: homeDir, configFile: "config.yaml", allowUnsafePath: allowUnsafePath} }() type path struct { - homeDir string - configFile string + homeDir string + configFile string + allowUnsafePath bool } // SetHomeDir is used to set the configuration path @@ -56,6 +70,27 @@ func (p *path) Resolve(path string) string { return path } +// IsSafePath return true if path is a subpath of homedir +func (p *path) IsSafePath(path string) bool { + if p.allowUnsafePath { + return true + } + homedir := p.HomeDir() + path = p.Resolve(path) + rel, err := filepath.Rel(homedir, path) + if err != nil { + return false + } + + return !strings.Contains(rel, "..") +} + +func (p *path) GetPathByHash(prefix, name string) string { + hash := md5.Sum([]byte(name)) + filename := hex.EncodeToString(hash[:]) + return filepath.Join(p.HomeDir(), prefix, filename) +} + func (p *path) MMDB() string { files, err := os.ReadDir(p.homeDir) if err != nil { @@ -66,13 +101,15 @@ func (p *path) MMDB() string { // 目录则直接跳过 continue } else { - if strings.EqualFold(fi.Name(), "Country.mmdb") { + 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, "Country.mmdb") + return P.Join(p.homeDir, "geoip.metadb") } func (p *path) OldCache() string { @@ -128,7 +165,7 @@ func (p *path) GetAssetLocation(file string) string { func (p *path) GetExecutableFullPath() string { exePath, err := os.Executable() if err != nil { - return "clash" + return "mihomo" } res, _ := filepath.EvalSymlinks(exePath) return res diff --git a/constant/provider/interface.go b/constant/provider/interface.go index b42bbe71..809db9c5 100644 --- a/constant/provider/interface.go +++ b/constant/provider/interface.go @@ -1,7 +1,8 @@ package provider import ( - "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/constant" ) // Vehicle Type @@ -71,19 +72,30 @@ type ProxyProvider interface { Touch() HealthCheck() Version() uint32 + RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) } -// Rule Type +// RuleProvider interface +type RuleProvider interface { + Provider + Behavior() RuleBehavior + Match(*constant.Metadata) bool + ShouldResolveIP() bool + ShouldFindProcess() bool + AsRule(adaptor string) constant.Rule +} + +// Rule Behavior const ( - Domain RuleType = iota + Domain RuleBehavior = iota IPCIDR Classical ) -// RuleType defined -type RuleType int +// RuleBehavior defined +type RuleBehavior int -func (rt RuleType) String() string { +func (rt RuleBehavior) String() string { switch rt { case Domain: return "Domain" @@ -96,12 +108,20 @@ func (rt RuleType) String() string { } } -// RuleProvider interface -type RuleProvider interface { - Provider - Behavior() RuleType - Match(*constant.Metadata) bool - ShouldResolveIP() bool - ShouldFindProcess() bool - AsRule(adaptor string) constant.Rule +const ( + YamlRule RuleFormat = iota + TextRule +) + +type RuleFormat int + +func (rf RuleFormat) String() string { + switch rf { + case YamlRule: + return "YamlRule" + case TextRule: + return "TextRule" + default: + return "Unknown" + } } diff --git a/constant/rule.go b/constant/rule.go index 28c629a0..906f3cef 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -14,12 +14,14 @@ const ( SrcPort DstPort InPort + InUser + InName + InType Process ProcessPath RuleSet Network Uid - INTYPE SubRules MATCH AND @@ -55,6 +57,12 @@ func (rt RuleType) String() string { return "DstPort" case InPort: return "InPort" + case InUser: + return "InUser" + case InName: + return "InName" + case InType: + return "InType" case Process: return "Process" case ProcessPath: @@ -67,8 +75,6 @@ func (rt RuleType) String() string { return "Network" case Uid: return "Uid" - case INTYPE: - return "InType" case SubRules: return "SubRules" case AND: diff --git a/constant/rule_extra.go b/constant/rule_extra.go index 3c5de5d5..62dc1cc3 100644 --- a/constant/rule_extra.go +++ b/constant/rule_extra.go @@ -1,7 +1,7 @@ package constant import ( - "github.com/Dreamacro/clash/component/geodata/router" + "github.com/metacubex/mihomo/component/geodata/router" ) type RuleGeoSite interface { diff --git a/constant/sniffer/sniffer.go b/constant/sniffer/sniffer.go index 6b20b3f6..36da69a3 100644 --- a/constant/sniffer/sniffer.go +++ b/constant/sniffer/sniffer.go @@ -1,10 +1,11 @@ package sniffer -import "github.com/Dreamacro/clash/constant" +import "github.com/metacubex/mihomo/constant" type Sniffer interface { SupportNetwork() constant.NetWork - SniffTCP(bytes []byte) (string, error) + // SniffData must not change input bytes + SniffData(bytes []byte) (string, error) Protocol() string SupportPort(port uint16) bool } @@ -12,10 +13,11 @@ type Sniffer interface { const ( TLS Type = iota HTTP + QUIC ) var ( - List = []Type{TLS, HTTP} + List = []Type{TLS, HTTP, QUIC} ) type Type int @@ -26,6 +28,8 @@ func (rt Type) String() string { return "TLS" case HTTP: return "HTTP" + case QUIC: + return "QUIC" default: return "Unknown" } diff --git a/constant/tun.go b/constant/tun.go index 38f51155..5e2841bc 100644 --- a/constant/tun.go +++ b/constant/tun.go @@ -10,12 +10,14 @@ var StackTypeMapping = map[string]TUNStack{ strings.ToLower(TunGvisor.String()): TunGvisor, strings.ToLower(TunSystem.String()): TunSystem, strings.ToLower(TunLWIP.String()): TunLWIP, + strings.ToLower(TunMixed.String()): TunMixed, } const ( TunGvisor TUNStack = iota TunSystem TunLWIP + TunMixed ) type TUNStack int @@ -64,6 +66,8 @@ func (e TUNStack) String() string { return "System" case TunLWIP: return "LWIP" + case TunMixed: + return "Mixed" default: return "unknown" } diff --git a/constant/tunnel.go b/constant/tunnel.go new file mode 100644 index 00000000..7c9d08e2 --- /dev/null +++ b/constant/tunnel.go @@ -0,0 +1,12 @@ +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 index cbb7ab61..c71024c2 100644 --- a/constant/version.go +++ b/constant/version.go @@ -1,8 +1,8 @@ package constant var ( - Meta = true - Version = "1.10.0" - BuildTime = "unknown time" - ClashName = "clash.meta" + Meta = true + Version = "1.10.0" + BuildTime = "unknown time" + MihomoName = "mihomo" ) diff --git a/context/conn.go b/context/conn.go index 08bbe3c7..bae07c23 100644 --- a/context/conn.go +++ b/context/conn.go @@ -1,25 +1,24 @@ package context import ( + "github.com/metacubex/mihomo/common/utils" "net" - N "github.com/Dreamacro/clash/common/net" - C "github.com/Dreamacro/clash/constant" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" - "github.com/gofrs/uuid" + "github.com/gofrs/uuid/v5" ) type ConnContext struct { id uuid.UUID metadata *C.Metadata - conn net.Conn + conn *N.BufferedConn } func NewConnContext(conn net.Conn, metadata *C.Metadata) *ConnContext { - id, _ := uuid.NewV4() - return &ConnContext{ - id: id, + id: utils.NewUUIDV4(), metadata: metadata, conn: N.NewBufferedConn(conn), } @@ -36,6 +35,6 @@ func (c *ConnContext) Metadata() *C.Metadata { } // Conn implement C.ConnContext Conn -func (c *ConnContext) Conn() net.Conn { +func (c *ConnContext) Conn() *N.BufferedConn { return c.conn } diff --git a/context/dns.go b/context/dns.go index 59130961..1cc2067d 100644 --- a/context/dns.go +++ b/context/dns.go @@ -2,8 +2,9 @@ package context import ( "context" + "github.com/metacubex/mihomo/common/utils" - "github.com/gofrs/uuid" + "github.com/gofrs/uuid/v5" "github.com/miekg/dns" ) @@ -22,11 +23,10 @@ type DNSContext struct { } func NewDNSContext(ctx context.Context, msg *dns.Msg) *DNSContext { - id, _ := uuid.NewV4() return &DNSContext{ Context: ctx, - id: id, + id: utils.NewUUIDV4(), msg: msg, } } diff --git a/context/packetconn.go b/context/packetconn.go index 3b005141..feab7666 100644 --- a/context/packetconn.go +++ b/context/packetconn.go @@ -3,9 +3,10 @@ package context import ( "net" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/common/utils" + C "github.com/metacubex/mihomo/constant" - "github.com/gofrs/uuid" + "github.com/gofrs/uuid/v5" ) type PacketConnContext struct { @@ -15,9 +16,8 @@ type PacketConnContext struct { } func NewPacketConnContext(metadata *C.Metadata) *PacketConnContext { - id, _ := uuid.NewV4() return &PacketConnContext{ - id: id, + id: utils.NewUUIDV4(), metadata: metadata, } } diff --git a/dns/client.go b/dns/client.go index c5a52281..95f0f29b 100644 --- a/dns/client.go +++ b/dns/client.go @@ -4,18 +4,17 @@ import ( "context" "crypto/tls" "fmt" - "math/rand" "net" "net/netip" "strings" - tlsC "github.com/Dreamacro/clash/component/tls" - "go.uber.org/atomic" - - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" + "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/resolver" + C "github.com/metacubex/mihomo/constant" D "github.com/miekg/dns" + "github.com/zhangyunhao116/fastrand" ) type client struct { @@ -23,8 +22,9 @@ type client struct { r *Resolver port string host string - iface *atomic.String - proxyAdapter string + iface string + proxyAdapter C.ProxyAdapter + proxyName string addr string } @@ -47,10 +47,6 @@ func (c *client) Address() string { return c.addr } -func (c *client) Exchange(m *D.Msg) (*D.Msg, error) { - return c.ExchangeContext(context.Background(), m) -} - func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { var ( ip netip.Addr @@ -68,7 +64,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) } else if len(ips) == 0 { return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, c.host) } - ip = ips[rand.Intn(len(ips))] + ip = ips[fastrand.Intn(len(ips))] } network := "udp" @@ -76,12 +72,12 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) network = "tcp" } - options := []dialer.Option{} - if c.iface != nil && c.iface.Load() != "" { - options = append(options, dialer.WithInterface(c.iface.Load())) + var options []dialer.Option + if c.iface != "" { + options = append(options, dialer.WithInterface(c.iface)) } - conn, err := getDialHandler(c.r, c.proxyAdapter, options...)(ctx, network, net.JoinHostPort(ip.String(), c.port)) + conn, err := getDialHandler(c.r, c.proxyAdapter, c.proxyName, options...)(ctx, network, net.JoinHostPort(ip.String(), c.port)) if err != nil { return nil, err } @@ -98,7 +94,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) ch := make(chan result, 1) go func() { if strings.HasSuffix(c.Client.Net, "tls") { - conn = tls.Client(conn, tlsC.GetGlobalTLSConfig(c.Client.TLSConfig)) + conn = tls.Client(conn, ca.GetGlobalTLSConfig(c.Client.TLSConfig)) } msg, _, err := c.Client.ExchangeWithConn(m, &D.Conn{ diff --git a/dns/dhcp.go b/dns/dhcp.go index 151e4421..dc1344f5 100644 --- a/dns/dhcp.go +++ b/dns/dhcp.go @@ -8,12 +8,8 @@ import ( "sync" "time" - "go.uber.org/atomic" - - "github.com/Dreamacro/clash/component/dhcp" - "github.com/Dreamacro/clash/component/iface" - "github.com/Dreamacro/clash/component/resolver" - + "github.com/metacubex/mihomo/component/dhcp" + "github.com/metacubex/mihomo/component/iface" D "github.com/miekg/dns" ) @@ -30,7 +26,7 @@ type dhcpClient struct { ifaceInvalidate time.Time dnsInvalidate time.Time - ifaceAddr *netip.Prefix + ifaceAddr netip.Prefix done chan struct{} clients []dnsClient err error @@ -47,20 +43,14 @@ func (d *dhcpClient) Address() string { return strings.Join(addrs, ",") } -func (d *dhcpClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { - ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) - defer cancel() - - return d.ExchangeContext(ctx, m) -} - func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { clients, err := d.resolve(ctx) if err != nil { return nil, err } - return batchExchange(ctx, clients, m) + msg, _, err = batchExchange(ctx, clients, m) + return } func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) { @@ -86,7 +76,7 @@ func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) { for _, item := range dns { nameserver = append(nameserver, NameServer{ Addr: net.JoinHostPort(item.String(), "53"), - Interface: atomic.NewString(d.ifaceName), + Interface: d.ifaceName, }) } diff --git a/dns/doh.go b/dns/doh.go index 1e6528d9..9e173c84 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -15,12 +15,13 @@ import ( "sync" "time" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/component/ca" + 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" ) @@ -63,7 +64,8 @@ type dnsOverHTTPS struct { url *url.URL r *Resolver httpVersions []C.HTTPVersion - proxyAdapter string + proxyAdapter C.ProxyAdapter + proxyName string addr string } @@ -71,7 +73,7 @@ type dnsOverHTTPS struct { var _ dnsClient = (*dnsOverHTTPS)(nil) // newDoH returns the DNS-over-HTTPS Upstream. -func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[string]string, proxyAdapter string) dnsClient { +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 { @@ -87,6 +89,7 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin addr: u.String(), r: r, proxyAdapter: proxyAdapter, + proxyName: proxyName, quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, TokenStore: newQUICTokenStore(), @@ -154,11 +157,6 @@ func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D. return msg, err } -// Exchange implements the Upstream interface for *dnsOverHTTPS. -func (doh *dnsOverHTTPS) Exchange(m *D.Msg) (*D.Msg, error) { - return doh.ExchangeContext(context.Background(), m) -} - // Close implements the Upstream interface for *dnsOverHTTPS. func (doh *dnsOverHTTPS) Close() (err error) { doh.clientMu.Lock() @@ -379,7 +377,7 @@ func (doh *dnsOverHTTPS) createClient(ctx context.Context) (*http.Client, error) // 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) { - tlsConfig := tlsC.GetGlobalTLSConfig( + tlsConfig := ca.GetGlobalTLSConfig( &tls.Config{ InsecureSkipVerify: false, MinVersion: tls.VersionTLS12, @@ -390,14 +388,17 @@ func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripp nextProtos = append(nextProtos, string(v)) } tlsConfig.NextProtos = nextProtos - dialContext := getDialHandler(doh.r, doh.proxyAdapter) - // 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, dialContext) - if err == nil { - log.Debugln("[%s] using HTTP/3 for this upstream: QUIC was faster", doh.url.String()) - return transportH3, nil + dialContext := getDialHandler(doh.r, doh.proxyAdapter, doh.proxyName) + + 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, dialContext) + 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) @@ -533,11 +534,21 @@ func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls. IP: net.ParseIP(ip), Port: portInt, } - conn, err := listenPacket(ctx, doh.proxyAdapter, "udp", addr, doh.r) + conn, err := listenPacket(ctx, doh.proxyAdapter, doh.proxyName, "udp", addr, doh.r) if err != nil { return nil, err } - return quic.DialEarlyContext(ctx, conn, &udpAddr, doh.url.Host, tlsCfg, cfg) + 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 diff --git a/dns/doq.go b/dns/doq.go index 1354f177..70b67c2a 100644 --- a/dns/doq.go +++ b/dns/doq.go @@ -12,10 +12,11 @@ import ( "sync" "time" - tlsC "github.com/Dreamacro/clash/component/tls" + "github.com/metacubex/mihomo/component/ca" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" "github.com/metacubex/quic-go" - "github.com/Dreamacro/clash/log" D "github.com/miekg/dns" ) @@ -60,7 +61,8 @@ type dnsOverQUIC struct { bytesPoolGuard sync.Mutex addr string - proxyAdapter string + proxyAdapter C.ProxyAdapter + proxyName string r *Resolver } @@ -68,10 +70,11 @@ type dnsOverQUIC struct { var _ dnsClient = (*dnsOverQUIC)(nil) // newDoQ returns the DNS-over-QUIC Upstream. -func newDoQ(resolver *Resolver, addr string, adapter string) (dnsClient, error) { +func newDoQ(resolver *Resolver, addr string, proxyAdapter C.ProxyAdapter, proxyName string) (dnsClient, error) { doq := &dnsOverQUIC{ addr: addr, - proxyAdapter: adapter, + proxyAdapter: proxyAdapter, + proxyName: proxyName, r: resolver, quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, @@ -131,11 +134,6 @@ func (doq *dnsOverQUIC) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.M return msg, err } -// Exchange implements the Upstream interface for *dnsOverQUIC. -func (doq *dnsOverQUIC) Exchange(m *D.Msg) (msg *D.Msg, err error) { - return doq.ExchangeContext(context.Background(), m) -} - // Close implements the Upstream interface for *dnsOverQUIC. func (doq *dnsOverQUIC) Close() (err error) { doq.connMu.Lock() @@ -299,18 +297,10 @@ func (doq *dnsOverQUIC) openStream(ctx context.Context, conn quic.Connection) (q // openConnection opens a new QUIC connection. func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connection, err error) { - tlsConfig := tlsC.GetGlobalTLSConfig( - &tls.Config{ - InsecureSkipVerify: false, - NextProtos: []string{ - NextProtoDQ, - }, - SessionTicketsDisabled: false, - }) // 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 := getDialHandler(doq.r, doq.proxyAdapter)(ctx, "udp", doq.addr) + rawConn, err := getDialHandler(doq.r, doq.proxyAdapter, doq.proxyName)(ctx, "udp", doq.addr) if err != nil { return nil, fmt.Errorf("failed to open a QUIC connection: %w", err) } @@ -325,7 +315,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connectio p, err := strconv.Atoi(port) udpAddr := net.UDPAddr{IP: net.ParseIP(ip), Port: p} - udp, err := listenPacket(ctx, doq.proxyAdapter, "udp", addr, doq.r) + udp, err := listenPacket(ctx, doq.proxyAdapter, doq.proxyName, "udp", addr, doq.r) if err != nil { return nil, err } @@ -335,7 +325,20 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connectio return nil, err } - conn, err = quic.DialContext(ctx, udp, &udpAddr, host, tlsConfig, doq.getQUICConfig()) + 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, tlsConfig, doq.getQUICConfig()) if err != nil { return nil, fmt.Errorf("opening quic connection to %s: %w", doq.addr, err) } diff --git a/dns/enhancer.go b/dns/enhancer.go index 76d4460e..82fdd35a 100644 --- a/dns/enhancer.go +++ b/dns/enhancer.go @@ -3,9 +3,9 @@ package dns import ( "net/netip" - "github.com/Dreamacro/clash/common/cache" - "github.com/Dreamacro/clash/component/fakeip" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/component/fakeip" + C "github.com/metacubex/mihomo/constant" ) type ResolverEnhancer struct { @@ -109,7 +109,7 @@ func NewEnhancer(cfg Config) *ResolverEnhancer { if cfg.EnhancedMode != C.DNSNormal { fakePool = cfg.Pool - mapping = cache.New(cache.WithSize[netip.Addr, string](4096), cache.WithStale[netip.Addr, string](true)) + mapping = cache.New(cache.WithSize[netip.Addr, string](4096)) } return &ResolverEnhancer{ diff --git a/dns/filters.go b/dns/filters.go index b51e6402..8eb1e48e 100644 --- a/dns/filters.go +++ b/dns/filters.go @@ -2,14 +2,14 @@ package dns import ( "net/netip" - - "github.com/Dreamacro/clash/component/geodata" - "github.com/Dreamacro/clash/component/geodata/router" - "github.com/Dreamacro/clash/component/mmdb" - "github.com/Dreamacro/clash/component/trie" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" "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/trie" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" ) type fallbackIPFilter interface { @@ -24,34 +24,20 @@ var geoIPMatcher *router.GeoIPMatcher func (gf *geoipFilter) Match(ip netip.Addr) bool { if !C.GeodataMode { - record, _ := mmdb.Instance().Country(ip.AsSlice()) - return !strings.EqualFold(record.Country.IsoCode, gf.code) && !ip.IsPrivate() + codes := mmdb.Instance().LookupCode(ip.AsSlice()) + for _, code := range codes { + if !strings.EqualFold(code, gf.code) && !ip.IsPrivate() { + return true + } + } + return false } if geoIPMatcher == nil { - countryCode := "cn" - geoLoader, err := geodata.GetGeoDataLoader(geodata.LoaderName()) + var err error + geoIPMatcher, _, err = geodata.LoadGeoIPMatcher("CN") if err != nil { - log.Errorln("[GeoIPFilter] GetGeoDataLoader error: %s", err.Error()) - return false - } - - records, err := geoLoader.LoadGeoIP(countryCode) - if err != nil { - log.Errorln("[GeoIPFilter] LoadGeoIP error: %s", err.Error()) - return false - } - - geoIP := &router.GeoIP{ - CountryCode: countryCode, - Cidr: records, - ReverseMatch: false, - } - - geoIPMatcher, err = router.NewGeoIPMatcher(geoIP) - - if err != nil { - log.Errorln("[GeoIPFilter] NewGeoIPMatcher error: %s", err.Error()) + log.Errorln("[GeoIPFilter] LoadGeoIPMatcher error: %s", err.Error()) return false } } @@ -59,7 +45,7 @@ func (gf *geoipFilter) Match(ip netip.Addr) bool { } type ipnetFilter struct { - ipnet *netip.Prefix + ipnet netip.Prefix } func (inf *ipnetFilter) Match(ip netip.Addr) bool { @@ -92,6 +78,10 @@ type geoSiteFilter struct { } func NewGeoSite(group string) (fallbackDomainFilter, error) { + if err := geodata.InitGeoSite(); err != nil { + log.Errorln("can't initial GeoSite: %s", err) + return nil, err + } matcher, _, err := geodata.LoadGeoSiteMatcher(group) if err != nil { return nil, err diff --git a/dns/patch.go b/dns/local.go similarity index 100% rename from dns/patch.go rename to dns/local.go diff --git a/dns/middleware.go b/dns/middleware.go index 7dc9622d..f8e051a0 100644 --- a/dns/middleware.go +++ b/dns/middleware.go @@ -5,13 +5,13 @@ import ( "strings" "time" - "github.com/Dreamacro/clash/common/cache" - "github.com/Dreamacro/clash/common/nnip" - "github.com/Dreamacro/clash/component/fakeip" - "github.com/Dreamacro/clash/component/trie" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/context" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/common/nnip" + "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" ) @@ -21,7 +21,7 @@ type ( middleware func(next handler) handler ) -func withHosts(hosts *trie.DomainTrie[netip.Addr], mapping *cache.LruCache[netip.Addr, string]) middleware { +func withHosts(hosts R.Hosts, mapping *cache.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] @@ -31,40 +31,68 @@ func withHosts(hosts *trie.DomainTrie[netip.Addr], mapping *cache.LruCache[netip } host := strings.TrimRight(q.Name, ".") - - record := hosts.Search(host) - if record == nil { + 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) } - ip := record.Data() msg := r.Copy() - - if ip.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 = ip.AsSlice() - - msg.Answer = []D.RR{rr} - } 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 := ip.As16() - rr.AAAA = ip[:] - msg.Answer = []D.RR{rr} - } else { - return next(ctx, r) + 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)) + } + } + } } - if mapping != nil { - mapping.SetWithExpire(ip, 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 } } @@ -101,6 +129,10 @@ func withMapping(mapping *cache.LruCache[netip.Addr, string]) middleware { continue } + if ttl < 1 { + ttl = 1 + } + mapping.SetWithExpire(ip, host, time.Now().Add(time.Second*time.Duration(ttl))) } @@ -149,6 +181,7 @@ func withFakeIP(fakePool *fakeip.Pool) middleware { 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 @@ -183,7 +216,7 @@ func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler { middlewares := []middleware{} if resolver.hosts != nil { - middlewares = append(middlewares, withHosts(resolver.hosts, mapper.mapping)) + middlewares = append(middlewares, withHosts(R.NewHosts(resolver.hosts), mapper.mapping)) } if mapper.mode == C.DNSFakeIP { diff --git a/dns/rcode.go b/dns/rcode.go new file mode 100644 index 00000000..9777d2e7 --- /dev/null +++ b/dns/rcode.go @@ -0,0 +1,50 @@ +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 +} diff --git a/dns/resolver.go b/dns/resolver.go index ac8917ca..610a06f0 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -3,28 +3,25 @@ package dns import ( "context" "errors" - "fmt" - "math/rand" "net/netip" "strings" "time" - "go.uber.org/atomic" - - "github.com/Dreamacro/clash/common/cache" - "github.com/Dreamacro/clash/component/fakeip" - "github.com/Dreamacro/clash/component/geodata/router" - "github.com/Dreamacro/clash/component/resolver" - "github.com/Dreamacro/clash/component/trie" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/component/fakeip" + "github.com/metacubex/mihomo/component/geodata/router" + "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/component/trie" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" + "github.com/samber/lo" "golang.org/x/sync/singleflight" ) type dnsClient interface { - Exchange(m *D.Msg) (msg *D.Msg, err error) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) Address() string } @@ -40,9 +37,15 @@ type geositePolicyRecord struct { inversedMatching bool } +type domainSetPolicyRecord struct { + domainSetProvider provider.RuleProvider + policy *Policy +} + type Resolver struct { ipv6 bool - hosts *trie.DomainTrie[netip.Addr] + ipv6Timeout time.Duration + hosts *trie.DomainTrie[resolver.HostValue] main []dnsClient fallback []dnsClient fallbackDomainFilters []fallbackDomainFilter @@ -50,6 +53,7 @@ type Resolver struct { group singleflight.Group lruCache *cache.LruCache[string, *D.Msg] policy *trie.DomainTrie[*Policy] + domainSetPolicy []domainSetPolicyRecord geositePolicy []geositePolicyRecord proxyServer []dnsClient } @@ -91,63 +95,36 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ips []netip.Addr, }() 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 <-time.After(30 * time.Millisecond): + case <-waitIPv6.C: // wait ipv6 result } return ips, nil } -// ResolveIP request with TypeA and TypeAAAA, priority return TypeA -func (r *Resolver) ResolveIP(ctx context.Context, host string) (ip netip.Addr, err error) { - ips, err := r.LookupIPPrimaryIPv4(ctx, host) - if err != nil { - return netip.Addr{}, err - } else if len(ips) == 0 { - return netip.Addr{}, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) - } - return ips[rand.Intn(len(ips))], nil -} - // LookupIPv4 request with TypeA func (r *Resolver) LookupIPv4(ctx context.Context, host string) ([]netip.Addr, error) { return r.lookupIP(ctx, host, D.TypeA) } -// ResolveIPv4 request with TypeA -func (r *Resolver) ResolveIPv4(ctx context.Context, host string) (ip netip.Addr, err error) { - ips, err := r.lookupIP(ctx, host, D.TypeA) - if err != nil { - return netip.Addr{}, err - } else if len(ips) == 0 { - return netip.Addr{}, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) - } - return ips[rand.Intn(len(ips))], nil -} - // LookupIPv6 request with TypeAAAA func (r *Resolver) LookupIPv6(ctx context.Context, host string) ([]netip.Addr, error) { return r.lookupIP(ctx, host, D.TypeAAAA) } -// ResolveIPv6 request with TypeAAAA -func (r *Resolver) ResolveIPv6(ctx context.Context, host string) (ip netip.Addr, err error) { - ips, err := r.lookupIP(ctx, host, D.TypeAAAA) - if err != nil { - return netip.Addr{}, err - } else if len(ips) == 0 { - return netip.Addr{}, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) - } - return ips[rand.Intn(len(ips))], nil -} - func (r *Resolver) shouldIPFallback(ip netip.Addr) bool { for _, filter := range r.fallbackIPFilters { if filter.Match(ip) { @@ -157,11 +134,6 @@ func (r *Resolver) shouldIPFallback(ip netip.Addr) bool { return false } -// Exchange a batch of dns request, and it use cache -func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) { - return r.ExchangeContext(context.Background(), m) -} - // ExchangeContext a batch of dns request with context.Context, and it use cache func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { if len(m.Question) == 0 { @@ -187,7 +159,8 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e setMsgTTL(msg, uint32(1)) // Continue fetch continueFetch = true } else { - setMsgTTL(msg, uint32(time.Until(expireTime).Seconds())) + // updating TTL by subtracting common delta time from each DNS record + updateMsgTTL(msg, uint32(time.Until(expireTime).Seconds())) } return } @@ -203,6 +176,7 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M fn := func() (result any, err error) { ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) // reset timeout in singleflight defer cancel() + cache := false defer func() { if err != nil { @@ -213,18 +187,27 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M msg := result.(*D.Msg) - putMsgToCache(r.lruCache, q.String(), msg) + 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.lruCache, q.String(), q, msg) + } }() isIPReq := isIPRequest(q) if isIPReq { + cache = true return r.ipExchange(ctx, m) } if matched := r.matchPolicy(m); len(matched) != 0 { - return r.batchExchange(ctx, matched, m) + result, cache, err = batchExchange(ctx, matched, m) + return } - return r.batchExchange(ctx, r.main, m) + result, cache, err = batchExchange(ctx, r.main, m) + return } ch := r.group.DoChan(q.String(), fn) @@ -265,13 +248,6 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M return } -func (r *Resolver) batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) { - ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDNSTimeout) - defer cancel() - - return batchExchange(ctx, clients, m) -} - func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient { if r.policy == nil { return nil @@ -294,6 +270,12 @@ func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient { return geositeRecord.policy.GetData() } } + metadata := &C.Metadata{Host: domain} + for _, domainSetRecord := range r.domainSetPolicy { + if ok := domainSetRecord.domainSetProvider.Match(metadata); ok { + return domainSetRecord.policy.GetData() + } + } return nil } @@ -341,7 +323,10 @@ func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err er res := <-msgCh if res.Error == nil { if ips := msgToIP(res.Msg); len(ips) != 0 { - if !r.shouldIPFallback(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 } @@ -386,22 +371,26 @@ func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (i func (r *Resolver) asyncExchange(ctx context.Context, client []dnsClient, msg *D.Msg) <-chan *result { ch := make(chan *result, 1) go func() { - res, err := r.batchExchange(ctx, client, msg) + res, _, err := batchExchange(ctx, client, msg) ch <- &result{Msg: res, Error: err} }() return ch } -// HasProxyServer has proxy server dns client -func (r *Resolver) HasProxyServer() bool { +// 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 } type NameServer struct { Net string Addr string - Interface *atomic.String - ProxyAdapter string + Interface string + ProxyAdapter C.ProxyAdapter + ProxyName string Params map[string]string PreferH3 bool } @@ -409,34 +398,39 @@ type NameServer struct { type FallbackFilter struct { GeoIP bool GeoIPCode string - IPCIDR []*netip.Prefix + IPCIDR []netip.Prefix Domain []string GeoSite []*router.DomainMatcher } type Config struct { - Main, Fallback []NameServer - Default []NameServer - ProxyServer []NameServer - IPv6 bool - EnhancedMode C.DNSMode - FallbackFilter FallbackFilter - Pool *fakeip.Pool - Hosts *trie.DomainTrie[netip.Addr] - Policy map[string][]NameServer + Main, Fallback []NameServer + Default []NameServer + ProxyServer []NameServer + IPv6 bool + IPv6Timeout uint + EnhancedMode C.DNSMode + FallbackFilter FallbackFilter + Pool *fakeip.Pool + Hosts *trie.DomainTrie[resolver.HostValue] + Policy map[string][]NameServer + DomainSetPolicy map[provider.RuleProvider][]NameServer + GeositePolicy map[router.DomainMatcher][]NameServer } func NewResolver(config Config) *Resolver { defaultResolver := &Resolver{ - main: transform(config.Default, nil), - lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), + main: transform(config.Default, nil), + lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), + ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } r := &Resolver{ - ipv6: config.IPv6, - main: transform(config.Main, defaultResolver), - lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), - hosts: config.Hosts, + ipv6: config.IPv6, + main: transform(config.Main, defaultResolver), + lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), + hosts: config.Hosts, + ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } if len(config.Fallback) != 0 { @@ -473,6 +467,14 @@ func NewResolver(config Config) *Resolver { } r.policy.Optimize() } + if len(config.DomainSetPolicy) > 0 { + for p, n := range config.DomainSetPolicy { + r.domainSetPolicy = append(r.domainSetPolicy, domainSetPolicyRecord{ + domainSetProvider: p, + policy: NewPolicy(transform(n, defaultResolver)), + }) + } + } fallbackIPFilters := []fallbackIPFilter{} if config.FallbackFilter.GeoIP { @@ -502,11 +504,14 @@ func NewResolver(config Config) *Resolver { func NewProxyServerHostResolver(old *Resolver) *Resolver { r := &Resolver{ - ipv6: old.ipv6, - main: old.proxyServer, - lruCache: old.lruCache, - hosts: old.hosts, - policy: old.policy, + ipv6: old.ipv6, + main: old.proxyServer, + lruCache: old.lruCache, + hosts: old.hosts, + policy: trie.New[*Policy](), + ipv6Timeout: old.ipv6Timeout, } return r } + +var ParseNameServer func(servers []string) ([]NameServer, error) // define in config/config.go diff --git a/dns/server.go b/dns/server.go index 5c5970db..1cf58d4d 100644 --- a/dns/server.go +++ b/dns/server.go @@ -5,9 +5,9 @@ import ( "errors" "net" - "github.com/Dreamacro/clash/common/sockopt" - "github.com/Dreamacro/clash/context" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/sockopt" + "github.com/metacubex/mihomo/context" + "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" ) diff --git a/dns/system.go b/dns/system.go new file mode 100644 index 00000000..37607a60 --- /dev/null +++ b/dns/system.go @@ -0,0 +1,113 @@ +package dns + +import ( + "context" + "fmt" + "net" + "strings" + "sync" + "time" + + "github.com/metacubex/mihomo/log" + + D "github.com/miekg/dns" + "golang.org/x/exp/slices" +) + +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 +} + +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 _, 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) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + dnsClients, err := c.getDnsClients() + if err != nil { + return + } + msg, _, err = batchExchange(ctx, dnsClients, m) + return +} + +// Address implements dnsClient +func (c *systemClient) Address() string { + dnsClients, _ := c.getDnsClients() + addrs := make([]string, 0, len(dnsClients)) + for _, c := range dnsClients { + addrs = append(addrs, c.Address()) + } + return fmt.Sprintf("system(%s)", strings.Join(addrs, ",")) +} + +var _ dnsClient = (*systemClient)(nil) + +func newSystemClient() *systemClient { + return &systemClient{ + dnsClients: map[string]*systemDnsClient{}, + } +} diff --git a/dns/system_posix.go b/dns/system_posix.go new file mode 100644 index 00000000..4d07d4ec --- /dev/null +++ b/dns/system_posix.go @@ -0,0 +1,43 @@ +//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 new file mode 100644 index 00000000..47c1ebaa --- /dev/null +++ b/dns/system_windows.go @@ -0,0 +1,77 @@ +//go:build windows + +package dns + +import ( + "net" + "os" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +func dnsReadConfig() (servers []string, err error) { + aas, err := adapterAddresses() + if err != nil { + return + } + for _, aa := range aas { + for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next { + sa, err := dns.Address.Sockaddr.Sockaddr() + if err != nil { + continue + } + var ip net.IP + switch sa := sa.(type) { + case *syscall.SockaddrInet4: + ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3]) + case *syscall.SockaddrInet6: + ip = make(net.IP, net.IPv6len) + copy(ip, sa.Addr[:]) + if ip[0] == 0xfe && ip[1] == 0xc0 { + // Ignore these fec0/10 ones. Windows seems to + // populate them as defaults on its misc rando + // interfaces. + continue + } + //continue + default: + // Unexpected type. + continue + } + servers = append(servers, ip.String()) + } + } + 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) + err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 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 index 203ab615..c354a73d 100644 --- a/dns/util.go +++ b/dns/util.go @@ -7,48 +7,69 @@ import ( "fmt" "net" "net/netip" + "strconv" "strings" "time" - "github.com/Dreamacro/clash/common/cache" - N "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/common/nnip" - "github.com/Dreamacro/clash/common/picker" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" + "github.com/metacubex/mihomo/common/cache" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/nnip" + "github.com/metacubex/mihomo/common/picker" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/resolver" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/tunnel" D "github.com/miekg/dns" + "github.com/samber/lo" ) const ( MaxMsgSize = 65535 ) -func putMsgToCache(c *cache.LruCache[string, *D.Msg], key string, msg *D.Msg) { - // skip dns cache for acme challenge - if len(msg.Question) != 0 { - if q := msg.Question[0]; 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 - } +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 } - var ttl uint32 - switch { - case len(msg.Answer) != 0: - ttl = msg.Answer[0].Header().Ttl - case len(msg.Ns) != 0: - ttl = msg.Ns[0].Header().Ttl - case len(msg.Extra) != 0: - ttl = msg.Extra[0].Header().Ttl - default: - log.Debugln("[DNS] response msg empty: %#v", msg) + 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 *cache.LruCache[string, *D.Msg], 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 } - c.SetWithExpire(key, msg.Copy(), time.Now().Add(time.Second*time.Duration(ttl))) + 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) { @@ -65,22 +86,34 @@ func setMsgTTL(msg *D.Msg, ttl uint32) { } } +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) + 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 := []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)) + 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); err == nil { + 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) @@ -103,6 +136,7 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { iface: s.Interface, r: resolver, proxyAdapter: s.ProxyAdapter, + proxyName: s.ProxyName, }) } return ret @@ -144,9 +178,9 @@ func msgToDomain(msg *D.Msg) string { type dialHandler func(ctx context.Context, network, addr string) (net.Conn, error) -func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dialHandler { +func getDialHandler(r *Resolver, proxyAdapter C.ProxyAdapter, proxyName string, opts ...dialer.Option) dialHandler { return func(ctx context.Context, network, addr string) (net.Conn, error) { - if len(proxyAdapter) == 0 { + if len(proxyName) == 0 && proxyAdapter == nil { opts = append(opts, dialer.WithResolver(r)) return dialer.DialContext(ctx, network, addr, opts...) } else { @@ -154,19 +188,35 @@ func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dia if err != nil { return nil, err } - adapter, ok := tunnel.Proxies()[proxyAdapter] - if !ok { - opts = append(opts, dialer.WithInterface(proxyAdapter)) + uintPort, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return nil, err } + if proxyAdapter == nil { + var ok bool + proxyAdapter, ok = tunnel.Proxies()[proxyName] + if !ok { + opts = append(opts, dialer.WithInterface(proxyName)) + } + } + if strings.Contains(network, "tcp") { // tcp can resolve host by remote metadata := &C.Metadata{ NetWork: C.TCP, Host: host, - DstPort: port, + DstPort: uint16(uintPort), } - if ok { - return adapter.DialContext(ctx, metadata, opts...) + if proxyAdapter != nil { + if proxyAdapter.IsL3Protocol(metadata) { // L3 proxy should resolve domain before to avoid loopback + dstIP, err := resolver.ResolveIPWithResolver(ctx, host, r) + if err != nil { + return nil, err + } + metadata.Host = "" + metadata.DstIP = dstIP + } + return proxyAdapter.DialContext(ctx, metadata, opts...) } opts = append(opts, dialer.WithResolver(r)) return dialer.DialContext(ctx, network, addr, opts...) @@ -180,17 +230,17 @@ func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dia NetWork: C.UDP, Host: "", DstIP: dstIP, - DstPort: port, + DstPort: uint16(uintPort), } - if !ok { + if proxyAdapter == nil { return dialer.DialContext(ctx, network, addr, opts...) } - if !adapter.SupportUDP() { + if !proxyAdapter.SupportUDP() { return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) } - packetConn, err := adapter.ListenPacketContext(ctx, metadata, opts...) + packetConn, err := proxyAdapter.ListenPacketContext(ctx, metadata, opts...) if err != nil { return nil, err } @@ -201,14 +251,21 @@ func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dia } } -func listenPacket(ctx context.Context, proxyAdapter string, network string, addr string, r *Resolver, opts ...dialer.Option) (net.PacketConn, error) { +func listenPacket(ctx context.Context, proxyAdapter C.ProxyAdapter, proxyName string, network string, addr string, r *Resolver, opts ...dialer.Option) (net.PacketConn, error) { host, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } - adapter, ok := tunnel.Proxies()[proxyAdapter] - if !ok && len(proxyAdapter) != 0 { - opts = append(opts, dialer.WithInterface(proxyAdapter)) + uintPort, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return nil, err + } + if proxyAdapter == nil { + var ok bool + proxyAdapter, ok = tunnel.Proxies()[proxyName] + if !ok { + opts = append(opts, dialer.WithInterface(proxyName)) + } } // udp must resolve host first @@ -220,45 +277,72 @@ func listenPacket(ctx context.Context, proxyAdapter string, network string, addr NetWork: C.UDP, Host: "", DstIP: dstIP, - DstPort: port, + DstPort: uint16(uintPort), } - if !ok { - return dialer.ListenPacket(ctx, dialer.ParseNetwork(network, dstIP), "", opts...) + if proxyAdapter == nil { + return dialer.NewDialer(opts...).ListenPacket(ctx, network, "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort)) } - if !adapter.SupportUDP() { + if !proxyAdapter.SupportUDP() { return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) } - return adapter.ListenPacketContext(ctx, metadata, opts...) + return proxyAdapter.ListenPacketContext(ctx, metadata, opts...) } -func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) { +var errIPNotFound = errors.New("couldn't find ip") + +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) + var noIpMsg *D.Msg for _, client := range clients { - r := client + 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 from %s", domain, r.Address()) - m, err := r.ExchangeContext(ctx, m) + log.Debugln("[DNS] resolve %s from %s", domain, client.Address()) + m, err := client.ExchangeContext(ctx, m) if err != nil { return nil, err - } else if m.Rcode == D.RcodeServerFailure || m.Rcode == D.RcodeRefused { - return nil, errors.New("server failure") + } 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]) + } + if ips := msgToIP(m); len(m.Question) > 0 { + qType := m.Question[0].Qtype + log.Debugln("[DNS] %s --> %s %s from %s", domain, ips, D.Type(qType), client.Address()) + switch qType { + case D.TypeAAAA: + if len(ips) == 0 { + noIpMsg = m + return nil, errIPNotFound + } + case D.TypeA: + if len(ips) == 0 { + noIpMsg = m + return nil, errIPNotFound + } + } } - log.Debugln("[DNS] %s --> %s, from %s", domain, msgToIP(m), r.Address()) return m, nil }) } - elm := fast.Wait() - if elm == nil { - err := errors.New("all DNS requests failed") - if fErr := fast.Error(); fErr != nil { - err = fmt.Errorf("%w, first error: %s", err, fErr.Error()) + 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 nil, err } - msg = elm return } diff --git a/docker/file-name.sh b/docker/file-name.sh index fb87cad0..3b2d61f9 100644 --- a/docker/file-name.sh +++ b/docker/file-name.sh @@ -1,26 +1,25 @@ #!/bin/sh -os="clash.meta-linux-" -arch=`uname -m` -case $arch in - "x86_64") +os="mihomo-linux-" +case $TARGETPLATFORM in + "linux/amd64") arch="amd64-compatible" ;; - "x86") - arch="386-cgo" + "linux/386") + arch="386" ;; - "aarch64") + "linux/arm64") arch="arm64" ;; - "armv7l") + "linux/arm/v7") arch="armv7" ;; "riscv64") - arch="riscv64-cgo" + arch="riscv64" ;; *) echo "Unknown architecture" exit 1 ;; esac -file_name="$os$arch" +file_name="$os$arch-$(cat bin/version.txt)" echo $file_name \ No newline at end of file diff --git a/docs/allocs.svg b/docs/allocs.svg deleted file mode 100644 index 57deec50..00000000 --- a/docs/allocs.svg +++ /dev/null @@ -1,2387 +0,0 @@ - - - - - - -unnamed - - -cluster_L - - - - -Type: alloc_space - -Type: alloc_space -Time: Jan 30, 2023 at 9:18pm (CST) -Showing nodes accounting for 1777.16MB, 96.71% of 1837.65MB total -Dropped 188 nodes (cum <= 9.19MB) -Dropped 2 edges (freq <= 1.84MB) -Showing top 55 nodes out of 117 -See https://git.io/JfYMW for how to read the graph - - - -N1 - - -quic-go -(*client) -dial -func1 -0 of 1648.40MB (89.70%) - - - - - -N3 - - -quic-go -(*connection) -run -0 of 1648.40MB (89.70%) - - - - - -N1->N3 - - - - - - - 1648.40MB - - - - - -N2 - - -quic-go -(*connection) -sendPacket -0 of 1412.44MB (76.86%) - - - - - -N4 - - -quic-go -(*packetPacker) -PackPacket -30MB (1.63%) -of 956.14MB (52.03%) - - - - - -N2->N4 - - - - - - - 956.14MB - - - - - -N10 - - -quic-go -(*connection) -sendPackedPacket -0 of 456.30MB (24.83%) - - - - - -N2->N10 - - - - - - - 456.30MB - - - - - -N3->N2 - - - - - - - 1412.44MB - - - - - -N36 - - -quic-go -(*connection) -handleUnpackedShortHeaderPacket -0 of 235.46MB (12.81%) - - - - - -N3->N36 - - - - - - - 235.46MB - - - - - -NN4_0 - - - - - -16B..64B - - - - - -N4->NN4_0 - - - - - - - 30MB - - - - - -N6 - - -quic-go -(*packetPacker) -maybeGetShortHeaderPacket -0 of 722.12MB (39.30%) - - - - - -N4->N6 - - - - - - - 722.12MB - - - - - -N13 - - -quic-go -(*packetPacker) -appendPacket -118.51MB (6.45%) -of 204.01MB (11.10%) - - - - - -N4->N13 - - - - - - - 204.01MB - - - - - -N5 - - -quic-go -(*packetPacker) -composeNextPacket -162.51MB (8.84%) -of 446.09MB (24.28%) - - - - - -NN5_0 - - - - - -48B..64B - - - - - -N5->NN5_0 - - - - - - - 93.50MB - - - - - -NN5_1 - - - - - -16B..32B - - - - - -N5->NN5_1 - - - - - - - 69MB - - - - - -N25 - - -quic-go -(*framerI) -AppendStreamFrames -15.50MB (0.84%) -of 157.55MB (8.57%) - - - - - -N5->N25 - - - - - - - 157.55MB - - - - - -N34 - - -ackhandler -(*receivedPacketTracker) -GetAckFrame -0 of 126.04MB (6.86%) - - - - - -N5->N34 - - - - - - - 126.04MB - - - - - -N6->N5 - - - - - - - 446.09MB - - - - - -N8 - - -quic-go -(*packetPacker) -getShortHeader -276.03MB (15.02%) - - - - - -N6->N8 - - - - - - - 276.03MB - - - - - -N7 - - -sync -(*Pool) -Get -0 of 248.65MB (13.53%) - - - - - -N12 - - -ackhandler -glob -func1 -160.01MB (8.71%) - - - - - -N7->N12 - - - - - - - 160.01MB - - - - - -N19 - - -wire -init -0 -func1 -70.60MB (3.84%) - - - - - -N7->N19 - - - - - - - 70.60MB - - - - - -N44 - - -wire -glob -func1 -15MB (0.82%) - - - - - -N7->N44 - - - - - - - 15MB - - - - - -NN8_0 - - - - - -128B - - - - - -N8->NN8_0 - - - - - - - 276.03MB - - - - - -N9 - - -congestion -NewConnectionStateOnSentPacket -236.03MB (12.84%) - - - - - -NN9_0 - - - - - -128B - - - - - -N9->NN9_0 - - - - - - - 236.03MB - - - - - -N10->N7 - - - - - - - 160.01MB - - - - - -N17 - - -ackhandler -(*sentPacketHandler) -SentPacket -0 of 296.29MB (16.12%) - - - - - -N10->N17 - - - - - - - 296.29MB - - - - - -N11 - - -linkedlist -(*List[…]) -insertValue -120MB (6.53%) - - - - - -NN11_0 - - - - - -32B - - - - - -N11->NN11_0 - - - - - - - 113.50MB - - - - - -NN12_0 - - - - - -96B - - - - - -N12->NN12_0 - - - - - - - 160.01MB - - - - - -NN13_0 - - - - - -64B - - - - - -N13->NN13_0 - - - - - - - 118.51MB - - - - - -N18 - - -bytes -NewBuffer -85.50MB (4.65%) - - - - - -N13->N18 - - - - - - - 85.50MB - (inline) - - - - - -N14 - - -ackhandler -(*receivedPacketHistory) -AppendAckRanges -115.54MB (6.29%) - - - - - -NN14_0 - - - - - -512B - - - - - -N14->NN14_0 - - - - - - - 51.53MB - - - - - -NN14_1 - - - - - -256B - - - - - -N14->NN14_1 - - - - - - - 29.51MB - - - - - -NN14_2 - - - - - -128B - - - - - -N14->NN14_2 - - - - - - - 20MB - - - - - -N15 - - -quic-go -(*connection) -handleFrames -0 of 228.96MB (12.46%) - - - - - -N21 - - -wire -(*frameParser) -parseFrame -0 of 109.58MB (5.96%) - - - - - -N15->N21 - - - - - - - 109.58MB - - - - - -N23 - - -quic-go -(*connection) -handleFrame -0 of 119.38MB (6.50%) - - - - - -N15->N23 - - - - - - - 119.38MB - - - - - -N16 - - -quic-go -(*sendStream) -popStreamFrame -111.50MB (6.07%) -of 142.05MB (7.73%) - - - - - -NN16_0 - - - - - -16B - - - - - -N16->NN16_0 - - - - - - - 57MB - - - - - -NN16_1 - - - - - -32B - - - - - -N16->NN16_1 - - - - - - - 54.50MB - - - - - -N29 - - -wire -GetStreamFrame -0 of 70.60MB (3.84%) - - - - - -N16->N29 - - - - - - - 30.54MB - (inline) - - - - - -N17->N9 - - - - - - - 236.03MB - (inline) - - - - - -N17->N11 - - - - - - - 57.50MB - (inline) - - - - - -NN18_0 - - - - - -48B - - - - - -N18->NN18_0 - - - - - - - 85.50MB - - - - - -NN19_0 - - - - - -1.50kB - - - - - -N19->NN19_0 - - - - - - - 70.60MB - - - - - -N20 - - -geodata -LoadGeoSiteMatcher -0 of 74.20MB (4.04%) - - - - - -N33 - - -router -NewMphMatcherGroup -0 of 57.15MB (3.11%) - - - - - -N20->N33 - - - - - - - 57.15MB - - - - - -N40 - - -reflect -New -13MB (0.71%) - - - - - -N20->N40 - - - - - - - 11.50MB - - - - - -N24 - - -wire -parseAckFrame -65.02MB (3.54%) -of 69.52MB (3.78%) - - - - - -N21->N24 - - - - - - - 69.52MB - - - - - -N21->N29 - - - - - - - 40.06MB - (inline) - - - - - -N22 - - -quic-go -(*basicConn) -ReadPacket -35.50MB (1.93%) -of 59.01MB (3.21%) - - - - - -N22->N7 - - - - - - - 2MB - - - - - -NN22_0 - - - - - -96B - - - - - -N22->NN22_0 - - - - - - - 35.50MB - - - - - -N37 - - -net -(*UDPConn) -ReadFrom -18.50MB (1.01%) -of 22MB (1.20%) - - - - - -N22->N37 - - - - - - - 21.50MB - - - - - -N41 - - -ackhandler -(*sentPacketHandler) -ReceivedAck -0 of 90.10MB (4.90%) - - - - - -N23->N41 - - - - - - - 90.10MB - - - - - -N42 - - -quic-go -(*receiveStream) -handleStreamFrameImpl -7MB (0.38%) -of 29.28MB (1.59%) - - - - - -N23->N42 - - - - - - - 29.28MB - - - - - -N24->N7 - - - - - - - 4.50MB - - - - - -NN24_0 - - - - - -512B - - - - - -N24->NN24_0 - - - - - - - 32.52MB - - - - - -NN24_1 - - - - - -256B - - - - - -N24->NN24_1 - - - - - - - 18MB - - - - - -N25->N16 - - - - - - - 142.05MB - - - - - -NN25_0 - - - - - -16B - - - - - -N25->NN25_0 - - - - - - - 15.50MB - - - - - -N26 - - -runtime -main -0 of 79.85MB (4.35%) - - - - - -N52 - - -main -main -0 of 78.85MB (4.29%) - - - - - -N26->N52 - - - - - - - 78.85MB - - - - - -N27 - - -strmatcher -(*MphMatcherGroup) -Build -37.85MB (2.06%) - - - - - -N28 - - -ackhandler -(*sentPacketHandler) -detectLostPackets -func1 -0 of 79.59MB (4.33%) - - - - - -N35 - - -quic-go -(*sendStream) -queueRetransmission -23.59MB (1.28%) - - - - - -N28->N35 - - - - - - - 23.59MB - - - - - -N43 - - -linkedlist -(*List[…]) -InsertAfter -0 of 62.50MB (3.40%) - - - - - -N28->N43 - - - - - - - 56MB - - - - - -N29->N7 - - - - - - - 70.60MB - - - - - -N30 - - -common -NewGEOSITE -0 of 74.20MB (4.04%) - - - - - -N30->N20 - - - - - - - 25.91MB - - - - - -N46 - - -geodata -Verify -0 of 51.44MB (2.80%) - - - - - -N30->N46 - - - - - - - 48.29MB - - - - - -N31 - - -quic-go -(*packetHandlerMap) -listen -0 of 59.01MB (3.21%) - - - - - -N31->N22 - - - - - - - 59.01MB - - - - - -N32 - - -http -HandlerFunc -ServeHTTP -0 of 26.60MB (1.45%) - - - - - -N47 - - -chi -(*Mux) -routeHTTP -0 of 26.60MB (1.45%) - - - - - -N32->N47 - - - - - - - 26.10MB - - - - - -N48 - - -chi -(*Mux) -ServeHTTP -0 of 26.10MB (1.42%) - - - - - -N32->N48 - - - - - - - 25.59MB - - - - - -N33->N27 - - - - - - - 37.85MB - - - - - -N38 - - -strmatcher -(*MphMatcherGroup) -AddFullOrDomainPattern -18.80MB (1.02%) - - - - - -N33->N38 - - - - - - - 18.80MB - (inline) - - - - - -N34->N7 - - - - - - - 10.50MB - - - - - -N34->N14 - - - - - - - 115.54MB - - - - - -N36->N15 - - - - - - - 228.96MB - - - - - -NN37_0 - - - - - -48B - - - - - -N37->NN37_0 - - - - - - - 18.50MB - - - - - -N39 - - -sync -(*poolChain) -pushHead -14.30MB (0.78%) - - - - - -NN40_0 - - - - - -96B - - - - - -N40->NN40_0 - - - - - - - 11.50MB - - - - - -N41->N28 - - - - - - - 79.59MB - - - - - -N41->N39 - - - - - - - 5.51MB - - - - - -NN42_0 - - - - - -16B - - - - - -N42->NN42_0 - - - - - - - 7MB - - - - - -N49 - - -quic-go -(*frameSorter) -push -5.28MB (0.29%) -of 22.28MB (1.21%) - - - - - -N42->N49 - - - - - - - 22.28MB - - - - - -N43->N11 - - - - - - - 62.50MB - (inline) - - - - - -NN44_0 - - - - - -64B - - - - - -N44->NN44_0 - - - - - - - 15MB - - - - - -N45 - - -flate -NewWriter -10.58MB (0.58%) -of 17.16MB (0.93%) - - - - - -NN45_0 - - - - - -648kB - - - - - -N45->NN45_0 - - - - - - - 10.58MB - - - - - -N46->N20 - - - - - - - 48.29MB - - - - - -N47->N32 - - - - - - - 26.60MB - - - - - -N53 - - -pprof -(*Profile) -WriteTo -0 of 22.36MB (1.22%) - - - - - -N47->N53 - - - - - - - 22.36MB - - - - - -N48->N32 - - - - - - - 26.10MB - - - - - -N51 - - -tree -insert[…] -11MB (0.6%) - - - - - -N49->N51 - - - - - - - 11MB - - - - - -N50 - - -http -(*conn) -serve -0 of 24.10MB (1.31%) - - - - - -N50->N48 - - - - - - - 24.10MB - - - - - -NN51_0 - - - - - -48B - - - - - -N51->NN51_0 - - - - - - - 11MB - - - - - -N52->N30 - - - - - - - 74.20MB - - - - - -N54 - - -pprof -writeHeapInternal -0 of 22.36MB (1.22%) - - - - - -N53->N54 - - - - - - - 22.36MB - - - - - -N55 - - -pprof -writeHeapProto -0 of 22.36MB (1.22%) - - - - - -N54->N55 - - - - - - - 22.36MB - - - - - -N55->N45 - - - - - - - 16.28MB - - - - - diff --git a/docs/config.yaml b/docs/config.yaml index 35e85ecc..d5e1174a 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -7,48 +7,85 @@ mixed-port: 10801 # HTTP(S) 和 SOCKS 代理混合端口 # tproxy-port: 7893 allow-lan: true # 允许局域网连接 -bind-address: "*" # 绑定IP地址,仅作用于 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 -# find-process-mode has 3 values: always, strict, off +# find-process-mode has 3 values:always, strict, off # - always, 开启,强制匹配所有进程 -# - strict, 默认,由clash判断是否开启 +# - strict, 默认,由 mihomo 判断是否开启 # - off, 不匹配进程,推荐在路由器上使用此模式 find-process-mode: strict -# global-client-fingerprint:全局TLS指纹,优先低于proxy内的 client-fingerprint -# accepts "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 - mode: rule -#自定义 geox-url +#自定义 geodata url geox-url: - geoip: "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat" - geosite: "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat" - mmdb: "https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/Country.mmdb" + 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" 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}` +# secret: "123456" # `Authorization:Bearer ${secret}` -# tcp-concurrent: true # TCP并发连接所有IP, 将使用最快握手的TCP -external-ui: /path/to/ui/folder # 配置WEB UI目录,使用http://{{external-controller}}/ui 访问 +# tcp-concurrent: true # TCP 并发连接所有 IP, 将使用最快握手的 TCP + +# 配置 WEB UI 目录,使用 http://{{external-controller}}/ui 访问 +external-ui: /path/to/ui/folder/ +external-ui-name: xd +external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip" # interface-name: en0 # 设置出口网卡 -# routing-mark: 6666 # 配置 fwmark 仅用于Linux +# 全局 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 +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: -# '*.clash.dev': 127.0.0.1 +# '*.mihomo.dev': 127.0.0.1 # '.dev': 127.0.0.1 -# 'alpha.clash.dev': '::1' +# 'alpha.mihomo.dev': '::1' +# test.com: [1.1.1.1, 2.2.2.2] +# mihomo.lan: mihomo # mihomo 为特别字段,将加入本地所有网卡的地址 +# baidu.com: google.com # 只允许配置一个别名 + +profile: # 存储 select 选择记录 + store-selected: false + + # 持久化 fake-ip + store-fake-ip: true # Tun 配置 tun: @@ -59,32 +96,32 @@ tun: # auto-detect-interface: true # 自动识别出口网卡 # auto-route: true # 配置路由表 # mtu: 9000 # 最大传输单元 - # strict_route: true # 将所有连接路由到tun来防止泄漏,但你的设备将无法其他设备被访问 - inet4_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 + # strict-route: true # 将所有连接路由到tun来防止泄漏,但你的设备将无法其他设备被访问 + inet4-route-address: # 启用 auto_route 时使用自定义路由而不是默认路由 - 0.0.0.0/1 - 128.0.0.0/1 - inet6_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 + inet6-route-address: # 启用 auto_route 时使用自定义路由而不是默认路由 - "::/1" - "8000::/1" - # endpoint_independent_nat: false # 启用独立于端点的 NAT - # include_uid: # UID 规则仅在 Linux 下被支持,并且需要 auto_route + # endpoint-independent-nat: false # 启用独立于端点的 NAT + # include-uid: # UID 规则仅在 Linux 下被支持,并且需要 auto_route # - 0 - # include_uid_range: # 限制被路由的的用户范围 + # include-uid-range: # 限制被路由的的用户范围 # - 1000-99999 - # exclude_uid: # 排除路由的的用户 + # exclude-uid: # 排除路由的的用户 #- 1000 - # exclude_uid_range: # 排除路由的的用户范围 + # exclude-uid-range: # 排除路由的的用户范围 # - 1000-99999 # Android 用户和应用规则仅在 Android 下被支持 - # 并且需要 auto_route + # 并且需要 auto-route - # include_android_user: # 限制被路由的 Android 用户 + # include-android-user: # 限制被路由的 Android 用户 # - 0 # - 10 - # include_package: # 限制被路由的 Android 应用包名 + # include-package: # 限制被路由的 Android 应用包名 # - com.android.chrome - # exclude_package: # 排除被路由的 Android 应用包名 + # exclude-package: # 排除被路由的 Android 应用包名 # - com.android.captiveportallogin #ebpf配置 @@ -105,15 +142,14 @@ sniffer: # 是否使用嗅探结果作为实际访问,默认 true # 全局配置,优先级低于 sniffer.sniff 实际配置 override-destination: false - sniff: - # TLS 默认如果不配置 ports 默认嗅探 443 + sniff: # TLS 和 QUIC 默认如果不配置 ports 默认嗅探 443 + QUIC: + # ports: [ 443 ] TLS: # ports: [443, 8443] # 默认嗅探 80 - HTTP: - # 需要嗅探的端口 - + HTTP: # 需要嗅探的端口 ports: [80, 8080-8880] # 可覆盖 sniffer.override-destination override-destination: true @@ -136,27 +172,7 @@ sniffer: - "443" # - 8000-9999 -# 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: -# - TOKEN -# 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 - -tunnels: - # one line config +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 @@ -165,20 +181,13 @@ tunnels: target: target.com proxy: proxy -profile: - # 存储select选择记录 - store-selected: false - - # 持久化fake-ip - store-fake-ip: true - # DNS配置 dns: enable: false # 关闭将使用系统 DNS prefer-h3: true # 开启 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: @@ -186,6 +195,7 @@ dns: - 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 池设置 @@ -243,148 +253,16 @@ dns: nameserver-policy: # 'www.baidu.com': '114.114.114.114' # '+.internal.crop.com': '10.0.0.1' - "geosite:cn": "https://doh.pub/dns-query" - "www.baidu.com": [https://doh.pub/dns-query,https://dns.alidns.com/dns-query] -proxies: - # 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 生效 - - 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 + "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 - - 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 - - - name: "ss4" - type: ss - server: server - port: 443 - cipher: chacha20-ietf-poly1305 - password: "password" - plugin: shadow-tls - plugin-opts: - host: "cloud.tencent.com" - password: "shadow_tls_password" - - # 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 - - - 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 - - # socks5 +proxies: # socks5 - name: "socks" type: socks5 server: server @@ -422,6 +300,276 @@ proxies: # 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 + + - 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 + + - 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 + + - 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 + # Trojan - name: "trojan" type: trojan @@ -461,9 +609,10 @@ proxies: # fingerprint: xxxx udp: true # ws-opts: - # path: /path - # headers: - # Host: example.com + # path: /path + # headers: + # Host: example.com + # v2ray-http-upgrade: false - name: "trojan-xtls" type: trojan @@ -477,41 +626,12 @@ proxies: # skip-cert-verify: true # fingerprint: xxxx - # 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-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 - #hysteria - name: "hysteria" type: hysteria server: server.com port: 443 + # ports: 1000,2000-3000,5000 # port 不可省略, auth_str: yourpassword # 将会在未来某个时候删除 # auth-str: yourpassword # obfs: obfs_str @@ -533,33 +653,83 @@ proxies: # fingerprint: xxxx # fast-open: true # 支持 TCP 快速打开,默认为 false + #hysteria2 + - name: "hysteria2" + type: hysteria2 + server: server.com + port: 443 + # 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" + + # 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 - private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= + # pre-shared-key: 31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM= + private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= udp: true - # reserved: 'U4An' + 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时生效 + # 如果peers不为空,该段落中的allowed_ips不可为空;前面段落的server,port,ip,ipv6,public-key,pre-shared-key均会被忽略,但private-key会被保留且只能在顶层指定 + # peers: + # - 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= + # allowed_ips: ['0.0.0.0/0'] + # reserved: [209,98,59] + + # 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 + disable-sni: true reduce-rtt: true - # request-timeout: 8000 + 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 @@ -582,8 +752,9 @@ proxies: # udp: true proxy-groups: - # 代理链,若落地协议支持 UDP over TCP 则可支持 UDP - # Traffic: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet + # 代理链,目前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: @@ -623,7 +794,7 @@ proxy-groups: - vmess1 url: "https://cp.cloudflare.com/generate_204" interval: 300 - # strategy: consistent-hashing # 可选 round-robin 和 sticky-sessions + # strategy: consistent-hashing # 可选 round-robin 和 sticky-sessions # select 用户自行选择节点 - name: Proxy @@ -652,13 +823,13 @@ proxy-groups: - Proxy - DIRECT -# Clash 格式的节点或支持 *ray 的分享格式 +# Mihomo 格式的节点或支持 *ray 的分享格式 proxy-providers: provider1: - type: http + type: http # http 的 path 可空置,默认储存路径为 homedir的proxies文件夹,文件名为url的md5 url: "url" interval: 3600 - path: ./provider1.yaml + path: ./provider1.yaml # 默认只允许存储在 mihomo 的 Home Dir,如果想存储到任意位置,添加环境变量 SKIP_SAFE_PATH_CHECK=1 health-check: enable: true interval: 600 @@ -675,8 +846,8 @@ rule-providers: rule1: behavior: classical # domain ipcidr interval: 259200 - path: /path/to/save/file.yaml - type: http + path: /path/to/save/file.yaml # 默认只允许存储在 mihomo 的 Home Dir,如果想存储到任意位置,添加环境变量 SKIP_SAFE_PATH_CHECK=1 + type: http # http 的 path 可空置,默认储存路径为 homedir的rules文件夹,文件名为url的md5 url: "url" rule2: behavior: classical @@ -689,7 +860,8 @@ rules: - DOMAIN-KEYWORD,google,ss1 - IP-CIDR,1.1.1.1/32,ss1 - IP-CIDR6,2409::/64,DIRECT - - SUB-RULE,(OR,((NETWORK,TCP),(NETWORK,UDP))),sub-rule-name1 # 当满足条件是 TCP 或 UDP 流量时,使用名为 sub-rule-name1 当规则集 + # 当满足条件是 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) @@ -716,15 +888,6 @@ sub-rules: - IP-CIDR,8.8.8.8/32,ss1 - DOMAIN,dns.alidns.com,REJECT -tls: - certificate: string # 证书 PEM 格式,或者 证书的路径 - private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 - # 自定义证书验证,将加入 Clash 证书验证中,绝大多数 TLS 相关支持,如:DNS - # 可用于自定义证书的验证 - custom-certificates: - - certificate: string # 证书 PEM 格式,或者 证书的路径 - private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 - # 流量入站 listeners: - name: socks5-in-1 @@ -784,6 +947,10 @@ listeners: - username: 1 uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 alterId: 1 + # ws-path: "/" # 如果不为空则开启websocket传输层 + # 下面两项如果填写则开启tls(需要同时填写) + # certificate: ./server.crt + # private-key: ./server.key - name: tuic-in-1 type: tuic @@ -791,8 +958,11 @@ listeners: listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) - # token: - # - TOKEN + # 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 @@ -826,19 +996,19 @@ listeners: 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" + # 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 + # - 1000 # exclude_uid_range: # 排除路由的的用户范围 # - 1000-99999 @@ -852,3 +1022,25 @@ listeners: # - 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/heap.svg b/docs/heap.svg deleted file mode 100644 index 0795977f..00000000 --- a/docs/heap.svg +++ /dev/null @@ -1,2182 +0,0 @@ - - - - - - -unnamed - - -cluster_L - - - - -Type: inuse_space - -Type: inuse_space -Time: Jan 30, 2023 at 9:18pm (CST) -Showing nodes accounting for 12146.04kB, 100% of 12146.04kB total -Showing top 69 nodes out of 71 -See https://git.io/JfYMW for how to read the graph - - - -N1 - - -runtime -allocm -5637.50kB (46.41%) - - - - - -NN1_0 - - - - - -1kB - - - - - -N1->NN1_0 - - - - - - - 5637.50kB - - - - - -N2 - - -runtime -schedule -0 of 5637.50kB (46.41%) - - - - - -N68 - - -runtime -resetspinning -0 of 5637.50kB (46.41%) - - - - - -N2->N68 - - - - - - - 5637.50kB - - - - - -N3 - - -runtime -mstart -0 of 3587.50kB (29.54%) - - - - - -N62 - - -runtime -mstart0 -0 of 3587.50kB (29.54%) - - - - - -N3->N62 - - - - - - - 3587.50kB - - - - - -N4 - - -runtime -main -0 of 2836.92kB (23.36%) - - - - - -N61 - - -main -main -0 of 2836.92kB (23.36%) - - - - - -N4->N61 - - - - - - - 2836.92kB - - - - - -N5 - - -geodata -LoadGeoSiteMatcher -0 of 2836.92kB (23.36%) - - - - - -N10 - - -router -NewMphMatcherGroup -0 of 1812.91kB (14.93%) - - - - - -N5->N10 - - - - - - - 1812.91kB - - - - - -N27 - - -geodata -(*loader) -LoadGeoSite -0 of 1024.02kB (8.43%) - - - - - -N5->N27 - - - - - - - 1024.02kB - - - - - -N6 - - -strmatcher -(*MphMatcherGroup) -Build -1300.89kB (10.71%) - - - - - -NN6_0 - - - - - -1.14MB - - - - - -N6->NN6_0 - - - - - - - 1300.89kB - - - - - -N7 - - -runtime -mcall -0 of 2050kB (16.88%) - - - - - -N67 - - -runtime -park_m -0 of 2050kB (16.88%) - - - - - -N7->N67 - - - - - - - 2050kB - - - - - -N8 - - -impl -(*MessageInfo) -unmarshalPointer -0 of 1024.02kB (8.43%) - - - - - -N9 - - -impl -consumeStringValidateUTF8 -1024.02kB (8.43%) - - - - - -N8->N9 - - - - - - - 1024.02kB - - - - - -N58 - - -impl -consumeMessageSliceInfo -0 of 1024.02kB (8.43%) - - - - - -N8->N58 - - - - - - - 1024.02kB - - - - - -NN9_0 - - - - - -16B - - - - - -N9->NN9_0 - - - - - - - 1024.02kB - - - - - -N10->N6 - - - - - - - 1300.89kB - - - - - -N31 - - -strmatcher -(*MphMatcherGroup) -AddPattern -0 of 512.01kB (4.22%) - - - - - -N10->N31 - - - - - - - 512.01kB - - - - - -N11 - - -runtime -gcBgMarkWorker -512.02kB (4.22%) - - - - - -NN11_0 - - - - - -32B - - - - - -N11->NN11_0 - - - - - - - 512.02kB - - - - - -N12 - - -congestion -(*ConnectionStates) -Insert -596.16kB (4.91%) - - - - - -NN12_0 - - - - - -160kB - - - - - -N12->NN12_0 - - - - - - - 596.16kB - - - - - -N13 - - -bufio -NewReaderSize -514kB (4.23%) - - - - - -NN13_0 - - - - - -4kB - - - - - -N13->NN13_0 - - - - - - - 514kB - - - - - -N14 - - -quic-go -init -0 -func1 -512.75kB (4.22%) - - - - - -NN14_0 - - - - - -1.50kB - - - - - -N14->NN14_0 - - - - - - - 512.75kB - - - - - -N15 - - -trie -(*Node[…]) -optimize -512.44kB (4.22%) - - - - - -NN15_0 - - - - - -896B - - - - - -N15->NN15_0 - - - - - - - 512.44kB - - - - - -N16 - - -runtime -malg -512.20kB (4.22%) - - - - - -NN16_0 - - - - - -416B - - - - - -N16->NN16_0 - - - - - - - 512.20kB - - - - - -N17 - - -time -NewTicker -512.05kB (4.22%) - - - - - -NN17_0 - - - - - -96B - - - - - -N17->NN17_0 - - - - - - - 512.05kB - - - - - -N18 - - -strmatcher -(*MphMatcherGroup) -AddFullOrDomainPattern -512.01kB (4.22%) - - - - - -NN18_0 - - - - - -24B - - - - - -N18->NN18_0 - - - - - - - 512.01kB - - - - - -N19 - - -quic-go -(*client) -dial -func1 -0 of 596.16kB (4.91%) - - - - - -N49 - - -quic-go -(*connection) -run -0 of 596.16kB (4.91%) - - - - - -N19->N49 - - - - - - - 596.16kB - - - - - -N20 - - -mixed -handleConn -0 of 514kB (4.23%) - - - - - -N26 - - -net -NewBufferedConn -0 of 514kB (4.23%) - - - - - -N20->N26 - - - - - - - 514kB - - - - - -N21 - - -quic-go -(*packetHandlerMap) -listen -0 of 512.75kB (4.22%) - - - - - -N48 - - -quic-go -(*basicConn) -ReadPacket -0 of 512.75kB (4.22%) - - - - - -N21->N48 - - - - - - - 512.75kB - - - - - -N22 - - -executor -loadRuleProvider -func1 -0 of 512.44kB (4.22%) - - - - - -N40 - - -executor -loadProvider -0 of 512.44kB (4.22%) - - - - - -N22->N40 - - - - - - - 512.44kB - - - - - -N23 - - -runtime -systemstack -0 of 512.20kB (4.22%) - - - - - -N65 - - -runtime -newproc -func1 -0 of 512.20kB (4.22%) - - - - - -N23->N65 - - - - - - - 512.20kB - - - - - -N24 - - -statistic -(*Manager) -handle -0 of 512.05kB (4.22%) - - - - - -N24->N17 - - - - - - - 512.05kB - - - - - -N25 - - -bufio -NewReader -0 of 514kB (4.23%) - - - - - -N25->N13 - - - - - - - 514kB - (inline) - - - - - -N26->N25 - - - - - - - 514kB - (inline) - - - - - -N28 - - -geodata -(*loader) -LoadGeoSiteWithAttr -0 of 1024.02kB (8.43%) - - - - - -N27->N28 - - - - - - - 1024.02kB - - - - - -N29 - - -memconservative -(*memConservativeLoader) -LoadSiteByPath -0 of 1024.02kB (8.43%) - - - - - -N28->N29 - - - - - - - 1024.02kB - - - - - -N30 - - -memconservative -GeoSiteCache -Unmarshal -0 of 1024.02kB (8.43%) - - - - - -N29->N30 - - - - - - - 1024.02kB - - - - - -N59 - - -proto -Unmarshal -0 of 1024.02kB (8.43%) - - - - - -N30->N59 - - - - - - - 1024.02kB - - - - - -N31->N18 - - - - - - - 512.01kB - (inline) - - - - - -N32 - - -trie -(*DomainTrie[…]) -Optimize -0 of 512.44kB (4.22%) - - - - - -N32->N15 - - - - - - - 512.44kB - - - - - -N33 - - -config -Parse -0 of 2836.92kB (23.36%) - - - - - -N34 - - -config -ParseRawConfig -0 of 2836.92kB (23.36%) - - - - - -N33->N34 - - - - - - - 2836.92kB - - - - - -N35 - - -config -parseRules -0 of 2836.92kB (23.36%) - - - - - -N34->N35 - - - - - - - 2836.92kB - - - - - -N41 - - -rules -ParseRule -0 of 2836.92kB (23.36%) - - - - - -N35->N41 - - - - - - - 2836.92kB - - - - - -N36 - - -hub -Parse -0 of 2836.92kB (23.36%) - - - - - -N37 - - -executor -Parse -0 of 2836.92kB (23.36%) - - - - - -N36->N37 - - - - - - - 2836.92kB - (inline) - - - - - -N39 - - -executor -ParseWithPath -0 of 2836.92kB (23.36%) - - - - - -N37->N39 - - - - - - - 2836.92kB - - - - - -N38 - - -executor -ParseWithBytes -0 of 2836.92kB (23.36%) - - - - - -N38->N33 - - - - - - - 2836.92kB - - - - - -N39->N38 - - - - - - - 2836.92kB - (inline) - - - - - -N44 - - -provider -(*ruleSetProvider) -Initial -0 of 512.44kB (4.22%) - - - - - -N40->N44 - - - - - - - 512.44kB - - - - - -N42 - - -common -NewGEOSITE -0 of 2836.92kB (23.36%) - - - - - -N41->N42 - - - - - - - 2836.92kB - - - - - -N42->N5 - - - - - - - 2836.92kB - - - - - -N43 - - -provider -(*domainStrategy) -OnUpdate -0 of 512.44kB (4.22%) - - - - - -N43->N32 - - - - - - - 512.44kB - - - - - -N45 - - -provider -NewRuleSetProvider -func1 -0 of 512.44kB (4.22%) - - - - - -N44->N45 - - - - - - - 512.44kB - - - - - -N45->N43 - - - - - - - 512.44kB - - - - - -N46 - - -congestion -(*BandwidthSampler) -OnPacketSent -0 of 596.16kB (4.91%) - - - - - -N46->N12 - - - - - - - 596.16kB - (inline) - - - - - -N47 - - -congestion -(*bbrSender) -OnPacketSent -0 of 596.16kB (4.91%) - - - - - -N47->N46 - - - - - - - 596.16kB - - - - - -N53 - - -quic-go -getPacketBuffer -0 of 512.75kB (4.22%) - - - - - -N48->N53 - - - - - - - 512.75kB - (inline) - - - - - -N52 - - -quic-go -(*connection) -sendPackets -0 of 596.16kB (4.91%) - - - - - -N49->N52 - - - - - - - 596.16kB - - - - - -N50 - - -quic-go -(*connection) -sendPackedPacket -0 of 596.16kB (4.91%) - - - - - -N55 - - -ackhandler -(*sentPacketHandler) -SentPacket -0 of 596.16kB (4.91%) - - - - - -N50->N55 - - - - - - - 596.16kB - - - - - -N51 - - -quic-go -(*connection) -sendPacket -0 of 596.16kB (4.91%) - - - - - -N51->N50 - - - - - - - 596.16kB - - - - - -N52->N51 - - - - - - - 596.16kB - - - - - -N53->N14 - - - - - - - 512.75kB - - - - - -N54 - - -ackhandler -(*ccAdapter) -OnPacketSent -0 of 596.16kB (4.91%) - - - - - -N54->N47 - - - - - - - 596.16kB - - - - - -N56 - - -ackhandler -(*sentPacketHandler) -sentPacketImpl -0 of 596.16kB (4.91%) - - - - - -N55->N56 - - - - - - - 596.16kB - - - - - -N56->N54 - - - - - - - 596.16kB - - - - - -N57 - - -impl -(*MessageInfo) -unmarshal -0 of 1024.02kB (8.43%) - - - - - -N57->N8 - - - - - - - 1024.02kB - - - - - -N58->N8 - - - - - - - 1024.02kB - - - - - -N60 - - -proto -UnmarshalOptions -unmarshal -0 of 1024.02kB (8.43%) - - - - - -N59->N60 - - - - - - - 1024.02kB - - - - - -N60->N57 - - - - - - - 1024.02kB - - - - - -N61->N36 - - - - - - - 2836.92kB - - - - - -N63 - - -runtime -mstart1 -0 of 3587.50kB (29.54%) - - - - - -N62->N63 - - - - - - - 3587.50kB - - - - - -N63->N2 - - - - - - - 3587.50kB - - - - - -N64 - - -runtime -newm -0 of 5637.50kB (46.41%) - - - - - -N64->N1 - - - - - - - 5637.50kB - - - - - -N66 - - -runtime -newproc1 -0 of 512.20kB (4.22%) - - - - - -N65->N66 - - - - - - - 512.20kB - - - - - -N66->N16 - - - - - - - 512.20kB - - - - - -N67->N2 - - - - - - - 2050kB - - - - - -N69 - - -runtime -startm -0 of 5637.50kB (46.41%) - - - - - -N68->N69 - - - - - - - 5637.50kB - - - - - -N69->N64 - - - - - - - 5637.50kB - - - - - diff --git a/flake.nix b/flake.nix index 88a6eacb..afe6e1c1 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Another Clash Kernel"; + description = "Another Mihomo Kernel"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/master"; @@ -15,7 +15,7 @@ }; in rec { - packages.default = pkgs.clash-meta; + packages.default = pkgs.mihomo-meta; } ) // ( @@ -23,12 +23,12 @@ { overlay = final: prev: { - clash-meta = final.buildGo119Module { - pname = "clash-meta"; + mihomo-meta = final.buildGo119Module { + pname = "mihomo-meta"; inherit version; src = ./.; - vendorSha256 = "sha256-8cbcE9gKJjU14DNTLPc6nneEPZg7Akt+FlSDlPRvG5k="; + vendorSha256 = "sha256-W5oiPtTRin0731QQWr98xZ2Vpk97HYcBtKoi1OKZz+w="; # Do not build testing suit excludedPackages = [ "./test" ]; @@ -38,8 +38,8 @@ ldflags = [ "-s" "-w" - "-X github.com/Dreamacro/clash/constant.Version=dev-${version}" - "-X github.com/Dreamacro/clash/constant.BuildTime=${version}" + "-X github.com/metacubex/mihomo/constant.Version=dev-${version}" + "-X github.com/metacubex/mihomo/constant.BuildTime=${version}" ]; tags = [ @@ -50,7 +50,7 @@ doCheck = false; postInstall = '' - mv $out/bin/clash $out/bin/clash-meta + mv $out/bin/mihomo $out/bin/mihomo-meta ''; }; diff --git a/go.mod b/go.mod index c2a979dc..da45cd28 100644 --- a/go.mod +++ b/go.mod @@ -1,83 +1,111 @@ -module github.com/Dreamacro/clash +module github.com/metacubex/mihomo -go 1.19 +go 1.20 require ( + github.com/3andne/restls-client-go v0.1.6 github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da - github.com/cilium/ebpf v0.9.3 - github.com/coreos/go-iptables v0.6.0 - github.com/dlclark/regexp2 v1.7.0 - github.com/go-chi/chi/v5 v5.0.8 + github.com/cilium/ebpf v0.12.0 + github.com/coreos/go-iptables v0.7.0 + github.com/dlclark/regexp2 v1.10.0 + github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/cors v1.2.1 - github.com/go-chi/render v1.0.2 - github.com/gofrs/uuid v4.3.1+incompatible - github.com/google/gopacket v1.1.19 - github.com/gorilla/websocket v1.5.0 - github.com/hashicorp/golang-lru v0.5.4 - github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 + github.com/go-chi/render v1.0.3 + github.com/gobwas/ws v1.3.0 + github.com/gofrs/uuid/v5 v5.0.0 + github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a github.com/jpillora/backoff v1.0.0 + github.com/klauspost/cpuid/v2 v2.2.5 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 - github.com/mdlayher/netlink v1.7.2-0.20221213171556-9881fafed8c7 - github.com/metacubex/quic-go v0.32.0 - github.com/metacubex/sing-shadowsocks v0.1.1-0.20230202072246-e2bef5f088c7 - github.com/metacubex/sing-tun v0.1.1-0.20230213124625-28d27a0c236b - github.com/metacubex/sing-wireguard v0.0.0-20230213124601-d04406a109b4 - github.com/miekg/dns v1.1.50 - github.com/mroth/weightedrand/v2 v2.0.0 - github.com/oschwald/geoip2-golang v1.8.0 - github.com/refraction-networking/utls v1.2.0 + github.com/mdlayher/netlink v1.7.2 + github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 + github.com/metacubex/quic-go v0.39.1-0.20231019030608-fd969d66f16b + github.com/metacubex/sing-quic v0.0.0-20231008050747-a684db516966 + github.com/metacubex/sing-shadowsocks v0.2.5 + github.com/metacubex/sing-shadowsocks2 v0.1.4 + github.com/metacubex/sing-tun v0.1.15-0.20231103033938-170591e8d5bd + github.com/metacubex/sing-vmess v0.1.9-0.20230921005247-a0488d7dac74 + github.com/metacubex/sing-wireguard v0.0.0-20231001110902-321836559170 + github.com/miekg/dns v1.1.56 + github.com/mroth/weightedrand/v2 v2.1.0 + github.com/openacid/low v0.1.21 + github.com/oschwald/maxminddb-golang v1.12.0 + github.com/puzpuzpuz/xsync/v2 v2.5.1 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 - github.com/sagernet/sing v0.1.7-0.20230207063819-27d2950cdbe9 - github.com/sagernet/sing-vmess v0.1.1-0.20230212211128-cb4e47dd0acb - github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d - github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c - github.com/samber/lo v1.37.0 - github.com/sirupsen/logrus v1.9.0 - github.com/stretchr/testify v1.8.1 - github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837 - go.etcd.io/bbolt v1.3.6 - go.uber.org/atomic v1.10.0 - go.uber.org/automaxprocs v1.5.1 - golang.org/x/crypto v0.5.0 - golang.org/x/exp v0.0.0-20221205204356-47842c84f3db - golang.org/x/net v0.5.0 - golang.org/x/sync v0.1.0 - golang.org/x/sys v0.4.0 - google.golang.org/protobuf v1.28.1 + github.com/sagernet/sing v0.2.14 + github.com/sagernet/sing-mux v0.1.3 + github.com/sagernet/sing-shadowtls v0.1.4 + github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 + github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 + github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f + github.com/samber/lo v1.38.1 + github.com/shirou/gopsutil/v3 v3.23.9 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.4 + github.com/zhangyunhao116/fastrand v0.3.0 + go.etcd.io/bbolt v1.3.7 + go.uber.org/automaxprocs v1.5.3 + golang.org/x/crypto v0.14.0 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d + golang.org/x/net v0.17.0 + golang.org/x/sync v0.4.0 + golang.org/x/sys v0.13.0 + google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v3 v3.0.1 - lukechampine.com/blake3 v1.1.7 + lukechampine.com/blake3 v1.2.1 ) require ( + github.com/RyuaNerin/go-krypto v1.0.2 // 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.4 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect - github.com/golang/mock v1.6.0 // indirect - github.com/google/btree v1.0.1 // 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/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.15.12 // indirect - github.com/klauspost/cpuid/v2 v2.0.12 // indirect - github.com/mdlayher/socket v0.4.0 // indirect - github.com/metacubex/gvisor v0.0.0-20230213124051-7a16c835d80e // indirect - github.com/onsi/ginkgo/v2 v2.2.0 // indirect - github.com/oschwald/maxminddb-golang v1.10.0 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/metacubex/gvisor v0.0.0-20231001104248-0f672c3fb8d8 // 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/quic-go/qtls-go1-18 v0.2.0 // indirect - github.com/quic-go/qtls-go1-19 v0.2.0 // indirect - github.com/quic-go/qtls-go1-20 v0.1.0 // indirect - github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e // indirect + github.com/quic-go/qtls-go1-20 v0.3.4 // indirect github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect - github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect + github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect + github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // 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/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 - golang.org/x/mod v0.6.0 // indirect - golang.org/x/text v0.6.0 // indirect - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect - golang.org/x/tools v0.2.0 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // 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/mod v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.14.0 // indirect ) -replace go.uber.org/atomic v1.10.0 => github.com/metacubex/uber-atomic v0.0.0-20230202125923-feb10b770370 +replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20231001053806-1230641572b9 diff --git a/go.sum b/go.sum index 076442ab..6a6356c1 100644 --- a/go.sum +++ b/go.sum @@ -1,255 +1,270 @@ +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.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/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.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 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/cilium/ebpf v0.9.3 h1:5KtxXZU+scyERvkJMEm16TbScVvuuMrlhPly78ZMbSc= -github.com/cilium/ebpf v0.9.3/go.mod h1:w27N4UjpaQ9X/DGrSugxUG+H+NhgntDuPb5lCzxCn8A= -github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= -github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/cilium/ebpf v0.12.0 h1:oQEuIQIXgYhe1v7sYUG0P9vtJTYZLLdA6tiQmrOB1mo= +github.com/cilium/ebpf v0.12.0/go.mod h1:u9H29/Iq+8cy70YqI6p5pfADkFl3vdnV2qXDg5JL0Zo= +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/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= -github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= -github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= -github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg= -github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= -github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +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.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= +github.com/gobwas/ws v1.3.0/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/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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= +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-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg= -github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE= +github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a h1:S33o3djA1nPRd+d/bf7jbbXytXuK/EoXow7+aa76grQ= +github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a/go.mod h1:zmdm3sTSDP3vOOX3CEWRkkRHtKr1DxBx+J1OQFoDQQs= 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/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= -github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= -github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= -github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= -github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +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.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/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/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= -github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= -github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= -github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= -github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= -github.com/mdlayher/netlink v1.7.2-0.20221213171556-9881fafed8c7 h1:HSkXG1bE/qcRuuPlZ2Jyf0Od8HLxOowi7CzKQqNtWn4= -github.com/mdlayher/netlink v1.7.2-0.20221213171556-9881fafed8c7/go.mod h1:1ztDZHGbU5MjN5lNZpkpG8ygndjjWzcojp/H7r6l6QQ= -github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= -github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= -github.com/metacubex/gvisor v0.0.0-20230213124051-7a16c835d80e h1:j4j2dlV2d//FAsQlRUriH6nvv36AEAhECbNy7narf1M= -github.com/metacubex/gvisor v0.0.0-20230213124051-7a16c835d80e/go.mod h1:abc7OdNmWlhcNHz84ECEosd5ND5pnWQmD8W55p/4cuc= -github.com/metacubex/quic-go v0.32.0 h1:dSD8LB4MSeBuD4otd8y1DUZcRdDcEB0Ax5esPOqn2Hw= -github.com/metacubex/quic-go v0.32.0/go.mod h1:yParIzDYUd/t/pzFlDtZKhnvSqbUu0bPChlKEGmJStA= -github.com/metacubex/sing-shadowsocks v0.1.1-0.20230202072246-e2bef5f088c7 h1:MNCGIpXhxXn9ck5bxfm/cW9Nr2FGQ5cakcGK0yKZcak= -github.com/metacubex/sing-shadowsocks v0.1.1-0.20230202072246-e2bef5f088c7/go.mod h1:8pBSYDKVxTtqUtGZyEh4ZpFJXwP6wBVVKrs6oQiOwmQ= -github.com/metacubex/sing-tun v0.1.1-0.20230213124625-28d27a0c236b h1:ZF/oNrSCaxIFoZmFQCiUx67t9aENZjyuqw2n4zw3L2o= -github.com/metacubex/sing-tun v0.1.1-0.20230213124625-28d27a0c236b/go.mod h1:TjuaYuR/g1MaY3um89xTfRNt61FJ2IcI/m5zD8QBxw4= -github.com/metacubex/sing-wireguard v0.0.0-20230213124601-d04406a109b4 h1:d96mCF/LYyC9kULd2xwcXfP0Jd8klrOngmRxuUIZg/8= -github.com/metacubex/sing-wireguard v0.0.0-20230213124601-d04406a109b4/go.mod h1:p2VpJuxRefgVMxc8cmatMGSFNvYbjMYMsXJOe7qFstw= -github.com/metacubex/uber-atomic v0.0.0-20230202125923-feb10b770370 h1:UkViS4DCESAUEYgbIEQdD02hyMacFt6Dny+1MOJtNIo= -github.com/metacubex/uber-atomic v0.0.0-20230202125923-feb10b770370/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/mroth/weightedrand/v2 v2.0.0 h1:ADehnByWbliEDIazDAKFdBHoqgHSXAkgyKqM/9YsPoo= -github.com/mroth/weightedrand/v2 v2.0.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= -github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= -github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= -github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= -github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= -github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= -github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +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.39.1-0.20231019030608-fd969d66f16b h1:uZ++sW8yg7Fr/Wvmmrb/V+SfxvRs0iMC+2+u2bRmO8g= +github.com/metacubex/quic-go v0.39.1-0.20231019030608-fd969d66f16b/go.mod h1:4pe6cY+nAMFU/Uxn1rfnxNIowsaJGDQ3uyy4VuiPkP4= +github.com/metacubex/sing v0.0.0-20231001053806-1230641572b9 h1:F0+IuW0tZ96QHEmrebXAdYnz7ab7Gz4l5yYC4g6Cg8k= +github.com/metacubex/sing v0.0.0-20231001053806-1230641572b9/go.mod h1:GQ673iPfUnkbK/dIPkfd1Xh1MjOGo36gkl/mkiHY7Jg= +github.com/metacubex/sing-quic v0.0.0-20231008050747-a684db516966 h1:wbOsbU3kfD5LRuJIntJwEPmgGSQukof8CgLNypi8az8= +github.com/metacubex/sing-quic v0.0.0-20231008050747-a684db516966/go.mod h1:GU7g2AZesXItk4CspDP8Dc7eGtlA2GVDihyCwsUXRSo= +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.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= +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/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/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= 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-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U= -github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc= -github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk= -github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= -github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI= -github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= -github.com/refraction-networking/utls v1.2.0 h1:U5f8wkij2NVinfLuJdFP3gCMwIHs+EzvhxmYdXgiapo= -github.com/refraction-networking/utls v1.2.0/go.mod h1:NPq+cVqzH7D1BeOkmOcb5O/8iVewAsiVt2x1/eO0hgQ= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e h1:5CFRo8FJbCuf5s/eTBdZpmMbn8Fe2eSMLNAYfKanA34= -github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e/go.mod h1:qbt0dWObotCfcjAJJ9AxtFPNSDUfZF+6dCpgKEOBn/g= +github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= +github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 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.7-0.20230207063819-27d2950cdbe9 h1:qnXh4RjHsNjdZXkfbqwVqAzYUfc160gfkS5gepmsA+A= -github.com/sagernet/sing v0.1.7-0.20230207063819-27d2950cdbe9/go.mod h1:JLSXsPTGRJFo/3X7EcAOCUgJH2/gAoxSJgBsnCZRp/w= -github.com/sagernet/sing-vmess v0.1.1-0.20230212211128-cb4e47dd0acb h1:oyd3w17fXNmWVYFUe17YVHJW5CLW9X2mxJFDP/IWrAM= -github.com/sagernet/sing-vmess v0.1.1-0.20230212211128-cb4e47dd0acb/go.mod h1:9KkmnQzTL4Gvv8U2TRAH2BOITCGsGPpHtUPP5sxn5sY= -github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d h1:trP/l6ZPWvQ/5Gv99Z7/t/v8iYy06akDMejxW1sznUk= -github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d/go.mod h1:jk6Ii8Y3En+j2KQDLgdgQGwb3M6y7EL567jFnGYhN9g= -github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= -github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c/go.mod h1:euOmN6O5kk9dQmgSS8Df4psAl3TCjxOz0NW60EWkSaI= -github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= -github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/sagernet/sing-mux v0.1.3 h1:fAf7PZa2A55mCeh0KKM02f1k2Y4vEmxuZZ/51ahkkLA= +github.com/sagernet/sing-mux v0.1.3/go.mod h1:wGeIeiiFLx4HUM5LAg65wrNZ/X1muOimqK0PEhNbPi0= +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.9 h1:ZI5bWVeu2ep4/DIxB4U9okeYJ7zp/QLTO4auRb/ty/E= +github.com/shirou/gopsutil/v3 v3.23.9/go.mod h1:x/NWSb71eMcjFIO0vhyGW5nZ7oSIgVjrCnADckb85GA= +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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww= -github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E= +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/xtls/go v0.0.0-20220914232946-0441cf4cf837 h1:AHhUwwFJGl27E46OpdJHplZkK09m7aETNBNzhT6t15M= -github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837/go.mod h1:YJTRELIWrGxR1s8xcEBgxcxBfwQfMGjdvNLTjN9XFgY= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk= -go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= +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.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +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.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.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-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= -lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index b3e33f98..6ea02989 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -2,35 +2,41 @@ package executor import ( "fmt" + "net" "net/netip" "os" "runtime" + "strconv" + "strings" "sync" + "time" - "github.com/Dreamacro/clash/adapter" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/adapter/outboundgroup" - "github.com/Dreamacro/clash/component/auth" - "github.com/Dreamacro/clash/component/dialer" - G "github.com/Dreamacro/clash/component/geodata" - "github.com/Dreamacro/clash/component/iface" - "github.com/Dreamacro/clash/component/profile" - "github.com/Dreamacro/clash/component/profile/cachefile" - "github.com/Dreamacro/clash/component/resolver" - SNI "github.com/Dreamacro/clash/component/sniffer" - CTLS "github.com/Dreamacro/clash/component/tls" - "github.com/Dreamacro/clash/component/trie" - "github.com/Dreamacro/clash/config" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/provider" - "github.com/Dreamacro/clash/dns" - "github.com/Dreamacro/clash/listener" - authStore "github.com/Dreamacro/clash/listener/auth" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/listener/inner" - "github.com/Dreamacro/clash/listener/tproxy" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" + "github.com/metacubex/mihomo/ntp" + + "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" + G "github.com/metacubex/mihomo/component/geodata" + "github.com/metacubex/mihomo/component/iface" + "github.com/metacubex/mihomo/component/profile" + "github.com/metacubex/mihomo/component/profile/cachefile" + "github.com/metacubex/mihomo/component/resolver" + SNI "github.com/metacubex/mihomo/component/sniffer" + "github.com/metacubex/mihomo/component/trie" + "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/tunnel" ) var mux sync.Mutex @@ -75,29 +81,44 @@ func ParseWithBytes(buf []byte) (*config.Config, error) { func ApplyConfig(cfg *config.Config, force bool) { mux.Lock() defer mux.Unlock() - preUpdateExperimental(cfg) + + 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()) + } + } + updateUsers(cfg.Users) updateProxies(cfg.Proxies, cfg.Providers) updateRules(cfg.Rules, cfg.SubRules, cfg.RuleProviders) updateSniffer(cfg.Sniffer) updateHosts(cfg.Hosts) - initInnerTcp() - updateDNS(cfg.DNS, cfg.General.IPv6) - loadProxyProvider(cfg.Providers) - updateProfile(cfg) - loadRuleProvider(cfg.RuleProviders) - updateGeneral(cfg.General, force) - updateListeners(cfg.Listeners) + updateGeneral(cfg.General) + updateNTP(cfg.NTP) + updateDNS(cfg.DNS, cfg.RuleProviders, cfg.General.IPv6) + updateListeners(cfg.General, cfg.Listeners, force) updateIPTables(cfg) updateTun(cfg.General) updateExperimental(cfg) updateTunnels(cfg.Tunnels) + tunnel.OnInnerLoading() + + initInnerTcp() + loadProxyProvider(cfg.Providers) + updateProfile(cfg) + loadRuleProvider(cfg.RuleProviders) + runtime.GC() + tunnel.OnRunning() + log.SetLevel(cfg.General.LogLevel) } func initInnerTcp() { - inner.New(tunnel.TCPIn()) + inner.New(tunnel.Tunnel) } func GetGeneral() *config.General { @@ -119,41 +140,67 @@ func GetGeneral() *config.General { ShadowSocksConfig: ports.ShadowSocksConfig, VmessConfig: ports.VmessConfig, Authentication: authenticator, + SkipAuthPrefixes: inbound.SkipAuthPrefixes(), AllowLan: listener.AllowLan(), BindAddress: listener.BindAddress(), }, + Controller: config.Controller{}, Mode: tunnel.Mode(), LogLevel: log.Level(), IPv6: !resolver.DisableIPv6, GeodataLoader: G.LoaderName(), Interface: dialer.DefaultInterface.Load(), Sniffing: tunnel.IsSniffing(), - TCPConcurrent: dialer.GetDial(), + TCPConcurrent: dialer.GetTcpConcurrent(), } return general } -func updateListeners(listeners map[string]C.InboundListener) { - tcpIn := tunnel.TCPIn() - udpIn := tunnel.UDPIn() - natTable := tunnel.NatTable() +func updateListeners(general *config.General, listeners map[string]C.InboundListener, force bool) { + listener.PatchInboundListeners(listeners, tunnel.Tunnel, true) + if !force { + return + } - listener.PatchInboundListeners(listeners, tcpIn, udpIn, natTable, true) + allowLan := general.AllowLan + listener.SetAllowLan(allowLan) + inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes) + + 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.ReCreateAutoRedir(general.EBpf.AutoRedir, 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 updateExperimental(c *config.Config) { - runtime.GC() -} - -func preUpdateExperimental(c *config.Config) { - CTLS.AddCertificate(c.TLS.PrivateKey, c.TLS.Certificate) - for _, c := range c.TLS.CustomTrustCert { - CTLS.AddCertificate(c.PrivateKey, c.Certificate) + if c.Experimental.QUICGoDisableGSO { + _ = os.Setenv("QUIC_GO_DISABLE_GSO", strconv.FormatBool(true)) + } + if c.Experimental.QUICGoDisableECN { + _ = os.Setenv("QUIC_GO_DISABLE_ECN", strconv.FormatBool(true)) } } -func updateDNS(c *config.DNS, generalIPv6 bool) { +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, ruleProvider map[string]provider.RuleProvider, generalIPv6 bool) { if !c.Enable { resolver.DefaultResolver = nil resolver.DefaultHostMapper = nil @@ -161,11 +208,30 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { dns.ReCreateServer("", nil, nil) return } - + policy := make(map[string][]dns.NameServer) + domainSetPolicies := make(map[provider.RuleProvider][]dns.NameServer) + for key, nameservers := range c.NameServerPolicy { + temp := strings.Split(key, ":") + if len(temp) == 2 { + prefix := temp[0] + key := temp[1] + switch strings.ToLower(prefix) { + case "rule-set": + if p, ok := ruleProvider[key]; ok { + domainSetPolicies[p] = nameservers + } + case "geosite": + // TODO: + } + } else { + policy[key] = nameservers + } + } 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, @@ -176,9 +242,10 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { Domain: c.FallbackFilter.Domain, GeoSite: c.FallbackFilter.GeoSite, }, - Default: c.DefaultNameserver, - Policy: c.NameServerPolicy, - ProxyServer: c.ProxyServerNameserver, + Default: c.DefaultNameserver, + Policy: c.NameServerPolicy, + ProxyServer: c.ProxyServerNameserver, + DomainSetPolicy: domainSetPolicies, } r := dns.NewResolver(cfg) @@ -194,15 +261,15 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { resolver.DefaultHostMapper = m resolver.DefaultLocalServer = dns.NewLocalServer(r, m) - if pr.HasProxyServer() { + if pr.Invalid() { resolver.ProxyServerHostResolver = pr } dns.ReCreateServer(c.Listen, r, m) } -func updateHosts(tree *trie.DomainTrie[netip.Addr]) { - resolver.DefaultHosts = tree +func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) { + resolver.DefaultHosts = resolver.NewHosts(tree) } func updateProxies(proxies map[string]C.Proxy, providers map[string]provider.ProxyProvider) { @@ -273,7 +340,7 @@ func updateTun(general *config.General) { if general == nil { return } - listener.ReCreateTun(LC.Tun(general.Tun), tunnel.TCPIn(), tunnel.UDPIn()) + listener.ReCreateTun(general.Tun, tunnel.Tunnel) listener.ReCreateRedirToTun(general.Tun.RedirectToTun) } @@ -301,65 +368,33 @@ func updateSniffer(sniffer *config.Sniffer) { } func updateTunnels(tunnels []LC.Tunnel) { - listener.PatchTunnel(tunnels, tunnel.TCPIn(), tunnel.UDPIn()) + listener.PatchTunnel(tunnels, tunnel.Tunnel) } -func updateGeneral(general *config.General, force bool) { +func updateGeneral(general *config.General) { tunnel.SetMode(general.Mode) tunnel.SetFindProcessMode(general.FindProcessMode) - dialer.DisableIPv6 = !general.IPv6 - if !dialer.DisableIPv6 { - log.Infoln("Use IPv6") - } - resolver.DisableIPv6 = dialer.DisableIPv6 + resolver.DisableIPv6 = !general.IPv6 if general.TCPConcurrent { - dialer.SetDial(general.TCPConcurrent) + dialer.SetTcpConcurrent(general.TCPConcurrent) log.Infoln("Use tcp concurrent") } + inbound.SetTfo(general.InboundTfo) + inbound.SetMPTCP(general.InboundMPTCP) + adapter.UnifiedDelay.Store(general.UnifiedDelay) + dialer.DefaultInterface.Store(general.Interface) - - if dialer.DefaultInterface.Load() != "" { - log.Infoln("Use interface name: %s", general.Interface) - } - dialer.DefaultRoutingMark.Store(int32(general.RoutingMark)) if general.RoutingMark > 0 { log.Infoln("Use routing mark: %#x", general.RoutingMark) } iface.FlushCache() - - if !force { - return - } - geodataLoader := general.GeodataLoader G.SetLoader(geodataLoader) - - allowLan := general.AllowLan - listener.SetAllowLan(allowLan) - - bindAddress := general.BindAddress - listener.SetBindAddress(bindAddress) - - inbound.SetTfo(general.InboundTfo) - - tcpIn := tunnel.TCPIn() - udpIn := tunnel.UDPIn() - natTable := tunnel.NatTable() - - listener.ReCreateHTTP(general.Port, tcpIn) - listener.ReCreateSocks(general.SocksPort, tcpIn, udpIn) - listener.ReCreateRedir(general.RedirPort, tcpIn, udpIn, natTable) - listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tcpIn, udpIn) - listener.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn, natTable) - listener.ReCreateMixed(general.MixedPort, tcpIn, udpIn) - listener.ReCreateShadowSocks(general.ShadowSocksConfig, tcpIn, udpIn) - listener.ReCreateVmess(general.VmessConfig, tcpIn, udpIn) - listener.ReCreateTuic(LC.TuicServer(general.TuicServer), tcpIn, udpIn) } func updateUsers(users []auth.AuthUser) { @@ -401,7 +436,7 @@ func patchSelectGroup(proxies map[string]C.Proxy) { continue } - selector.Set(selected) + selector.ForceSet(selected) } } @@ -466,9 +501,9 @@ func updateIPTables(cfg *config.Config) { } func Shutdown() { - listener.Cleanup(false) + listener.Cleanup() tproxy.CleanupTProxyIPTables() resolver.StoreFakePoolState() - log.Warnln("Clash shutting down") + log.Warnln("Mihomo shutting down") } diff --git a/hub/hub.go b/hub/hub.go index 1e925bfe..323f8749 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -1,9 +1,10 @@ package hub import ( - "github.com/Dreamacro/clash/config" - "github.com/Dreamacro/clash/hub/executor" - "github.com/Dreamacro/clash/hub/route" + "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) @@ -26,7 +27,7 @@ func WithSecret(secret string) Option { } } -// Parse call at the beginning of clash +// Parse call at the beginning of mihomo func Parse(options ...Option) error { cfg, err := executor.Parse() if err != nil { @@ -43,7 +44,7 @@ func Parse(options ...Option) error { if cfg.General.ExternalController != "" { go route.Start(cfg.General.ExternalController, cfg.General.ExternalControllerTLS, - cfg.General.Secret, cfg.TLS.Certificate, cfg.TLS.PrivateKey) + cfg.General.Secret, cfg.TLS.Certificate, cfg.TLS.PrivateKey, cfg.General.LogLevel == log.DEBUG) } executor.ApplyConfig(cfg, true) diff --git a/hub/route/cache.go b/hub/route/cache.go index bdfd2e35..f07eb33a 100644 --- a/hub/route/cache.go +++ b/hub/route/cache.go @@ -3,7 +3,7 @@ package route import ( "net/http" - "github.com/Dreamacro/clash/component/resolver" + "github.com/metacubex/mihomo/component/resolver" "github.com/go-chi/chi/v5" "github.com/go-chi/render" diff --git a/hub/route/configs.go b/hub/route/configs.go index 9e630b29..3b5f62b3 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -2,19 +2,21 @@ package route import ( "net/http" + "net/netip" "path/filepath" "sync" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/resolver" - "github.com/Dreamacro/clash/config" - "github.com/Dreamacro/clash/constant" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/hub/executor" - P "github.com/Dreamacro/clash/listener" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" + "github.com/metacubex/mihomo/adapter/inbound" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/config" + "github.com/metacubex/mihomo/constant" + 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" @@ -47,6 +49,7 @@ type configSchema struct { TcptunConfig *string `json:"tcptun-config"` UdptunConfig *string `json:"udptun-config"` AllowLan *bool `json:"allow-lan"` + SkipAuthPrefixes *[]netip.Prefix `json:"skip-auth-prefixes"` BindAddress *string `json:"bind-address"` Mode *tunnel.TunnelMode `json:"mode"` LogLevel *log.LogLevel `json:"log-level"` @@ -66,33 +69,38 @@ type tunSchema struct { //RedirectToTun []string `yaml:"-" json:"-"` MTU *uint32 `yaml:"mtu" json:"mtu,omitempty"` - //Inet4Address *[]config.ListenPrefix `yaml:"inet4-address" json:"inet4-address,omitempty"` - Inet6Address *[]LC.ListenPrefix `yaml:"inet6-address" json:"inet6-address,omitempty"` - StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` - Inet4RouteAddress *[]LC.ListenPrefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` - Inet6RouteAddress *[]LC.ListenPrefix `yaml:"inet6-route-address" json:"inet6-route-address,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"` + //Inet4Address *[]netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` + Inet6Address *[]netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` + StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` + 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"` + 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"` } type tuicServerSchema struct { - Enable bool `yaml:"enable" json:"enable"` - Listen *string `yaml:"listen" json:"listen"` - Token *[]string `yaml:"token" json:"token"` - 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"` + 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) { @@ -142,6 +150,18 @@ func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { if p.Inet6Address != nil { def.Inet6Address = *p.Inet6Address } + 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.IncludeUID != nil { def.IncludeUID = *p.IncludeUID } @@ -169,6 +189,9 @@ func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { if p.UDPTimeout != nil { def.UDPTimeout = *p.UDPTimeout } + if p.FileDescriptor != nil { + def.FileDescriptor = *p.FileDescriptor + } } return def } @@ -182,6 +205,9 @@ func pointerOrDefaultTuicServer(p *tuicServerSchema, def LC.TuicServer) LC.TuicS if p.Token != nil { def.Token = *p.Token } + if p.Users != nil { + def.Users = *p.Users + } if p.Certificate != nil { def.Certificate = *p.Certificate } @@ -203,6 +229,9 @@ func pointerOrDefaultTuicServer(p *tuicServerSchema, def LC.TuicServer) LC.TuicS if p.MaxUdpRelayPacketSize != nil { def.MaxUdpRelayPacketSize = *p.MaxUdpRelayPacketSize } + if p.CWND != nil { + def.CWND = *p.CWND + } } return def } @@ -219,6 +248,10 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) { P.SetAllowLan(*general.AllowLan) } + if general.SkipAuthPrefixes != nil { + inbound.SetSkipAuthPrefixes(*general.SkipAuthPrefixes) + } + if general.BindAddress != nil { P.SetBindAddress(*general.BindAddress) } @@ -228,7 +261,7 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) { } if general.TcpConcurrent != nil { - dialer.SetDial(*general.TcpConcurrent) + dialer.SetTcpConcurrent(*general.TcpConcurrent) } if general.InterfaceName != nil { @@ -237,19 +270,15 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) { ports := P.GetPorts() - tcpIn := tunnel.TCPIn() - udpIn := tunnel.UDPIn() - natTable := tunnel.NatTable() - - P.ReCreateHTTP(pointerOrDefault(general.Port, ports.Port), tcpIn) - P.ReCreateSocks(pointerOrDefault(general.SocksPort, ports.SocksPort), tcpIn, udpIn) - P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tcpIn, udpIn, natTable) - P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn, natTable) - P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn) - P.ReCreateTun(pointerOrDefaultTun(general.Tun, P.LastTunConf), tcpIn, udpIn) - P.ReCreateShadowSocks(pointerOrDefaultString(general.ShadowSocksConfig, ports.ShadowSocksConfig), tcpIn, udpIn) - P.ReCreateVmess(pointerOrDefaultString(general.VmessConfig, ports.VmessConfig), tcpIn, udpIn) - P.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, P.LastTuicConf), tcpIn, udpIn) + 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(pointerOrDefaultString(general.ShadowSocksConfig, ports.ShadowSocksConfig), tunnel.Tunnel) + P.ReCreateVmess(pointerOrDefaultString(general.VmessConfig, ports.VmessConfig), tunnel.Tunnel) + P.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, P.LastTuicConf), tunnel.Tunnel) if general.Mode != nil { tunnel.SetMode(*general.Mode) diff --git a/hub/route/connections.go b/hub/route/connections.go index bfe3b42d..e0ff2426 100644 --- a/hub/route/connections.go +++ b/hub/route/connections.go @@ -7,11 +7,12 @@ import ( "strconv" "time" - "github.com/Dreamacro/clash/tunnel/statistic" + "github.com/metacubex/mihomo/tunnel/statistic" "github.com/go-chi/chi/v5" "github.com/go-chi/render" - "github.com/gorilla/websocket" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" ) func connectionRouter() http.Handler { @@ -23,13 +24,13 @@ func connectionRouter() http.Handler { } func getConnections(w http.ResponseWriter, r *http.Request) { - if !websocket.IsWebSocketUpgrade(r) { + if !(r.Header.Get("Upgrade") == "websocket") { snapshot := statistic.DefaultManager.Snapshot() render.JSON(w, r, snapshot) return } - conn, err := upgrader.Upgrade(w, r, nil) + conn, _, _, err := ws.UpgradeHTTP(r, w) if err != nil { return } @@ -55,7 +56,7 @@ func getConnections(w http.ResponseWriter, r *http.Request) { return err } - return conn.WriteMessage(websocket.TextMessage, buf.Bytes()) + return wsutil.WriteMessage(conn, ws.StateServerSide, ws.OpText, buf.Bytes()) } if err := sendSnapshot(); err != nil { @@ -73,20 +74,16 @@ func getConnections(w http.ResponseWriter, r *http.Request) { func closeConnection(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") - snapshot := statistic.DefaultManager.Snapshot() - for _, c := range snapshot.Connections { - if id == c.ID() { - c.Close() - break - } + if c := statistic.DefaultManager.Get(id); c != nil { + _ = c.Close() } render.NoContent(w, r) } func closeAllConnections(w http.ResponseWriter, r *http.Request) { - snapshot := statistic.DefaultManager.Snapshot() - for _, c := range snapshot.Connections { - c.Close() - } + 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 index 56370192..6883b208 100644 --- a/hub/route/ctxkeys.go +++ b/hub/route/ctxkeys.go @@ -10,5 +10,5 @@ var ( type contextKey string func (c contextKey) String() string { - return "clash context key " + string(c) + return "mihomo context key " + string(c) } diff --git a/hub/route/dns.go b/hub/route/dns.go index 2918b059..1762c947 100644 --- a/hub/route/dns.go +++ b/hub/route/dns.go @@ -5,7 +5,7 @@ import ( "math" "net/http" - "github.com/Dreamacro/clash/component/resolver" + "github.com/metacubex/mihomo/component/resolver" "github.com/go-chi/chi/v5" "github.com/go-chi/render" diff --git a/hub/route/groups.go b/hub/route/groups.go index 13133e9c..e36b8ab0 100644 --- a/hub/route/groups.go +++ b/hub/route/groups.go @@ -2,14 +2,17 @@ package route import ( "context" - "github.com/Dreamacro/clash/adapter" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/tunnel" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "net/http" "strconv" "time" + + "github.com/metacubex/mihomo/adapter" + "github.com/metacubex/mihomo/adapter/outboundgroup" + "github.com/metacubex/mihomo/common/utils" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/tunnel" ) func GroupRouter() http.Handler { @@ -55,6 +58,11 @@ func getGroupDelay(w http.ResponseWriter, r *http.Request) { return } + if proxy.(*adapter.Proxy).Type() == C.URLTest { + URLTestGroup := proxy.(*adapter.Proxy).ProxyAdapter.(*outboundgroup.URLTest) + URLTestGroup.ForceSet("") + } + query := r.URL.Query() url := query.Get("url") timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32) @@ -64,11 +72,17 @@ func getGroupDelay(w http.ResponseWriter, r *http.Request) { return } + expectedStatus, err := utils.NewIntRanges[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) - + dm, err := group.URLTest(ctx, url, expectedStatus) if err != nil { render.Status(r, http.StatusGatewayTimeout) render.JSON(w, r, newError(err.Error())) diff --git a/hub/route/provider.go b/hub/route/provider.go index 1ba0d32c..a8611a79 100644 --- a/hub/route/provider.go +++ b/hub/route/provider.go @@ -4,22 +4,35 @@ import ( "context" "net/http" - "github.com/Dreamacro/clash/constant/provider" - "github.com/Dreamacro/clash/tunnel" + 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("/{name}", func(r chi.Router) { + 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 } @@ -54,7 +67,7 @@ func healthCheckProvider(w http.ResponseWriter, r *http.Request) { func parseProviderName(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - name := getEscapeParam(r, "name") + name := getEscapeParam(r, "providerName") ctx := context.WithValue(r.Context(), CtxKeyProviderName, name) next.ServeHTTP(w, r.WithContext(ctx)) }) @@ -76,6 +89,27 @@ func findProviderByName(next http.Handler) http.Handler { }) } +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) diff --git a/hub/route/proxies.go b/hub/route/proxies.go index 5bf6eb9c..759e64d2 100644 --- a/hub/route/proxies.go +++ b/hub/route/proxies.go @@ -7,11 +7,12 @@ import ( "strconv" "time" - "github.com/Dreamacro/clash/adapter" - "github.com/Dreamacro/clash/adapter/outboundgroup" - "github.com/Dreamacro/clash/component/profile/cachefile" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/tunnel" + "github.com/metacubex/mihomo/adapter" + "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" @@ -45,7 +46,7 @@ func parseProxyName(next http.Handler) http.Handler { func findProxyByName(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := r.Context().Value(CtxKeyProxyName).(string) - proxies := tunnel.Proxies() + proxies := tunnel.ProxiesWithProviders() proxy, exist := proxies[name] if !exist { render.Status(r, http.StatusNotFound) @@ -59,7 +60,7 @@ func findProxyByName(next http.Handler) http.Handler { } func getProxies(w http.ResponseWriter, r *http.Request) { - proxies := tunnel.Proxies() + proxies := tunnel.ProxiesWithProviders() render.JSON(w, r, render.M{ "proxies": proxies, }) @@ -112,12 +113,19 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) { return } + expectedStatus, err := utils.NewIntRanges[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) + delay, err := proxy.URLTest(ctx, url, expectedStatus, C.ExtraHistory) if ctx.Err() != nil { render.Status(r, http.StatusGatewayTimeout) render.JSON(w, r, ErrRequestTimeout) @@ -126,7 +134,11 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) { if err != nil || delay == 0 { render.Status(r, http.StatusServiceUnavailable) - render.JSON(w, r, newError("An error occurred in the delay test")) + if err != nil && delay != 0 { + render.JSON(w, r, err) + } else { + render.JSON(w, r, newError("An error occurred in the delay test")) + } return } diff --git a/hub/route/restart.go b/hub/route/restart.go new file mode 100644 index 00000000..49d7e517 --- /dev/null +++ b/hub/route/restart.go @@ -0,0 +1,67 @@ +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 index 51f8f01c..43d33299 100644 --- a/hub/route/rules.go +++ b/hub/route/rules.go @@ -1,10 +1,10 @@ package route import ( - "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/constant" "net/http" - "github.com/Dreamacro/clash/tunnel" + "github.com/metacubex/mihomo/tunnel" "github.com/go-chi/chi/v5" "github.com/go-chi/render" diff --git a/hub/route/server.go b/hub/route/server.go index 0d6a47ac..d510e986 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -2,22 +2,28 @@ package route import ( "bytes" + "crypto/subtle" "crypto/tls" "encoding/json" + "net" "net/http" + "runtime/debug" "strings" "time" - "github.com/Dreamacro/clash/adapter/inbound" - CN "github.com/Dreamacro/clash/common/net" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel/statistic" + "github.com/metacubex/mihomo/adapter/inbound" + CN "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/utils" + 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/cors" "github.com/go-chi/render" - "github.com/gorilla/websocket" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" ) var ( @@ -25,12 +31,6 @@ var ( serverAddr = "" uiPath = "" - - upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - } ) type Traffic struct { @@ -38,12 +38,17 @@ type Traffic struct { Down int64 `json:"down"` } +type Memory struct { + Inuse uint64 `json:"inuse"` + OSLimit uint64 `json:"oslimit"` // maybe we need it in the future +} + func SetUIPath(path string) { uiPath = C.Path.Resolve(path) } func Start(addr string, tlsAddr string, secret string, - certificat, privateKey string) { + certificat, privateKey string, isDebug bool) { if serverAddr != "" { return } @@ -59,11 +64,23 @@ func Start(addr string, tlsAddr string, secret string, MaxAge: 300, }) r.Use(corsM.Handler) + 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) { r.Use(authentication) 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()) @@ -74,6 +91,9 @@ func Start(addr string, tlsAddr string, secret string, r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/cache", cacheRouter()) r.Mount("/dns", dnsRouter()) + r.Mount("/restart", restartRouter()) + r.Mount("/upgrade", upgradeRouter()) + }) if uiPath != "" { @@ -88,7 +108,7 @@ func Start(addr string, tlsAddr string, secret string, if len(tlsAddr) > 0 { go func() { - c, err := CN.ParseCert(certificat, privateKey) + c, err := CN.ParseCert(certificat, privateKey, C.Path) if err != nil { log.Errorln("External controller tls listen error: %s", err) return @@ -128,6 +148,12 @@ func Start(addr string, tlsAddr string, secret string, } +func safeEuqal(a, b string) bool { + aBuf := utils.ImmutableBytesFromString(a) + bBuf := utils.ImmutableBytesFromString(b) + return subtle.ConstantTimeCompare(aBuf, bBuf) == 1 +} + func authentication(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { if serverSecret == "" { @@ -136,9 +162,9 @@ func authentication(next http.Handler) http.Handler { } // Browser websocket not support custom header - if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" { + if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" { token := r.URL.Query().Get("token") - if token != serverSecret { + if !safeEuqal(token, serverSecret) { render.Status(r, http.StatusUnauthorized) render.JSON(w, r, ErrUnauthorized) return @@ -151,7 +177,7 @@ func authentication(next http.Handler) http.Handler { bearer, token, found := strings.Cut(header, " ") hasInvalidHeader := bearer != "Bearer" - hasInvalidSecret := !found || token != serverSecret + hasInvalidSecret := !found || !safeEuqal(token, serverSecret) if hasInvalidHeader || hasInvalidSecret { render.Status(r, http.StatusUnauthorized) render.JSON(w, r, ErrUnauthorized) @@ -163,14 +189,14 @@ func authentication(next http.Handler) http.Handler { } func hello(w http.ResponseWriter, r *http.Request) { - render.JSON(w, r, render.M{"hello": "clash.meta"}) + render.JSON(w, r, render.M{"hello": "mihomo"}) } func traffic(w http.ResponseWriter, r *http.Request) { - var wsConn *websocket.Conn - if websocket.IsWebSocketUpgrade(r) { + var wsConn net.Conn + if r.Header.Get("Upgrade") == "websocket" { var err error - wsConn, err = upgrader.Upgrade(w, r, nil) + wsConn, _, _, err = ws.UpgradeHTTP(r, w) if err != nil { return } @@ -200,7 +226,57 @@ func traffic(w http.ResponseWriter, r *http.Request) { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { - err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) + 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 { @@ -213,6 +289,16 @@ 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") @@ -220,6 +306,12 @@ func getLogs(w http.ResponseWriter, r *http.Request) { 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) @@ -227,10 +319,10 @@ func getLogs(w http.ResponseWriter, r *http.Request) { return } - var wsConn *websocket.Conn - if websocket.IsWebSocketUpgrade(r) { + var wsConn net.Conn + if r.Header.Get("Upgrade") == "websocket" { var err error - wsConn, err = upgrader.Upgrade(w, r, nil) + wsConn, _, _, err = ws.UpgradeHTTP(r, w) if err != nil { return } @@ -262,11 +354,26 @@ func getLogs(w http.ResponseWriter, r *http.Request) { } buf.Reset() - if err := json.NewEncoder(buf).Encode(Log{ - Type: logM.Type(), - Payload: logM.Payload, - }); err != nil { - break + 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 @@ -274,7 +381,7 @@ func getLogs(w http.ResponseWriter, r *http.Request) { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { - err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) + err = wsutil.WriteMessage(wsConn, ws.StateServerSide, ws.OpText, buf.Bytes()) } if err != nil { diff --git a/hub/route/upgrade.go b/hub/route/upgrade.go new file mode 100644 index 00000000..ea371798 --- /dev/null +++ b/hub/route/upgrade.go @@ -0,0 +1,69 @@ +package route + +import ( + "errors" + "fmt" + "net/http" + "os" + + "github.com/metacubex/mihomo/config" + "github.com/metacubex/mihomo/hub/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("/", upgradeCore) + r.Post("/ui", updateUI) + 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.Update(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 := config.UpdateUI() + if err != nil { + if errors.Is(err, config.ErrIncompleteConf) { + log.Warnln("%s", err) + render.Status(r, http.StatusNotImplemented) + render.JSON(w, r, newError(fmt.Sprintf("%s", err))) + } else { + 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/hub/updater/limitedreader.go b/hub/updater/limitedreader.go new file mode 100644 index 00000000..c31db601 --- /dev/null +++ b/hub/updater/limitedreader.go @@ -0,0 +1,67 @@ +package updater + +import ( + "fmt" + "io" + + "golang.org/x/exp/constraints" +) + +// 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/hub/updater/updater.go b/hub/updater/updater.go new file mode 100644 index 00000000..a3bc9a42 --- /dev/null +++ b/hub/updater/updater.go @@ -0,0 +1,489 @@ +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" + "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" + } +} + +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 Update(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", constant.Version, latestVersion) + + if latestVersion == constant.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 { + 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 9 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, http.Header{"User-Agent": {"mihomo"}}, 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, http.Header{"User-Agent": {"mihomo"}}, 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 + 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/listener/auth/auth.go b/listener/auth/auth.go index 70473114..46f552b8 100644 --- a/listener/auth/auth.go +++ b/listener/auth/auth.go @@ -1,7 +1,7 @@ package auth import ( - "github.com/Dreamacro/clash/component/auth" + "github.com/metacubex/mihomo/component/auth" ) var authenticator auth.Authenticator diff --git a/listener/autoredir/tcp.go b/listener/autoredir/tcp.go index 854d31d6..2b21b087 100644 --- a/listener/autoredir/tcp.go +++ b/listener/autoredir/tcp.go @@ -4,10 +4,11 @@ import ( "net" "net/netip" - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/transport/socks5" ) type Listener struct { @@ -42,7 +43,7 @@ func (l *Listener) SetLookupFunc(lookupFunc func(netip.AddrPort) (socks5.Addr, e l.lookupFunc = lookupFunc } -func (l *Listener) handleRedir(conn net.Conn, in chan<- C.ConnContext) { +func (l *Listener) handleRedir(conn net.Conn, tunnel C.Tunnel) { if l.lookupFunc == nil { log.Errorln("[Auto Redirect] lookup function is nil") return @@ -55,12 +56,12 @@ func (l *Listener) handleRedir(conn net.Conn, in chan<- C.ConnContext) { return } - _ = conn.(*net.TCPConn).SetKeepAlive(true) + N.TCPKeepAlive(conn) - in <- inbound.NewSocket(target, conn, C.REDIR, l.additions...) + tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.REDIR, l.additions...)) } -func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { +func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-REDIR"), @@ -86,7 +87,7 @@ func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (* } continue } - go rl.handleRedir(c, in) + go rl.handleRedir(c, tunnel) } }() diff --git a/listener/config/hysteria2.go b/listener/config/hysteria2.go new file mode 100644 index 00000000..5520babc --- /dev/null +++ b/listener/config/hysteria2.go @@ -0,0 +1,25 @@ +package config + +import "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"` +} + +func (h Hysteria2Server) String() string { + b, _ := json.Marshal(h) + return string(b) +} diff --git a/listener/config/shadowsocks.go b/listener/config/shadowsocks.go index cfe31f62..60540bbd 100644 --- a/listener/config/shadowsocks.go +++ b/listener/config/shadowsocks.go @@ -9,6 +9,7 @@ type ShadowsocksServer struct { Listen string Password string Cipher string + Udp bool } func (t ShadowsocksServer) String() string { diff --git a/listener/config/tuic.go b/listener/config/tuic.go index c584bbf5..191cb59c 100644 --- a/listener/config/tuic.go +++ b/listener/config/tuic.go @@ -5,16 +5,19 @@ import ( ) type TuicServer struct { - Enable bool `yaml:"enable" json:"enable"` - Listen string `yaml:"listen" json:"listen"` - Token []string `yaml:"token" json:"token"` - 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"` + 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"` } func (t TuicServer) String() string { diff --git a/listener/config/tun.go b/listener/config/tun.go index 2e1d1a71..6db1fd66 100644 --- a/listener/config/tun.go +++ b/listener/config/tun.go @@ -1,72 +1,19 @@ package config import ( - "encoding/json" "net/netip" - C "github.com/Dreamacro/clash/constant" - - "gopkg.in/yaml.v3" + C "github.com/metacubex/mihomo/constant" ) -type ListenPrefix netip.Prefix - -func (p ListenPrefix) MarshalJSON() ([]byte, error) { - prefix := netip.Prefix(p) - if !prefix.IsValid() { - return json.Marshal(nil) - } - return json.Marshal(prefix.String()) -} - -func (p ListenPrefix) MarshalYAML() (interface{}, error) { - prefix := netip.Prefix(p) - if !prefix.IsValid() { - return nil, nil - } - return prefix.String(), nil -} - -func (p *ListenPrefix) UnmarshalJSON(bytes []byte) error { - var value string - err := json.Unmarshal(bytes, &value) - if err != nil { - return err - } - prefix, err := netip.ParsePrefix(value) - if err != nil { - return err - } - *p = ListenPrefix(prefix) - return nil -} - -func (p *ListenPrefix) UnmarshalYAML(node *yaml.Node) error { - var value string - err := node.Decode(&value) - if err != nil { - return err - } - prefix, err := netip.ParsePrefix(value) - if err != nil { - return err - } - *p = ListenPrefix(prefix) - return nil -} - -func (p ListenPrefix) Build() netip.Prefix { - return netip.Prefix(p) -} - -func StringSliceToListenPrefixSlice(ss []string) ([]ListenPrefix, error) { - lps := make([]ListenPrefix, 0, len(ss)) +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, ListenPrefix(prefix)) + lps = append(lps, prefix) } return lps, nil } @@ -80,19 +27,22 @@ type Tun struct { AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` RedirectToTun []string `yaml:"-" json:"-"` - MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` - Inet4Address []ListenPrefix `yaml:"inet4-address" json:"inet4-address,omitempty"` - Inet6Address []ListenPrefix `yaml:"inet6-address" json:"inet6-address,omitempty"` - StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` - Inet4RouteAddress []ListenPrefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` - Inet6RouteAddress []ListenPrefix `yaml:"inet6-route-address" json:"inet6-route-address,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"` + MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` + Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` + Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` + StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` + 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"` + 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"` } diff --git a/listener/config/vmess.go b/listener/config/vmess.go index cc49433e..1cf2d46c 100644 --- a/listener/config/vmess.go +++ b/listener/config/vmess.go @@ -11,9 +11,12 @@ type VmessUser struct { } type VmessServer struct { - Enable bool - Listen string - Users []VmessUser + Enable bool + Listen string + Users []VmessUser + WsPath string + Certificate string + PrivateKey string } func (t VmessServer) String() string { diff --git a/listener/http/client.go b/listener/http/client.go index 15c21f91..c35cadad 100644 --- a/listener/http/client.go +++ b/listener/http/client.go @@ -7,12 +7,12 @@ import ( "net/http" "time" - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/adapter/inbound" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" ) -func newClient(source net.Addr, in chan<- C.ConnContext, additions ...inbound.Addition) *http.Client { +func newClient(srcConn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) *http.Client { return &http.Client{ Transport: &http.Transport{ // from http.DefaultTransport @@ -32,7 +32,7 @@ func newClient(source net.Addr, in chan<- C.ConnContext, additions ...inbound.Ad left, right := net.Pipe() - in <- inbound.NewHTTP(dstAddr, source, right, additions...) + go tunnel.HandleTCPConn(inbound.NewHTTP(dstAddr, srcConn, right, additions...)) return left, nil }, diff --git a/listener/http/proxy.go b/listener/http/proxy.go index a95f7195..76da4d95 100644 --- a/listener/http/proxy.go +++ b/listener/http/proxy.go @@ -6,16 +6,16 @@ import ( "net/http" "strings" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/common/cache" - N "github.com/Dreamacro/clash/common/net" - C "github.com/Dreamacro/clash/constant" - authStore "github.com/Dreamacro/clash/listener/auth" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/adapter/inbound" + "github.com/metacubex/mihomo/common/cache" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + authStore "github.com/metacubex/mihomo/listener/auth" + "github.com/metacubex/mihomo/log" ) -func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[string, bool], additions ...inbound.Addition) { - client := newClient(c.RemoteAddr(), in, additions...) +func HandleConn(c net.Conn, tunnel C.Tunnel, cache *cache.LruCache[string, bool], additions ...inbound.Addition) { + client := newClient(c, tunnel, additions...) defer client.CloseIdleConnections() conn := N.NewBufferedConn(c) @@ -48,7 +48,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[strin break // close connection } - in <- inbound.NewHTTPS(request, conn, additions...) + tunnel.HandleTCPConn(inbound.NewHTTPS(request, conn, additions...)) return // hijack connection } @@ -61,7 +61,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[strin request.RequestURI = "" if isUpgradeRequest(request) { - handleUpgrade(conn, request, in, additions...) + handleUpgrade(conn, request, tunnel, additions...) return // hijack connection } @@ -100,6 +100,9 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[strin func authenticate(request *http.Request, cache *cache.LruCache[string, bool]) *http.Response { authenticator := authStore.Authenticator() + if inbound.SkipAuthRemoteAddress(request.RemoteAddr) { + authenticator = nil + } if authenticator != nil { credential := parseBasicProxyAuthorization(request) if credential == "" { diff --git a/listener/http/server.go b/listener/http/server.go index 8819af11..a75e2092 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -3,9 +3,9 @@ package http import ( "net" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/common/cache" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/adapter/inbound" + "github.com/metacubex/mihomo/common/cache" + C "github.com/metacubex/mihomo/constant" ) type Listener struct { @@ -30,11 +30,11 @@ func (l *Listener) Close() error { return l.listener.Close() } -func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { - return NewWithAuthenticate(addr, in, true, additions...) +func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { + return NewWithAuthenticate(addr, tunnel, true, additions...) } -func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool, additions ...inbound.Addition) (*Listener, error) { +func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-HTTP"), @@ -65,7 +65,7 @@ func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool } continue } - go HandleConn(conn, in, c, additions...) + go HandleConn(conn, tunnel, c, additions...) } }() diff --git a/listener/http/upgrade.go b/listener/http/upgrade.go index 90e28f0a..8a6291d1 100644 --- a/listener/http/upgrade.go +++ b/listener/http/upgrade.go @@ -7,10 +7,10 @@ import ( "net/http" "strings" - "github.com/Dreamacro/clash/adapter/inbound" - N "github.com/Dreamacro/clash/common/net" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "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 { @@ -25,7 +25,7 @@ func isUpgradeRequest(req *http.Request) bool { return false } -func handleUpgrade(conn net.Conn, request *http.Request, in chan<- C.ConnContext, additions ...inbound.Addition) { +func handleUpgrade(conn net.Conn, request *http.Request, tunnel C.Tunnel, additions ...inbound.Addition) { defer conn.Close() removeProxyHeaders(request.Header) @@ -43,7 +43,7 @@ func handleUpgrade(conn net.Conn, request *http.Request, in chan<- C.ConnContext left, right := net.Pipe() - in <- inbound.NewHTTP(dstAddr, conn.RemoteAddr(), right, additions...) + go tunnel.HandleTCPConn(inbound.NewHTTP(dstAddr, conn, right, additions...)) var bufferedLeft *N.BufferedConn if request.TLS != nil { diff --git a/listener/inbound/base.go b/listener/inbound/base.go index b132ac6c..e8f860a0 100644 --- a/listener/inbound/base.go +++ b/listener/inbound/base.go @@ -6,8 +6,8 @@ import ( "net/netip" "strconv" - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/adapter/inbound" + C "github.com/metacubex/mihomo/constant" ) type Base struct { @@ -61,7 +61,7 @@ func (b *Base) RawAddress() string { } // Listen implements constant.InboundListener -func (*Base) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (*Base) Listen(tunnel C.Tunnel) error { return nil } diff --git a/listener/inbound/http.go b/listener/inbound/http.go index a93f9684..f5301f46 100644 --- a/listener/inbound/http.go +++ b/listener/inbound/http.go @@ -1,9 +1,9 @@ package inbound import ( - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/listener/http" - "github.com/Dreamacro/clash/log" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/listener/http" + "github.com/metacubex/mihomo/log" ) type HTTPOption struct { @@ -42,9 +42,9 @@ func (h *HTTP) Address() string { } // Listen implements constant.InboundListener -func (h *HTTP) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (h *HTTP) Listen(tunnel C.Tunnel) error { var err error - h.l, err = http.New(h.RawAddress(), tcpIn, h.Additions()...) + h.l, err = http.New(h.RawAddress(), tunnel, h.Additions()...) if err != nil { return err } diff --git a/listener/inbound/hysteria2.go b/listener/inbound/hysteria2.go new file mode 100644 index 00000000..112d03f8 --- /dev/null +++ b/listener/inbound/hysteria2.go @@ -0,0 +1,95 @@ +package inbound + +import ( + 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"` +} + +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, + }, + }, nil +} + +// Config implements constant.InboundListener +func (t *Hysteria2) Config() C.InboundConfig { + return t.config +} + +// Address implements constant.InboundListener +func (t *Hysteria2) Address() string { + if t.l != nil { + for _, addr := range t.l.AddrList() { + return addr.String() + } + } + return "" +} + +// 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/mixed.go b/listener/inbound/mixed.go index dbba264c..fc643821 100644 --- a/listener/inbound/mixed.go +++ b/listener/inbound/mixed.go @@ -3,11 +3,11 @@ package inbound import ( "fmt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" - "github.com/Dreamacro/clash/listener/mixed" - "github.com/Dreamacro/clash/listener/socks" + "github.com/metacubex/mihomo/listener/mixed" + "github.com/metacubex/mihomo/listener/socks" ) type MixedOption struct { @@ -50,14 +50,14 @@ func (m *Mixed) Address() string { } // Listen implements constant.InboundListener -func (m *Mixed) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (m *Mixed) Listen(tunnel C.Tunnel) error { var err error - m.l, err = mixed.New(m.RawAddress(), tcpIn, m.Additions()...) + m.l, err = mixed.New(m.RawAddress(), tunnel, m.Additions()...) if err != nil { return err } if m.udp { - m.lUDP, err = socks.NewUDP(m.RawAddress(), udpIn, m.Additions()...) + m.lUDP, err = socks.NewUDP(m.RawAddress(), tunnel, m.Additions()...) if err != nil { return err } diff --git a/listener/inbound/redir.go b/listener/inbound/redir.go index 4b88d895..ee090ade 100644 --- a/listener/inbound/redir.go +++ b/listener/inbound/redir.go @@ -1,9 +1,9 @@ package inbound import ( - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/listener/redir" - "github.com/Dreamacro/clash/log" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/listener/redir" + "github.com/metacubex/mihomo/log" ) type RedirOption struct { @@ -42,9 +42,9 @@ func (r *Redir) Address() string { } // Listen implements constant.InboundListener -func (r *Redir) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (r *Redir) Listen(tunnel C.Tunnel) error { var err error - r.l, err = redir.New(r.RawAddress(), tcpIn, r.Additions()...) + r.l, err = redir.New(r.RawAddress(), tunnel, r.Additions()...) if err != nil { return err } diff --git a/listener/inbound/shadowsocks.go b/listener/inbound/shadowsocks.go index 40907485..cb32dcfb 100644 --- a/listener/inbound/shadowsocks.go +++ b/listener/inbound/shadowsocks.go @@ -1,16 +1,17 @@ package inbound import ( - C "github.com/Dreamacro/clash/constant" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/listener/sing_shadowsocks" - "github.com/Dreamacro/clash/log" + 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"` } func (o ShadowSocksOption) Equal(config C.InboundConfig) bool { @@ -37,6 +38,7 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) { Listen: base.RawAddress(), Password: options.Password, Cipher: options.Cipher, + Udp: options.UDP, }, }, nil } @@ -57,9 +59,9 @@ func (s *ShadowSocks) Address() string { } // Listen implements constant.InboundListener -func (s *ShadowSocks) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (s *ShadowSocks) Listen(tunnel C.Tunnel) error { var err error - s.l, err = sing_shadowsocks.New(s.ss, tcpIn, udpIn, s.Additions()...) + s.l, err = sing_shadowsocks.New(s.ss, tunnel, s.Additions()...) if err != nil { return err } diff --git a/listener/inbound/socks.go b/listener/inbound/socks.go index aac2ee23..7e10d93a 100644 --- a/listener/inbound/socks.go +++ b/listener/inbound/socks.go @@ -2,9 +2,9 @@ package inbound import ( "fmt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/listener/socks" - "github.com/Dreamacro/clash/log" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/listener/socks" + "github.com/metacubex/mihomo/log" ) type SocksOption struct { @@ -68,13 +68,13 @@ func (s *Socks) Address() string { } // Listen implements constant.InboundListener -func (s *Socks) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (s *Socks) Listen(tunnel C.Tunnel) error { var err error - if s.stl, err = socks.New(s.RawAddress(), tcpIn, s.Additions()...); err != nil { + if s.stl, err = socks.New(s.RawAddress(), tunnel, s.Additions()...); err != nil { return err } if s.udp { - if s.sul, err = socks.NewUDP(s.RawAddress(), udpIn, s.Additions()...); err != nil { + if s.sul, err = socks.NewUDP(s.RawAddress(), tunnel, s.Additions()...); err != nil { return err } } diff --git a/listener/inbound/tproxy.go b/listener/inbound/tproxy.go index fa458d2c..acc8cb5e 100644 --- a/listener/inbound/tproxy.go +++ b/listener/inbound/tproxy.go @@ -3,9 +3,9 @@ package inbound import ( "fmt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/listener/tproxy" - "github.com/Dreamacro/clash/log" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/listener/tproxy" + "github.com/metacubex/mihomo/log" ) type TProxyOption struct { @@ -49,20 +49,17 @@ func (t *TProxy) Address() string { } // Listen implements constant.InboundListener -func (t *TProxy) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (t *TProxy) Listen(tunnel C.Tunnel) error { var err error - t.lTCP, err = tproxy.New(t.RawAddress(), tcpIn, t.Additions()...) + t.lTCP, err = tproxy.New(t.RawAddress(), tunnel, t.Additions()...) if err != nil { return err } if t.udp { - if t.lUDP != nil { - t.lUDP, err = tproxy.NewUDP(t.RawAddress(), udpIn, natTable, t.Additions()...) - if err != nil { - return err - } + t.lUDP, err = tproxy.NewUDP(t.RawAddress(), tunnel, t.Additions()...) + if err != nil { + return err } - } log.Infoln("TProxy[%s] proxy listening at: %s", t.Name(), t.Address()) return nil diff --git a/listener/inbound/tuic.go b/listener/inbound/tuic.go index f6641500..c2a73b84 100644 --- a/listener/inbound/tuic.go +++ b/listener/inbound/tuic.go @@ -1,22 +1,24 @@ package inbound import ( - C "github.com/Dreamacro/clash/constant" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/listener/tuic" - "github.com/Dreamacro/clash/log" + 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"` - 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"` + 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"` } func (o TuicOption) Equal(config C.InboundConfig) bool { @@ -42,6 +44,7 @@ func NewTuic(options *TuicOption) (*Tuic, error) { Enable: true, Listen: base.RawAddress(), Token: options.Token, + Users: options.Users, Certificate: options.Certificate, PrivateKey: options.PrivateKey, CongestionController: options.CongestionController, @@ -49,6 +52,7 @@ func NewTuic(options *TuicOption) (*Tuic, error) { AuthenticationTimeout: options.AuthenticationTimeout, ALPN: options.ALPN, MaxUdpRelayPacketSize: options.MaxUdpRelayPacketSize, + CWND: options.CWND, }, }, nil } @@ -69,9 +73,9 @@ func (t *Tuic) Address() string { } // Listen implements constant.InboundListener -func (t *Tuic) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (t *Tuic) Listen(tunnel C.Tunnel) error { var err error - t.l, err = tuic.New(t.ts, tcpIn, udpIn, t.Additions()...) + t.l, err = tuic.New(t.ts, tunnel, t.Additions()...) if err != nil { return err } diff --git a/listener/inbound/tun.go b/listener/inbound/tun.go index ad215989..d1044b8e 100644 --- a/listener/inbound/tun.go +++ b/listener/inbound/tun.go @@ -4,10 +4,10 @@ import ( "errors" "strings" - C "github.com/Dreamacro/clash/constant" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/listener/sing_tun" - "github.com/Dreamacro/clash/log" + 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 { @@ -18,21 +18,24 @@ type TunOption struct { AutoRoute bool `inbound:"auto-route,omitempty"` AutoDetectInterface bool `inbound:"auto-detect-interface,omitempty"` - MTU uint32 `inbound:"mtu,omitempty"` - Inet4Address []string `inbound:"inet4_address,omitempty"` - Inet6Address []string `inbound:"inet6_address,omitempty"` - StrictRoute bool `inbound:"strict_route,omitempty"` - Inet4RouteAddress []string `inbound:"inet4_route_address,omitempty"` - Inet6RouteAddress []string `inbound:"inet6_route_address,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"` - 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"` + MTU uint32 `inbound:"mtu,omitempty"` + Inet4Address []string `inbound:"inet4_address,omitempty"` + Inet6Address []string `inbound:"inet6_address,omitempty"` + StrictRoute bool `inbound:"strict_route,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"` + 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"` + 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"` } func (o TunOption) Equal(config C.InboundConfig) bool { @@ -55,19 +58,27 @@ func NewTun(options *TunOption) (*Tun, error) { if !exist { return nil, errors.New("invalid tun stack") } - inet4Address, err := LC.StringSliceToListenPrefixSlice(options.Inet4Address) + inet4Address, err := LC.StringSliceToNetipPrefixSlice(options.Inet4Address) if err != nil { return nil, err } - inet6Address, err := LC.StringSliceToListenPrefixSlice(options.Inet6Address) + inet6Address, err := LC.StringSliceToNetipPrefixSlice(options.Inet6Address) if err != nil { return nil, err } - inet4RouteAddress, err := LC.StringSliceToListenPrefixSlice(options.Inet4RouteAddress) + inet4RouteAddress, err := LC.StringSliceToNetipPrefixSlice(options.Inet4RouteAddress) if err != nil { return nil, err } - inet6RouteAddress, err := LC.StringSliceToListenPrefixSlice(options.Inet6RouteAddress) + 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 } @@ -75,27 +86,30 @@ func NewTun(options *TunOption) (*Tun, error) { 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, - Inet4Address: inet4Address, - Inet6Address: inet6Address, - StrictRoute: options.StrictRoute, - Inet4RouteAddress: inet4RouteAddress, - Inet6RouteAddress: inet6RouteAddress, - IncludeUID: options.IncludeUID, - IncludeUIDRange: options.IncludeUIDRange, - ExcludeUID: options.ExcludeUID, - ExcludeUIDRange: options.ExcludeUIDRange, - IncludeAndroidUser: options.IncludeAndroidUser, - IncludePackage: options.IncludePackage, - ExcludePackage: options.ExcludePackage, - EndpointIndependentNat: options.EndpointIndependentNat, - UDPTimeout: options.UDPTimeout, + Enable: true, + Device: options.Device, + Stack: stack, + DNSHijack: options.DNSHijack, + AutoRoute: options.AutoRoute, + AutoDetectInterface: options.AutoDetectInterface, + MTU: options.MTU, + Inet4Address: inet4Address, + Inet6Address: inet6Address, + StrictRoute: options.StrictRoute, + Inet4RouteAddress: inet4RouteAddress, + Inet6RouteAddress: inet6RouteAddress, + Inet4RouteExcludeAddress: inet4RouteExcludeAddress, + Inet6RouteExcludeAddress: inet6RouteExcludeAddress, + IncludeUID: options.IncludeUID, + IncludeUIDRange: options.IncludeUIDRange, + ExcludeUID: options.ExcludeUID, + ExcludeUIDRange: options.ExcludeUIDRange, + IncludeAndroidUser: options.IncludeAndroidUser, + IncludePackage: options.IncludePackage, + ExcludePackage: options.ExcludePackage, + EndpointIndependentNat: options.EndpointIndependentNat, + UDPTimeout: options.UDPTimeout, + FileDescriptor: options.FileDescriptor, }, }, nil } @@ -111,9 +125,9 @@ func (t *Tun) Address() string { } // Listen implements constant.InboundListener -func (t *Tun) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (t *Tun) Listen(tunnel C.Tunnel) error { var err error - t.l, err = sing_tun.New(t.tun, tcpIn, udpIn, t.Additions()...) + t.l, err = sing_tun.New(t.tun, tunnel, t.Additions()...) if err != nil { return err } diff --git a/listener/inbound/tunnel.go b/listener/inbound/tunnel.go index 41d024ef..2dfaac74 100644 --- a/listener/inbound/tunnel.go +++ b/listener/inbound/tunnel.go @@ -3,9 +3,9 @@ package inbound import ( "fmt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/listener/tunnel" - "github.com/Dreamacro/clash/log" + C "github.com/metacubex/mihomo/constant" + LT "github.com/metacubex/mihomo/listener/tunnel" + "github.com/metacubex/mihomo/log" ) type TunnelOption struct { @@ -21,8 +21,8 @@ func (o TunnelOption) Equal(config C.InboundConfig) bool { type Tunnel struct { *Base config *TunnelOption - ttl *tunnel.Listener - tul *tunnel.PacketConn + ttl *LT.Listener + tul *LT.PacketConn } func NewTunnel(options *TunnelOption) (*Tunnel, error) { @@ -74,16 +74,16 @@ func (t *Tunnel) Address() string { } // Listen implements constant.InboundListener -func (t *Tunnel) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (t *Tunnel) Listen(tunnel C.Tunnel) error { var err error for _, network := range t.config.Network { switch network { case "tcp": - if t.ttl, err = tunnel.New(t.RawAddress(), t.config.Target, t.config.SpecialProxy, tcpIn, t.Additions()...); err != nil { + if t.ttl, err = LT.New(t.RawAddress(), t.config.Target, t.config.SpecialProxy, tunnel, t.Additions()...); err != nil { return err } case "udp": - if t.tul, err = tunnel.NewUDP(t.RawAddress(), t.config.Target, t.config.SpecialProxy, udpIn, t.Additions()...); err != nil { + if t.tul, err = LT.NewUDP(t.RawAddress(), t.config.Target, t.config.SpecialProxy, tunnel, t.Additions()...); err != nil { return err } default: diff --git a/listener/inbound/vmess.go b/listener/inbound/vmess.go index 70e840a5..3508aa3c 100644 --- a/listener/inbound/vmess.go +++ b/listener/inbound/vmess.go @@ -1,15 +1,18 @@ package inbound import ( - C "github.com/Dreamacro/clash/constant" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/listener/sing_vmess" - "github.com/Dreamacro/clash/log" + 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"` + Users []VmessUser `inbound:"users"` + WsPath string `inbound:"ws-path,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` } type VmessUser struct { @@ -46,9 +49,12 @@ func NewVmess(options *VmessOption) (*Vmess, error) { Base: base, config: options, vs: LC.VmessServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, + Enable: true, + Listen: base.RawAddress(), + Users: users, + WsPath: options.WsPath, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, }, }, nil } @@ -69,7 +75,7 @@ func (v *Vmess) Address() string { } // Listen implements constant.InboundListener -func (v *Vmess) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { +func (v *Vmess) Listen(tunnel C.Tunnel) error { var err error users := make([]LC.VmessUser, len(v.config.Users)) for i, v := range v.config.Users { @@ -79,7 +85,7 @@ func (v *Vmess) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, AlterID: v.AlterID, } } - v.l, err = sing_vmess.New(v.vs, tcpIn, udpIn, v.Additions()...) + v.l, err = sing_vmess.New(v.vs, tunnel, v.Additions()...) if err != nil { return err } diff --git a/listener/inner/tcp.go b/listener/inner/tcp.go index a7e38588..373fd2b4 100644 --- a/listener/inner/tcp.go +++ b/listener/inner/tcp.go @@ -1,20 +1,43 @@ package inner import ( - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" + "errors" "net" + "net/netip" + "strconv" + + C "github.com/metacubex/mihomo/constant" ) -var tcpIn chan<- C.ConnContext +var tunnel C.Tunnel -func New(in chan<- C.ConnContext) { - tcpIn = in +func New(t C.Tunnel) { + tunnel = t } -func HandleTcp(dst string, host string) net.Conn { +func HandleTcp(address string) (conn net.Conn, err error) { + if tunnel == nil { + return nil, errors.New("tcp uninitialized") + } + // executor Parsed conn1, conn2 := net.Pipe() - context := inbound.NewInner(conn2, dst, host) - tcpIn <- context - return conn1 + + metadata := &C.Metadata{} + metadata.NetWork = C.TCP + metadata.Type = C.INNER + metadata.DNSMode = C.DNSNormal + metadata.Process = C.MihomoName + if h, port, err := net.SplitHostPort(address); err == nil { + if port, err := strconv.ParseUint(port, 10, 16); err == nil { + metadata.DstPort = uint16(port) + } + if ip, err := netip.ParseAddr(h); err == nil { + metadata.DstIP = ip + } else { + metadata.Host = h + } + } + + go tunnel.HandleTCPConn(conn2, metadata) + return conn1, nil } diff --git a/listener/listener.go b/listener/listener.go index d8eb5c0c..903cb64b 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -9,22 +9,22 @@ import ( "strings" "sync" - "github.com/Dreamacro/clash/component/ebpf" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/listener/autoredir" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/listener/http" - "github.com/Dreamacro/clash/listener/mixed" - "github.com/Dreamacro/clash/listener/redir" - embedSS "github.com/Dreamacro/clash/listener/shadowsocks" - "github.com/Dreamacro/clash/listener/sing_shadowsocks" - "github.com/Dreamacro/clash/listener/sing_tun" - "github.com/Dreamacro/clash/listener/sing_vmess" - "github.com/Dreamacro/clash/listener/socks" - "github.com/Dreamacro/clash/listener/tproxy" - "github.com/Dreamacro/clash/listener/tuic" - "github.com/Dreamacro/clash/listener/tunnel" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/component/ebpf" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/listener/autoredir" + 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" ) @@ -42,8 +42,8 @@ var ( tproxyUDPListener *tproxy.UDPListener mixedListener *mixed.Listener mixedUDPLister *socks.UDPListener - tunnelTCPListeners = map[string]*tunnel.Listener{} - tunnelUDPListeners = map[string]*tunnel.PacketConn{} + tunnelTCPListeners = map[string]*LT.Listener{} + tunnelUDPListeners = map[string]*LT.PacketConn{} inboundListeners = map[string]C.InboundListener{} tunLister *sing_tun.Listener shadowSocksListener C.MultiAddrListener @@ -84,9 +84,7 @@ type Ports struct { func GetTunConf() LC.Tun { if tunLister == nil { - return LC.Tun{ - Enable: false, - } + return LastTunConf } return tunLister.Config() } @@ -114,7 +112,7 @@ func SetBindAddress(host string) { bindAddress = host } -func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) { +func ReCreateHTTP(port int, tunnel C.Tunnel) { httpMux.Lock() defer httpMux.Unlock() @@ -139,7 +137,7 @@ func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) { return } - httpListener, err = http.New(addr, tcpIn) + httpListener, err = http.New(addr, tunnel) if err != nil { log.Errorln("Start HTTP server error: %s", err.Error()) return @@ -148,7 +146,7 @@ func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) { log.Infoln("HTTP proxy listening at: %s", httpListener.Address()) } -func ReCreateSocks(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { +func ReCreateSocks(port int, tunnel C.Tunnel) { socksMux.Lock() defer socksMux.Unlock() @@ -190,12 +188,12 @@ func ReCreateSocks(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAd return } - tcpListener, err := socks.New(addr, tcpIn) + tcpListener, err := socks.New(addr, tunnel) if err != nil { return } - udpListener, err := socks.NewUDP(addr, udpIn) + udpListener, err := socks.NewUDP(addr, tunnel) if err != nil { tcpListener.Close() return @@ -207,7 +205,7 @@ func ReCreateSocks(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAd log.Infoln("SOCKS proxy listening at: %s", socksListener.Address()) } -func ReCreateRedir(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) { +func ReCreateRedir(port int, tunnel C.Tunnel) { redirMux.Lock() defer redirMux.Unlock() @@ -240,12 +238,12 @@ func ReCreateRedir(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAd return } - redirListener, err = redir.New(addr, tcpIn) + redirListener, err = redir.New(addr, tunnel) if err != nil { return } - redirUDPListener, err = tproxy.NewUDP(addr, udpIn, natTable) + redirUDPListener, err = tproxy.NewUDP(addr, tunnel) if err != nil { log.Warnln("Failed to start Redir UDP Listener: %s", err) } @@ -253,7 +251,7 @@ func ReCreateRedir(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAd log.Infoln("Redirect proxy listening at: %s", redirListener.Address()) } -func ReCreateShadowSocks(shadowSocksConfig string, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { +func ReCreateShadowSocks(shadowSocksConfig string, tunnel C.Tunnel) { ssMux.Lock() defer ssMux.Unlock() @@ -271,6 +269,7 @@ func ReCreateShadowSocks(shadowSocksConfig string, tcpIn chan<- C.ConnContext, u Listen: addr, Password: password, Cipher: cipher, + Udp: true, } } @@ -293,7 +292,7 @@ func ReCreateShadowSocks(shadowSocksConfig string, tcpIn chan<- C.ConnContext, u return } - listener, err := sing_shadowsocks.New(ssConfig, tcpIn, udpIn) + listener, err := sing_shadowsocks.New(ssConfig, tunnel) if err != nil { return } @@ -306,7 +305,7 @@ func ReCreateShadowSocks(shadowSocksConfig string, tcpIn chan<- C.ConnContext, u return } -func ReCreateVmess(vmessConfig string, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { +func ReCreateVmess(vmessConfig string, tunnel C.Tunnel) { vmessMux.Lock() defer vmessMux.Unlock() @@ -345,7 +344,7 @@ func ReCreateVmess(vmessConfig string, tcpIn chan<- C.ConnContext, udpIn chan<- return } - listener, err := sing_vmess.New(vsConfig, tcpIn, udpIn) + listener, err := sing_vmess.New(vsConfig, tunnel) if err != nil { return } @@ -358,7 +357,7 @@ func ReCreateVmess(vmessConfig string, tcpIn chan<- C.ConnContext, udpIn chan<- return } -func ReCreateTuic(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { +func ReCreateTuic(config LC.TuicServer, tunnel C.Tunnel) { tuicMux.Lock() defer func() { LastTuicConf = config @@ -390,7 +389,7 @@ func ReCreateTuic(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- return } - listener, err := tuic.New(config, tcpIn, udpIn) + listener, err := tuic.New(config, tunnel) if err != nil { return } @@ -403,7 +402,7 @@ func ReCreateTuic(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- return } -func ReCreateTProxy(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) { +func ReCreateTProxy(port int, tunnel C.Tunnel) { tproxyMux.Lock() defer tproxyMux.Unlock() @@ -436,12 +435,12 @@ func ReCreateTProxy(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketA return } - tproxyListener, err = tproxy.New(addr, tcpIn) + tproxyListener, err = tproxy.New(addr, tunnel) if err != nil { return } - tproxyUDPListener, err = tproxy.NewUDP(addr, udpIn, natTable) + tproxyUDPListener, err = tproxy.NewUDP(addr, tunnel) if err != nil { log.Warnln("Failed to start TProxy UDP Listener: %s", err) } @@ -449,7 +448,7 @@ func ReCreateTProxy(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketA log.Infoln("TProxy server listening at: %s", tproxyListener.Address()) } -func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { +func ReCreateMixed(port int, tunnel C.Tunnel) { mixedMux.Lock() defer mixedMux.Unlock() @@ -490,12 +489,12 @@ func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAd return } - mixedListener, err = mixed.New(addr, tcpIn) + mixedListener, err = mixed.New(addr, tunnel) if err != nil { return } - mixedUDPLister, err = socks.NewUDP(addr, udpIn) + mixedUDPLister, err = socks.NewUDP(addr, tunnel) if err != nil { mixedListener.Close() return @@ -504,7 +503,7 @@ func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAd log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address()) } -func ReCreateTun(tunConf LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { +func ReCreateTun(tunConf LC.Tun, tunnel C.Tunnel) { tunMux.Lock() defer func() { LastTunConf = tunConf @@ -515,7 +514,7 @@ func ReCreateTun(tunConf LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.Pack defer func() { if err != nil { log.Errorln("Start TUN listening error: %s", err.Error()) - Cleanup(false) + tunConf.Enable = false } }() @@ -526,13 +525,13 @@ func ReCreateTun(tunConf LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.Pack return } - Cleanup(true) + closeTunListener() if !tunConf.Enable { return } - lister, err := sing_tun.New(tunConf, tcpIn, udpIn) + lister, err := sing_tun.New(tunConf, tunnel) if err != nil { return } @@ -574,7 +573,7 @@ func ReCreateRedirToTun(ifaceNames []string) { log.Infoln("Attached tc ebpf program to interfaces %v", tcProgram.RawNICs()) } -func ReCreateAutoRedir(ifaceNames []string, tcpIn chan<- C.ConnContext, _ chan<- C.PacketAdapter) { +func ReCreateAutoRedir(ifaceNames []string, tunnel C.Tunnel) { autoRedirMux.Lock() defer autoRedirMux.Unlock() @@ -615,7 +614,7 @@ func ReCreateAutoRedir(ifaceNames []string, tcpIn chan<- C.ConnContext, _ chan<- addr := genAddr("*", C.TcpAutoRedirPort, true) - autoRedirListener, err = autoredir.New(addr, tcpIn) + autoRedirListener, err = autoredir.New(addr, tunnel) if err != nil { return } @@ -630,7 +629,7 @@ func ReCreateAutoRedir(ifaceNames []string, tcpIn chan<- C.ConnContext, _ chan<- log.Infoln("Auto redirect proxy listening at: %s, attached tc ebpf program to interfaces %v", autoRedirListener.Address(), autoRedirProgram.RawNICs()) } -func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) { +func PatchTunnel(tunnels []LC.Tunnel, tunnel C.Tunnel) { tunnelMux.Lock() defer tunnelMux.Unlock() @@ -700,7 +699,7 @@ func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C for _, elm := range needCreate { key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy) if elm.network == "tcp" { - l, err := tunnel.New(elm.addr, elm.target, elm.proxy, tcpIn) + 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 @@ -708,7 +707,7 @@ func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C tunnelTCPListeners[key] = l log.Infoln("Tunnel(tcp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelTCPListeners[key].Address()) } else { - l, err := tunnel.NewUDP(elm.addr, elm.target, elm.proxy, udpIn) + 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 @@ -719,7 +718,7 @@ func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C } } -func PatchInboundListeners(newListenerMap map[string]C.InboundListener, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable, dropOld bool) { +func PatchInboundListeners(newListenerMap map[string]C.InboundListener, tunnel C.Tunnel, dropOld bool) { inboundMux.Lock() defer inboundMux.Unlock() @@ -731,7 +730,7 @@ func PatchInboundListeners(newListenerMap map[string]C.InboundListener, tcpIn ch continue } } - if err := newListener.Listen(tcpIn, udpIn, natTable); err != nil { + if err := newListener.Listen(tunnel); err != nil { log.Errorln("Listener %s listen err: %s", name, err.Error()) continue } @@ -821,7 +820,8 @@ func hasTunConfigChange(tunConf *LC.Tun) bool { LastTunConf.MTU != tunConf.MTU || LastTunConf.StrictRoute != tunConf.StrictRoute || LastTunConf.EndpointIndependentNat != tunConf.EndpointIndependentNat || - LastTunConf.UDPTimeout != tunConf.UDPTimeout { + LastTunConf.UDPTimeout != tunConf.UDPTimeout || + LastTunConf.FileDescriptor != tunConf.FileDescriptor { return true } @@ -834,19 +834,27 @@ func hasTunConfigChange(tunConf *LC.Tun) bool { }) sort.Slice(tunConf.Inet4Address, func(i, j int) bool { - return tunConf.Inet4Address[i].Build().String() < tunConf.Inet4Address[j].Build().String() + return tunConf.Inet4Address[i].String() < tunConf.Inet4Address[j].String() }) sort.Slice(tunConf.Inet6Address, func(i, j int) bool { - return tunConf.Inet6Address[i].Build().String() < tunConf.Inet6Address[j].Build().String() + return tunConf.Inet6Address[i].String() < tunConf.Inet6Address[j].String() }) sort.Slice(tunConf.Inet4RouteAddress, func(i, j int) bool { - return tunConf.Inet4RouteAddress[i].Build().String() < tunConf.Inet4RouteAddress[j].Build().String() + return tunConf.Inet4RouteAddress[i].String() < tunConf.Inet4RouteAddress[j].String() }) sort.Slice(tunConf.Inet6RouteAddress, func(i, j int) bool { - return tunConf.Inet6RouteAddress[i].Build().String() < tunConf.Inet6RouteAddress[j].Build().String() + return tunConf.Inet6RouteAddress[i].String() < tunConf.Inet6RouteAddress[j].String() + }) + + sort.Slice(tunConf.Inet4RouteExcludeAddress, func(i, j int) bool { + return tunConf.Inet4RouteExcludeAddress[i].String() < tunConf.Inet4RouteExcludeAddress[j].String() + }) + + sort.Slice(tunConf.Inet6RouteExcludeAddress, func(i, j int) bool { + return tunConf.Inet6RouteExcludeAddress[i].String() < tunConf.Inet6RouteExcludeAddress[j].String() }) sort.Slice(tunConf.IncludeUID, func(i, j int) bool { @@ -882,6 +890,8 @@ func hasTunConfigChange(tunConf *LC.Tun) bool { !slices.Equal(tunConf.Inet6Address, LastTunConf.Inet6Address) || !slices.Equal(tunConf.Inet4RouteAddress, LastTunConf.Inet4RouteAddress) || !slices.Equal(tunConf.Inet6RouteAddress, LastTunConf.Inet6RouteAddress) || + !slices.Equal(tunConf.Inet4RouteExcludeAddress, LastTunConf.Inet4RouteExcludeAddress) || + !slices.Equal(tunConf.Inet6RouteExcludeAddress, LastTunConf.Inet6RouteExcludeAddress) || !slices.Equal(tunConf.IncludeUID, LastTunConf.IncludeUID) || !slices.Equal(tunConf.IncludeUIDRange, LastTunConf.IncludeUIDRange) || !slices.Equal(tunConf.ExcludeUID, LastTunConf.ExcludeUID) || @@ -895,10 +905,13 @@ func hasTunConfigChange(tunConf *LC.Tun) bool { return false } -func Cleanup(wait bool) { +func closeTunListener() { if tunLister != nil { tunLister.Close() tunLister = nil } - LastTunConf = LC.Tun{} +} + +func Cleanup() { + closeTunListener() } diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go index e8385873..97d1407c 100644 --- a/listener/mixed/mixed.go +++ b/listener/mixed/mixed.go @@ -1,16 +1,16 @@ package mixed import ( - "github.com/Dreamacro/clash/adapter/inbound" "net" - "github.com/Dreamacro/clash/common/cache" - N "github.com/Dreamacro/clash/common/net" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/listener/http" - "github.com/Dreamacro/clash/listener/socks" - "github.com/Dreamacro/clash/transport/socks4" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/adapter/inbound" + "github.com/metacubex/mihomo/common/cache" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/listener/http" + "github.com/metacubex/mihomo/listener/socks" + "github.com/metacubex/mihomo/transport/socks4" + "github.com/metacubex/mihomo/transport/socks5" ) type Listener struct { @@ -36,7 +36,7 @@ func (l *Listener) Close() error { return l.listener.Close() } -func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { +func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-MIXED"), @@ -62,15 +62,15 @@ func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (* } continue } - go handleConn(c, in, ml.cache, additions...) + go handleConn(c, tunnel, ml.cache, additions...) } }() return ml, nil } -func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[string, bool], additions ...inbound.Addition) { - conn.(*net.TCPConn).SetKeepAlive(true) +func handleConn(conn net.Conn, tunnel C.Tunnel, cache *cache.LruCache[string, bool], additions ...inbound.Addition) { + N.TCPKeepAlive(conn) bufConn := N.NewBufferedConn(conn) head, err := bufConn.Peek(1) @@ -80,10 +80,10 @@ func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[st switch head[0] { case socks4.Version: - socks.HandleSocks4(bufConn, in, additions...) + socks.HandleSocks4(bufConn, tunnel, additions...) case socks5.Version: - socks.HandleSocks5(bufConn, in, additions...) + socks.HandleSocks5(bufConn, tunnel, additions...) default: - http.HandleConn(bufConn, in, cache, additions...) + http.HandleConn(bufConn, tunnel, cache, additions...) } } diff --git a/listener/parse.go b/listener/parse.go index aa9e39ac..1c8b6463 100644 --- a/listener/parse.go +++ b/listener/parse.go @@ -3,9 +3,9 @@ package listener import ( "fmt" - "github.com/Dreamacro/clash/common/structure" - C "github.com/Dreamacro/clash/constant" - IN "github.com/Dreamacro/clash/listener/inbound" + "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) { @@ -73,7 +73,7 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) { } listener, err = IN.NewTun(tunOption) case "shadowsocks": - shadowsocksOption := &IN.ShadowSocksOption{} + shadowsocksOption := &IN.ShadowSocksOption{UDP: true} err = decoder.Decode(mapping, shadowsocksOption) if err != nil { return nil, err @@ -86,6 +86,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) { return nil, err } listener, err = IN.NewVmess(vmessOption) + 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, diff --git a/listener/redir/tcp.go b/listener/redir/tcp.go index ad4a91bc..8474a8e2 100644 --- a/listener/redir/tcp.go +++ b/listener/redir/tcp.go @@ -3,8 +3,9 @@ package redir import ( "net" - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" ) type Listener struct { @@ -29,7 +30,7 @@ func (l *Listener) Close() error { return l.listener.Close() } -func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { +func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-REDIR"), @@ -54,18 +55,19 @@ func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (* } continue } - go handleRedir(c, in, additions...) + go handleRedir(c, tunnel, additions...) } }() return rl, nil } -func handleRedir(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { + +func handleRedir(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { target, err := parserPacket(conn) if err != nil { conn.Close() return } - conn.(*net.TCPConn).SetKeepAlive(true) - in <- inbound.NewSocket(target, conn, C.REDIR, additions...) + N.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 index 5a2f331c..6e1821bb 100644 --- a/listener/redir/tcp_darwin.go +++ b/listener/redir/tcp_darwin.go @@ -5,7 +5,7 @@ import ( "syscall" "unsafe" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/transport/socks5" ) func parserPacket(c net.Conn) (socks5.Addr, error) { diff --git a/listener/redir/tcp_freebsd.go b/listener/redir/tcp_freebsd.go index 12c4ba6a..9eb199f0 100644 --- a/listener/redir/tcp_freebsd.go +++ b/listener/redir/tcp_freebsd.go @@ -1,12 +1,16 @@ package redir import ( + "encoding/binary" "errors" "net" + "net/netip" "syscall" "unsafe" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/transport/socks5" + + "golang.org/x/sys/unix" ) const ( @@ -25,28 +29,38 @@ func parserPacket(conn net.Conn) (socks5.Addr, error) { return nil, err } - var addr socks5.Addr + var addr netip.AddrPort rc.Control(func(fd uintptr) { - addr, err = getorigdst(fd) + if ip4 := c.LocalAddr().(*net.TCPAddr).IP.To4(); ip4 != nil { + addr, err = getorigdst(fd) + } else { + addr, err = getorigdst6(fd) + } }) - return addr, err + return socks5.AddrFromStdAddrPort(addr), err } // Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c -func getorigdst(fd uintptr) (socks5.Addr, error) { - raw := syscall.RawSockaddrInet4{} - siz := unsafe.Sizeof(raw) - _, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0) +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 nil, err + return netip.AddrPort{}, err } - - addr := make([]byte, 1+net.IPv4len+2) - addr[0] = socks5.AtypIPv4 - copy(addr[1:1+net.IPv4len], raw.Addr[:]) - port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian - addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1] - return addr, nil + 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 index b65c34ee..fce74678 100644 --- a/listener/redir/tcp_linux.go +++ b/listener/redir/tcp_linux.go @@ -8,7 +8,7 @@ import ( "syscall" "unsafe" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/transport/socks5" "golang.org/x/sys/unix" ) diff --git a/listener/redir/tcp_other.go b/listener/redir/tcp_other.go index a01550c7..ae3bebfd 100644 --- a/listener/redir/tcp_other.go +++ b/listener/redir/tcp_other.go @@ -6,7 +6,7 @@ import ( "errors" "net" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/transport/socks5" ) func parserPacket(conn net.Conn) (socks5.Addr, error) { diff --git a/listener/shadowsocks/tcp.go b/listener/shadowsocks/tcp.go index 21db5b63..c08667de 100644 --- a/listener/shadowsocks/tcp.go +++ b/listener/shadowsocks/tcp.go @@ -4,11 +4,12 @@ import ( "net" "strings" - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/transport/shadowsocks/core" - "github.com/Dreamacro/clash/transport/socks5" + "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/transport/shadowsocks/core" + "github.com/metacubex/mihomo/transport/socks5" ) type Listener struct { @@ -21,7 +22,7 @@ type Listener struct { var _listener *Listener -func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter) (*Listener, error) { +func New(config LC.ShadowsocksServer, tunnel C.Tunnel) (*Listener, error) { pickCipher, err := core.PickCipher(config.Cipher, nil, config.Password) if err != nil { return nil, err @@ -33,12 +34,14 @@ func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C for _, addr := range strings.Split(config.Listen, ",") { addr := addr - //UDP - ul, err := NewUDP(addr, pickCipher, udpIn) - if err != nil { - return nil, err + if config.Udp { + //UDP + ul, err := NewUDP(addr, pickCipher, tunnel) + if err != nil { + return nil, err + } + sl.udpListeners = append(sl.udpListeners, ul) } - sl.udpListeners = append(sl.udpListeners, ul) //TCP l, err := inbound.Listen("tcp", addr) @@ -56,8 +59,8 @@ func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C } continue } - _ = c.(*net.TCPConn).SetKeepAlive(true) - go sl.HandleConn(c, tcpIn) + N.TCPKeepAlive(c) + go sl.HandleConn(c, tunnel) } }() } @@ -96,20 +99,21 @@ func (l *Listener) AddrList() (addrList []net.Addr) { return } -func (l *Listener) HandleConn(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { +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.ReadAddr(conn, make([]byte, socks5.MaxAddrLen)) + target, err := socks5.ReadAddr0(conn) if err != nil { _ = conn.Close() return } - in <- inbound.NewSocket(target, conn, C.SHADOWSOCKS, additions...) + tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.SHADOWSOCKS, additions...)) } -func HandleShadowSocks(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) bool { +func HandleShadowSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) bool { if _listener != nil && _listener.pickCipher != nil { - go _listener.HandleConn(conn, in, additions...) + go _listener.HandleConn(conn, tunnel, additions...) return true } return false diff --git a/listener/shadowsocks/udp.go b/listener/shadowsocks/udp.go index 3f058406..4336db22 100644 --- a/listener/shadowsocks/udp.go +++ b/listener/shadowsocks/udp.go @@ -3,13 +3,13 @@ package shadowsocks import ( "net" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/common/sockopt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/shadowsocks/core" - "github.com/Dreamacro/clash/transport/socks5" + "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 { @@ -17,7 +17,7 @@ type UDPListener struct { closed bool } -func NewUDP(addr string, pickCipher core.Cipher, in chan<- C.PacketAdapter) (*UDPListener, error) { +func NewUDP(addr string, pickCipher core.Cipher, tunnel C.Tunnel) (*UDPListener, error) { l, err := net.ListenPacket("udp", addr) if err != nil { return nil, err @@ -29,19 +29,20 @@ func NewUDP(addr string, pickCipher core.Cipher, in chan<- C.PacketAdapter) (*UD } sl := &UDPListener{l, false} - conn := pickCipher.PacketConn(l) + conn := pickCipher.PacketConn(N.NewEnhancePacketConn(l)) go func() { for { - buf := pool.Get(pool.RelayBufferSize) - n, remoteAddr, err := conn.ReadFrom(buf) + data, put, remoteAddr, err := conn.WaitReadFrom() if err != nil { - pool.Put(buf) + if put != nil { + put() + } if sl.closed { break } continue } - handleSocksUDP(conn, in, buf[:n], remoteAddr) + handleSocksUDP(conn, tunnel, data, put, remoteAddr) } }() @@ -57,24 +58,23 @@ func (l *UDPListener) LocalAddr() net.Addr { return l.packetConn.LocalAddr() } -func handleSocksUDP(pc net.PacketConn, in chan<- C.PacketAdapter, buf []byte, addr net.Addr) { +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 - pool.Put(buf) + if put != nil { + put() + } return } - target := socks5.ParseAddr(tgtAddr.String()) + target := tgtAddr payload := buf[len(tgtAddr):] packet := &packet{ pc: pc, rAddr: addr, payload: payload, - bufRef: buf, - } - select { - case in <- inbound.NewPacket(target, packet, C.SHADOWSOCKS): - default: + put: put, } + tunnel.HandleUDPPacket(inbound.NewPacket(target, packet, C.SHADOWSOCKS, additions...)) } diff --git a/listener/shadowsocks/utils.go b/listener/shadowsocks/utils.go index 2e9fd003..5d6a2977 100644 --- a/listener/shadowsocks/utils.go +++ b/listener/shadowsocks/utils.go @@ -6,15 +6,14 @@ import ( "net" "net/url" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/transport/socks5" ) type packet struct { pc net.PacketConn rAddr net.Addr payload []byte - bufRef []byte + put func() } func (c *packet) Data() []byte { @@ -37,7 +36,11 @@ func (c *packet) LocalAddr() net.Addr { } func (c *packet) Drop() { - pool.Put(c.bufRef) + if c.put != nil { + c.put() + c.put = nil + } + c.payload = nil } func (c *packet) InAddr() net.Addr { diff --git a/listener/sing/context.go b/listener/sing/context.go index f7aed851..e1e8b452 100644 --- a/listener/sing/context.go +++ b/listener/sing/context.go @@ -2,8 +2,11 @@ package sing import ( "context" + "golang.org/x/exp/slices" - "github.com/Dreamacro/clash/adapter/inbound" + "github.com/metacubex/mihomo/adapter/inbound" + + "github.com/sagernet/sing/common/auth" ) type contextKey string @@ -14,11 +17,15 @@ func WithAdditions(ctx context.Context, additions ...inbound.Addition) context.C return context.WithValue(ctx, ctxKeyAdditions, additions) } -func getAdditions(ctx context.Context) []inbound.Addition { +func getAdditions(ctx context.Context) (additions []inbound.Addition) { if v := ctx.Value(ctxKeyAdditions); v != nil { if a, ok := v.([]inbound.Addition); ok { - return a + additions = a } } - return nil + if user, ok := auth.UserFromContext[string](ctx); ok { + additions = slices.Clone(additions) + additions = append(additions, inbound.WithInUser(user)) + } + return } diff --git a/listener/sing/log.go b/listener/sing/log.go deleted file mode 100644 index 4847e063..00000000 --- a/listener/sing/log.go +++ /dev/null @@ -1,41 +0,0 @@ -package sing - -import ( - "fmt" - - "github.com/Dreamacro/clash/log" - - L "github.com/sagernet/sing/common/logger" -) - -type logger struct{} - -func (l logger) Trace(args ...any) { - log.Debugln(fmt.Sprint(args...)) -} - -func (l logger) Debug(args ...any) { - log.Debugln(fmt.Sprint(args...)) -} - -func (l logger) Info(args ...any) { - log.Infoln(fmt.Sprint(args...)) -} - -func (l logger) Warn(args ...any) { - log.Warnln(fmt.Sprint(args...)) -} - -func (l logger) Error(args ...any) { - log.Errorln(fmt.Sprint(args...)) -} - -func (l logger) Fatal(args ...any) { - log.Fatalln(fmt.Sprint(args...)) -} - -func (l logger) Panic(args ...any) { - log.Fatalln(fmt.Sprint(args...)) -} - -var Logger L.Logger = logger{} diff --git a/listener/sing/sing.go b/listener/sing/sing.go index a3e15154..306bd705 100644 --- a/listener/sing/sing.go +++ b/listener/sing/sing.go @@ -3,18 +3,21 @@ package sing import ( "context" "errors" - "golang.org/x/exp/slices" "net" + "net/netip" "sync" "time" - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" - vmess "github.com/sagernet/sing-vmess" + vmess "github.com/metacubex/sing-vmess" + mux "github.com/sagernet/sing-mux" "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/bufio/deadline" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/network" @@ -24,61 +27,83 @@ import ( const UDPTimeout = 5 * time.Minute type ListenerHandler struct { - TcpIn chan<- C.ConnContext - UdpIn chan<- C.PacketAdapter - Type C.Type - Additions []inbound.Addition + Tunnel C.Tunnel + Type C.Type + Additions []inbound.Addition + UDPTimeout time.Duration } -type waitCloseConn struct { - net.Conn - wg *sync.WaitGroup - close sync.Once - rAddr net.Addr +func UpstreamMetadata(metadata M.Metadata) M.Metadata { + return M.Metadata{ + Source: metadata.Source, + Destination: metadata.Destination, + } } -func (c *waitCloseConn) Close() error { // call from handleTCPConn(connCtx C.ConnContext) - c.close.Do(func() { - c.wg.Done() - }) - return c.Conn.Close() +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 (c *waitCloseConn) RemoteAddr() net.Addr { - return c.rAddr +func (h *ListenerHandler) IsSpecialFqdn(fqdn string) bool { + switch fqdn { + case mux.Destination.Fqdn: + case vmess.MuxDestination.Fqdn: + case uot.MagicAddress: + case uot.LegacyMagicAddress: + default: + return false + } + return true } -func (c *waitCloseConn) Upstream() any { - return c.Conn +func (h *ListenerHandler) ParseSpecialFqdn(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + switch metadata.Destination.Fqdn { + case mux.Destination.Fqdn: + return mux.HandleConnection(ctx, h, log.SingLogger, 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 { - additions := h.Additions - if ctxAdditions := getAdditions(ctx); len(ctxAdditions) > 0 { - additions = slices.Clone(additions) - additions = append(additions, ctxAdditions...) + if h.IsSpecialFqdn(metadata.Destination.Fqdn) { + return h.ParseSpecialFqdn(ctx, conn, metadata) } - switch metadata.Destination.Fqdn { - case vmess.MuxDestination.Fqdn: - return vmess.HandleMuxConnection(ctx, conn, h) - case uot.UOTMagicAddress: - metadata.Destination = M.Socksaddr{} - return h.NewPacketConnection(ctx, uot.NewClientConn(conn), metadata) - } - target := socks5.ParseAddr(metadata.Destination.String()) - wg := &sync.WaitGroup{} - defer wg.Wait() // this goroutine must exit after conn.Close() - wg.Add(1) - h.TcpIn <- inbound.NewSocket(target, &waitCloseConn{Conn: conn, wg: wg, rAddr: metadata.Source.TCPAddr()}, h.Type, additions...) + if deadline.NeedAdditionalReadDeadline(conn) { + conn = N.NewDeadlineConn(conn) // conn from sing should check NeedAdditionalReadDeadline + } + + cMetadata := &C.Metadata{ + NetWork: C.TCP, + Type: h.Type, + } + inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(metadata.Destination), inbound.WithSrcAddr(metadata.Source), inbound.WithInAddr(conn.LocalAddr())) + inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...) + inbound.ApplyAdditions(cMetadata, h.Additions...) + + 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 { - additions := h.Additions - if ctxAdditions := getAdditions(ctx); len(ctxAdditions) > 0 { - additions = slices.Clone(additions) - additions = append(additions, ctxAdditions...) + if deadline.NeedAdditionalReadDeadline(conn) { + conn = deadline.NewFallbackPacketConn(bufio.NewNetPacketConn(conn)) // conn from sing should check NeedAdditionalReadDeadline } defer func() { _ = conn.Close() }() mutex := sync.Mutex{} @@ -88,28 +113,52 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. defer mutex.Unlock() conn2 = nil }() + var buff *buf.Buffer + newBuffer := func() *buf.Buffer { + buff = buf.NewPacket() // do not use stack buffer + return buff + } + readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) + if isReadWaiter { + readWaiter.InitializeReadWaiter(newBuffer) + } for { - buff := buf.NewPacket() // do not use stack buffer - dest, err := conn.ReadPacket(buff) + var ( + dest M.Socksaddr + err error + ) + buff = nil // clear last loop status, avoid repeat release + if isReadWaiter { + dest, err = readWaiter.WaitReadPacket() + } else { + dest, err = conn.ReadPacket(newBuffer()) + } if err != nil { - buff.Release() - if E.IsClosed(err) { + if buff != nil { + buff.Release() + } + if ShouldIgnorePacketError(err) { break } return err } - target := socks5.ParseAddr(dest.String()) - packet := &packet{ + cPacket := &packet{ conn: &conn2, mutex: &mutex, rAddr: metadata.Source.UDPAddr(), lAddr: conn.LocalAddr(), buff: buff, } - select { - case h.UdpIn <- inbound.NewPacket(target, packet, h.Type, additions...): - default: + + cMetadata := &C.Metadata{ + NetWork: C.UDP, + Type: h.Type, } + inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(dest), inbound.WithSrcAddr(metadata.Source), inbound.WithInAddr(conn.LocalAddr())) + inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...) + inbound.ApplyAdditions(cMetadata, h.Additions...) + + h.Tunnel.HandleUDPPacket(cPacket, cMetadata) } return nil } @@ -118,6 +167,14 @@ func (h *ListenerHandler) NewError(ctx context.Context, err error) { log.Warnln("%s listener get error: %+v", h.Type.String(), err) } +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.PacketConn mutex *sync.Mutex @@ -136,12 +193,6 @@ func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { err = errors.New("address is invalid") return } - buff := buf.NewPacket() - defer buff.Release() - n, err = buff.Write(b) - if err != nil { - return - } c.mutex.Lock() defer c.mutex.Unlock() @@ -150,7 +201,18 @@ func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { err = errors.New("writeBack to closed connection") return } + + buff := buf.NewPacket() + defer buff.Release() + n, err = buff.Write(b) + if err != nil { + return + } + err = conn.WritePacket(buff, M.SocksaddrFromNet(addr)) + if err != nil { + return + } return } @@ -166,11 +228,3 @@ func (c *packet) Drop() { func (c *packet) InAddr() net.Addr { return c.lAddr } - -func (c *packet) SetNatTable(natTable C.NatTable) { - // no need -} - -func (c *packet) SetUdpInChan(in chan<- C.PacketAdapter) { - // no need -} diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go new file mode 100644 index 00000000..96553995 --- /dev/null +++ b/listener/sing_hysteria2/server.go @@ -0,0 +1,180 @@ +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" + CN "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/sockopt" + 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" + + E "github.com/sagernet/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 := &sing.ListenerHandler{ + Tunnel: tunnel, + Type: C.HYSTERIA2, + Additions: additions, + } + + sl = &Listener{false, config, nil, nil} + + cert, err := CN.ParseCert(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) + } + } + + 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: tlsConfig, + IgnoreClientBandwidth: config.IgnoreClientBandwidth, + Handler: h, + MasqueradeHandler: masqueradeHandler, + CWND: config.CWND, + }) + 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 := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) + if 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 index 305a9496..5a4896af 100644 --- a/listener/sing_shadowsocks/server.go +++ b/listener/sing_shadowsocks/server.go @@ -6,13 +6,15 @@ import ( "net" "strings" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/common/sockopt" - C "github.com/Dreamacro/clash/constant" - LC "github.com/Dreamacro/clash/listener/config" - embedSS "github.com/Dreamacro/clash/listener/shadowsocks" - "github.com/Dreamacro/clash/listener/sing" - "github.com/Dreamacro/clash/log" + "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" + 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" @@ -20,7 +22,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" - "github.com/sagernet/sing/common/metadata" + M "github.com/sagernet/sing/common/metadata" ) type Listener struct { @@ -33,7 +35,7 @@ type Listener struct { var _listener *Listener -func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (C.MultiAddrListener, error) { +func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addition) (C.MultiAddrListener, error) { var sl *Listener var err error if len(additions) == 0 { @@ -49,8 +51,7 @@ func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C udpTimeout := int64(sing.UDPTimeout.Seconds()) h := &sing.ListenerHandler{ - TcpIn: tcpIn, - UdpIn: udpIn, + Tunnel: tunnel, Type: C.SHADOWSOCKS, Additions: additions, } @@ -63,10 +64,10 @@ func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C 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) + 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, tcpIn, udpIn) + return embedSS.New(config, tunnel) } if err != nil { return nil, err @@ -75,37 +76,58 @@ func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C for _, addr := range strings.Split(config.Listen, ",") { addr := addr - //UDP - ul, err := net.ListenPacket("udp", addr) - if err != nil { - return nil, err - } - - err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) - if err != nil { - log.Warnln("Failed to Reuse UDP Address: %s", err) - } - - sl.udpListeners = append(sl.udpListeners, ul) - - go func() { - conn := bufio.NewPacketConn(ul) - for { - buff := buf.NewPacket() - remoteAddr, err := conn.ReadPacket(buff) - if err != nil { - buff.Release() - if sl.closed { - break - } - continue - } - _ = sl.service.NewPacket(context.TODO(), conn, buff, metadata.Metadata{ - Protocol: "shadowsocks", - Source: remoteAddr, - }) + if config.Udp { + //UDP + ul, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err } - }() + + err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) + if err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + } + + sl.udpListeners = append(sl.udpListeners, ul) + + go func() { + conn := bufio.NewPacketConn(ul) + var buff *buf.Buffer + newBuffer := func() *buf.Buffer { + buff = buf.NewPacket() // do not use stack buffer + return buff + } + readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) + if isReadWaiter { + readWaiter.InitializeReadWaiter(newBuffer) + } + for { + var ( + dest M.Socksaddr + err error + ) + buff = nil // clear last loop status, avoid repeat release + if isReadWaiter { + dest, err = readWaiter.WaitReadPacket() + } else { + dest, err = conn.ReadPacket(newBuffer()) + } + if err != nil { + if buff != 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) @@ -123,9 +145,9 @@ func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C } continue } - _ = c.(*net.TCPConn).SetKeepAlive(true) + N.TCPKeepAlive(c) - go sl.HandleConn(c, tcpIn) + go sl.HandleConn(c, tunnel) } }() } @@ -165,11 +187,11 @@ func (l *Listener) AddrList() (addrList []net.Addr) { return } -func (l *Listener) HandleConn(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { +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{ + err := l.service.NewConnection(ctx, conn, M.Metadata{ Protocol: "shadowsocks", - Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()), + Source: M.ParseSocksaddr(conn.RemoteAddr().String()), }) if err != nil { _ = conn.Close() @@ -177,10 +199,10 @@ func (l *Listener) HandleConn(conn net.Conn, in chan<- C.ConnContext, additions } } -func HandleShadowSocks(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) bool { +func HandleShadowSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) bool { if _listener != nil && _listener.service != nil { - go _listener.HandleConn(conn, in, additions...) + go _listener.HandleConn(conn, tunnel, additions...) return true } - return embedSS.HandleShadowSocks(conn, in, additions...) + return embedSS.HandleShadowSocks(conn, tunnel, additions...) } diff --git a/listener/sing_tun/dns.go b/listener/sing_tun/dns.go index 21dee43c..62a15c6c 100644 --- a/listener/sing_tun/dns.go +++ b/listener/sing_tun/dns.go @@ -9,15 +9,15 @@ import ( "sync" "time" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/component/resolver" - "github.com/Dreamacro/clash/listener/sing" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/listener/sing" + "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/network" ) @@ -109,17 +109,40 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. defer mutex.Unlock() conn2 = nil }() + + var buff *buf.Buffer + newBuffer := func() *buf.Buffer { + // safe size which is 1232 from https://dnsflagday.net/2020/. + // so 2048 is enough + buff = buf.NewSize(2 * 1024) + return buff + } + readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) + if isReadWaiter { + readWaiter.InitializeReadWaiter(newBuffer) + } for { - buff := buf.NewPacket() - dest, err := conn.ReadPacket(buff) + var ( + dest M.Socksaddr + err error + ) + _ = conn.SetReadDeadline(time.Now().Add(DefaultDnsReadTimeout)) + buff = nil // clear last loop status, avoid repeat release + if isReadWaiter { + dest, err = readWaiter.WaitReadPacket() + } else { + dest, err = conn.ReadPacket(newBuffer()) + } if err != nil { - buff.Release() - if E.IsClosed(err) { + if buff != nil { + buff.Release() + } + if sing.ShouldIgnorePacketError(err) { break } return err } - go func() { + go func(buff *buf.Buffer) { ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) defer cancel() inData := buff.Bytes() @@ -144,7 +167,7 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. if err != nil { return } - }() + }(buff) // catch buff at goroutine create, avoid next loop change buff } return nil } diff --git a/listener/sing_tun/server.go b/listener/sing_tun/server.go index 5c387a8d..212c3d90 100644 --- a/listener/sing_tun/server.go +++ b/listener/sing_tun/server.go @@ -8,14 +8,15 @@ import ( "runtime" "strconv" "strings" + "time" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/component/iface" - C "github.com/Dreamacro/clash/constant" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/listener/sing" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/adapter/inbound" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/iface" + C "github.com/metacubex/mihomo/constant" + LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/listener/sing" + "github.com/metacubex/mihomo/log" tun "github.com/metacubex/sing-tun" "github.com/sagernet/sing/common" @@ -67,7 +68,27 @@ func CalculateInterfaceName(name string) (tunName string) { return } -func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (l *Listener, err error) { +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"), @@ -75,7 +96,7 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte } } tunName := options.Device - if tunName == "" { + if tunName == "" || !checkTunName(tunName) { tunName = CalculateInterfaceName(InterfaceName) options.Device = tunName } @@ -121,20 +142,20 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte dnsAdds = append(dnsAdds, addrPort) } for _, a := range options.Inet4Address { - addrPort := netip.AddrPortFrom(a.Build().Addr().Next(), 53) + addrPort := netip.AddrPortFrom(a.Addr().Next(), 53) dnsAdds = append(dnsAdds, addrPort) } for _, a := range options.Inet6Address { - addrPort := netip.AddrPortFrom(a.Build().Addr().Next(), 53) + addrPort := netip.AddrPortFrom(a.Addr().Next(), 53) dnsAdds = append(dnsAdds, addrPort) } handler := &ListenerHandler{ ListenerHandler: sing.ListenerHandler{ - TcpIn: tcpIn, - UdpIn: udpIn, - Type: C.TUN, - Additions: additions, + Tunnel: tunnel, + Type: C.TUN, + Additions: additions, + UDPTimeout: time.Second * time.Duration(udpTimeout), }, DnsAdds: dnsAdds, } @@ -150,7 +171,7 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte } }() - networkUpdateMonitor, err := tun.NewNetworkUpdateMonitor(handler) + networkUpdateMonitor, err := tun.NewNetworkUpdateMonitor(log.SingLogger) if err != nil { err = E.Cause(err, "create NetworkUpdateMonitor") return @@ -162,15 +183,14 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte return } - defaultInterfaceMonitor, err := tun.NewDefaultInterfaceMonitor(networkUpdateMonitor, tun.DefaultInterfaceMonitorOptions{OverrideAndroidVPN: true}) + defaultInterfaceMonitor, err := tun.NewDefaultInterfaceMonitor(networkUpdateMonitor, log.SingLogger, tun.DefaultInterfaceMonitorOptions{OverrideAndroidVPN: true}) if err != nil { err = E.Cause(err, "create DefaultInterfaceMonitor") return } l.defaultInterfaceMonitor = defaultInterfaceMonitor - defaultInterfaceMonitor.RegisterCallback(func(event int) error { + defaultInterfaceMonitor.RegisterCallback(func(event int) { l.FlushDefaultInterface() - return nil }) err = defaultInterfaceMonitor.Start() if err != nil { @@ -179,21 +199,24 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte } tunOptions := tun.Options{ - Name: tunName, - MTU: tunMTU, - Inet4Address: common.Map(options.Inet4Address, LC.ListenPrefix.Build), - Inet6Address: common.Map(options.Inet6Address, LC.ListenPrefix.Build), - AutoRoute: options.AutoRoute, - StrictRoute: options.StrictRoute, - Inet4RouteAddress: common.Map(options.Inet4RouteAddress, LC.ListenPrefix.Build), - Inet6RouteAddress: common.Map(options.Inet6RouteAddress, LC.ListenPrefix.Build), - IncludeUID: includeUID, - ExcludeUID: excludeUID, - IncludeAndroidUser: options.IncludeAndroidUser, - IncludePackage: options.IncludePackage, - ExcludePackage: options.ExcludePackage, - InterfaceMonitor: defaultInterfaceMonitor, - TableIndex: 2022, + Name: tunName, + MTU: tunMTU, + Inet4Address: options.Inet4Address, + Inet6Address: options.Inet6Address, + AutoRoute: options.AutoRoute, + StrictRoute: options.StrictRoute, + Inet4RouteAddress: options.Inet4RouteAddress, + Inet6RouteAddress: options.Inet6RouteAddress, + Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress, + Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress, + IncludeUID: includeUID, + ExcludeUID: excludeUID, + IncludeAndroidUser: options.IncludeAndroidUser, + IncludePackage: options.IncludePackage, + ExcludePackage: options.ExcludePackage, + FileDescriptor: options.FileDescriptor, + InterfaceMonitor: defaultInterfaceMonitor, + TableIndex: 2022, } err = l.buildAndroidRules(&tunOptions) @@ -201,13 +224,13 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte err = E.Cause(err, "build android rules") return } - tunIf, err := tunOpen(tunOptions) + tunIf, err := tunNew(tunOptions) if err != nil { err = E.Cause(err, "configure tun interface") return } - l.tunIf = tunIf - l.tunStack, err = tun.NewStack(strings.ToLower(options.Stack.String()), tun.StackOptions{ + + stackOptions := tun.StackOptions{ Context: context.TODO(), Tun: tunIf, MTU: tunOptions.MTU, @@ -217,8 +240,17 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte EndpointIndependentNat: options.EndpointIndependentNat, UDPTimeout: udpTimeout, Handler: handler, - Logger: sing.Logger, - }) + Logger: log.SingLogger, + } + + if options.FileDescriptor > 0 { + if tunName, err := getTunnelName(int32(options.FileDescriptor)); err != nil { + stackOptions.Name = tunName + stackOptions.ForwarderBindInterface = true + } + } + l.tunIf = tunIf + l.tunStack, err = tun.NewStack(strings.ToLower(options.Stack.String()), stackOptions) if err != nil { return } diff --git a/listener/sing_tun/server_android.go b/listener/sing_tun/server_android.go index 4f85c418..ac41282d 100644 --- a/listener/sing_tun/server_android.go +++ b/listener/sing_tun/server_android.go @@ -1,7 +1,7 @@ package sing_tun import ( - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/log" tun "github.com/metacubex/sing-tun" "github.com/sagernet/netlink" "golang.org/x/sys/unix" diff --git a/listener/sing_tun/server_notwindows.go b/listener/sing_tun/server_notwindows.go index d3280c5c..eda79dc0 100644 --- a/listener/sing_tun/server_notwindows.go +++ b/listener/sing_tun/server_notwindows.go @@ -6,6 +6,6 @@ import ( tun "github.com/metacubex/sing-tun" ) -func tunOpen(options tun.Options) (tun.Tun, error) { - return tun.Open(options) +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 index 7b745cac..8da21287 100644 --- a/listener/sing_tun/server_windows.go +++ b/listener/sing_tun/server_windows.go @@ -3,16 +3,16 @@ package sing_tun import ( "time" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/log" tun "github.com/metacubex/sing-tun" ) -func tunOpen(options tun.Options) (tunIf tun.Tun, err error) { +func tunNew(options tun.Options) (tunIf tun.Tun, err error) { maxRetry := 3 for i := 0; i < maxRetry; i++ { timeBegin := time.Now() - tunIf, err = tun.Open(options) + tunIf, err = tun.New(options) if err == nil { return } diff --git a/listener/sing_tun/tun_name_darwin.go b/listener/sing_tun/tun_name_darwin.go new file mode 100644 index 00000000..5b4686ea --- /dev/null +++ b/listener/sing_tun/tun_name_darwin.go @@ -0,0 +1,11 @@ +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 new file mode 100644 index 00000000..9ae9800b --- /dev/null +++ b/listener/sing_tun/tun_name_linux.go @@ -0,0 +1,25 @@ +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 new file mode 100644 index 00000000..c47c8cbe --- /dev/null +++ b/listener/sing_tun/tun_name_other.go @@ -0,0 +1,9 @@ +//go:build !(darwin || linux) + +package sing_tun + +import "os" + +func getTunnelName(fd int32) (string, error) { + return "", os.ErrInvalid +} diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index bb89ba99..e790e3bc 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -2,16 +2,21 @@ package sing_vmess import ( "context" + "crypto/tls" "net" + "net/http" "net/url" "strings" - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/listener/sing" + "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/ntp" + mihomoVMess "github.com/metacubex/mihomo/transport/vmess" - vmess "github.com/sagernet/sing-vmess" + vmess "github.com/metacubex/sing-vmess" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/metadata" ) @@ -25,7 +30,7 @@ type Listener struct { var _listener *Listener -func New(config LC.VmessServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (sl *Listener, err error) { +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"), @@ -36,13 +41,12 @@ func New(config LC.VmessServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.Packe }() } h := &sing.ListenerHandler{ - TcpIn: tcpIn, - UdpIn: udpIn, + Tunnel: tunnel, Type: C.VMESS, Additions: additions, } - service := vmess.NewService[string](h, vmess.ServiceWithDisableHeaderProtection()) + 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 @@ -64,6 +68,29 @@ func New(config LC.VmessServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.Packe sl = &Listener{false, config, nil, service} + tlsConfig := &tls.Config{} + var httpMux *http.ServeMux + + if config.Certificate != "" && config.PrivateKey != "" { + cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + 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) + }) + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") + } + for _, addr := range strings.Split(config.Listen, ",") { addr := addr @@ -72,9 +99,16 @@ func New(config LC.VmessServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.Packe if err != nil { return nil, err } + if len(tlsConfig.Certificates) > 0 { + l = tls.NewListener(l, tlsConfig) + } sl.listeners = append(sl.listeners, l) go func() { + if httpMux != nil { + _ = http.Serve(l, httpMux) + return + } for { c, err := l.Accept() if err != nil { @@ -83,9 +117,9 @@ func New(config LC.VmessServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.Packe } continue } - _ = c.(*net.TCPConn).SetKeepAlive(true) + N.TCPKeepAlive(c) - go sl.HandleConn(c, tcpIn) + go sl.HandleConn(c, tunnel) } }() } @@ -120,7 +154,7 @@ func (l *Listener) AddrList() (addrList []net.Addr) { return } -func (l *Listener) HandleConn(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { +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", @@ -132,9 +166,9 @@ func (l *Listener) HandleConn(conn net.Conn, in chan<- C.ConnContext, additions } } -func HandleVmess(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) bool { +func HandleVmess(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) bool { if _listener != nil && _listener.service != nil { - go _listener.HandleConn(conn, in, additions...) + go _listener.HandleConn(conn, tunnel, additions...) return true } return false diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go index cbaac987..c8c33e7b 100644 --- a/listener/socks/tcp.go +++ b/listener/socks/tcp.go @@ -4,12 +4,12 @@ import ( "io" "net" - "github.com/Dreamacro/clash/adapter/inbound" - N "github.com/Dreamacro/clash/common/net" - C "github.com/Dreamacro/clash/constant" - authStore "github.com/Dreamacro/clash/listener/auth" - "github.com/Dreamacro/clash/transport/socks4" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + authStore "github.com/metacubex/mihomo/listener/auth" + "github.com/metacubex/mihomo/transport/socks4" + "github.com/metacubex/mihomo/transport/socks5" ) type Listener struct { @@ -34,7 +34,7 @@ func (l *Listener) Close() error { return l.listener.Close() } -func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { +func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-SOCKS"), @@ -59,15 +59,15 @@ func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (* } continue } - go handleSocks(c, in, additions...) + go handleSocks(c, tunnel, additions...) } }() return sl, nil } -func handleSocks(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { - conn.(*net.TCPConn).SetKeepAlive(true) +func handleSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + N.TCPKeepAlive(conn) bufConn := N.NewBufferedConn(conn) head, err := bufConn.Peek(1) if err != nil { @@ -77,25 +77,33 @@ func handleSocks(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Ad switch head[0] { case socks4.Version: - HandleSocks4(bufConn, in, additions...) + HandleSocks4(bufConn, tunnel, additions...) case socks5.Version: - HandleSocks5(bufConn, in, additions...) + HandleSocks5(bufConn, tunnel, additions...) default: conn.Close() } } -func HandleSocks4(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { - addr, _, err := socks4.ServerHandshake(conn, authStore.Authenticator()) +func HandleSocks4(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + authenticator := authStore.Authenticator() + if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) { + authenticator = nil + } + addr, _, err := socks4.ServerHandshake(conn, authenticator) if err != nil { conn.Close() return } - in <- inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...) + tunnel.HandleTCPConn(inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...)) } -func HandleSocks5(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { - target, command, err := socks5.ServerHandshake(conn, authStore.Authenticator()) +func HandleSocks5(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + authenticator := authStore.Authenticator() + if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) { + authenticator = nil + } + target, command, err := socks5.ServerHandshake(conn, authenticator) if err != nil { conn.Close() return @@ -105,5 +113,5 @@ func HandleSocks5(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.A io.Copy(io.Discard, conn) return } - in <- inbound.NewSocket(target, conn, C.SOCKS5, additions...) + tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.SOCKS5, additions...)) } diff --git a/listener/socks/udp.go b/listener/socks/udp.go index f375dade..ef31b20e 100644 --- a/listener/socks/udp.go +++ b/listener/socks/udp.go @@ -3,12 +3,12 @@ package socks import ( "net" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/common/sockopt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/socks5" + "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 { @@ -33,7 +33,7 @@ func (l *UDPListener) Close() error { return l.packetConn.Close() } -func NewUDP(addr string, in chan<- C.PacketAdapter, additions ...inbound.Addition) (*UDPListener, error) { +func NewUDP(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-SOCKS"), @@ -53,39 +53,40 @@ func NewUDP(addr string, in chan<- C.PacketAdapter, additions ...inbound.Additio packetConn: l, addr: addr, } + conn := N.NewEnhancePacketConn(l) go func() { for { - buf := pool.Get(pool.UDPBufferSize) - n, remoteAddr, err := l.ReadFrom(buf) + data, put, remoteAddr, err := conn.WaitReadFrom() if err != nil { - pool.Put(buf) + if put != nil { + put() + } if sl.closed { break } continue } - handleSocksUDP(l, in, buf[:n], remoteAddr, additions...) + handleSocksUDP(l, tunnel, data, put, remoteAddr, additions...) } }() return sl, nil } -func handleSocksUDP(pc net.PacketConn, in chan<- C.PacketAdapter, buf []byte, addr net.Addr, additions ...inbound.Addition) { +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 - pool.Put(buf) + if put != nil { + put() + } return } packet := &packet{ pc: pc, rAddr: addr, payload: payload, - bufRef: buf, - } - select { - case in <- inbound.NewPacket(target, packet, C.SOCKS5, additions...): - default: + put: put, } + tunnel.HandleUDPPacket(inbound.NewPacket(target, packet, C.SOCKS5, additions...)) } diff --git a/listener/socks/utils.go b/listener/socks/utils.go index 4c53b9e5..d113d45c 100644 --- a/listener/socks/utils.go +++ b/listener/socks/utils.go @@ -3,15 +3,14 @@ package socks import ( "net" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/transport/socks5" ) type packet struct { pc net.PacketConn rAddr net.Addr payload []byte - bufRef []byte + put func() } func (c *packet) Data() []byte { @@ -33,7 +32,11 @@ func (c *packet) LocalAddr() net.Addr { } func (c *packet) Drop() { - pool.Put(c.bufRef) + if c.put != nil { + c.put() + c.put = nil + } + c.payload = nil } func (c *packet) InAddr() net.Addr { diff --git a/listener/tproxy/packet.go b/listener/tproxy/packet.go index 2a274f61..e4852665 100644 --- a/listener/tproxy/packet.go +++ b/listener/tproxy/packet.go @@ -6,18 +6,17 @@ import ( "net" "net/netip" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/common/pool" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "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 - in chan<- C.PacketAdapter - natTable C.NatTable + pc net.PacketConn + lAddr netip.AddrPort + buf []byte + tunnel C.Tunnel } func (c *packet) Data() []byte { @@ -26,7 +25,7 @@ func (c *packet) Data() []byte { // 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.in, c.natTable) + tc, err := createOrGetLocalConn(addr, c.LocalAddr(), c.tunnel) if err != nil { n = 0 return @@ -41,7 +40,8 @@ func (c *packet) LocalAddr() net.Addr { } func (c *packet) Drop() { - pool.Put(c.buf) + _ = pool.Put(c.buf) + c.buf = nil } func (c *packet) InAddr() net.Addr { @@ -51,19 +51,19 @@ func (c *packet) InAddr() net.Addr { // 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, in chan<- C.PacketAdapter, natTable C.NatTable) (*net.UDPConn, error) { +func createOrGetLocalConn(rAddr, lAddr net.Addr, tunnel C.Tunnel) (*net.UDPConn, error) { remote := rAddr.String() local := lAddr.String() - localConn := natTable.GetLocalConn(local, remote) + natTable := tunnel.NatTable() + localConn := natTable.GetForLocalConn(local, remote) // localConn not exist if localConn == nil { - lockKey := remote + "-lock" - cond, loaded := natTable.GetOrCreateLockForLocalConn(local, lockKey) + cond, loaded := natTable.GetOrCreateLockForLocalConn(local, remote) if loaded { cond.L.Lock() cond.Wait() // we should get localConn here - localConn = natTable.GetLocalConn(local, remote) + localConn = natTable.GetForLocalConn(local, remote) if localConn == nil { return nil, fmt.Errorf("localConn is nil, nat entry not exist") } @@ -73,15 +73,15 @@ func createOrGetLocalConn(rAddr, lAddr net.Addr, in chan<- C.PacketAdapter, natT return nil, fmt.Errorf("cond is nil, nat entry not exist") } defer func() { - natTable.DeleteLocalConnMap(local, lockKey) + natTable.DeleteLockForLocalConn(local, remote) cond.Broadcast() }() - conn, err := listenLocalConn(rAddr, lAddr, in, natTable) + conn, err := listenLocalConn(rAddr, lAddr, tunnel) if err != nil { - log.Errorln("listenLocalConn failed with error: %s, packet loss", err.Error()) + 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.AddLocalConn(local, remote, conn) + natTable.AddForLocalConn(local, remote, conn) localConn = conn } } @@ -90,7 +90,7 @@ func createOrGetLocalConn(rAddr, lAddr net.Addr, in chan<- C.PacketAdapter, natT // this function listen at rAddr // and send what received to program itself, then send to real remote -func listenLocalConn(rAddr, lAddr net.Addr, in chan<- C.PacketAdapter, natTable C.NatTable) (*net.UDPConn, error) { +func listenLocalConn(rAddr, lAddr net.Addr, tunnel C.Tunnel) (*net.UDPConn, error) { additions := []inbound.Addition{ inbound.WithInName("DEFAULT-TPROXY"), inbound.WithSpecialRules(""), @@ -113,7 +113,7 @@ func listenLocalConn(rAddr, lAddr net.Addr, in chan<- C.PacketAdapter, natTable } // since following localPackets are pass through this socket which listen rAddr // I choose current listener as packet's packet conn - handlePacketConn(lc, in, natTable, buf[:br], lAddr.(*net.UDPAddr).AddrPort(), rAddr.(*net.UDPAddr).AddrPort(), additions...) + handlePacketConn(lc, tunnel, buf[:br], lAddr.(*net.UDPAddr).AddrPort(), rAddr.(*net.UDPAddr).AddrPort(), additions...) } }() return lc, nil diff --git a/listener/tproxy/tproxy.go b/listener/tproxy/tproxy.go index 198481f7..efb144a9 100644 --- a/listener/tproxy/tproxy.go +++ b/listener/tproxy/tproxy.go @@ -3,9 +3,10 @@ package tproxy import ( "net" - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "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" ) type Listener struct { @@ -30,13 +31,13 @@ func (l *Listener) Close() error { return l.listener.Close() } -func (l *Listener) handleTProxy(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { +func (l *Listener) handleTProxy(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { target := socks5.ParseAddrToSocksAddr(conn.LocalAddr()) - conn.(*net.TCPConn).SetKeepAlive(true) - in <- inbound.NewSocket(target, conn, C.TPROXY, additions...) + N.TCPKeepAlive(conn) + tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.TPROXY, additions...)) } -func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { +func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-TPROXY"), @@ -73,7 +74,7 @@ func New(addr string, in chan<- C.ConnContext, additions ...inbound.Addition) (* } continue } - go rl.handleTProxy(c, in, additions...) + go rl.handleTProxy(c, tunnel, additions...) } }() diff --git a/listener/tproxy/tproxy_iptables.go b/listener/tproxy/tproxy_iptables.go index 31ac24e5..5ddd7b4c 100644 --- a/listener/tproxy/tproxy_iptables.go +++ b/listener/tproxy/tproxy_iptables.go @@ -6,9 +6,9 @@ import ( "net" "runtime" - "github.com/Dreamacro/clash/common/cmd" - "github.com/Dreamacro/clash/component/dialer" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/cmd" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/log" ) var ( @@ -48,25 +48,25 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 execCmd(fmt.Sprintf("iptables -t filter -A FORWARD -i %s -o %s -j ACCEPT", interfaceName, interfaceName)) } - // set clash divert - execCmd("iptables -t mangle -N clash_divert") - execCmd("iptables -t mangle -F clash_divert") - execCmd(fmt.Sprintf("iptables -t mangle -A clash_divert -j MARK --set-mark %s", PROXY_FWMARK)) - execCmd("iptables -t mangle -A clash_divert -j ACCEPT") + // 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 clash_prerouting") - execCmd("iptables -t mangle -F clash_prerouting") - execCmd("iptables -t mangle -A clash_prerouting -s 172.17.0.0/16 -j RETURN") - execCmd("iptables -t mangle -A clash_prerouting -p udp --dport 53 -j ACCEPT") - execCmd("iptables -t mangle -A clash_prerouting -p tcp --dport 53 -j ACCEPT") - execCmd("iptables -t mangle -A clash_prerouting -m addrtype --dst-type LOCAL -j RETURN") - addLocalnetworkToChain("clash_prerouting", bypass) - execCmd("iptables -t mangle -A clash_prerouting -p tcp -m socket -j clash_divert") - execCmd("iptables -t mangle -A clash_prerouting -p udp -m socket -j clash_divert") - execCmd(fmt.Sprintf("iptables -t mangle -A clash_prerouting -p tcp -j TPROXY --on-port %d --tproxy-mark %s/%s", tProxyPort, PROXY_FWMARK, PROXY_FWMARK)) - execCmd(fmt.Sprintf("iptables -t mangle -A clash_prerouting -p udp -j TPROXY --on-port %d --tproxy-mark %s/%s", tProxyPort, PROXY_FWMARK, PROXY_FWMARK)) - execCmd("iptables -t mangle -A PREROUTING -j clash_prerouting") + 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") + 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") 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)) @@ -77,27 +77,27 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 } // set output - execCmd("iptables -t mangle -N clash_output") - execCmd("iptables -t mangle -F clash_output") - execCmd(fmt.Sprintf("iptables -t mangle -A clash_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) - execCmd("iptables -t mangle -A clash_output -p udp -m multiport --dports 53,123,137 -j ACCEPT") - execCmd("iptables -t mangle -A clash_output -p tcp --dport 53 -j ACCEPT") - execCmd("iptables -t mangle -A clash_output -m addrtype --dst-type LOCAL -j RETURN") - execCmd("iptables -t mangle -A clash_output -m addrtype --dst-type BROADCAST -j RETURN") - addLocalnetworkToChain("clash_output", bypass) - execCmd(fmt.Sprintf("iptables -t mangle -A clash_output -p tcp -j MARK --set-mark %s", PROXY_FWMARK)) - execCmd(fmt.Sprintf("iptables -t mangle -A clash_output -p udp -j MARK --set-mark %s", PROXY_FWMARK)) - execCmd(fmt.Sprintf("iptables -t mangle -I OUTPUT -o %s -j clash_output", interfaceName)) + 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())) + 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 - execCmd("iptables -t nat -N clash_dns_output") - execCmd("iptables -t nat -F clash_dns_output") - execCmd(fmt.Sprintf("iptables -t nat -A clash_dns_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) - execCmd("iptables -t nat -A clash_dns_output -s 172.17.0.0/16 -j RETURN") - execCmd(fmt.Sprintf("iptables -t nat -A clash_dns_output -p udp -j REDIRECT --to-ports %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -A clash_dns_output -p tcp -j REDIRECT --to-ports %d", dnsPort)) - execCmd("iptables -t nat -I OUTPUT -p tcp --dport 53 -j clash_dns_output") - execCmd("iptables -t nat -I OUTPUT -p udp --dport 53 -j clash_dns_output") + 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 } @@ -113,7 +113,7 @@ func CleanupTProxyIPTables() { dialer.DefaultRoutingMark.Store(0) } - if _, err := cmd.ExecCmd("iptables -t mangle -L clash_divert"); err != nil { + if _, err := cmd.ExecCmd("iptables -t mangle -L mihomo_divert"); err != nil { return } @@ -132,7 +132,7 @@ func CleanupTProxyIPTables() { // clean PREROUTING 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 clash_prerouting") + execCmd("iptables -t mangle -D PREROUTING -j mihomo_prerouting") // clean POSTROUTING if interfaceName != "lo" { @@ -140,19 +140,19 @@ func CleanupTProxyIPTables() { } // clean OUTPUT - execCmd(fmt.Sprintf("iptables -t mangle -D OUTPUT -o %s -j clash_output", interfaceName)) - execCmd("iptables -t nat -D OUTPUT -p tcp --dport 53 -j clash_dns_output") - execCmd("iptables -t nat -D OUTPUT -p udp --dport 53 -j clash_dns_output") + execCmd(fmt.Sprintf("iptables -t mangle -D OUTPUT -o %s -j mihomo_output", interfaceName)) + 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 clash_prerouting") - execCmd("iptables -t mangle -X clash_prerouting") - execCmd("iptables -t mangle -F clash_divert") - execCmd("iptables -t mangle -X clash_divert") - execCmd("iptables -t mangle -F clash_output") - execCmd("iptables -t mangle -X clash_output") - execCmd("iptables -t nat -F clash_dns_output") - execCmd("iptables -t nat -X clash_dns_output") + 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") + execCmd("iptables -t nat -F mihomo_dns_output") + execCmd("iptables -t nat -X mihomo_dns_output") interfaceName = "" tProxyPort = 0 diff --git a/listener/tproxy/udp.go b/listener/tproxy/udp.go index d3727180..aa0fee19 100644 --- a/listener/tproxy/udp.go +++ b/listener/tproxy/udp.go @@ -4,10 +4,10 @@ import ( "net" "net/netip" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/common/pool" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "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 { @@ -32,7 +32,7 @@ func (l *UDPListener) Close() error { return l.packetConn.Close() } -func NewUDP(addr string, in chan<- C.PacketAdapter, natTable C.NatTable, additions ...inbound.Addition) (*UDPListener, error) { +func NewUDP(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { if len(additions) == 0 { additions = []inbound.Addition{ inbound.WithInName("DEFAULT-TPROXY"), @@ -83,24 +83,20 @@ func NewUDP(addr string, in chan<- C.PacketAdapter, natTable C.NatTable, additio // try to unmap 4in6 address lAddr = netip.AddrPortFrom(lAddr.Addr().Unmap(), lAddr.Port()) } - handlePacketConn(l, in, natTable, buf[:n], lAddr, rAddr, additions...) + handlePacketConn(l, tunnel, buf[:n], lAddr, rAddr, additions...) } }() return rl, nil } -func handlePacketConn(pc net.PacketConn, in chan<- C.PacketAdapter, natTable C.NatTable, buf []byte, lAddr, rAddr netip.AddrPort, additions ...inbound.Addition) { +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, - in: in, - natTable: natTable, - } - select { - case in <- inbound.NewPacket(target, pkt, C.TPROXY, additions...): - default: + pc: pc, + lAddr: lAddr, + buf: buf, + tunnel: tunnel, } + tunnel.HandleUDPPacket(inbound.NewPacket(target, pkt, C.TPROXY, additions...)) } diff --git a/listener/tuic/server.go b/listener/tuic/server.go index a7ad69f6..7fa7b18e 100644 --- a/listener/tuic/server.go +++ b/listener/tuic/server.go @@ -1,21 +1,25 @@ package tuic import ( + "context" "crypto/tls" "net" "strings" "time" - "github.com/metacubex/quic-go" + "github.com/metacubex/mihomo/adapter/inbound" + CN "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/sockopt" + 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/Dreamacro/clash/adapter/inbound" - CN "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/common/sockopt" - C "github.com/Dreamacro/clash/constant" - LC "github.com/Dreamacro/clash/listener/config" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/socks5" - "github.com/Dreamacro/clash/transport/tuic" + "github.com/gofrs/uuid/v5" + "github.com/metacubex/quic-go" + "golang.org/x/exp/slices" ) const ServerMaxIncomingStreams = (1 << 32) - 1 @@ -27,14 +31,20 @@ type Listener struct { servers []*tuic.Server } -func New(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (*Listener, error) { +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(""), } } - cert, err := CN.ParseCert(config.Certificate, config.PrivateKey) + h := &sing.ListenerHandler{ + Tunnel: tunnel, + Type: C.TUIC, + Additions: additions, + } + + cert, err := CN.ParseCert(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } @@ -52,38 +62,85 @@ func New(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.Packet MaxIncomingStreams: ServerMaxIncomingStreams, MaxIncomingUniStreams: ServerMaxIncomingStreams, EnableDatagrams: true, - Allow0RTT: func(addr net.Addr) bool { - return true - }, + Allow0RTT: true, } quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10 quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10 quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow - tokens := make([][32]byte, len(config.Token)) - for i, token := range config.Token { - tokens[i] = tuic.GenTKN(token) + 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...) + if h.IsSpecialFqdn(metadata.Host) { + go func() { // ParseSpecialFqdn will block, so open a new goroutine + _ = h.ParseSpecialFqdn( + sing.WithAdditions(context.Background(), newAdditions...), + conn, + sing.ConvertMetadata(metadata), + ) + }() + return nil + } + go tunnel.HandleTCPConn(conn, metadata) + 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: func(conn net.Conn, addr socks5.Addr) error { - tcpIn <- inbound.NewSocket(addr, conn, C.TUIC, additions...) - return nil - }, - HandleUdpFn: func(addr socks5.Addr, packet C.UDPPacket) error { - select { - case udpIn <- inbound.NewPacket(addr, packet, C.TUIC, additions...): - default: - } - return nil - }, + HandleTcpFn: handleTcpFn, + HandleUdpFn: handleUdpFn, TlsConfig: tlsConfig, QuicConfig: quicConfig, - Tokens: tokens, 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} @@ -103,7 +160,8 @@ func New(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.Packet sl.udpListeners = append(sl.udpListeners, ul) - server, err := tuic.NewServer(option, ul) + var server *tuic.Server + server, err = tuic.NewServer(option, ul) if err != nil { return nil, err } diff --git a/listener/tunnel/packet.go b/listener/tunnel/packet.go index 602f7675..165004d6 100644 --- a/listener/tunnel/packet.go +++ b/listener/tunnel/packet.go @@ -3,7 +3,7 @@ package tunnel import ( "net" - "github.com/Dreamacro/clash/common/pool" + "github.com/metacubex/mihomo/common/pool" ) type packet struct { @@ -27,7 +27,8 @@ func (c *packet) LocalAddr() net.Addr { } func (c *packet) Drop() { - pool.Put(c.payload) + _ = pool.Put(c.payload) + c.payload = nil } func (c *packet) InAddr() net.Addr { diff --git a/listener/tunnel/tcp.go b/listener/tunnel/tcp.go index bf278c1c..794dc8ac 100644 --- a/listener/tunnel/tcp.go +++ b/listener/tunnel/tcp.go @@ -4,9 +4,10 @@ import ( "fmt" "net" - "github.com/Dreamacro/clash/adapter/inbound" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "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" ) type Listener struct { @@ -33,15 +34,13 @@ func (l *Listener) Close() error { return l.listener.Close() } -func (l *Listener) handleTCP(conn net.Conn, in chan<- C.ConnContext, additions ...inbound.Addition) { - conn.(*net.TCPConn).SetKeepAlive(true) - ctx := inbound.NewSocket(l.target, conn, C.TUNNEL, additions...) - ctx.Metadata().SpecialProxy = l.proxy - in <- ctx +func (l *Listener) handleTCP(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + N.TCPKeepAlive(conn) + tunnel.HandleTCPConn(inbound.NewSocket(l.target, conn, C.TUNNEL, additions...)) } -func New(addr, target, proxy string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { - l, err := net.Listen("tcp", addr) +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 } @@ -58,6 +57,10 @@ func New(addr, target, proxy string, in chan<- C.ConnContext, additions ...inbou addr: addr, } + if proxy != "" { + additions = append([]inbound.Addition{inbound.WithSpecialProxy(proxy)}, additions...) + } + go func() { for { c, err := l.Accept() @@ -67,7 +70,7 @@ func New(addr, target, proxy string, in chan<- C.ConnContext, additions ...inbou } continue } - go rl.handleTCP(c, in, additions...) + go rl.handleTCP(c, tunnel, additions...) } }() diff --git a/listener/tunnel/udp.go b/listener/tunnel/udp.go index 0795084c..f7d980ab 100644 --- a/listener/tunnel/udp.go +++ b/listener/tunnel/udp.go @@ -4,10 +4,10 @@ import ( "fmt" "net" - "github.com/Dreamacro/clash/adapter/inbound" - "github.com/Dreamacro/clash/common/pool" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "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 { @@ -34,7 +34,7 @@ func (l *PacketConn) Close() error { return l.conn.Close() } -func NewUDP(addr, target, proxy string, in chan<- C.PacketAdapter, additions ...inbound.Addition) (*PacketConn, error) { +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 @@ -51,6 +51,11 @@ func NewUDP(addr, target, proxy string, in chan<- C.PacketAdapter, additions ... proxy: proxy, addr: addr, } + + if proxy != "" { + additions = append([]inbound.Addition{inbound.WithSpecialProxy(proxy)}, additions...) + } + go func() { for { buf := pool.Get(pool.UDPBufferSize) @@ -62,24 +67,19 @@ func NewUDP(addr, target, proxy string, in chan<- C.PacketAdapter, additions ... } continue } - sl.handleUDP(l, in, buf[:n], remoteAddr, additions...) + sl.handleUDP(l, tunnel, buf[:n], remoteAddr, additions...) } }() return sl, nil } -func (l *PacketConn) handleUDP(pc net.PacketConn, in chan<- C.PacketAdapter, buf []byte, addr net.Addr, additions ...inbound.Addition) { - packet := &packet{ +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, } - ctx := inbound.NewPacket(l.target, packet, C.TUNNEL, additions...) - ctx.Metadata().SpecialProxy = l.proxy - select { - case in <- ctx: - default: - } + tunnel.HandleUDPPacket(inbound.NewPacket(l.target, cPacket, C.TUNNEL, additions...)) } diff --git a/log/log.go b/log/log.go index acddeaff..d431dcb1 100644 --- a/log/log.go +++ b/log/log.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/Dreamacro/clash/common/observable" + "github.com/metacubex/mihomo/common/observable" log "github.com/sirupsen/logrus" ) diff --git a/log/sing.go b/log/sing.go new file mode 100644 index 00000000..818acc79 --- /dev/null +++ b/log/sing.go @@ -0,0 +1,68 @@ +package log + +import ( + "context" + "fmt" + + L "github.com/sagernet/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...)) +} + +var SingLogger L.ContextLogger = singLogger{} diff --git a/main.go b/main.go index e13b8dc8..fd1e065c 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,7 @@ package main import ( "flag" "fmt" - "github.com/Dreamacro/clash/constant/features" + "github.com/metacubex/mihomo/constant/features" "os" "os/signal" "path/filepath" @@ -11,17 +11,16 @@ import ( "strings" "syscall" - "github.com/Dreamacro/clash/config" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/hub" - "github.com/Dreamacro/clash/hub/executor" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/config" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/hub" + "github.com/metacubex/mihomo/hub/executor" + "github.com/metacubex/mihomo/log" "go.uber.org/automaxprocs/maxprocs" ) var ( - flagset map[string]bool version bool testConfig bool geodataMode bool @@ -33,26 +32,21 @@ var ( ) func init() { - flag.StringVar(&homeDir, "d", "", "set configuration directory") - flag.StringVar(&configFile, "f", "", "specify configuration file") - flag.StringVar(&externalUI, "ext-ui", "", "override external ui directory") - flag.StringVar(&externalController, "ext-ctl", "", "override external controller address") - flag.StringVar(&secret, "secret", "", "override secret for RESTful API") + flag.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(&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(&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 clash") + flag.BoolVar(&version, "v", false, "show current version of mihomo") flag.BoolVar(&testConfig, "t", false, "test configuration and exit") flag.Parse() - - flagset = map[string]bool{} - flag.Visit(func(f *flag.Flag) { - flagset[f.Name] = true - }) } func main() { _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) if version { - fmt.Printf("Clash Meta %s %s %s with %s %s\n", + fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) if len(features.TAGS) != 0 { fmt.Printf("Use tags: %s\n", strings.Join(features.TAGS, ", ")) @@ -99,13 +93,13 @@ func main() { } var options []hub.Option - if flagset["ext-ui"] { + if externalUI != "" { options = append(options, hub.WithExternalUI(externalUI)) } - if flagset["ext-ctl"] { + if externalController != "" { options = append(options, hub.WithExternalController(externalController)) } - if flagset["secret"] { + if secret != "" { options = append(options, hub.WithSecret(secret)) } @@ -115,7 +109,20 @@ func main() { defer executor.Shutdown() - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - <-sigCh + 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 cfg, err := executor.ParseWithPath(C.Path.Config()); err == nil { + executor.ApplyConfig(cfg, true) + } else { + log.Errorln("Parse config error: %s", err.Error()) + } + } + } } diff --git a/ntp/service.go b/ntp/service.go new file mode 100644 index 00000000..4c95045a --- /dev/null +++ b/ntp/service.go @@ -0,0 +1,124 @@ +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/sagernet/sing/common/metadata" + "github.com/sagernet/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) + service.running = true + srv.update() + 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() { + var response *ntp.Response + var err error + for i := 0; i < 3; i++ { + response, err = ntp.Exchange(context.Background(), srv.dialer, srv.server) + if err != nil { + if i == 2 { + log.Errorln("Initialize NTP time failed: %s", err) + return + } + time.Sleep(time.Second * 2) // wait for 2 seconds before the next try + continue + } + break + } + 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 + err = setSystemTime(timeNow) + if err == nil { + log.Infoln("Sync system time success: %s", timeNow.Local().Format(ntp.TimeLayout)) + } else { + log.Errorln("Write time to system: %s", err) + srv.syncSystemTime = false + } + } +} + +func (srv *Service) loopUpdate() { + for { + select { + case <-srv.ctx.Done(): + return + case <-srv.ticker.C: + } + srv.update() + } +} + +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 new file mode 100644 index 00000000..12050983 --- /dev/null +++ b/ntp/time_stub.go @@ -0,0 +1,12 @@ +//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 new file mode 100644 index 00000000..9e819473 --- /dev/null +++ b/ntp/time_unix.go @@ -0,0 +1,14 @@ +//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 new file mode 100644 index 00000000..8ef29b1b --- /dev/null +++ b/ntp/time_windows.go @@ -0,0 +1,32 @@ +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/patch/add_debug_api.patch b/patch/add_debug_api.patch deleted file mode 100644 index 7134b378..00000000 --- a/patch/add_debug_api.patch +++ /dev/null @@ -1,53 +0,0 @@ -Subject: [PATCH] Chore: add debug api ---- -Index: hub/route/debug.go -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/hub/route/debug.go b/hub/route/debug.go -new file mode 100644 ---- /dev/null (revision df1007e2b14f7a526d176410995998bf06054657) -+++ b/hub/route/debug.go (revision df1007e2b14f7a526d176410995998bf06054657) -@@ -0,0 +1,21 @@ -+package route -+ -+import ( -+ "github.com/Dreamacro/clash/log" -+ "github.com/go-chi/chi/v5" -+ "github.com/go-chi/chi/v5/middleware" -+ "net/http" -+ "runtime" -+) -+ -+func debugRouter() http.Handler { -+ handler := middleware.Profiler() -+ r := chi.NewRouter() -+ r.Mount("/", handler) -+ r.Put("/gc", func(writer http.ResponseWriter, request *http.Request) { -+ log.Debugln("trigger GC") -+ runtime.GC() -+ }) -+ -+ return r -+} -Index: hub/route/server.go -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/hub/route/server.go b/hub/route/server.go ---- a/hub/route/server.go (revision f83fd6c690928ca7861196e3ca5af566303f95d5) -+++ b/hub/route/server.go (revision df1007e2b14f7a526d176410995998bf06054657) -@@ -59,6 +59,11 @@ - MaxAge: 300, - }) - r.Use(corsM.Handler) -+ -+ r.Group(func(r chi.Router) { -+ r.Mount("/debug", debugRouter()) -+ }) -+ - r.Group(func(r chi.Router) { - r.Use(authentication) - r.Get("/", hello) diff --git a/rules/common/domain.go b/rules/common/domain.go index 6b3eba22..23f21185 100644 --- a/rules/common/domain.go +++ b/rules/common/domain.go @@ -1,17 +1,15 @@ package common import ( - "golang.org/x/net/idna" "strings" - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" ) type Domain struct { *Base domain string adapter string - isIDNA bool } func (d *Domain) RuleType() C.RuleType { @@ -27,20 +25,14 @@ func (d *Domain) Adapter() string { } func (d *Domain) Payload() string { - domain := d.domain - if d.isIDNA { - domain, _ = idna.ToUnicode(domain) - } - return domain + return d.domain } func NewDomain(domain string, adapter string) *Domain { - actualDomain, _ := idna.ToASCII(domain) return &Domain{ Base: &Base{}, - domain: strings.ToLower(actualDomain), + domain: strings.ToLower(domain), adapter: adapter, - isIDNA: actualDomain != domain, } } diff --git a/rules/common/domain_keyword.go b/rules/common/domain_keyword.go index 94d2a949..ec01293a 100644 --- a/rules/common/domain_keyword.go +++ b/rules/common/domain_keyword.go @@ -1,17 +1,15 @@ package common import ( - "golang.org/x/net/idna" "strings" - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" ) type DomainKeyword struct { *Base keyword string adapter string - isIDNA bool } func (dk *DomainKeyword) RuleType() C.RuleType { @@ -28,20 +26,14 @@ func (dk *DomainKeyword) Adapter() string { } func (dk *DomainKeyword) Payload() string { - keyword := dk.keyword - if dk.isIDNA { - keyword, _ = idna.ToUnicode(keyword) - } - return keyword + return dk.keyword } func NewDomainKeyword(keyword string, adapter string) *DomainKeyword { - actualDomainKeyword, _ := idna.ToASCII(keyword) return &DomainKeyword{ Base: &Base{}, - keyword: strings.ToLower(actualDomainKeyword), + keyword: strings.ToLower(keyword), adapter: adapter, - isIDNA: keyword != actualDomainKeyword, } } diff --git a/rules/common/domain_suffix.go b/rules/common/domain_suffix.go index 4bdc2e2e..b7b1794d 100644 --- a/rules/common/domain_suffix.go +++ b/rules/common/domain_suffix.go @@ -1,17 +1,15 @@ package common import ( - "golang.org/x/net/idna" "strings" - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" ) type DomainSuffix struct { *Base suffix string adapter string - isIDNA bool } func (ds *DomainSuffix) RuleType() C.RuleType { @@ -28,20 +26,14 @@ func (ds *DomainSuffix) Adapter() string { } func (ds *DomainSuffix) Payload() string { - suffix := ds.suffix - if ds.isIDNA { - suffix, _ = idna.ToUnicode(suffix) - } - return suffix + return ds.suffix } func NewDomainSuffix(suffix string, adapter string) *DomainSuffix { - actualDomainSuffix, _ := idna.ToASCII(suffix) return &DomainSuffix{ Base: &Base{}, - suffix: strings.ToLower(actualDomainSuffix), + suffix: strings.ToLower(suffix), adapter: adapter, - isIDNA: suffix != actualDomainSuffix, } } diff --git a/rules/common/final.go b/rules/common/final.go index 8aa5ed7b..d3a415a0 100644 --- a/rules/common/final.go +++ b/rules/common/final.go @@ -1,7 +1,7 @@ package common import ( - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" ) type Match struct { diff --git a/rules/common/geoip.go b/rules/common/geoip.go index 0c134c63..3a29fae4 100644 --- a/rules/common/geoip.go +++ b/rules/common/geoip.go @@ -4,12 +4,12 @@ import ( "fmt" "strings" - "github.com/Dreamacro/clash/component/geodata" - "github.com/Dreamacro/clash/component/geodata/router" - "github.com/Dreamacro/clash/component/mmdb" - "github.com/Dreamacro/clash/component/resolver" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "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" ) type GEOIP struct { @@ -40,8 +40,13 @@ func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) { resolver.IsFakeBroadcastIP(ip), g.adapter } if !C.GeodataMode { - record, _ := mmdb.Instance().Country(ip.AsSlice()) - return strings.EqualFold(record.Country.IsoCode, g.country), g.adapter + codes := mmdb.Instance().LookupCode(ip.AsSlice()) + for _, code := range codes { + if strings.EqualFold(code, g.country) { + return true, g.adapter + } + } + return false, g.adapter } return g.geoIPMatcher.Match(ip.AsSlice()), g.adapter } diff --git a/rules/common/geosite.go b/rules/common/geosite.go index e89dc19b..e9b19d0e 100644 --- a/rules/common/geosite.go +++ b/rules/common/geosite.go @@ -3,12 +3,12 @@ package common import ( "fmt" - "github.com/Dreamacro/clash/component/geodata" - _ "github.com/Dreamacro/clash/component/geodata/memconservative" - "github.com/Dreamacro/clash/component/geodata/router" - _ "github.com/Dreamacro/clash/component/geodata/standard" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "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 { diff --git a/rules/common/in_name.go b/rules/common/in_name.go new file mode 100644 index 00000000..9b14ef6a --- /dev/null +++ b/rules/common/in_name.go @@ -0,0 +1,49 @@ +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 index 520c9594..fc73b208 100644 --- a/rules/common/in_type.go +++ b/rules/common/in_type.go @@ -2,7 +2,7 @@ package common import ( "fmt" - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" "strings" ) @@ -23,7 +23,7 @@ func (u *InType) Match(metadata *C.Metadata) (bool, string) { } func (u *InType) RuleType() C.RuleType { - return C.INTYPE + return C.InType } func (u *InType) Adapter() string { @@ -37,7 +37,7 @@ func (u *InType) Payload() string { func NewInType(iTypes, adapter string) (*InType, error) { types := strings.Split(iTypes, "/") if len(types) == 0 { - return nil, fmt.Errorf("in type could be empty") + return nil, fmt.Errorf("in type couldn't be empty") } tps, err := parseInTypes(types) diff --git a/rules/common/in_user.go b/rules/common/in_user.go new file mode 100644 index 00000000..ebe881af --- /dev/null +++ b/rules/common/in_user.go @@ -0,0 +1,49 @@ +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/ipcidr.go b/rules/common/ipcidr.go index 8ab6cf5a..663c9397 100644 --- a/rules/common/ipcidr.go +++ b/rules/common/ipcidr.go @@ -3,7 +3,7 @@ package common import ( "net/netip" - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" ) type IPCIDROption func(*IPCIDR) @@ -22,7 +22,7 @@ func WithIPCIDRNoResolve(noResolve bool) IPCIDROption { type IPCIDR struct { *Base - ipnet *netip.Prefix + ipnet netip.Prefix adapter string isSourceIP bool noResolveIP bool @@ -63,7 +63,7 @@ func NewIPCIDR(s string, adapter string, opts ...IPCIDROption) (*IPCIDR, error) ipcidr := &IPCIDR{ Base: &Base{}, - ipnet: &ipnet, + ipnet: ipnet, adapter: adapter, } diff --git a/rules/common/ipsuffix.go b/rules/common/ipsuffix.go index b01557dc..3251faf8 100644 --- a/rules/common/ipsuffix.go +++ b/rules/common/ipsuffix.go @@ -1,7 +1,7 @@ package common import ( - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" "net/netip" ) diff --git a/rules/common/network_type.go b/rules/common/network_type.go index fb6b5077..83a332d8 100644 --- a/rules/common/network_type.go +++ b/rules/common/network_type.go @@ -2,7 +2,7 @@ package common import ( "fmt" - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" "strings" ) @@ -21,10 +21,8 @@ func NewNetworkType(network, adapter string) (*NetworkType, error) { switch strings.ToUpper(network) { case "TCP": ntType.network = C.TCP - break case "UDP": ntType.network = C.UDP - break default: return nil, fmt.Errorf("unsupported network type, only TCP/UDP") } diff --git a/rules/common/port.go b/rules/common/port.go index 3b7ea1fc..ec76cf30 100644 --- a/rules/common/port.go +++ b/rules/common/port.go @@ -2,19 +2,17 @@ package common import ( "fmt" - "strconv" - "strings" - "github.com/Dreamacro/clash/common/utils" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/common/utils" + C "github.com/metacubex/mihomo/constant" ) type Port struct { *Base - adapter string - port string - ruleType C.RuleType - portList []utils.Range[uint16] + adapter string + port string + ruleType C.RuleType + portRanges utils.IntRanges[uint16] } func (p *Port) RuleType() C.RuleType { @@ -29,7 +27,7 @@ func (p *Port) Match(metadata *C.Metadata) (bool, string) { case C.SrcPort: targetPort = metadata.SrcPort } - return p.matchPortReal(targetPort), p.adapter + return p.portRanges.Check(targetPort), p.adapter } func (p *Port) Adapter() string { @@ -40,64 +38,22 @@ func (p *Port) Payload() string { return p.port } -func (p *Port) matchPortReal(portRef string) bool { - port, _ := strconv.Atoi(portRef) - - for _, pr := range p.portList { - if pr.Contains(uint16(port)) { - return true - } - } - - return false -} - func NewPort(port string, adapter string, ruleType C.RuleType) (*Port, error) { - ports := strings.Split(port, "/") - if len(ports) > 28 { - return nil, fmt.Errorf("%s, too many ports to use, maximum support 28 ports", errPayload.Error()) + portRanges, err := utils.NewIntRanges[uint16](port) + if err != nil { + return nil, fmt.Errorf("%w, %w", errPayload, err) } - var portRange []utils.Range[uint16] - for _, p := range ports { - if p == "" { - continue - } - - subPorts := strings.Split(p, "-") - subPortsLen := len(subPorts) - if subPortsLen > 2 { - return nil, errPayload - } - - portStart, err := strconv.ParseUint(strings.Trim(subPorts[0], "[ ]"), 10, 16) - if err != nil { - return nil, errPayload - } - - switch subPortsLen { - case 1: - portRange = append(portRange, *utils.NewRange(uint16(portStart), uint16(portStart))) - case 2: - portEnd, err := strconv.ParseUint(strings.Trim(subPorts[1], "[ ]"), 10, 16) - if err != nil { - return nil, errPayload - } - - portRange = append(portRange, *utils.NewRange(uint16(portStart), uint16(portEnd))) - } - } - - if len(portRange) == 0 { + if len(portRanges) == 0 { return nil, errPayload } return &Port{ - Base: &Base{}, - adapter: adapter, - port: port, - ruleType: ruleType, - portList: portRange, + Base: &Base{}, + adapter: adapter, + port: port, + ruleType: ruleType, + portRanges: portRanges, }, nil } diff --git a/rules/common/process.go b/rules/common/process.go index e972d2bc..ce643594 100644 --- a/rules/common/process.go +++ b/rules/common/process.go @@ -3,7 +3,7 @@ package common import ( "strings" - C "github.com/Dreamacro/clash/constant" + C "github.com/metacubex/mihomo/constant" ) type Process struct { diff --git a/rules/common/uid.go b/rules/common/uid.go index ea275c28..de46c409 100644 --- a/rules/common/uid.go +++ b/rules/common/uid.go @@ -2,57 +2,28 @@ package common import ( "fmt" - "github.com/Dreamacro/clash/common/utils" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" "runtime" - "strconv" - "strings" + + "github.com/metacubex/mihomo/common/utils" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" ) type Uid struct { *Base - uids []utils.Range[uint32] + uids utils.IntRanges[uint32] oUid string adapter string } func NewUid(oUid, adapter string) (*Uid, error) { - //if len(_uids) > 28 { - // return nil, fmt.Errorf("%s, too many uid to use, maximum support 28 uid", errPayload.Error()) - //} if !(runtime.GOOS == "linux" || runtime.GOOS == "android") { return nil, fmt.Errorf("uid rule not support this platform") } - var uidRange []utils.Range[uint32] - for _, u := range strings.Split(oUid, "/") { - if u == "" { - continue - } - - subUids := strings.Split(u, "-") - subUidsLen := len(subUids) - if subUidsLen > 2 { - return nil, errPayload - } - - uidStart, err := strconv.ParseUint(strings.Trim(subUids[0], "[ ]"), 10, 32) - if err != nil { - return nil, errPayload - } - - switch subUidsLen { - case 1: - uidRange = append(uidRange, *utils.NewRange(uint32(uidStart), uint32(uidStart))) - case 2: - uidEnd, err := strconv.ParseUint(strings.Trim(subUids[1], "[ ]"), 10, 32) - if err != nil { - return nil, errPayload - } - - uidRange = append(uidRange, *utils.NewRange(uint32(uidStart), uint32(uidEnd))) - } + uidRange, err := utils.NewIntRanges[uint32](oUid) + if err != nil { + return nil, fmt.Errorf("%w, %w", errPayload, err) } if len(uidRange) == 0 { @@ -72,10 +43,8 @@ func (u *Uid) RuleType() C.RuleType { func (u *Uid) Match(metadata *C.Metadata) (bool, string) { if metadata.Uid != 0 { - for _, uid := range u.uids { - if uid.Contains(metadata.Uid) { - return true, u.adapter - } + if u.uids.Check(metadata.Uid) { + return true, u.adapter } } log.Warnln("[UID] could not get uid from %s", metadata.String()) diff --git a/rules/logic/logic.go b/rules/logic/logic.go index a53503df..4256a200 100644 --- a/rules/logic/logic.go +++ b/rules/logic/logic.go @@ -5,9 +5,9 @@ import ( "regexp" "strings" - "github.com/Dreamacro/clash/common/collections" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/rules/common" + "github.com/metacubex/mihomo/common/collections" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/rules/common" ) type Logic struct { diff --git a/rules/logic_test/logic_test.go b/rules/logic_test/logic_test.go index de5ae569..e88c8578 100644 --- a/rules/logic_test/logic_test.go +++ b/rules/logic_test/logic_test.go @@ -2,10 +2,10 @@ package logic_test import ( // https://github.com/golang/go/wiki/CodeReviewComments#import-dot - . "github.com/Dreamacro/clash/rules/logic" + . "github.com/metacubex/mihomo/rules/logic" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/rules" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/rules" "github.com/stretchr/testify/assert" "testing" ) @@ -20,7 +20,7 @@ func TestAND(t *testing.T) { m, _ := and.Match(&C.Metadata{ Host: "baidu.com", NetWork: C.TCP, - DstPort: "20000", + DstPort: 20000, }) assert.Equal(t, true, m) @@ -35,7 +35,7 @@ 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", + DstPort: 6100, }) assert.Equal(t, false, m) diff --git a/rules/parser.go b/rules/parser.go index 1a336225..b1baa758 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -2,10 +2,10 @@ package rules import ( "fmt" - C "github.com/Dreamacro/clash/constant" - RC "github.com/Dreamacro/clash/rules/common" - "github.com/Dreamacro/clash/rules/logic" - RP "github.com/Dreamacro/clash/rules/provider" + 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) { @@ -47,6 +47,10 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] 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": diff --git a/rules/provider/classical_strategy.go b/rules/provider/classical_strategy.go index 25360ec7..f8042164 100644 --- a/rules/provider/classical_strategy.go +++ b/rules/provider/classical_strategy.go @@ -2,8 +2,8 @@ package provider import ( "fmt" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" "strings" ) @@ -37,36 +37,38 @@ func (c *classicalStrategy) ShouldFindProcess() bool { return c.shouldFindProcess } -func (c *classicalStrategy) OnUpdate(rules []string) { - var classicalRules []C.Rule - shouldResolveIP := false - for _, rawRule := range rules { - ruleType, rule, params := ruleParse(rawRule) +func (c *classicalStrategy) Reset() { + c.rules = nil + c.count = 0 + c.shouldFindProcess = false + c.shouldResolveIP = false +} - if ruleType == "PROCESS-NAME" { +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 rule error:[%s]", err.Error()) + } else { + if r.ShouldResolveIP() { + c.shouldResolveIP = true + } + if r.ShouldFindProcess() { c.shouldFindProcess = true } - r, err := c.parse(ruleType, rule, "", params) - if err != nil { - log.Warnln("parse rule error:[%s]", err.Error()) - } else { - if !shouldResolveIP { - shouldResolveIP = r.ShouldResolveIP() - } - - if !c.shouldFindProcess { - c.shouldFindProcess = r.ShouldFindProcess() - } - - classicalRules = append(classicalRules, r) - } + c.rules = append(c.rules, r) + c.count++ } - - c.rules = classicalRules - c.count = len(classicalRules) } +func (c *classicalStrategy) FinishInsert() {} + func ruleParse(ruleRaw string) (string, string, []string) { item := strings.Split(ruleRaw, ",") if len(item) == 1 { @@ -74,7 +76,11 @@ func ruleParse(ruleRaw string) (string, string, []string) { } else if len(item) == 2 { return item[0], item[1], nil } else if len(item) > 2 { - return item[0], item[1], item[2:] + if item[0] == "NOT" || item[0] == "OR" || item[0] == "AND" || item[0] == "SUB-RULE" { + return item[0], strings.Join(item[1:len(item)], ","), nil + } else { + return item[0], item[1], item[2:] + } } return "", "", nil @@ -83,7 +89,7 @@ func ruleParse(ruleRaw string) (string, string, []string) { 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", "SUB-RULE": + case "MATCH": return nil, fmt.Errorf("unsupported rule type on rule-set") default: return parse(tp, payload, target, params, nil) diff --git a/rules/provider/domain_strategy.go b/rules/provider/domain_strategy.go index add64e76..c0787d58 100644 --- a/rules/provider/domain_strategy.go +++ b/rules/provider/domain_strategy.go @@ -1,15 +1,15 @@ package provider import ( - "github.com/Dreamacro/clash/component/trie" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "golang.org/x/net/idna" + "github.com/metacubex/mihomo/component/trie" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" ) type domainStrategy struct { - count int - domainRules *trie.DomainTrie[struct{}] + count int + domainTrie *trie.DomainTrie[struct{}] + domainSet *trie.DomainSet } func (d *domainStrategy) ShouldFindProcess() bool { @@ -17,7 +17,7 @@ func (d *domainStrategy) ShouldFindProcess() bool { } func (d *domainStrategy) Match(metadata *C.Metadata) bool { - return d.domainRules != nil && d.domainRules.Search(metadata.RuleHost()) != nil + return d.domainSet != nil && d.domainSet.Has(metadata.RuleHost()) } func (d *domainStrategy) Count() int { @@ -28,22 +28,24 @@ func (d *domainStrategy) ShouldResolveIP() bool { return false } -func (d *domainStrategy) OnUpdate(rules []string) { - domainTrie := trie.New[struct{}]() - count := 0 - for _, rule := range rules { - actualDomain, _ := idna.ToASCII(rule) - err := domainTrie.Insert(actualDomain, struct{}{}) - if err != nil { - log.Warnln("invalid domain:[%s]", rule) - } else { - count++ - } - } - domainTrie.Optimize() +func (d *domainStrategy) Reset() { + d.domainTrie = trie.New[struct{}]() + d.domainSet = nil + d.count = 0 +} - d.domainRules = domainTrie - d.count = count +func (d *domainStrategy) Insert(rule string) { + 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 NewDomainStrategy() *domainStrategy { diff --git a/rules/provider/ipcidr_strategy.go b/rules/provider/ipcidr_strategy.go index 88228301..321e901a 100644 --- a/rules/provider/ipcidr_strategy.go +++ b/rules/provider/ipcidr_strategy.go @@ -1,9 +1,9 @@ package provider import ( - "github.com/Dreamacro/clash/component/trie" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/component/trie" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" ) type ipcidrStrategy struct { @@ -28,23 +28,24 @@ func (i *ipcidrStrategy) ShouldResolveIP() bool { return i.shouldResolveIP } -func (i *ipcidrStrategy) OnUpdate(rules []string) { - ipCidrTrie := trie.NewIpCidrTrie() - count := 0 - for _, rule := range rules { - err := ipCidrTrie.AddIpCidrForString(rule) - if err != nil { - log.Warnln("invalid Ipcidr:[%s]", rule) - } else { - count++ - } - } - - i.trie = ipCidrTrie - i.count = count - i.shouldResolveIP = i.count > 0 +func (i *ipcidrStrategy) Reset() { + i.trie = trie.NewIpCidrTrie() + i.count = 0 + i.shouldResolveIP = false } +func (i *ipcidrStrategy) Insert(rule string) { + err := i.trie.AddIpCidrForString(rule) + if err != nil { + log.Warnln("invalid Ipcidr:[%s]", rule) + } else { + i.shouldResolveIP = true + i.count++ + } +} + +func (i *ipcidrStrategy) FinishInsert() {} + func NewIPCidrStrategy() *ipcidrStrategy { return &ipcidrStrategy{} } diff --git a/rules/provider/parse.go b/rules/provider/parse.go index 206bef10..3a5c4fd7 100644 --- a/rules/provider/parse.go +++ b/rules/provider/parse.go @@ -1,19 +1,26 @@ package provider import ( + "errors" "fmt" - "github.com/Dreamacro/clash/common/structure" - "github.com/Dreamacro/clash/component/resource" - C "github.com/Dreamacro/clash/constant" - P "github.com/Dreamacro/clash/constant/provider" "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" +) + +var ( + errSubPath = errors.New("path is not subpath of home directory") ) type ruleProviderSchema struct { Type string `provider:"type"` Behavior string `provider:"behavior"` - Path string `provider:"path"` + Path string `provider:"path,omitempty"` URL string `provider:"url,omitempty"` + Format string `provider:"format,omitempty"` Interval int `provider:"interval,omitempty"` } @@ -23,7 +30,7 @@ func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(t if err := decoder.Decode(mapping, schema); err != nil { return nil, err } - var behavior P.RuleType + var behavior P.RuleBehavior switch schema.Behavior { case "domain": @@ -36,16 +43,37 @@ func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(t return nil, fmt.Errorf("unsupported behavior type: %s", schema.Behavior) } - path := C.Path.Resolve(schema.Path) + var format P.RuleFormat + + switch schema.Format { + case "", "yaml": + format = P.YamlRule + case "text": + format = P.TextRule + default: + return nil, fmt.Errorf("unsupported format type: %s", schema.Format) + } + var vehicle P.Vehicle switch schema.Type { case "file": + path := C.Path.Resolve(schema.Path) vehicle = resource.NewFileVehicle(path) case "http": - vehicle = resource.NewHTTPVehicle(schema.URL, path) + if schema.Path != "" { + path := C.Path.Resolve(schema.Path) + if !C.Path.IsSafePath(path) { + return nil, fmt.Errorf("%w: %s", errSubPath, path) + } + vehicle = resource.NewHTTPVehicle(schema.URL, path) + } else { + path := C.Path.GetPathByHash("rules", schema.URL) + vehicle = resource.NewHTTPVehicle(schema.URL, path) + } + default: return nil, fmt.Errorf("unsupported vehicle type: %s", schema.Type) } - return NewRuleSetProvider(name, behavior, time.Duration(uint(schema.Interval))*time.Second, vehicle, parse), nil + return NewRuleSetProvider(name, behavior, format, time.Duration(uint(schema.Interval))*time.Second, vehicle, parse), nil } diff --git a/rules/provider/provider.go b/rules/provider/provider.go index 175917c2..adc2e44a 100644 --- a/rules/provider/provider.go +++ b/rules/provider/provider.go @@ -1,13 +1,18 @@ package provider import ( + "bytes" "encoding/json" - "github.com/Dreamacro/clash/component/resource" - C "github.com/Dreamacro/clash/constant" - P "github.com/Dreamacro/clash/constant/provider" + "errors" "gopkg.in/yaml.v3" "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" ) var ( @@ -16,7 +21,8 @@ var ( type ruleSetProvider struct { *resource.Fetcher[any] - behavior P.RuleType + behavior P.RuleBehavior + format P.RuleFormat strategy ruleStrategy } @@ -29,8 +35,8 @@ type RulePayload struct { key: Domain or IP Cidr value: Rule type or is empty */ - Rules []string `yaml:"payload"` - Rules2 []string `yaml:"rules"` + Payload []string `yaml:"payload"` + Rules []string `yaml:"rules"` } type ruleStrategy interface { @@ -38,7 +44,9 @@ type ruleStrategy interface { Count() int ShouldResolveIP() bool ShouldFindProcess() bool - OnUpdate(rules []string) + Reset() + Insert(rule string) + FinishInsert() } func RuleProviders() map[string]P.RuleProvider { @@ -75,7 +83,7 @@ func (rp *ruleSetProvider) Update() error { return err } -func (rp *ruleSetProvider) Behavior() P.RuleType { +func (rp *ruleSetProvider) Behavior() P.RuleBehavior { return rp.behavior } @@ -99,6 +107,7 @@ func (rp *ruleSetProvider) MarshalJSON() ([]byte, error) { return json.Marshal( map[string]interface{}{ "behavior": rp.behavior.String(), + "format": rp.format.String(), "name": rp.Name(), "ruleCount": rp.strategy.Count(), "type": rp.Type().String(), @@ -107,20 +116,20 @@ func (rp *ruleSetProvider) MarshalJSON() ([]byte, error) { }) } -func NewRuleSetProvider(name string, behavior P.RuleType, interval time.Duration, vehicle P.Vehicle, +func NewRuleSetProvider(name string, behavior P.RuleBehavior, format P.RuleFormat, interval time.Duration, vehicle P.Vehicle, parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) P.RuleProvider { rp := &ruleSetProvider{ behavior: behavior, + format: format, } onUpdate := func(elm interface{}) { - rulesRaw := elm.([]string) - rp.strategy.OnUpdate(rulesRaw) + strategy := elm.(ruleStrategy) + rp.strategy = strategy } - fetcher := resource.NewFetcher(name, interval, vehicle, rulesParse, onUpdate) - rp.Fetcher = fetcher rp.strategy = newStrategy(behavior, parse) + rp.Fetcher = resource.NewFetcher(name, interval, vehicle, func(bytes []byte) (any, error) { return rulesParse(bytes, newStrategy(behavior, parse), format) }, onUpdate) wrapper := &RuleSetProvider{ rp, @@ -131,7 +140,7 @@ func NewRuleSetProvider(name string, behavior P.RuleType, interval time.Duration return wrapper } -func newStrategy(behavior P.RuleType, parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) ruleStrategy { +func newStrategy(behavior P.RuleBehavior, parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) ruleStrategy { switch behavior { case P.Domain: strategy := NewDomainStrategy() @@ -147,12 +156,94 @@ func newStrategy(behavior P.RuleType, parse func(tp, payload, target string, par } } -func rulesParse(buf []byte) (any, error) { - rulePayload := RulePayload{} - err := yaml.Unmarshal(buf, &rulePayload) - if err != nil { - return nil, err +var ErrNoPayload = errors.New("file must have a `payload` field") + +func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (any, error) { + strategy.Reset() + + 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 { // no head or only one line body + return nil, ErrNoPayload + } + } + var str string + switch format { + case P.TextRule: + firstLineLength = -1 // don't return ErrNoPayload when read last line + 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] + } + } + + if str == "" { + continue + } + + strategy.Insert(str) } - return append(rulePayload.Rules, rulePayload.Rules2...), nil + strategy.FinishInsert() + + return strategy, nil } diff --git a/rules/provider/rule_set.go b/rules/provider/rule_set.go index 45cddf6c..1d940188 100644 --- a/rules/provider/rule_set.go +++ b/rules/provider/rule_set.go @@ -2,9 +2,9 @@ package provider import ( "fmt" - C "github.com/Dreamacro/clash/constant" - P "github.com/Dreamacro/clash/constant/provider" - "github.com/Dreamacro/clash/rules/common" + C "github.com/metacubex/mihomo/constant" + P "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/rules/common" ) type RuleSet struct { diff --git a/test/.golangci.yaml b/test/.golangci.yaml index b65afc5e..e1fbbf76 100644 --- a/test/.golangci.yaml +++ b/test/.golangci.yaml @@ -10,7 +10,7 @@ linters-settings: gci: sections: - standard - - prefix(github.com/Dreamacro/clash) + - prefix(github.com/metacubex/mihomo) - default staticcheck: go: '1.19' diff --git a/test/README.md b/test/README.md index a95f3aea..e9fa5630 100644 --- a/test/README.md +++ b/test/README.md @@ -1,4 +1,4 @@ -## Clash testing suit +## Mihomo testing suit ### Protocol testing suit @@ -51,8 +51,8 @@ $ make test benchmark (Linux) > Cannot represent the throughput of the protocol on your machine -> but you can compare the corresponding throughput of the protocol on clash -> (change chunkSize to measure the maximum throughput of clash on your machine) +> 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 index 3fdca5d0..90ac9d22 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -16,13 +16,13 @@ import ( "testing" "time" - "github.com/Dreamacro/clash/adapter/outbound" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/hub/executor" - "github.com/Dreamacro/clash/transport/socks5" "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" ) @@ -556,7 +556,7 @@ 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", + DstPort: 10001, }) require.NoError(t, err) return conn @@ -565,7 +565,7 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) { assert.NoError(t, testLargeDataWithConn(t, func() net.Conn { conn, err := proxy.DialContext(context.Background(), &C.Metadata{ Host: localIP.String(), - DstPort: "10001", + DstPort: 10001, }) require.NoError(t, err) return conn @@ -578,7 +578,7 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) { pc, err := proxy.ListenPacketContext(context.Background(), &C.Metadata{ NetWork: C.UDP, DstIP: localIP, - DstPort: "10001", + DstPort: 10001, }) require.NoError(t, err) defer pc.Close() @@ -588,7 +588,7 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) { pc, err = proxy.ListenPacketContext(context.Background(), &C.Metadata{ NetWork: C.UDP, DstIP: localIP, - DstPort: "10001", + DstPort: 10001, }) require.NoError(t, err) defer pc.Close() @@ -598,7 +598,7 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) { pc, err = proxy.ListenPacketContext(context.Background(), &C.Metadata{ NetWork: C.UDP, DstIP: localIP, - DstPort: "10001", + DstPort: 10001, }) require.NoError(t, err) defer pc.Close() @@ -635,7 +635,7 @@ func benchmarkProxy(b *testing.B, proxy C.ProxyAdapter) { conn, err := proxy.DialContext(context.Background(), &C.Metadata{ Host: localIP.String(), - DstPort: "10001", + DstPort: 10001, }) require.NoError(b, err) @@ -658,7 +658,7 @@ func benchmarkProxy(b *testing.B, proxy C.ProxyAdapter) { }) } -func TestClash_Basic(t *testing.T) { +func TestMihomo_Basic(t *testing.T) { basic := ` mixed-port: 10000 log-level: silent diff --git a/test/dns_test.go b/test/dns_test.go index 8e30ba98..f45ffbe0 100644 --- a/test/dns_test.go +++ b/test/dns_test.go @@ -21,7 +21,7 @@ func exchange(address, domain string, tp uint16) ([]dns.RR, error) { return r.Answer, nil } -func TestClash_DNS(t *testing.T) { +func TestMihomo_DNS(t *testing.T) { basic := ` log-level: silent dns: @@ -49,11 +49,11 @@ dns: assert.Empty(t, rr) } -func TestClash_DNSHostAndFakeIP(t *testing.T) { +func TestMihomo_DNSHostAndFakeIP(t *testing.T) { basic := ` log-level: silent hosts: - foo.clash.dev: 1.1.1.1 + foo.mihomo.dev: 1.1.1.1 dns: enable: true listen: 0.0.0.0:8553 @@ -81,7 +81,7 @@ dns: {"foo.org", "198.18.0.4"}, {"bar.org", "198.18.0.5"}, {"foo.org", "198.18.0.4"}, - {"foo.clash.dev", "1.1.1.1"}, + {"foo.mihomo.dev", "1.1.1.1"}, } for _, pair := range list { diff --git a/test/go.mod b/test/go.mod index 6cbf50e8..adb42a2c 100644 --- a/test/go.mod +++ b/test/go.mod @@ -1,85 +1,118 @@ -module clash-test +module mihomo-test -go 1.19 +go 1.20 require ( - github.com/Dreamacro/clash v0.0.0 github.com/docker/docker v20.10.21+incompatible github.com/docker/go-connections v0.4.0 - github.com/miekg/dns v1.1.50 - github.com/stretchr/testify v1.8.1 - golang.org/x/net v0.2.1-0.20221117215542-ecf7fda6a59e + github.com/metacubex/mihomo v0.0.0 + github.com/miekg/dns v1.1.56 + github.com/stretchr/testify v1.8.4 + golang.org/x/net v0.17.0 ) -replace github.com/Dreamacro/clash => ../ +replace github.com/metacubex/mihomo => ../ require ( - github.com/Microsoft/go-winio v0.5.1 // indirect + 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/cilium/ebpf v0.9.3 // indirect - github.com/coreos/go-iptables v0.6.0 // indirect - github.com/database64128/tfo-go/v2 v2.0.2 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/cilium/ebpf v0.12.0 // indirect + github.com/coreos/go-iptables v0.7.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.7.0 // indirect - github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect - github.com/gofrs/uuid v4.3.1+incompatible // 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.0 // indirect + github.com/gofrs/uuid/v5 v5.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/mock v1.6.0 // indirect - github.com/google/btree v1.0.1 // indirect + github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect - github.com/gorilla/websocket v1.5.0 // indirect - github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c // indirect - github.com/josharian/native v1.0.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a // indirect + github.com/josharian/native v1.1.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect - github.com/klauspost/cpuid/v2 v2.0.12 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect - github.com/marten-seemann/qpack v0.3.0 // indirect - github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect - github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect - github.com/mdlayher/netlink v1.7.0 // indirect - github.com/mdlayher/socket v0.4.0 // indirect - github.com/metacubex/quic-go v0.31.1-0.20221127023445-9f0ce65a734e // indirect - github.com/metacubex/sing-shadowsocks v0.1.0 // indirect - github.com/metacubex/sing-tun v0.1.0 // indirect - github.com/metacubex/sing-wireguard v0.0.0-20221109114053-16c22adda03c // indirect - github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c // 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.39.1-0.20231019030608-fd969d66f16b // indirect + github.com/metacubex/sing-quic v0.0.0-20231008050747-a684db516966 // 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/onsi/ginkgo/v2 v2.2.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/geoip2-golang v1.8.0 // indirect - github.com/oschwald/maxminddb-golang v1.10.0 // 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/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-20 v0.3.4 // 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.1.0 // indirect - github.com/sagernet/sing-vmess v0.1.0 // indirect - github.com/sagernet/wireguard-go v0.0.0-20221108054404-7c2acadba17c // indirect - github.com/samber/lo v1.35.0 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect - github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect + github.com/sagernet/sing v0.2.14 // indirect + github.com/sagernet/sing-mux v0.1.3 // 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.9 // 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/xtls/go v0.0.0-20220914232946-0441cf4cf837 // indirect - go.etcd.io/bbolt v1.3.6 // indirect - go.uber.org/atomic v1.10.0 // indirect - golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a // indirect - golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.2.1-0.20221110211117-d684c6f88669 // indirect - golang.org/x/text v0.4.0 // indirect - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect - golang.org/x/tools v0.1.12 // indirect - google.golang.org/protobuf v1.28.1 // 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.etcd.io/bbolt v1.3.7 // indirect + go.uber.org/mock v0.3.0 // indirect + go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/sync v0.4.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.4.0 // indirect - gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c // indirect - lukechampine.com/blake3 v1.1.7 // indirect + lukechampine.com/blake3 v1.2.1 // indirect ) diff --git a/test/go.sum b/test/go.sum index 22e24a16..c7524eff 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,276 +1,304 @@ +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.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= -github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +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/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 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/cilium/ebpf v0.9.3 h1:5KtxXZU+scyERvkJMEm16TbScVvuuMrlhPly78ZMbSc= -github.com/cilium/ebpf v0.9.3/go.mod h1:w27N4UjpaQ9X/DGrSugxUG+H+NhgntDuPb5lCzxCn8A= -github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= -github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/database64128/tfo-go/v2 v2.0.2 h1:5rGgkJeLEKlNaqredfrPQNLnctn1b+1fq/8tdKdOzJg= -github.com/database64128/tfo-go/v2 v2.0.2/go.mod h1:FDdt4JaAsRU66wsYHxSVytYimPkKIHupVsxM+5DhvjY= +github.com/cilium/ebpf v0.12.0 h1:oQEuIQIXgYhe1v7sYUG0P9vtJTYZLLdA6tiQmrOB1mo= +github.com/cilium/ebpf v0.12.0/go.mod h1:u9H29/Iq+8cy70YqI6p5pfADkFl3vdnV2qXDg5JL0Zo= +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/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= -github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker 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/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= -github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= -github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +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.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= +github.com/gobwas/ws v1.3.0/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/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= +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-20221001123530-5308ebe5334c h1:OCFM4+DXTWfNlyeoddrTwdup/ztkGSyAMR2UGcPckNQ= -github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= -github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= -github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a h1:S33o3djA1nPRd+d/bf7jbbXytXuK/EoXow7+aa76grQ= +github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a/go.mod h1:zmdm3sTSDP3vOOX3CEWRkkRHtKr1DxBx+J1OQFoDQQs= +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/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= -github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= -github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= -github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 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/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +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.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/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/marten-seemann/qpack v0.3.0 h1:UiWstOgT8+znlkDPOg2+3rIuYXJ2CnGDkGUXN6ki6hE= -github.com/marten-seemann/qpack v0.3.0/go.mod h1:cGfKPBiP4a9EQdxCwEwI/GEeWAsjSekBvx/X8mh58+g= -github.com/marten-seemann/qtls-go1-18 v0.1.3 h1:R4H2Ks8P6pAtUagjFty2p7BVHn3XiwDAl7TTQf5h7TI= -github.com/marten-seemann/qtls-go1-18 v0.1.3/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= -github.com/marten-seemann/qtls-go1-19 v0.1.1 h1:mnbxeq3oEyQxQXwI4ReCgW9DPoPR94sNlqWoDZnjRIE= -github.com/marten-seemann/qtls-go1-19 v0.1.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= -github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= -github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= -github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= -github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= -github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= -github.com/mdlayher/netlink v1.7.0 h1:ZNGI4V7i1fJ94DPYtWhI/R85i/Q7ZxnuhUJQcJMoodI= -github.com/mdlayher/netlink v1.7.0/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ= -github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= -github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= -github.com/metacubex/quic-go v0.31.1-0.20221127023445-9f0ce65a734e h1:RnfC6+sShJ3biU2Q2wuh4FxZ8/3fp1QG+1zAfswVehA= -github.com/metacubex/quic-go v0.31.1-0.20221127023445-9f0ce65a734e/go.mod h1:7NPWVTLiX2Ss9q9gBNZaNHsPqZ3Tg/ApyrXxxUYbl78= -github.com/metacubex/sing-shadowsocks v0.1.0 h1:uGBtNkpy4QFlofaNkJf+iFegeLU11VzTUlkC46FHF8A= -github.com/metacubex/sing-shadowsocks v0.1.0/go.mod h1:8pBSYDKVxTtqUtGZyEh4ZpFJXwP6wBVVKrs6oQiOwmQ= -github.com/metacubex/sing-tun v0.1.0 h1:iQj0+0WjJynSKAtfv87wOZlVKWl3w9RvkOSkVe9zuMg= -github.com/metacubex/sing-tun v0.1.0/go.mod h1:l4JyI6RTrlHLQz5vSakg+wxA+LwGVI0Mz5ZtlOv67dA= -github.com/metacubex/sing-wireguard v0.0.0-20221109114053-16c22adda03c h1:VHtXDny/TNOF7YDT9d9Qkr+x6K1O4cejXLlyPUXDeXQ= -github.com/metacubex/sing-wireguard v0.0.0-20221109114053-16c22adda03c/go.mod h1:fULJ451x1/XlpIhl+Oo+EPGKla9tFZaqT5dKLrZ+NvM= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c h1:RC8WMpjonrBfyAh6VN/POIPtYD5tRAq0qMqCRjQNK+g= -github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c/go.mod h1:9OcmHNQQUTbk4XCffrLgN1NEKc2mh5u++biHVrvHsSU= +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.39.1-0.20231019030608-fd969d66f16b h1:uZ++sW8yg7Fr/Wvmmrb/V+SfxvRs0iMC+2+u2bRmO8g= +github.com/metacubex/quic-go v0.39.1-0.20231019030608-fd969d66f16b/go.mod h1:4pe6cY+nAMFU/Uxn1rfnxNIowsaJGDQ3uyy4VuiPkP4= +github.com/metacubex/sing-quic v0.0.0-20231008050747-a684db516966 h1:wbOsbU3kfD5LRuJIntJwEPmgGSQukof8CgLNypi8az8= +github.com/metacubex/sing-quic v0.0.0-20231008050747-a684db516966/go.mod h1:GU7g2AZesXItk4CspDP8Dc7eGtlA2GVDihyCwsUXRSo= +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.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= +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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= -github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +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/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= -github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= -github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= -github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e h1:5CFRo8FJbCuf5s/eTBdZpmMbn8Fe2eSMLNAYfKanA34= -github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e/go.mod h1:qbt0dWObotCfcjAJJ9AxtFPNSDUfZF+6dCpgKEOBn/g= +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/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= +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.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= +github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 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.0 h1:FGmaP2BVPYO2IyC/3R1DaQa/zr+kOKHRgWqrmOF+Gu8= -github.com/sagernet/sing v0.1.0/go.mod h1:zvgDYKI+vCAW9RyfyrKTgleI+DOa8lzHMPC7VZo3OL4= -github.com/sagernet/sing-vmess v0.1.0 h1:x0tYBJRbVi7zVXpMEW45eApGpXIDs9ub3raglouAKMo= -github.com/sagernet/sing-vmess v0.1.0/go.mod h1:4lwj6EHrUlgRnKhbmtboGbt+wtl5+tHMv96Ez8LZArw= -github.com/sagernet/wireguard-go v0.0.0-20221108054404-7c2acadba17c h1:qP3ZOHnjZalvqbjundbXiv/YrNlo3HOgrKc+S1QGs0U= -github.com/sagernet/wireguard-go v0.0.0-20221108054404-7c2acadba17c/go.mod h1:euOmN6O5kk9dQmgSS8Df4psAl3TCjxOz0NW60EWkSaI= -github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= -github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= +github.com/sagernet/sing v0.2.14 h1:L3AXDh22nsOOYz2nTRU1JvpRsmzViWKI1B8TsQYG1eY= +github.com/sagernet/sing v0.2.14/go.mod h1:AhNEHu0GXrpqkuzvTwvC8+j2cQUU/dh+zLEmq4C99pg= +github.com/sagernet/sing-mux v0.1.3 h1:fAf7PZa2A55mCeh0KKM02f1k2Y4vEmxuZZ/51ahkkLA= +github.com/sagernet/sing-mux v0.1.3/go.mod h1:wGeIeiiFLx4HUM5LAg65wrNZ/X1muOimqK0PEhNbPi0= +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.9 h1:ZI5bWVeu2ep4/DIxB4U9okeYJ7zp/QLTO4auRb/ty/E= +github.com/shirou/gopsutil/v3 v3.23.9/go.mod h1:x/NWSb71eMcjFIO0vhyGW5nZ7oSIgVjrCnADckb85GA= +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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= -github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= -github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= +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/xtls/go v0.0.0-20220914232946-0441cf4cf837 h1:AHhUwwFJGl27E46OpdJHplZkK09m7aETNBNzhT6t15M= -github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837/go.mod h1:YJTRELIWrGxR1s8xcEBgxcxBfwQfMGjdvNLTjN9XFgY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +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.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.uber.org/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.3.1-0.20221117191849-2c476679df9a h1:diz9pEYuTIuLMJLs3rGDkeaTsNyRs6duYdFyPAxzE/U= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4= -golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.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-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/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-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.2.1-0.20221117215542-ecf7fda6a59e h1:IVOjWZQH/57UDcpX19vSmMz8w3ohroOMWohn8qWpRkg= -golang.org/x/net v0.2.1-0.20221117215542-ecf7fda6a59e/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.1-0.20221110211117-d684c6f88669 h1:pvmSpBoSG0gD2LLPAX15QHPig8xsbU0tu1sSAmResqk= -golang.org/x/sys v0.2.1-0.20221110211117-d684c6f88669/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4= -gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= -lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= -lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +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 index ae638e62..e783d9c2 100644 --- a/test/hysteria_test.go +++ b/test/hysteria_test.go @@ -5,13 +5,13 @@ import ( "testing" "time" - "github.com/Dreamacro/clash/adapter/outbound" - C "github.com/Dreamacro/clash/constant" "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 TestClash_Hysteria(t *testing.T) { +func TestMihomo_Hysteria(t *testing.T) { cfg := &container.Config{ Image: ImageHysteria, ExposedPorts: defaultExposedPorts, diff --git a/test/snell_test.go b/test/snell_test.go index ae9ce0c1..311ca7b7 100644 --- a/test/snell_test.go +++ b/test/snell_test.go @@ -5,13 +5,13 @@ import ( "testing" "time" - "github.com/Dreamacro/clash/adapter/outbound" - C "github.com/Dreamacro/clash/constant" "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 TestClash_SnellObfsHTTP(t *testing.T) { +func TestMihomo_SnellObfsHTTP(t *testing.T) { cfg := &container.Config{ Image: ImageSnell, ExposedPorts: defaultExposedPorts, @@ -44,7 +44,7 @@ func TestClash_SnellObfsHTTP(t *testing.T) { testSuit(t, proxy) } -func TestClash_SnellObfsTLS(t *testing.T) { +func TestMihomo_SnellObfsTLS(t *testing.T) { cfg := &container.Config{ Image: ImageSnell, ExposedPorts: defaultExposedPorts, @@ -77,7 +77,7 @@ func TestClash_SnellObfsTLS(t *testing.T) { testSuit(t, proxy) } -func TestClash_Snell(t *testing.T) { +func TestMihomo_Snell(t *testing.T) { cfg := &container.Config{ Image: ImageSnell, ExposedPorts: defaultExposedPorts, @@ -107,7 +107,7 @@ func TestClash_Snell(t *testing.T) { testSuit(t, proxy) } -func TestClash_Snellv3(t *testing.T) { +func TestMihomo_Snellv3(t *testing.T) { cfg := &container.Config{ Image: ImageSnell, ExposedPorts: defaultExposedPorts, diff --git a/test/ss_test.go b/test/ss_test.go index bec1734b..866fe3a8 100644 --- a/test/ss_test.go +++ b/test/ss_test.go @@ -8,13 +8,13 @@ import ( "testing" "time" - "github.com/Dreamacro/clash/adapter/outbound" - C "github.com/Dreamacro/clash/constant" "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 TestClash_Shadowsocks(t *testing.T) { +func TestMihomo_Shadowsocks(t *testing.T) { for _, method := range []string{ "aes-128-ctr", "aes-192-ctr", @@ -30,7 +30,7 @@ func TestClash_Shadowsocks(t *testing.T) { "xchacha20-ietf-poly1305", } { t.Run(method, func(t *testing.T) { - testClash_Shadowsocks(t, method, "FzcLbKs2dY9mhL") + testMihomo_Shadowsocks(t, method, "FzcLbKs2dY9mhL") }) } for _, method := range []string{ @@ -39,17 +39,17 @@ func TestClash_Shadowsocks(t *testing.T) { "chacha20-ietf-poly1305", } { t.Run(method, func(t *testing.T) { - testClash_ShadowsocksRust(t, method, "FzcLbKs2dY9mhL") + testMihomo_ShadowsocksRust(t, method, "FzcLbKs2dY9mhL") }) } } -func TestClash_Shadowsocks2022(t *testing.T) { +func TestMihomo_Shadowsocks2022(t *testing.T) { for _, method := range []string{ "2022-blake3-aes-128-gcm", } { t.Run(method, func(t *testing.T) { - testClash_ShadowsocksRust(t, method, mkKey(16)) + testMihomo_ShadowsocksRust(t, method, mkKey(16)) }) } for _, method := range []string{ @@ -57,7 +57,7 @@ func TestClash_Shadowsocks2022(t *testing.T) { "2022-blake3-chacha20-poly1305", } { t.Run(method, func(t *testing.T) { - testClash_ShadowsocksRust(t, method, mkKey(32)) + testMihomo_ShadowsocksRust(t, method, mkKey(32)) }) } } @@ -68,7 +68,7 @@ func mkKey(bits int) string { return base64.StdEncoding.EncodeToString(k) } -func testClash_Shadowsocks(t *testing.T, method string, password string) { +func testMihomo_Shadowsocks(t *testing.T, method string, password string) { cfg := &container.Config{ Image: ImageShadowsocks, Env: []string{ @@ -102,7 +102,7 @@ func testClash_Shadowsocks(t *testing.T, method string, password string) { testSuit(t, proxy) } -func testClash_ShadowsocksRust(t *testing.T, method string, password string) { +func testMihomo_ShadowsocksRust(t *testing.T, method string, password string) { cfg := &container.Config{ Image: ImageShadowsocksRust, Entrypoint: []string{"ssserver"}, @@ -134,7 +134,7 @@ func testClash_ShadowsocksRust(t *testing.T, method string, password string) { testSuit(t, proxy) } -func TestClash_ShadowsocksObfsHTTP(t *testing.T) { +func TestMihomo_ShadowsocksObfsHTTP(t *testing.T) { cfg := &container.Config{ Image: ImageShadowsocks, Env: []string{ @@ -172,7 +172,7 @@ func TestClash_ShadowsocksObfsHTTP(t *testing.T) { testSuit(t, proxy) } -func TestClash_ShadowsocksObfsTLS(t *testing.T) { +func TestMihomo_ShadowsocksObfsTLS(t *testing.T) { cfg := &container.Config{ Image: ImageShadowsocks, Env: []string{ @@ -210,7 +210,7 @@ func TestClash_ShadowsocksObfsTLS(t *testing.T) { testSuit(t, proxy) } -func TestClash_ShadowsocksV2RayPlugin(t *testing.T) { +func TestMihomo_ShadowsocksV2RayPlugin(t *testing.T) { cfg := &container.Config{ Image: ImageShadowsocks, Env: []string{ @@ -280,7 +280,7 @@ func Benchmark_Shadowsocks(b *testing.B) { benchmarkProxy(b, proxy) } -func TestClash_ShadowsocksUoT(t *testing.T) { +func TestMihomo_ShadowsocksUoT(t *testing.T) { configPath := C.Path.Resolve("xray-shadowsocks.json") cfg := &container.Config{ diff --git a/test/trojan_test.go b/test/trojan_test.go index 4885fd3b..c6b1fea0 100644 --- a/test/trojan_test.go +++ b/test/trojan_test.go @@ -6,13 +6,13 @@ import ( "testing" "time" - "github.com/Dreamacro/clash/adapter/outbound" - C "github.com/Dreamacro/clash/constant" "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 TestClash_Trojan(t *testing.T) { +func TestMihomo_Trojan(t *testing.T) { cfg := &container.Config{ Image: ImageTrojan, ExposedPorts: defaultExposedPorts, @@ -48,7 +48,7 @@ func TestClash_Trojan(t *testing.T) { testSuit(t, proxy) } -func TestClash_TrojanGrpc(t *testing.T) { +func TestMihomo_TrojanGrpc(t *testing.T) { cfg := &container.Config{ Image: ImageXray, ExposedPorts: defaultExposedPorts, @@ -87,7 +87,7 @@ func TestClash_TrojanGrpc(t *testing.T) { testSuit(t, proxy) } -func TestClash_TrojanWebsocket(t *testing.T) { +func TestMihomo_TrojanWebsocket(t *testing.T) { cfg := &container.Config{ Image: ImageTrojanGo, ExposedPorts: defaultExposedPorts, @@ -123,7 +123,7 @@ func TestClash_TrojanWebsocket(t *testing.T) { testSuit(t, proxy) } -func TestClash_TrojanXTLS(t *testing.T) { +func TestMihomo_TrojanXTLS(t *testing.T) { cfg := &container.Config{ Image: ImageXray, ExposedPorts: defaultExposedPorts, diff --git a/test/vless_test.go b/test/vless_test.go index b75fb3ad..d0e6f071 100644 --- a/test/vless_test.go +++ b/test/vless_test.go @@ -5,14 +5,14 @@ import ( "testing" "time" - "github.com/Dreamacro/clash/adapter/outbound" - C "github.com/Dreamacro/clash/constant" "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 TestClash_VlessTLS(t *testing.T) { +func TestMihomo_VlessTLS(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, @@ -51,7 +51,7 @@ func TestClash_VlessTLS(t *testing.T) { } // TODO: fix udp test -func TestClash_VlessXTLS(t *testing.T) { +func TestMihomo_VlessXTLS(t *testing.T) { cfg := &container.Config{ Image: ImageXray, ExposedPorts: defaultExposedPorts, @@ -81,7 +81,6 @@ func TestClash_VlessXTLS(t *testing.T) { ServerName: "example.org", UDP: true, Flow: "xtls-rprx-direct", - FlowShow: true, }) if err != nil { assert.FailNow(t, err.Error()) @@ -92,7 +91,7 @@ func TestClash_VlessXTLS(t *testing.T) { } // TODO: fix udp test -func TestClash_VlessWS(t *testing.T) { +func TestMihomo_VlessWS(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, diff --git a/test/vmess_test.go b/test/vmess_test.go index fd83fff8..80c3d4d8 100644 --- a/test/vmess_test.go +++ b/test/vmess_test.go @@ -5,13 +5,13 @@ import ( "testing" "time" - "github.com/Dreamacro/clash/adapter/outbound" - C "github.com/Dreamacro/clash/constant" "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 TestClash_Vmess(t *testing.T) { +func TestMihomo_Vmess(t *testing.T) { configPath := C.Path.Resolve("vmess.json") cfg := &container.Config{ @@ -44,7 +44,7 @@ func TestClash_Vmess(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessAuthenticatedLength(t *testing.T) { +func TestMihomo_VmessAuthenticatedLength(t *testing.T) { configPath := C.Path.Resolve("vmess.json") cfg := &container.Config{ @@ -78,7 +78,7 @@ func TestClash_VmessAuthenticatedLength(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessPacketAddr(t *testing.T) { +func TestMihomo_VmessPacketAddr(t *testing.T) { configPath := C.Path.Resolve("vmess.json") cfg := &container.Config{ @@ -112,7 +112,7 @@ func TestClash_VmessPacketAddr(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessTLS(t *testing.T) { +func TestMihomo_VmessTLS(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, @@ -149,7 +149,7 @@ func TestClash_VmessTLS(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessHTTP2(t *testing.T) { +func TestMihomo_VmessHTTP2(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, @@ -191,7 +191,7 @@ func TestClash_VmessHTTP2(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessHTTP(t *testing.T) { +func TestMihomo_VmessHTTP(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, @@ -241,7 +241,7 @@ func TestClash_VmessHTTP(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessWebsocket(t *testing.T) { +func TestMihomo_VmessWebsocket(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, @@ -274,7 +274,7 @@ func TestClash_VmessWebsocket(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessWebsocketTLS(t *testing.T) { +func TestMihomo_VmessWebsocketTLS(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, @@ -311,7 +311,7 @@ func TestClash_VmessWebsocketTLS(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessGrpc(t *testing.T) { +func TestMihomo_VmessGrpc(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, @@ -352,7 +352,7 @@ func TestClash_VmessGrpc(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessWebsocket0RTT(t *testing.T) { +func TestMihomo_VmessWebsocket0RTT(t *testing.T) { cfg := &container.Config{ Image: ImageVmess, ExposedPorts: defaultExposedPorts, @@ -390,7 +390,7 @@ func TestClash_VmessWebsocket0RTT(t *testing.T) { testSuit(t, proxy) } -func TestClash_VmessWebsocketXray0RTT(t *testing.T) { +func TestMihomo_VmessWebsocketXray0RTT(t *testing.T) { cfg := &container.Config{ Image: ImageXray, ExposedPorts: defaultExposedPorts, diff --git a/transport/gun/gun.go b/transport/gun/gun.go index 920e7adc..cf986c8e 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -17,10 +17,11 @@ import ( "sync" "time" - "github.com/Dreamacro/clash/common/buf" - "github.com/Dreamacro/clash/common/pool" - tlsC "github.com/Dreamacro/clash/component/tls" - "go.uber.org/atomic" + "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" + "golang.org/x/net/http2" ) @@ -42,7 +43,7 @@ type Conn struct { transport *TransportWrap writer *io.PipeWriter once sync.Once - close *atomic.Bool + close atomic.Bool err error remain int br *bufio.Reader @@ -146,6 +147,7 @@ func (g *Conn) WriteBuffer(buffer *buf.Buffer) error { 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 @@ -189,7 +191,7 @@ func (g *Conn) SetDeadline(t time.Time) error { return nil } -func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string) *TransportWrap { +func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string, realityConfig *tlsC.RealityConfig) *TransportWrap { wrap := TransportWrap{} dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { @@ -197,24 +199,44 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string) *T if err != nil { return nil, err } - wrap.remoteAddr = pconn.RemoteAddr() + if tlsConfig == nil { + return pconn, nil + } + if len(Fingerprint) != 0 { - if fingerprint, exists := tlsC.GetFingerprint(Fingerprint); exists { - utlsConn := tlsC.UClient(pconn, cfg, fingerprint) - if err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx); err != nil { + if realityConfig == nil { + if fingerprint, exists := tlsC.GetFingerprint(Fingerprint); exists { + utlsConn := tlsC.UClient(pconn, cfg, fingerprint) + if err := utlsConn.HandshakeContext(ctx); err != nil { + pconn.Close() + return nil, err + } + state := utlsConn.ConnectionState() + if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { + utlsConn.Close() + return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) + } + return utlsConn, nil + } + } else { + realityConn, err := tlsC.GetRealityConn(ctx, pconn, Fingerprint, cfg, realityConfig) + if err != nil { pconn.Close() return nil, err } - state := utlsConn.(*tlsC.UConn).ConnectionState() - if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { - utlsConn.Close() - return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) - } - return utlsConn, nil + //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 { @@ -274,11 +296,11 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er return conn, nil } -func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config) (net.Conn, error) { +func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config, realityConfig *tlsC.RealityConfig) (net.Conn, error) { dialFn := func(network, addr string) (net.Conn, error) { return conn, nil } - transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint) + transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, realityConfig) return StreamGunWithTransport(transport, cfg) } diff --git a/transport/gun/utils.go b/transport/gun/utils.go index e5f6e019..e4a66315 100644 --- a/transport/gun/utils.go +++ b/transport/gun/utils.go @@ -1,10 +1,26 @@ package gun func UVarintLen(x uint64) int { - i := 0 - for x >= 0x80 { - x >>= 7 - i++ + 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 } - return i + 1 } diff --git a/transport/hysteria/acl/engine.go b/transport/hysteria/acl/engine.go deleted file mode 100644 index adff5690..00000000 --- a/transport/hysteria/acl/engine.go +++ /dev/null @@ -1,100 +0,0 @@ -package acl - -import ( - "github.com/Dreamacro/clash/transport/hysteria/utils" - lru "github.com/hashicorp/golang-lru" - "github.com/oschwald/geoip2-golang" - "net" -) - -const entryCacheSize = 1024 - -type Engine struct { - DefaultAction Action - Entries []Entry - Cache *lru.ARCCache - ResolveIPAddr func(string) (*net.IPAddr, error) - GeoIPReader *geoip2.Reader -} - -type cacheKey struct { - Host string - Port uint16 - IsUDP bool -} - -type cacheValue struct { - Action Action - Arg string -} - -// action, arg, isDomain, resolvedIP, error -func (e *Engine) ResolveAndMatch(host string, port uint16, isUDP bool) (Action, string, bool, *net.IPAddr, error) { - ip, zone := utils.ParseIPZone(host) - if ip == nil { - // Domain - ipAddr, err := e.ResolveIPAddr(host) - if v, ok := e.Cache.Get(cacheKey{host, port, isUDP}); ok { - // Cache hit - ce := v.(cacheValue) - return ce.Action, ce.Arg, true, ipAddr, err - } - for _, entry := range e.Entries { - mReq := MatchRequest{ - Domain: host, - Port: port, - DB: e.GeoIPReader, - } - if ipAddr != nil { - mReq.IP = ipAddr.IP - } - if isUDP { - mReq.Protocol = ProtocolUDP - } else { - mReq.Protocol = ProtocolTCP - } - if entry.Match(mReq) { - e.Cache.Add(cacheKey{host, port, isUDP}, - cacheValue{entry.Action, entry.ActionArg}) - return entry.Action, entry.ActionArg, true, ipAddr, err - } - } - e.Cache.Add(cacheKey{host, port, isUDP}, cacheValue{e.DefaultAction, ""}) - return e.DefaultAction, "", true, ipAddr, err - } else { - // IP - if v, ok := e.Cache.Get(cacheKey{ip.String(), port, isUDP}); ok { - // Cache hit - ce := v.(cacheValue) - return ce.Action, ce.Arg, false, &net.IPAddr{ - IP: ip, - Zone: zone, - }, nil - } - for _, entry := range e.Entries { - mReq := MatchRequest{ - IP: ip, - Port: port, - DB: e.GeoIPReader, - } - if isUDP { - mReq.Protocol = ProtocolUDP - } else { - mReq.Protocol = ProtocolTCP - } - if entry.Match(mReq) { - e.Cache.Add(cacheKey{ip.String(), port, isUDP}, - cacheValue{entry.Action, entry.ActionArg}) - return entry.Action, entry.ActionArg, false, &net.IPAddr{ - IP: ip, - Zone: zone, - }, nil - } - } - e.Cache.Add(cacheKey{ip.String(), port, isUDP}, cacheValue{e.DefaultAction, ""}) - return e.DefaultAction, "", false, &net.IPAddr{ - IP: ip, - Zone: zone, - }, nil - } -} diff --git a/transport/hysteria/acl/engine_test.go b/transport/hysteria/acl/engine_test.go deleted file mode 100644 index 4c30884d..00000000 --- a/transport/hysteria/acl/engine_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package acl - -import ( - "errors" - lru "github.com/hashicorp/golang-lru" - "net" - "strings" - "testing" -) - -func TestEngine_ResolveAndMatch(t *testing.T) { - cache, _ := lru.NewARC(16) - e := &Engine{ - DefaultAction: ActionDirect, - Entries: []Entry{ - { - Action: ActionProxy, - ActionArg: "", - Matcher: &domainMatcher{ - matcherBase: matcherBase{ - Protocol: ProtocolTCP, - Port: 443, - }, - Domain: "google.com", - Suffix: false, - }, - }, - { - Action: ActionHijack, - ActionArg: "good.org", - Matcher: &domainMatcher{ - matcherBase: matcherBase{}, - Domain: "evil.corp", - Suffix: true, - }, - }, - { - Action: ActionProxy, - ActionArg: "", - Matcher: &netMatcher{ - matcherBase: matcherBase{}, - Net: &net.IPNet{ - IP: net.ParseIP("10.0.0.0"), - Mask: net.CIDRMask(8, 32), - }, - }, - }, - { - Action: ActionBlock, - ActionArg: "", - Matcher: &allMatcher{}, - }, - }, - Cache: cache, - ResolveIPAddr: func(s string) (*net.IPAddr, error) { - if strings.Contains(s, "evil.corp") { - return nil, errors.New("resolve error") - } - return net.ResolveIPAddr("ip", s) - }, - } - tests := []struct { - name string - host string - port uint16 - isUDP bool - wantAction Action - wantArg string - wantErr bool - }{ - { - name: "domain proxy", - host: "google.com", - port: 443, - isUDP: false, - wantAction: ActionProxy, - wantArg: "", - }, - { - name: "domain block", - host: "google.com", - port: 80, - isUDP: false, - wantAction: ActionBlock, - wantArg: "", - }, - { - name: "domain suffix 1", - host: "evil.corp", - port: 8899, - isUDP: true, - wantAction: ActionHijack, - wantArg: "good.org", - wantErr: true, - }, - { - name: "domain suffix 2", - host: "notevil.corp", - port: 22, - isUDP: false, - wantAction: ActionBlock, - wantArg: "", - wantErr: true, - }, - { - name: "domain suffix 3", - host: "im.real.evil.corp", - port: 443, - isUDP: true, - wantAction: ActionHijack, - wantArg: "good.org", - wantErr: true, - }, - { - name: "ip match", - host: "10.2.3.4", - port: 80, - isUDP: false, - wantAction: ActionProxy, - wantArg: "", - }, - { - name: "ip mismatch", - host: "100.5.6.0", - port: 1234, - isUDP: false, - wantAction: ActionBlock, - wantArg: "", - }, - { - name: "domain proxy cache", - host: "google.com", - port: 443, - isUDP: false, - wantAction: ActionProxy, - wantArg: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotAction, gotArg, _, _, err := e.ResolveAndMatch(tt.host, tt.port, tt.isUDP) - if (err != nil) != tt.wantErr { - t.Errorf("ResolveAndMatch() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotAction != tt.wantAction { - t.Errorf("ResolveAndMatch() gotAction = %v, wantAction %v", gotAction, tt.wantAction) - } - if gotArg != tt.wantArg { - t.Errorf("ResolveAndMatch() gotArg = %v, wantAction %v", gotArg, tt.wantArg) - } - }) - } -} diff --git a/transport/hysteria/acl/entry.go b/transport/hysteria/acl/entry.go deleted file mode 100644 index bc345aa1..00000000 --- a/transport/hysteria/acl/entry.go +++ /dev/null @@ -1,331 +0,0 @@ -package acl - -import ( - "errors" - "fmt" - "github.com/oschwald/geoip2-golang" - "net" - "strconv" - "strings" -) - -type Action byte -type Protocol byte - -const ( - ActionDirect = Action(iota) - ActionProxy - ActionBlock - ActionHijack -) - -const ( - ProtocolAll = Protocol(iota) - ProtocolTCP - ProtocolUDP -) - -var protocolPortAliases = map[string]string{ - "echo": "*/7", - "ftp-data": "*/20", - "ftp": "*/21", - "ssh": "*/22", - "telnet": "*/23", - "domain": "*/53", - "dns": "*/53", - "http": "*/80", - "sftp": "*/115", - "ntp": "*/123", - "https": "*/443", - "quic": "udp/443", - "socks": "*/1080", -} - -type Entry struct { - Action Action - ActionArg string - Matcher Matcher -} - -type MatchRequest struct { - IP net.IP - Domain string - - Protocol Protocol - Port uint16 - - DB *geoip2.Reader -} - -type Matcher interface { - Match(MatchRequest) bool -} - -type matcherBase struct { - Protocol Protocol - Port uint16 // 0 for all ports -} - -func (m *matcherBase) MatchProtocolPort(p Protocol, port uint16) bool { - return (m.Protocol == ProtocolAll || m.Protocol == p) && (m.Port == 0 || m.Port == port) -} - -func parseProtocolPort(s string) (Protocol, uint16, error) { - if protocolPortAliases[s] != "" { - s = protocolPortAliases[s] - } - if len(s) == 0 || s == "*" { - return ProtocolAll, 0, nil - } - parts := strings.Split(s, "/") - if len(parts) != 2 { - return ProtocolAll, 0, errors.New("invalid protocol/port syntax") - } - protocol := ProtocolAll - switch parts[0] { - case "tcp": - protocol = ProtocolTCP - case "udp": - protocol = ProtocolUDP - case "*": - protocol = ProtocolAll - default: - return ProtocolAll, 0, errors.New("invalid protocol") - } - if parts[1] == "*" { - return protocol, 0, nil - } - port, err := strconv.ParseUint(parts[1], 10, 16) - if err != nil { - return ProtocolAll, 0, errors.New("invalid port") - } - return protocol, uint16(port), nil -} - -type netMatcher struct { - matcherBase - Net *net.IPNet -} - -func (m *netMatcher) Match(r MatchRequest) bool { - if r.IP == nil { - return false - } - return m.Net.Contains(r.IP) && m.MatchProtocolPort(r.Protocol, r.Port) -} - -type domainMatcher struct { - matcherBase - Domain string - Suffix bool -} - -func (m *domainMatcher) Match(r MatchRequest) bool { - if len(r.Domain) == 0 { - return false - } - domain := strings.ToLower(r.Domain) - return (m.Domain == domain || (m.Suffix && strings.HasSuffix(domain, "."+m.Domain))) && - m.MatchProtocolPort(r.Protocol, r.Port) -} - -type countryMatcher struct { - matcherBase - Country string // ISO 3166-1 alpha-2 country code, upper case -} - -func (m *countryMatcher) Match(r MatchRequest) bool { - if r.IP == nil || r.DB == nil { - return false - } - c, err := r.DB.Country(r.IP) - if err != nil { - return false - } - return c.Country.IsoCode == m.Country && m.MatchProtocolPort(r.Protocol, r.Port) -} - -type allMatcher struct { - matcherBase -} - -func (m *allMatcher) Match(r MatchRequest) bool { - return m.MatchProtocolPort(r.Protocol, r.Port) -} - -func (e Entry) Match(r MatchRequest) bool { - return e.Matcher.Match(r) -} - -func ParseEntry(s string) (Entry, error) { - fields := strings.Fields(s) - if len(fields) < 2 { - return Entry{}, fmt.Errorf("expected at least 2 fields, got %d", len(fields)) - } - e := Entry{} - action := fields[0] - conds := fields[1:] - switch strings.ToLower(action) { - case "direct": - e.Action = ActionDirect - case "proxy": - e.Action = ActionProxy - case "block": - e.Action = ActionBlock - case "hijack": - if len(conds) < 2 { - return Entry{}, fmt.Errorf("hijack requires at least 3 fields, got %d", len(fields)) - } - e.Action = ActionHijack - e.ActionArg = conds[len(conds)-1] - conds = conds[:len(conds)-1] - default: - return Entry{}, fmt.Errorf("invalid action %s", fields[0]) - } - m, err := condsToMatcher(conds) - if err != nil { - return Entry{}, err - } - e.Matcher = m - return e, nil -} - -func condsToMatcher(conds []string) (Matcher, error) { - if len(conds) < 1 { - return nil, errors.New("no condition specified") - } - typ, args := conds[0], conds[1:] - switch strings.ToLower(typ) { - case "domain": - // domain - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for domain: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - return &domainMatcher{ - matcherBase: mb, - Domain: args[0], - Suffix: false, - }, nil - case "domain-suffix": - // domain-suffix - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for domain-suffix: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - return &domainMatcher{ - matcherBase: mb, - Domain: args[0], - Suffix: true, - }, nil - case "cidr": - // cidr - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for cidr: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - _, ipNet, err := net.ParseCIDR(args[0]) - if err != nil { - return nil, err - } - return &netMatcher{ - matcherBase: mb, - Net: ipNet, - }, nil - case "ip": - // ip - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for ip: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - ip := net.ParseIP(args[0]) - if ip == nil { - return nil, fmt.Errorf("invalid ip: %s", args[0]) - } - var ipNet *net.IPNet - if ip.To4() != nil { - ipNet = &net.IPNet{ - IP: ip, - Mask: net.CIDRMask(32, 32), - } - } else { - ipNet = &net.IPNet{ - IP: ip, - Mask: net.CIDRMask(128, 128), - } - } - return &netMatcher{ - matcherBase: mb, - Net: ipNet, - }, nil - case "country": - // country - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for country: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - return &countryMatcher{ - matcherBase: mb, - Country: strings.ToUpper(args[0]), - }, nil - case "all": - // all - if len(args) > 1 { - return nil, fmt.Errorf("invalid number of arguments for all: %d, expected 0 or 1", len(args)) - } - mb := matcherBase{} - if len(args) == 1 { - protocol, port, err := parseProtocolPort(args[0]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - return &allMatcher{ - matcherBase: mb, - }, nil - default: - return nil, fmt.Errorf("invalid condition type: %s", typ) - } -} diff --git a/transport/hysteria/acl/entry_test.go b/transport/hysteria/acl/entry_test.go deleted file mode 100644 index 37b88071..00000000 --- a/transport/hysteria/acl/entry_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package acl - -import ( - "net" - "reflect" - "testing" -) - -func TestParseEntry(t *testing.T) { - _, ok3net, _ := net.ParseCIDR("8.8.8.0/24") - - type args struct { - s string - } - tests := []struct { - name string - args args - want Entry - wantErr bool - }{ - {name: "empty", args: args{""}, want: Entry{}, wantErr: true}, - {name: "ok 1", args: args{"direct domain-suffix google.com"}, - want: Entry{ActionDirect, "", &domainMatcher{ - matcherBase: matcherBase{}, - Domain: "google.com", - Suffix: true, - }}, - wantErr: false}, - {name: "ok 2", args: args{"proxy domain shithole"}, - want: Entry{ActionProxy, "", &domainMatcher{ - matcherBase: matcherBase{}, - Domain: "shithole", - Suffix: false, - }}, - wantErr: false}, - {name: "ok 3", args: args{"block cidr 8.8.8.0/24 */53"}, - want: Entry{ActionBlock, "", &netMatcher{ - matcherBase: matcherBase{ProtocolAll, 53}, - Net: ok3net, - }}, - wantErr: false}, - {name: "ok 4", args: args{"hijack all udp/* udpblackhole.net"}, - want: Entry{ActionHijack, "udpblackhole.net", &allMatcher{ - matcherBase: matcherBase{ProtocolUDP, 0}, - }}, - wantErr: false}, - {name: "err 1", args: args{"what the heck"}, - want: Entry{}, - wantErr: true}, - {name: "err 2", args: args{"proxy sucks ass"}, - want: Entry{}, - wantErr: true}, - {name: "err 3", args: args{"block ip 999.999.999.999"}, - want: Entry{}, - wantErr: true}, - {name: "err 4", args: args{"hijack domain google.com"}, - want: Entry{}, - wantErr: true}, - {name: "err 5", args: args{"hijack domain google.com bing.com 123"}, - want: Entry{}, - wantErr: true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ParseEntry(tt.args.s) - if (err != nil) != tt.wantErr { - t.Errorf("ParseEntry() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ParseEntry() got = %v, wantAction %v", got, tt.want) - } - }) - } -} diff --git a/transport/hysteria/congestion/brutal.go b/transport/hysteria/congestion/brutal.go index 8f02ef14..601949de 100644 --- a/transport/hysteria/congestion/brutal.go +++ b/transport/hysteria/congestion/brutal.go @@ -8,11 +8,13 @@ import ( const ( initMaxDatagramSize = 1252 - pktInfoSlotCount = 4 + 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 @@ -49,8 +51,8 @@ func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Ti return b.pacer.TimeUntilSend() } -func (b *BrutalSender) HasPacingBudget() bool { - return b.pacer.Budget(time.Now()) >= b.maxDatagramSize +func (b *BrutalSender) HasPacingBudget(now time.Time) bool { + return b.pacer.Budget(now) >= b.maxDatagramSize } func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool { @@ -72,30 +74,25 @@ func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion 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].AckCount++ + 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 = 1 - b.pktInfoSlots[slot].LossCount = 0 - } - b.updateAckRate(currentTimestamp) -} - -func (b *BrutalSender) OnPacketLost(number congestion.PacketNumber, lostBytes congestion.ByteCount, - priorInFlight congestion.ByteCount) { - currentTimestamp := time.Now().Unix() - slot := currentTimestamp % pktInfoSlotCount - if b.pktInfoSlots[slot].Timestamp == currentTimestamp { - b.pktInfoSlots[slot].LossCount++ - } else { - // uninitialized slot or too old, reset - b.pktInfoSlots[slot].Timestamp = currentTimestamp - b.pktInfoSlots[slot].AckCount = 0 - b.pktInfoSlots[slot].LossCount = 1 + b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets)) + b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets)) } b.updateAckRate(currentTimestamp) } @@ -117,10 +114,12 @@ func (b *BrutalSender) updateAckRate(currentTimestamp int64) { } if ackCount+lossCount < minSampleCount { b.ackRate = 1 + return } rate := float64(ackCount) / float64(ackCount+lossCount) if rate < minAckRate { b.ackRate = minAckRate + return } b.ackRate = rate } diff --git a/transport/hysteria/conns/faketcp/obfs.go b/transport/hysteria/conns/faketcp/obfs.go index 35f7d013..cf58e569 100644 --- a/transport/hysteria/conns/faketcp/obfs.go +++ b/transport/hysteria/conns/faketcp/obfs.go @@ -1,7 +1,7 @@ package faketcp import ( - "github.com/Dreamacro/clash/transport/hysteria/obfs" + "github.com/metacubex/mihomo/transport/hysteria/obfs" "net" "sync" "syscall" diff --git a/transport/hysteria/conns/faketcp/tcp_linux.go b/transport/hysteria/conns/faketcp/tcp_linux.go index dadb0912..2aaaf139 100644 --- a/transport/hysteria/conns/faketcp/tcp_linux.go +++ b/transport/hysteria/conns/faketcp/tcp_linux.go @@ -1,5 +1,5 @@ -//go:build linux -// +build linux +//go:build linux && !no_fake_tcp +// +build linux,!no_fake_tcp package faketcp @@ -17,8 +17,10 @@ import ( "time" "github.com/coreos/go-iptables/iptables" - "github.com/google/gopacket" - "github.com/google/gopacket/layers" + "github.com/metacubex/gopacket" + "github.com/metacubex/gopacket/layers" + + "github.com/metacubex/mihomo/component/dialer" ) var ( @@ -392,21 +394,35 @@ func (conn *TCPConn) SyscallConn() (syscall.RawConn, error) { // 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 + if ifaceName := dialer.DefaultInterface.Load(); len(ifaceName) > 0 { + rAddrPort := raddr.AddrPort() + 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", nil, &net.IPAddr{IP: raddr.IP}) + 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, nil, raddr) + tcpconn, err := net.DialTCP(network, lTcpAddr, raddr) if err != nil { return nil, err } @@ -464,6 +480,8 @@ func Dial(network, address string) (*TCPConn, error) { // 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) diff --git a/transport/hysteria/conns/faketcp/tcp_stub.go b/transport/hysteria/conns/faketcp/tcp_stub.go index 9bc55077..9f9ff97d 100644 --- a/transport/hysteria/conns/faketcp/tcp_stub.go +++ b/transport/hysteria/conns/faketcp/tcp_stub.go @@ -1,5 +1,5 @@ -//go:build !linux -// +build !linux +//go:build !linux || no_fake_tcp +// +build !linux no_fake_tcp package faketcp diff --git a/transport/hysteria/conns/udp/hop.go b/transport/hysteria/conns/udp/hop.go index 53830ae4..eb0732f0 100644 --- a/transport/hysteria/conns/udp/hop.go +++ b/transport/hysteria/conns/udp/hop.go @@ -2,7 +2,6 @@ package udp import ( "errors" - "math/rand" "net" "strconv" "strings" @@ -10,8 +9,10 @@ import ( "syscall" "time" - "github.com/Dreamacro/clash/transport/hysteria/obfs" - "github.com/Dreamacro/clash/transport/hysteria/utils" + "github.com/metacubex/mihomo/transport/hysteria/obfs" + "github.com/metacubex/mihomo/transport/hysteria/utils" + + "github.com/zhangyunhao116/fastrand" ) const ( @@ -85,7 +86,7 @@ func NewObfsUDPHopClientPacketConn(server string, serverPorts string, hopInterva serverAddrs: serverAddrs, hopInterval: hopInterval, obfs: obfs, - addrIndex: rand.Intn(len(serverAddrs)), + addrIndex: fastrand.Intn(len(serverAddrs)), recvQueue: make(chan *udpPacket, packetQueueSize), closeChan: make(chan struct{}), bufPool: sync.Pool{ @@ -176,7 +177,7 @@ func (c *ObfsUDPHopClientPacketConn) hop(dialer utils.PacketDialer, rAddr net.Ad _ = trySetPacketConnWriteBuffer(c.currentConn, c.writeBufferSize) } go c.recvRoutine(c.currentConn) - c.addrIndex = rand.Intn(len(c.serverAddrs)) + c.addrIndex = fastrand.Intn(len(c.serverAddrs)) } func (c *ObfsUDPHopClientPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { diff --git a/transport/hysteria/conns/udp/obfs.go b/transport/hysteria/conns/udp/obfs.go index d63034b5..a5c6c06c 100644 --- a/transport/hysteria/conns/udp/obfs.go +++ b/transport/hysteria/conns/udp/obfs.go @@ -1,7 +1,7 @@ package udp import ( - "github.com/Dreamacro/clash/transport/hysteria/obfs" + "github.com/metacubex/mihomo/transport/hysteria/obfs" "net" "sync" "time" diff --git a/transport/hysteria/conns/wechat/obfs.go b/transport/hysteria/conns/wechat/obfs.go index 815aa52f..4266d268 100644 --- a/transport/hysteria/conns/wechat/obfs.go +++ b/transport/hysteria/conns/wechat/obfs.go @@ -2,12 +2,14 @@ package wechat import ( "encoding/binary" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/hysteria/obfs" - "math/rand" "net" "sync" "time" + + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/transport/hysteria/obfs" + + "github.com/zhangyunhao116/fastrand" ) const udpBufferSize = 65535 @@ -29,7 +31,7 @@ func NewObfsWeChatUDPConn(orig net.PacketConn, obfs obfs.Obfuscator) *ObfsWeChat obfs: obfs, readBuf: make([]byte, udpBufferSize), writeBuf: make([]byte, udpBufferSize), - sn: rand.Uint32() & 0xFFFF, + sn: fastrand.Uint32() & 0xFFFF, } } diff --git a/transport/hysteria/core/client.go b/transport/hysteria/core/client.go index 1df14242..b556c70d 100644 --- a/transport/hysteria/core/client.go +++ b/transport/hysteria/core/client.go @@ -6,20 +6,20 @@ import ( "crypto/tls" "errors" "fmt" - "math/rand" "net" "strconv" "sync" "time" + "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/Dreamacro/clash/transport/hysteria/obfs" - "github.com/Dreamacro/clash/transport/hysteria/pmtud_fix" - "github.com/Dreamacro/clash/transport/hysteria/transport" - "github.com/Dreamacro/clash/transport/hysteria/utils" + "github.com/zhangyunhao116/fastrand" ) var ( @@ -135,7 +135,7 @@ func (c *Client) handleControlStream(qs quic.Connection, stream quic.Stream) (bo func (c *Client) handleMessage(qs quic.Connection) { for { - msg, err := qs.ReceiveMessage() + msg, err := qs.ReceiveMessage(context.Background()) if err != nil { break } @@ -194,11 +194,7 @@ func (c *Client) openStreamWithReconnect(dialer utils.PacketDialer) (quic.Connec return c.quicSession, &wrappedQUICStream{stream}, err } -func (c *Client) DialTCP(addr string, dialer utils.PacketDialer) (net.Conn, error) { - host, port, err := utils.SplitHostPort(addr) - if err != nil { - return nil, 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 @@ -408,7 +404,7 @@ func (c *quicPktConn) WriteTo(p []byte, addr string) error { if err != nil { if errSize, ok := err.(quic.ErrMessageTooLarge); ok { // need to frag - msg.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 + msg.MsgID = uint16(fastrand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 fragMsgs := fragUDPMessage(msg, int(errSize)) for _, fragMsg := range fragMsgs { msgBuf.Reset() diff --git a/transport/hysteria/obfs/xplus.go b/transport/hysteria/obfs/xplus.go index dd636452..171bf281 100644 --- a/transport/hysteria/obfs/xplus.go +++ b/transport/hysteria/obfs/xplus.go @@ -2,9 +2,8 @@ package obfs import ( "crypto/sha256" - "math/rand" - "sync" - "time" + + "github.com/zhangyunhao116/fastrand" ) // [salt][obfuscated payload] @@ -12,16 +11,12 @@ import ( const saltLen = 16 type XPlusObfuscator struct { - Key []byte - RandSrc *rand.Rand - - lk sync.Mutex + Key []byte } func NewXPlusObfuscator(key []byte) *XPlusObfuscator { return &XPlusObfuscator{ - Key: key, - RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), + Key: key, } } @@ -40,9 +35,7 @@ func (x *XPlusObfuscator) Deobfuscate(in []byte, out []byte) int { } func (x *XPlusObfuscator) Obfuscate(in []byte, out []byte) int { - x.lk.Lock() - _, _ = x.RandSrc.Read(out[:saltLen]) // salt - x.lk.Unlock() + _, _ = fastrand.Read(out[:saltLen]) // salt // Obfuscate the payload key := sha256.Sum256(append(x.Key, out[:saltLen]...)) for i, c := range in { diff --git a/transport/hysteria/pmtud_fix/avail.go b/transport/hysteria/pmtud_fix/avail.go index 2f2bce83..af248f5c 100644 --- a/transport/hysteria/pmtud_fix/avail.go +++ b/transport/hysteria/pmtud_fix/avail.go @@ -1,5 +1,4 @@ -//go:build linux || windows -// +build linux windows +//go:build linux || windows || darwin package pmtud_fix diff --git a/transport/hysteria/pmtud_fix/unavail.go b/transport/hysteria/pmtud_fix/unavail.go index 0eeb83df..35b849d2 100644 --- a/transport/hysteria/pmtud_fix/unavail.go +++ b/transport/hysteria/pmtud_fix/unavail.go @@ -1,5 +1,4 @@ -//go:build !linux && !windows -// +build !linux,!windows +//go:build !linux && !windows && !darwin package pmtud_fix diff --git a/transport/hysteria/transport/client.go b/transport/hysteria/transport/client.go index e65e5016..f5cc9f07 100644 --- a/transport/hysteria/transport/client.go +++ b/transport/hysteria/transport/client.go @@ -9,11 +9,11 @@ import ( "github.com/metacubex/quic-go" - "github.com/Dreamacro/clash/transport/hysteria/conns/faketcp" - "github.com/Dreamacro/clash/transport/hysteria/conns/udp" - "github.com/Dreamacro/clash/transport/hysteria/conns/wechat" - obfsPkg "github.com/Dreamacro/clash/transport/hysteria/obfs" - "github.com/Dreamacro/clash/transport/hysteria/utils" + "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" ) type ClientTransport struct { @@ -76,7 +76,10 @@ func (ct *ClientTransport) QUICDial(proto string, server string, serverPorts str return nil, err } - qs, err := quic.DialContext(dialer.Context(), pktConn, serverUDPAddr, server, tlsConfig, quicConfig) + 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 diff --git a/transport/restls/restls.go b/transport/restls/restls.go new file mode 100644 index 00000000..0f3ba8ac --- /dev/null +++ b/transport/restls/restls.go @@ -0,0 +1,39 @@ +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() +} + +// NewRestls return a Restls Connection +func NewRestls(ctx context.Context, conn net.Conn, config *tls.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/core/cipher.go b/transport/shadowsocks/core/cipher.go index 7f4f7f71..44b2e8d4 100644 --- a/transport/shadowsocks/core/cipher.go +++ b/transport/shadowsocks/core/cipher.go @@ -7,8 +7,9 @@ import ( "sort" "strings" - "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" - "github.com/Dreamacro/clash/transport/shadowsocks/shadowstream" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" + "github.com/metacubex/mihomo/transport/shadowsocks/shadowstream" ) type Cipher interface { @@ -21,7 +22,7 @@ type StreamConnCipher interface { } type PacketConnCipher interface { - PacketConn(net.PacketConn) net.PacketConn + PacketConn(N.EnhancePacketConn) N.EnhancePacketConn } // ErrCipherNotSupported occurs when a cipher is not supported (likely because of security concerns). @@ -128,7 +129,7 @@ type AeadCipher struct { } func (aead *AeadCipher) StreamConn(c net.Conn) net.Conn { return shadowaead.NewConn(c, aead) } -func (aead *AeadCipher) PacketConn(c net.PacketConn) net.PacketConn { +func (aead *AeadCipher) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { return shadowaead.NewPacketConn(c, aead) } @@ -139,7 +140,7 @@ type StreamCipher struct { } func (ciph *StreamCipher) StreamConn(c net.Conn) net.Conn { return shadowstream.NewConn(c, ciph) } -func (ciph *StreamCipher) PacketConn(c net.PacketConn) net.PacketConn { +func (ciph *StreamCipher) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { return shadowstream.NewPacketConn(c, ciph) } @@ -147,8 +148,8 @@ func (ciph *StreamCipher) PacketConn(c net.PacketConn) net.PacketConn { type dummy struct{} -func (dummy) StreamConn(c net.Conn) net.Conn { return c } -func (dummy) PacketConn(c net.PacketConn) net.PacketConn { return c } +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 { diff --git a/transport/shadowsocks/shadowaead/packet.go b/transport/shadowsocks/shadowaead/packet.go index 7043ead7..f9d21ec7 100644 --- a/transport/shadowsocks/shadowaead/packet.go +++ b/transport/shadowsocks/shadowaead/packet.go @@ -6,7 +6,8 @@ import ( "io" "net" - "github.com/Dreamacro/clash/common/pool" + 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. @@ -57,15 +58,15 @@ func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) { } type PacketConn struct { - net.PacketConn + N.EnhancePacketConn Cipher } const maxPacketSize = 64 * 1024 -// NewPacketConn wraps a net.PacketConn with cipher -func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn { - return &PacketConn{PacketConn: c, Cipher: ciph} +// 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. @@ -76,13 +77,13 @@ func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { if err != nil { return 0, err } - _, err = c.PacketConn.WriteTo(buf, addr) + _, 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.PacketConn.ReadFrom(b) + n, addr, err := c.EnhancePacketConn.ReadFrom(b) if err != nil { return n, addr, err } @@ -93,3 +94,20 @@ func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { 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 index e92bddab..de0993b2 100644 --- a/transport/shadowsocks/shadowaead/stream.go +++ b/transport/shadowsocks/shadowaead/stream.go @@ -7,7 +7,7 @@ import ( "io" "net" - "github.com/Dreamacro/clash/common/pool" + "github.com/metacubex/mihomo/common/pool" ) const ( diff --git a/transport/shadowsocks/shadowstream/packet.go b/transport/shadowsocks/shadowstream/packet.go index 0b46dea1..39d09a70 100644 --- a/transport/shadowsocks/shadowstream/packet.go +++ b/transport/shadowsocks/shadowstream/packet.go @@ -6,7 +6,8 @@ import ( "io" "net" - "github.com/Dreamacro/clash/common/pool" + 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. @@ -43,13 +44,13 @@ func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) { } type PacketConn struct { - net.PacketConn + N.EnhancePacketConn Cipher } -// NewPacketConn wraps a net.PacketConn with stream cipher encryption/decryption. -func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn { - return &PacketConn{PacketConn: c, Cipher: ciph} +// 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 @@ -61,12 +62,12 @@ func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { if err != nil { return 0, err } - _, err = c.PacketConn.WriteTo(buf, addr) + _, err = c.EnhancePacketConn.WriteTo(buf, addr) return len(b), err } func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { - n, addr, err := c.PacketConn.ReadFrom(b) + n, addr, err := c.EnhancePacketConn.ReadFrom(b) if err != nil { return n, addr, err } @@ -77,3 +78,20 @@ func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { 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.IVSize():], data, c) + if err != nil { + if put != nil { + put() + } + data = nil + put = nil + return + } + return +} diff --git a/transport/shadowtls/shadowtls.go b/transport/shadowtls/shadowtls.go index 2c0c5946..a0a3d7fb 100644 --- a/transport/shadowtls/shadowtls.go +++ b/transport/shadowtls/shadowtls.go @@ -11,8 +11,8 @@ import ( "io" "net" - "github.com/Dreamacro/clash/common/pool" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/common/pool" + C "github.com/metacubex/mihomo/constant" ) const ( diff --git a/transport/simple-obfs/http.go b/transport/simple-obfs/http.go index a06bad23..681c1864 100644 --- a/transport/simple-obfs/http.go +++ b/transport/simple-obfs/http.go @@ -5,11 +5,12 @@ import ( "encoding/base64" "fmt" "io" - "math/rand" "net" "net/http" - "github.com/Dreamacro/clash/common/pool" + "github.com/metacubex/mihomo/common/pool" + + "github.com/zhangyunhao116/fastrand" ) // HTTPObfs is shadowsocks http simple-obfs implementation @@ -63,9 +64,9 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) { func (ho *HTTPObfs) Write(b []byte) (int, error) { if ho.firstRequest { randBytes := make([]byte, 16) - rand.Read(randBytes) + fastrand.Read(randBytes) req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:])) - req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2)) + req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", fastrand.Int()%54, fastrand.Int()%2)) req.Header.Set("Upgrade", "websocket") req.Header.Set("Connection", "Upgrade") req.Host = ho.host diff --git a/transport/simple-obfs/tls.go b/transport/simple-obfs/tls.go index fed8a483..78317f0a 100644 --- a/transport/simple-obfs/tls.go +++ b/transport/simple-obfs/tls.go @@ -4,16 +4,13 @@ import ( "bytes" "encoding/binary" "io" - "math/rand" "net" "time" - "github.com/Dreamacro/clash/common/pool" -) + "github.com/metacubex/mihomo/common/pool" -func init() { - rand.Seed(time.Now().Unix()) -} + "github.com/zhangyunhao116/fastrand" +) const ( chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024 @@ -31,10 +28,10 @@ type TLSObfs struct { 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 } - pool.Put(buf) sizeBuf := make([]byte, 2) _, err = io.ReadFull(to.Conn, sizeBuf) @@ -130,8 +127,8 @@ func NewTLSObfs(conn net.Conn, server string) net.Conn { func makeClientHelloMsg(data []byte, server string) []byte { random := make([]byte, 28) sessionID := make([]byte, 32) - rand.Read(random) - rand.Read(sessionID) + fastrand.Read(random) + fastrand.Read(sessionID) buf := &bytes.Buffer{} diff --git a/transport/sing-shadowtls/shadowtls.go b/transport/sing-shadowtls/shadowtls.go new file mode 100644 index 00000000..982d847a --- /dev/null +++ b/transport/sing-shadowtls/shadowtls.go @@ -0,0 +1,89 @@ +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/sagernet/sing-shadowtls" + sing_common "github.com/sagernet/sing/common" + utls "github.com/sagernet/utls" +) + +const ( + Mode string = "shadow-tls" +) + +var ( + DefaultALPN = []string{"h2", "http/1.1"} +) + +type ShadowTLSOption struct { + Password string + Host string + Fingerprint string + ClientFingerprint string + SkipCertVerify bool + Version int +} + +func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) (net.Conn, error) { + tlsConfig := &tls.Config{ + NextProtos: DefaultALPN, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: option.SkipCertVerify, + ServerName: option.Host, + } + + var err error + tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + if err != nil { + return nil, err + } + + tlsHandshake := shadowtls.DefaultTLSHandshakeFunc(option.Password, tlsConfig) + if len(option.ClientFingerprint) != 0 { + if fingerprint, exists := tlsC.GetFingerprint(option.ClientFingerprint); exists { + tlsHandshake = uTLSHandshakeFunc(tlsConfig, *fingerprint.ClientHelloID) + } + } + 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, clientHelloID utls.ClientHelloID) shadowtls.TLSHandshakeFunc { + return func(ctx context.Context, conn net.Conn, sessionIDGenerator shadowtls.TLSSessionIDGeneratorFunc) error { + tlsConfig := &utls.Config{ + Rand: config.Rand, + Time: config.Time, + 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: sing_common.Map(config.CurvePreferences, func(it tls.CurveID) utls.CurveID { + return utls.CurveID(it) + }), + SessionTicketsDisabled: config.SessionTicketsDisabled, + Renegotiation: utls.RenegotiationSupport(config.Renegotiation), + SessionIDGenerator: sessionIDGenerator, + } + tlsConn := utls.UClient(conn, tlsConfig, clientHelloID) + return tlsConn.HandshakeContext(ctx) + } +} diff --git a/transport/snell/cipher.go b/transport/snell/cipher.go index 24999e28..e18ce510 100644 --- a/transport/snell/cipher.go +++ b/transport/snell/cipher.go @@ -4,7 +4,7 @@ import ( "crypto/aes" "crypto/cipher" - "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" "golang.org/x/crypto/argon2" "golang.org/x/crypto/chacha20poly1305" diff --git a/transport/snell/pool.go b/transport/snell/pool.go index 2a233a75..cc097df7 100644 --- a/transport/snell/pool.go +++ b/transport/snell/pool.go @@ -5,8 +5,8 @@ import ( "net" "time" - "github.com/Dreamacro/clash/component/pool" - "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" + "github.com/metacubex/mihomo/component/pool" + "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" ) type Pool struct { @@ -61,7 +61,7 @@ func (pc *PoolConn) Write(b []byte) (int, error) { } func (pc *PoolConn) Close() error { - // clash use SetReadDeadline to break bidirectional copy between client and server. + // 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) diff --git a/transport/snell/snell.go b/transport/snell/snell.go index e2bd2820..fe3e4ee0 100644 --- a/transport/snell/snell.go +++ b/transport/snell/snell.go @@ -8,9 +8,9 @@ import ( "net" "sync" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" + "github.com/metacubex/mihomo/transport/socks5" ) const ( diff --git a/transport/socks4/socks4.go b/transport/socks4/socks4.go index 0d5c5a77..9533a1c0 100644 --- a/transport/socks4/socks4.go +++ b/transport/socks4/socks4.go @@ -9,7 +9,7 @@ import ( "net/netip" "strconv" - "github.com/Dreamacro/clash/component/auth" + "github.com/metacubex/mihomo/component/auth" ) const Version = 0x04 diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 7d4f11ae..c97c370c 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -9,7 +9,7 @@ import ( "net/netip" "strconv" - "github.com/Dreamacro/clash/component/auth" + "github.com/metacubex/mihomo/component/auth" ) // Error represents a SOCKS error @@ -299,6 +299,50 @@ func ReadAddr(r io.Reader, b []byte) (Addr, error) { 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 diff --git a/transport/ssr/obfs/http_simple.go b/transport/ssr/obfs/http_simple.go index c1ea7673..359ca342 100644 --- a/transport/ssr/obfs/http_simple.go +++ b/transport/ssr/obfs/http_simple.go @@ -4,12 +4,13 @@ import ( "bytes" "encoding/hex" "io" - "math/rand" "net" "strconv" "strings" - "github.com/Dreamacro/clash/common/pool" + "github.com/metacubex/mihomo/common/pool" + + "github.com/zhangyunhao116/fastrand" ) func init() { @@ -81,7 +82,7 @@ func (c *httpConn) Write(b []byte) (int, error) { bLength := len(b) headDataLength := bLength if bLength-headLength > 64 { - headDataLength = headLength + rand.Intn(65) + headDataLength = headLength + fastrand.Intn(65) } headData := b[:headDataLength] b = b[headDataLength:] @@ -99,7 +100,7 @@ func (c *httpConn) Write(b []byte) (int, error) { } } hosts := strings.Split(host, ",") - host = hosts[rand.Intn(len(hosts))] + host = hosts[fastrand.Intn(len(hosts))] buf := pool.GetBuffer() defer pool.PutBuffer(buf) @@ -118,7 +119,7 @@ func (c *httpConn) Write(b []byte) (int, error) { buf.WriteString(body + "\r\n\r\n") } else { buf.WriteString("User-Agent: ") - buf.WriteString(userAgent[rand.Intn(len(userAgent))]) + buf.WriteString(userAgent[fastrand.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) @@ -146,7 +147,7 @@ func packBoundary(buf *bytes.Buffer) { buf.WriteString("Content-Type: multipart/form-data; boundary=") set := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" for i := 0; i < 32; i++ { - buf.WriteByte(set[rand.Intn(62)]) + buf.WriteByte(set[fastrand.Intn(62)]) } buf.WriteString("\r\n") } diff --git a/transport/ssr/obfs/random_head.go b/transport/ssr/obfs/random_head.go index b10b01c5..9a2072fe 100644 --- a/transport/ssr/obfs/random_head.go +++ b/transport/ssr/obfs/random_head.go @@ -3,10 +3,11 @@ package obfs import ( "encoding/binary" "hash/crc32" - "math/rand" "net" - "github.com/Dreamacro/clash/common/pool" + "github.com/metacubex/mihomo/common/pool" + + "github.com/zhangyunhao116/fastrand" ) func init() { @@ -53,10 +54,10 @@ func (c *randomHeadConn) Write(b []byte) (int, error) { c.buf = append(c.buf, b...) if !c.hasSentHeader { c.hasSentHeader = true - dataLength := rand.Intn(96) + 4 + dataLength := fastrand.Intn(96) + 4 buf := pool.Get(dataLength + 4) defer pool.Put(buf) - rand.Read(buf[:dataLength]) + fastrand.Read(buf[:dataLength]) binary.LittleEndian.PutUint32(buf[dataLength:], 0xffffffff-crc32.ChecksumIEEE(buf[:dataLength])) _, err := c.Conn.Write(buf) return len(b), err diff --git a/transport/ssr/obfs/tls1.2_ticket_auth.go b/transport/ssr/obfs/tls1.2_ticket_auth.go index 10f2786a..d5e3ca88 100644 --- a/transport/ssr/obfs/tls1.2_ticket_auth.go +++ b/transport/ssr/obfs/tls1.2_ticket_auth.go @@ -4,13 +4,14 @@ import ( "bytes" "crypto/hmac" "encoding/binary" - "math/rand" "net" "strings" "time" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/transport/ssr/tools" + "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/transport/ssr/tools" + + "github.com/zhangyunhao116/fastrand" ) func init() { @@ -25,7 +26,7 @@ type tls12Ticket struct { func newTLS12Ticket(b *Base) Obfs { r := &tls12Ticket{Base: b, authData: &authData{}} - rand.Read(r.clientID[:]) + fastrand.Read(r.clientID[:]) return r } @@ -90,7 +91,7 @@ func (c *tls12TicketConn) Write(b []byte) (int, error) { buf := pool.GetBuffer() defer pool.PutBuffer(buf) for len(b) > 2048 { - size := rand.Intn(4096) + 100 + size := fastrand.Intn(4096) + 100 if len(b) < size { size = len(b) } @@ -196,7 +197,7 @@ func packSNIData(buf *bytes.Buffer, u string) { } func (c *tls12TicketConn) packTicketBuf(buf *bytes.Buffer, u string) { - length := 16 * (rand.Intn(17) + 8) + length := 16 * (fastrand.Intn(17) + 8) buf.Write([]byte{0, 0x23}) binary.Write(buf, binary.BigEndian, uint16(length)) tools.AppendRandBytes(buf, length) @@ -221,6 +222,6 @@ func (t *tls12Ticket) getHost() string { host = "" } hosts := strings.Split(host, ",") - host = hosts[rand.Intn(len(hosts))] + host = hosts[fastrand.Intn(len(hosts))] return host } diff --git a/transport/ssr/protocol/auth_aes128_md5.go b/transport/ssr/protocol/auth_aes128_md5.go index d3bc9417..c6ae415e 100644 --- a/transport/ssr/protocol/auth_aes128_md5.go +++ b/transport/ssr/protocol/auth_aes128_md5.go @@ -1,6 +1,6 @@ package protocol -import "github.com/Dreamacro/clash/transport/ssr/tools" +import "github.com/metacubex/mihomo/transport/ssr/tools" func init() { register("auth_aes128_md5", newAuthAES128MD5, 9) diff --git a/transport/ssr/protocol/auth_aes128_sha1.go b/transport/ssr/protocol/auth_aes128_sha1.go index 7b4da962..6ee4160e 100644 --- a/transport/ssr/protocol/auth_aes128_sha1.go +++ b/transport/ssr/protocol/auth_aes128_sha1.go @@ -4,14 +4,16 @@ import ( "bytes" "encoding/binary" "math" - "math/rand" "net" "strconv" "strings" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/ssr/tools" + 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/zhangyunhao116/fastrand" ) type ( @@ -64,7 +66,7 @@ func (a *authAES128) initUserData() { } if len(a.userKey) == 0 { a.userKey = a.Key - rand.Read(a.userID[:]) + fastrand.Read(a.userID[:]) } } @@ -81,13 +83,13 @@ func (a *authAES128) StreamConn(c net.Conn, iv []byte) net.Conn { return &Conn{Conn: c, Protocol: p} } -func (a *authAES128) PacketConn(c net.PacketConn) net.PacketConn { +func (a *authAES128) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { p := &authAES128{ Base: a.Base, authAES128Function: a.authAES128Function, userData: a.userData, } - return &PacketConn{PacketConn: c, Protocol: p} + return &PacketConn{EnhancePacketConn: c, Protocol: p} } func (a *authAES128) Decode(dst, src *bytes.Buffer) error { @@ -198,7 +200,7 @@ func (a *authAES128) packData(poolBuf *bytes.Buffer, data []byte, fullDataLength } func trapezoidRandom(max int, d float64) int { - base := rand.Float64() + base := fastrand.Float64() if d-0 > 1e-6 { a := 1 - d base = (math.Sqrt(a*a+4*d*base) - a) / (2 * d) @@ -219,10 +221,10 @@ func (a *authAES128) getRandDataLengthForPackData(dataLength, fullDataLength int if revLength > -1460 { return trapezoidRandom(revLength+1460, -0.3) } - return rand.Intn(32) + return fastrand.Intn(32) } if dataLength > 900 { - return rand.Intn(revLength) + return fastrand.Intn(revLength) } return trapezoidRandom(revLength, -0.3) } @@ -247,7 +249,7 @@ func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) { copy(macKey, a.iv) copy(macKey[len(a.iv):], a.Key) - poolBuf.WriteByte(byte(rand.Intn(256))) + poolBuf.WriteByte(byte(fastrand.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) @@ -263,9 +265,9 @@ func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) { func (a *authAES128) getRandDataLengthForPackAuthData(size int) int { if size > 400 { - return rand.Intn(512) + return fastrand.Intn(512) } - return rand.Intn(1024) + return fastrand.Intn(1024) } func (a *authAES128) packRandData(poolBuf *bytes.Buffer, size int) { diff --git a/transport/ssr/protocol/auth_chain_a.go b/transport/ssr/protocol/auth_chain_a.go index 6b12ab9b..396172ef 100644 --- a/transport/ssr/protocol/auth_chain_a.go +++ b/transport/ssr/protocol/auth_chain_a.go @@ -7,14 +7,16 @@ import ( "crypto/rc4" "encoding/base64" "encoding/binary" + "errors" "net" "strconv" "strings" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/shadowsocks/core" - "github.com/Dreamacro/clash/transport/ssr/tools" + 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() { @@ -83,13 +85,13 @@ func (a *authChainA) StreamConn(c net.Conn, iv []byte) net.Conn { return &Conn{Conn: c, Protocol: p} } -func (a *authChainA) PacketConn(c net.PacketConn) net.PacketConn { +func (a *authChainA) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { p := &authChainA{ Base: a.Base, salt: a.salt, userData: a.userData, } - return &PacketConn{PacketConn: c, Protocol: p} + return &PacketConn{EnhancePacketConn: c, Protocol: p} } func (a *authChainA) Decode(dst, src *bytes.Buffer) error { @@ -106,6 +108,10 @@ func (a *authChainA) Decode(dst, src *bytes.Buffer) error { 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 @@ -129,6 +135,11 @@ func (a *authChainA) Decode(dst, src *bytes.Buffer) error { 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 { diff --git a/transport/ssr/protocol/auth_chain_b.go b/transport/ssr/protocol/auth_chain_b.go index 857b2a3a..223613a9 100644 --- a/transport/ssr/protocol/auth_chain_b.go +++ b/transport/ssr/protocol/auth_chain_b.go @@ -4,7 +4,7 @@ import ( "net" "sort" - "github.com/Dreamacro/clash/transport/ssr/tools" + "github.com/metacubex/mihomo/transport/ssr/tools" ) func init() { diff --git a/transport/ssr/protocol/auth_sha1_v4.go b/transport/ssr/protocol/auth_sha1_v4.go index 30392c9e..ed1a39f1 100644 --- a/transport/ssr/protocol/auth_sha1_v4.go +++ b/transport/ssr/protocol/auth_sha1_v4.go @@ -5,11 +5,13 @@ import ( "encoding/binary" "hash/adler32" "hash/crc32" - "math/rand" "net" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/transport/ssr/tools" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/transport/ssr/tools" + + "github.com/zhangyunhao116/fastrand" ) func init() { @@ -34,7 +36,7 @@ func (a *authSHA1V4) StreamConn(c net.Conn, iv []byte) net.Conn { return &Conn{Conn: c, Protocol: p} } -func (a *authSHA1V4) PacketConn(c net.PacketConn) net.PacketConn { +func (a *authSHA1V4) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { return c } @@ -176,7 +178,7 @@ func (a *authSHA1V4) getRandDataLength(size int) int { return 0 } if size > 400 { - return rand.Intn(256) + return fastrand.Intn(256) } - return rand.Intn(512) + return fastrand.Intn(512) } diff --git a/transport/ssr/protocol/base.go b/transport/ssr/protocol/base.go index 4bf799b3..e26a6587 100644 --- a/transport/ssr/protocol/base.go +++ b/transport/ssr/protocol/base.go @@ -6,13 +6,14 @@ import ( "crypto/cipher" "encoding/base64" "encoding/binary" - "math/rand" "sync" "time" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/shadowsocks/core" + "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/transport/shadowsocks/core" + + "github.com/zhangyunhao116/fastrand" ) type Base struct { @@ -37,8 +38,8 @@ func (a *authData) next() *authData { a.mutex.Lock() defer a.mutex.Unlock() if a.connectionID > 0xff000000 || a.connectionID == 0 { - rand.Read(a.clientID[:]) - a.connectionID = rand.Uint32() & 0xffffff + fastrand.Read(a.clientID[:]) + a.connectionID = fastrand.Uint32() & 0xffffff } a.connectionID++ copy(r.clientID[:], a.clientID[:]) diff --git a/transport/ssr/protocol/origin.go b/transport/ssr/protocol/origin.go index 80fdfa9a..4d8b630a 100644 --- a/transport/ssr/protocol/origin.go +++ b/transport/ssr/protocol/origin.go @@ -3,6 +3,8 @@ package protocol import ( "bytes" "net" + + N "github.com/metacubex/mihomo/common/net" ) type origin struct{} @@ -13,7 +15,7 @@ func newOrigin(b *Base) Protocol { return &origin{} } func (o *origin) StreamConn(c net.Conn, iv []byte) net.Conn { return c } -func (o *origin) PacketConn(c net.PacketConn) net.PacketConn { return c } +func (o *origin) PacketConn(c N.EnhancePacketConn) N.EnhancePacketConn { return c } func (o *origin) Decode(dst, src *bytes.Buffer) error { dst.ReadFrom(src) diff --git a/transport/ssr/protocol/packet.go b/transport/ssr/protocol/packet.go index 249db70a..86d573f3 100644 --- a/transport/ssr/protocol/packet.go +++ b/transport/ssr/protocol/packet.go @@ -3,11 +3,12 @@ package protocol import ( "net" - "github.com/Dreamacro/clash/common/pool" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/pool" ) type PacketConn struct { - net.PacketConn + N.EnhancePacketConn Protocol } @@ -18,12 +19,12 @@ func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { if err != nil { return 0, err } - _, err = c.PacketConn.WriteTo(buf.Bytes(), addr) + _, 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.PacketConn.ReadFrom(b) + n, addr, err := c.EnhancePacketConn.ReadFrom(b) if err != nil { return n, addr, err } @@ -34,3 +35,20 @@ func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { 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 index 41bd984c..a04e6bd4 100644 --- a/transport/ssr/protocol/protocol.go +++ b/transport/ssr/protocol/protocol.go @@ -4,8 +4,11 @@ import ( "bytes" "errors" "fmt" - "math/rand" "net" + + N "github.com/metacubex/mihomo/common/net" + + "github.com/zhangyunhao116/fastrand" ) var ( @@ -21,7 +24,7 @@ var ( type Protocol interface { StreamConn(net.Conn, []byte) net.Conn - PacketConn(net.PacketConn) net.PacketConn + PacketConn(N.EnhancePacketConn) N.EnhancePacketConn Decode(dst, src *bytes.Buffer) error Encode(buf *bytes.Buffer, b []byte) error DecodePacket([]byte) ([]byte, error) @@ -68,7 +71,7 @@ func getHeadSize(b []byte, defaultValue int) int { func getDataLength(b []byte) int { bLength := len(b) - dataLength := getHeadSize(b, 30) + rand.Intn(32) + dataLength := getHeadSize(b, 30) + fastrand.Intn(32) if bLength < dataLength { return bLength } diff --git a/transport/ssr/protocol/stream.go b/transport/ssr/protocol/stream.go index 3c846157..436859c3 100644 --- a/transport/ssr/protocol/stream.go +++ b/transport/ssr/protocol/stream.go @@ -4,7 +4,7 @@ import ( "bytes" "net" - "github.com/Dreamacro/clash/common/pool" + "github.com/metacubex/mihomo/common/pool" ) type Conn struct { diff --git a/transport/ssr/tools/random.go b/transport/ssr/tools/random.go index 338543ea..c76011e4 100644 --- a/transport/ssr/tools/random.go +++ b/transport/ssr/tools/random.go @@ -3,7 +3,7 @@ package tools import ( "encoding/binary" - "github.com/Dreamacro/clash/common/pool" + "github.com/metacubex/mihomo/common/pool" ) // XorShift128Plus - a pseudorandom number generator diff --git a/transport/trojan/trojan.go b/transport/trojan/trojan.go index e336d9db..c4bd1167 100644 --- a/transport/trojan/trojan.go +++ b/transport/trojan/trojan.go @@ -7,19 +7,18 @@ import ( "encoding/binary" "encoding/hex" "errors" - "fmt" "io" "net" "net/http" "sync" - "github.com/Dreamacro/clash/common/pool" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" - "github.com/Dreamacro/clash/transport/vless" - "github.com/Dreamacro/clash/transport/vmess" - xtls "github.com/xtls/go" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/component/ca" + tlsC "github.com/metacubex/mihomo/component/tls" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" + "github.com/metacubex/mihomo/transport/vmess" ) const ( @@ -40,7 +39,7 @@ const ( CommandTCP byte = 1 CommandUDP byte = 3 - // for XTLS + // deprecated XTLS commands, as souvenirs commandXRD byte = 0xf0 // XTLS direct mode commandXRO byte = 0xf1 // XTLS origin mode ) @@ -51,16 +50,16 @@ type Option struct { ServerName string SkipCertVerify bool Fingerprint string - Flow string - FlowShow bool ClientFingerprint string + Reality *tlsC.RealityConfig } type WebsocketOption struct { - Host string - Port string - Path string - Headers http.Header + Host string + Port string + Path string + Headers http.Header + V2rayHttpUpgrade bool } type Trojan struct { @@ -68,78 +67,55 @@ type Trojan struct { hexPassword []byte } -func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) { +func (t *Trojan) StreamConn(ctx context.Context, conn net.Conn) (net.Conn, error) { alpn := defaultALPN if len(t.option.ALPN) != 0 { alpn = t.option.ALPN } - switch t.option.Flow { - case vless.XRO, vless.XRD, vless.XRS: - xtlsConfig := &xtls.Config{ - NextProtos: alpn, - MinVersion: xtls.VersionTLS12, - InsecureSkipVerify: t.option.SkipCertVerify, - ServerName: t.option.ServerName, - } + tlsConfig := &tls.Config{ + NextProtos: alpn, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: t.option.SkipCertVerify, + ServerName: t.option.ServerName, + } - if len(t.option.Fingerprint) == 0 { - xtlsConfig = tlsC.GetGlobalXTLSConfig(xtlsConfig) - } else { - var err error - if xtlsConfig, err = tlsC.GetSpecifiedFingerprintXTLSConfig(xtlsConfig, t.option.Fingerprint); err != nil { - return nil, err - } - } + var err error + tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, t.option.Fingerprint) + if err != nil { + return nil, err + } - xtlsConn := xtls.Client(conn, xtlsConfig) - - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() - if err := xtlsConn.HandshakeContext(ctx); err != nil { - return nil, err - } - return xtlsConn, nil - default: - tlsConfig := &tls.Config{ - NextProtos: alpn, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: t.option.SkipCertVerify, - ServerName: t.option.ServerName, - } - - if len(t.option.Fingerprint) == 0 { - tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } else { - var err error - if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, t.option.Fingerprint); err != nil { - return nil, err - } - } - - if len(t.option.ClientFingerprint) != 0 { - utlsConn, valid := vmess.GetUtlsConnWithClientFingerprint(conn, t.option.ClientFingerprint, tlsConfig) + if len(t.option.ClientFingerprint) != 0 { + if t.option.Reality == nil { + utlsConn, valid := vmess.GetUTLSConn(conn, t.option.ClientFingerprint, tlsConfig) if valid { ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) defer cancel() err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx) return utlsConn, err - } + } else { + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + return tlsC.GetRealityConn(ctx, conn, t.option.ClientFingerprint, tlsConfig, t.option.Reality) } - - tlsConn := tls.Client(conn, tlsConfig) - - // fix tls handshake not timeout - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() - - err := tlsConn.HandshakeContext(ctx) - return tlsConn, err } + if t.option.Reality != nil { + return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint") + } + + tlsConn := tls.Client(conn, tlsConfig) + + // fix tls handshake not timeout + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + + err = tlsConn.HandshakeContext(ctx) + return tlsConn, err } -func (t *Trojan) StreamWebsocketConn(conn net.Conn, wsOptions *WebsocketOption) (net.Conn, error) { +func (t *Trojan) StreamWebsocketConn(ctx context.Context, conn net.Conn, wsOptions *WebsocketOption) (net.Conn, error) { alpn := defaultWebsocketALPN if len(t.option.ALPN) != 0 { alpn = t.option.ALPN @@ -152,48 +128,19 @@ func (t *Trojan) StreamWebsocketConn(conn net.Conn, wsOptions *WebsocketOption) ServerName: t.option.ServerName, } - return vmess.StreamWebsocketConn(conn, &vmess.WebsocketConfig{ + return vmess.StreamWebsocketConn(ctx, conn, &vmess.WebsocketConfig{ Host: wsOptions.Host, Port: wsOptions.Port, Path: wsOptions.Path, Headers: wsOptions.Headers, + V2rayHttpUpgrade: wsOptions.V2rayHttpUpgrade, TLS: true, TLSConfig: tlsConfig, ClientFingerprint: t.option.ClientFingerprint, }) } -func (t *Trojan) PresetXTLSConn(conn net.Conn) (net.Conn, error) { - switch t.option.Flow { - case vless.XRO, vless.XRD, vless.XRS: - if xtlsConn, ok := conn.(*xtls.Conn); ok { - xtlsConn.RPRX = true - xtlsConn.SHOW = t.option.FlowShow - xtlsConn.MARK = "XTLS" - if t.option.Flow == vless.XRS { - t.option.Flow = vless.XRD - } - - if t.option.Flow == vless.XRD { - xtlsConn.DirectMode = true - } - } else { - return conn, fmt.Errorf("failed to use %s, maybe \"security\" is not \"xtls\"", t.option.Flow) - } - } - - return conn, nil -} - func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) error { - if command == CommandTCP { - if t.option.Flow == vless.XRD { - command = commandXRD - } else if t.option.Flow == vless.XRO { - command = commandXRO - } - } - buf := pool.GetBuffer() defer pool.PutBuffer(buf) @@ -293,6 +240,8 @@ func New(option *Option) *Trojan { return &Trojan{option, hexSha224([]byte(option.Password))} } +var _ N.EnhancePacketConn = (*PacketConn)(nil) + type PacketConn struct { net.Conn remain int @@ -340,10 +289,52 @@ func (pc *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { 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 hexSha224(data []byte) []byte { buf := make([]byte, 56) - hash := sha256.New224() - hash.Write(data) - hex.Encode(buf, hash.Sum(nil)) + hash := sha256.Sum224(data) + hex.Encode(buf, hash[:]) return buf } diff --git a/transport/tuic/common/congestion.go b/transport/tuic/common/congestion.go new file mode 100644 index 00000000..485e2e6a --- /dev/null +++ b/transport/tuic/common/congestion.go @@ -0,0 +1,57 @@ +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.RemoteAddr()), + false, + ), + ) + case "new_reno": + quicConn.SetCongestionControl( + congestion.NewCubicSender( + congestion.DefaultClock{}, + congestion.GetInitialPacketSize(quicConn.RemoteAddr()), + true, + ), + ) + case "bbr_meta_v1": + quicConn.SetCongestionControl( + congestion.NewBBRSender( + congestion.DefaultClock{}, + congestion.GetInitialPacketSize(quicConn.RemoteAddr()), + 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.RemoteAddr()), + c.ByteCount(cwnd), + ), + ) + } +} diff --git a/transport/tuic/common/stream.go b/transport/tuic/common/stream.go new file mode 100644 index 00000000..e65f9a49 --- /dev/null +++ b/transport/tuic/common/stream.go @@ -0,0 +1,67 @@ +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 new file mode 100644 index 00000000..c663fa0b --- /dev/null +++ b/transport/tuic/common/type.go @@ -0,0 +1,46 @@ +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_sampler.go b/transport/tuic/congestion/bandwidth_sampler.go index b82d391f..e415fe7a 100644 --- a/transport/tuic/congestion/bandwidth_sampler.go +++ b/transport/tuic/congestion/bandwidth_sampler.go @@ -296,9 +296,9 @@ func (s *BandwidthSampler) onPacketAckedInner(ackTime time.Time, lastAckedPacket return sample } -// OnPacketLost Informs the sampler that a packet is considered lost and it should no +// OnCongestionEvent Informs the sampler that a packet is considered lost and it should no // longer keep track of it. -func (s *BandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber) SendTimeState { +func (s *BandwidthSampler) OnCongestionEvent(packetNumber congestion.PacketNumber) SendTimeState { ok, sentPacket := s.connectionStats.Remove(packetNumber) sendTimeState := SendTimeState{ isValid: ok, diff --git a/transport/tuic/congestion/bbr_sender.go b/transport/tuic/congestion/bbr_sender.go index 5adbd0b7..8c18c616 100644 --- a/transport/tuic/congestion/bbr_sender.go +++ b/transport/tuic/congestion/bbr_sender.go @@ -5,11 +5,11 @@ package congestion import ( "fmt" "math" - "math/rand" "net" "time" "github.com/metacubex/quic-go/congestion" + "github.com/zhangyunhao116/fastrand" ) const ( @@ -17,17 +17,12 @@ const ( InitialMaxDatagramSize = 1252 InitialPacketSizeIPv4 = 1252 InitialPacketSizeIPv6 = 1232 - InitialCongestionWindow = 10 + InitialCongestionWindow = 32 DefaultBBRMaxCongestionWindow = 10000 ) -const ( - initialMinCongestionWindow = 4 - minInitialPacketSize = 1200 -) - func GetInitialPacketSize(addr net.Addr) congestion.ByteCount { - maxSize := congestion.ByteCount(minInitialPacketSize) + maxSize := congestion.ByteCount(1200) // If this is not a UDP address, we don't know anything about the MTU. // Use the minimum size of an Initial packet as the max packet size. if udpAddr, ok := addr.(*net.UDPAddr); ok { @@ -37,7 +32,7 @@ func GetInitialPacketSize(addr net.Addr) congestion.ByteCount { maxSize = InitialPacketSizeIPv6 } } - return maxSize + return congestion.ByteCount(maxSize) } var ( @@ -45,8 +40,8 @@ var ( // Default initial rtt used before any samples are received. InitialRtt = 100 * time.Millisecond - // The gain used for the STARTUP, equal to 2/ln(2). - DefaultHighGain = 2.89 + // 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 @@ -281,7 +276,7 @@ func (b *bbrSender) maxCongestionWindow() congestion.ByteCount { } func (b *bbrSender) minCongestionWindow() congestion.ByteCount { - return b.maxDatagramSize * initialMinCongestionWindow + return b.maxDatagramSize * b.initialCongestionWindow } func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { @@ -298,8 +293,8 @@ func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time return b.pacer.TimeUntilSend() } -func (b *bbrSender) HasPacingBudget() bool { - return b.pacer.Budget(b.clock.Now()) >= b.maxDatagramSize +func (b *bbrSender) HasPacingBudget(now time.Time) bool { + return b.pacer.Budget(now) >= b.maxDatagramSize } func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { @@ -352,15 +347,36 @@ 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 - lastAckedPacket := number - isRoundStart = b.UpdateRoundTripCounter(lastAckedPacket) - minRttExpired = b.UpdateBandwidthAndMinRtt(eventTime, number, ackedBytes) - b.UpdateRecoveryState(false, isRoundStart) - bytesAcked := b.sampler.totalBytesAcked - totalBytesAckedBefore - excessAcked := b.UpdateAckAggregationBytes(eventTime, bytesAcked) + 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 { @@ -371,38 +387,12 @@ func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes con // Handle logic specific to PROBE_RTT. b.MaybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) - // After the model is updated, recalculate the pacing rate and congestion - // window. - b.CalculatePacingRate() - b.CalculateCongestionWindow(bytesAcked, excessAcked) - b.CalculateRecoveryWindow(bytesAcked, congestion.ByteCount(0)) - -} - -func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes congestion.ByteCount, priorInFlight congestion.ByteCount) { - eventTime := time.Now() - totalBytesAckedBefore := b.sampler.totalBytesAcked - isRoundStart, minRttExpired := false, false - - b.DiscardLostPackets(number, lostBytes) - - // Input the new data into the BBR model of the connection. - var excessAcked congestion.ByteCount - - // Handle logic specific to PROBE_BW mode. - if b.mode == PROBE_BW { - b.UpdateGainCyclePhase(time.Now(), priorInFlight, true) - } - - // Handle logic specific to STARTUP and DRAIN modes. - 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 := lostBytes + bytesLost := congestion.ByteCount(0) + for _, packet := range lostPackets { + bytesLost += packet.BytesLost + } // After the model is updated, recalculate the pacing rate and congestion // window. @@ -411,53 +401,6 @@ func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes conge b.CalculateRecoveryWindow(bytesAcked, bytesLost) } -//func (b *bbrSender) OnCongestionEvent(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets, lostPackets []*congestion.Packet) { -// 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(lastAckedPacket, 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.Length -// } -// -// // 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) { // //} @@ -558,30 +501,32 @@ func (b *bbrSender) UpdateRoundTripCounter(lastAckedPacket congestion.PacketNumb return false } -func (b *bbrSender) UpdateBandwidthAndMinRtt(now time.Time, number congestion.PacketNumber, ackedBytes congestion.ByteCount) bool { +func (b *bbrSender) UpdateBandwidthAndMinRtt(now time.Time, ackedPackets []congestion.AckedPacketInfo) bool { sampleMinRtt := InfiniteRTT - if !b.alwaysGetBwSampleWhenAcked && ackedBytes == 0 { - // Skip acked packets with 0 in flight bytes when updating bandwidth. - return false - } - bandwidthSample := b.sampler.OnPacketAcked(now, number) - 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) + 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. @@ -624,14 +569,16 @@ func (b *bbrSender) ShouldExtendMinRttExpiry() bool { return false } -func (b *bbrSender) DiscardLostPackets(number congestion.PacketNumber, lostBytes congestion.ByteCount) { - b.sampler.OnPacketLost(number) - if b.mode == STARTUP { - // if b.rttStats != nil { - // TODO: slow start. - // } - if b.startupRateReductionMultiplier != 0 { - b.startupBytesLost += lostBytes +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 + } } } } @@ -780,7 +727,7 @@ func (b *bbrSender) EnterProbeBandwidthMode(now time.Time) { // 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 = rand.Int() % (GainCycleLength - 1) + b.cycleCurrentOffset = fastrand.Int() % (GainCycleLength - 1) if b.cycleCurrentOffset >= 1 { b.cycleCurrentOffset += 1 } @@ -955,7 +902,7 @@ func (b *bbrSender) CalculateRecoveryWindow(ackedBytes, lostBytes congestion.Byt b.recoveryWindow = maxByteCount(b.recoveryWindow, b.minCongestionWindow()) } -var _ congestion.CongestionControl = &bbrSender{} +var _ congestion.CongestionControl = (*bbrSender)(nil) func (b *bbrSender) GetMinRtt() time.Duration { if b.minRtt > 0 { diff --git a/transport/tuic/congestion/cubic_sender.go b/transport/tuic/congestion/cubic_sender.go index c55db7bd..f544cd74 100644 --- a/transport/tuic/congestion/cubic_sender.go +++ b/transport/tuic/congestion/cubic_sender.go @@ -5,7 +5,6 @@ import ( "time" "github.com/metacubex/quic-go/congestion" - "github.com/metacubex/quic-go/logging" ) const ( @@ -54,9 +53,6 @@ type cubicSender struct { initialMaxCongestionWindow congestion.ByteCount maxDatagramSize congestion.ByteCount - - lastState logging.CongestionState - tracer logging.ConnectionTracer } var ( @@ -68,7 +64,6 @@ func NewCubicSender( clock Clock, initialMaxDatagramSize congestion.ByteCount, reno bool, - tracer logging.ConnectionTracer, ) *cubicSender { return newCubicSender( clock, @@ -76,7 +71,6 @@ func NewCubicSender( initialMaxDatagramSize, initialCongestionWindow*initialMaxDatagramSize, MaxCongestionWindowPackets*initialMaxDatagramSize, - tracer, ) } @@ -86,7 +80,6 @@ func newCubicSender( initialMaxDatagramSize, initialCongestionWindow, initialMaxCongestionWindow congestion.ByteCount, - tracer logging.ConnectionTracer, ) *cubicSender { c := &cubicSender{ largestSentPacketNumber: InvalidPacketNumber, @@ -99,14 +92,9 @@ func newCubicSender( cubic: NewCubic(clock), clock: clock, reno: reno, - tracer: tracer, maxDatagramSize: initialMaxDatagramSize, } c.pacer = newPacer(c.BandwidthEstimate) - if c.tracer != nil { - c.lastState = logging.CongestionStateSlowStart - c.tracer.UpdatedCongestionState(logging.CongestionStateSlowStart) - } return c } @@ -119,8 +107,8 @@ func (c *cubicSender) TimeUntilSend(_ congestion.ByteCount) time.Time { return c.pacer.TimeUntilSend() } -func (c *cubicSender) HasPacingBudget() bool { - return c.pacer.Budget(c.clock.Now()) >= c.maxDatagramSize +func (c *cubicSender) HasPacingBudget(now time.Time) bool { + return c.pacer.Budget(now) >= c.maxDatagramSize } func (c *cubicSender) maxCongestionWindow() congestion.ByteCount { @@ -167,7 +155,6 @@ func (c *cubicSender) MaybeExitSlowStart() { c.hybridSlowStart.ShouldExitSlowStart(c.rttStats.LatestRTT(), c.rttStats.MinRTT(), c.GetCongestionWindow()/c.maxDatagramSize) { // exit slow start c.slowStartThreshold = c.congestionWindow - c.maybeTraceStateChange(logging.CongestionStateCongestionAvoidance) } } @@ -187,14 +174,13 @@ func (c *cubicSender) OnPacketAcked( } } -func (c *cubicSender) OnPacketLost(packetNumber congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { +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() - c.maybeTraceStateChange(logging.CongestionStateRecovery) if c.reno { c.congestionWindow = congestion.ByteCount(float64(c.congestionWindow) * renoBeta) @@ -211,6 +197,10 @@ func (c *cubicSender) OnPacketLost(packetNumber congestion.PacketNumber, lostByt 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( @@ -223,7 +213,6 @@ func (c *cubicSender) maybeIncreaseCwnd( // the current window. if !c.isCwndLimited(priorInFlight) { c.cubic.OnApplicationLimited() - c.maybeTraceStateChange(logging.CongestionStateApplicationLimited) return } if c.congestionWindow >= c.maxCongestionWindow() { @@ -232,11 +221,9 @@ func (c *cubicSender) maybeIncreaseCwnd( if c.InSlowStart() { // TCP slow start, exponential growth, increase by one for each ACK. c.congestionWindow += c.maxDatagramSize - c.maybeTraceStateChange(logging.CongestionStateSlowStart) return } // Congestion avoidance - c.maybeTraceStateChange(logging.CongestionStateCongestionAvoidance) if c.reno { // Classic Reno congestion avoidance. c.numAckedPackets++ @@ -297,14 +284,6 @@ func (c *cubicSender) OnConnectionMigration() { c.slowStartThreshold = c.initialMaxCongestionWindow } -func (c *cubicSender) maybeTraceStateChange(new logging.CongestionState) { - if c.tracer == nil || new == c.lastState { - return - } - c.tracer.UpdatedCongestionState(new) - c.lastState = new -} - 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)) diff --git a/transport/tuic/congestion/minmax.go b/transport/tuic/congestion/minmax.go index ed75072e..0a8f4ad4 100644 --- a/transport/tuic/congestion/minmax.go +++ b/transport/tuic/congestion/minmax.go @@ -3,27 +3,11 @@ package congestion import ( "math" "time" - - "golang.org/x/exp/constraints" ) // InfDuration is a duration of infinite length const InfDuration = time.Duration(math.MaxInt64) -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 -} - // MinNonZeroDuration return the minimum duration that's not zero. func MinNonZeroDuration(a, b time.Duration) time.Duration { if a == 0 { diff --git a/transport/tuic/congestion/minmax_go120.go b/transport/tuic/congestion/minmax_go120.go new file mode 100644 index 00000000..1266edbc --- /dev/null +++ b/transport/tuic/congestion/minmax_go120.go @@ -0,0 +1,19 @@ +//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 new file mode 100644 index 00000000..65b06726 --- /dev/null +++ b/transport/tuic/congestion/minmax_go121.go @@ -0,0 +1,13 @@ +//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/bandwidth.go b/transport/tuic/congestion_v2/bandwidth.go new file mode 100644 index 00000000..df39a077 --- /dev/null +++ b/transport/tuic/congestion_v2/bandwidth.go @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000..9028df64 --- /dev/null +++ b/transport/tuic/congestion_v2/bandwidth_sampler.go @@ -0,0 +1,874 @@ +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 new file mode 100644 index 00000000..084f85b1 --- /dev/null +++ b/transport/tuic/congestion_v2/bbr_sender.go @@ -0,0 +1,946 @@ +package congestion + +// src from https://github.com/google/quiche/blob/e7872fc9e12bb1d46a118949c3d4da36de58aa44/quiche/quic/core/congestion_control/bbr_sender.cc + +import ( + "fmt" + "net" + "time" + + "github.com/metacubex/quic-go/congestion" + + "github.com/zhangyunhao116/fastrand" +) + +// 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.InitialPacketSizeIPv4) + + // 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 = 3 +) + +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) +} + +// 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 + + b.maybeApplimited(priorInFlight) + + // 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 +} + +// What's the current estimated bandwidth in bytes 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(fastrand.Int31n(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 + drainLimited := b.mode == bbrModeDrain && bytesInFlight > congestionWindow/2 + if !drainLimited || 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(addr net.Addr) congestion.ByteCount { + // If this is not a UDP address, we don't know anything about the MTU. + // Use the minimum size of an Initial packet as the max packet size. + if udpAddr, ok := addr.(*net.UDPAddr); ok { + if udpAddr.IP.To4() != nil { + return congestion.InitialPacketSizeIPv4 + } else { + return congestion.InitialPacketSizeIPv6 + } + } else { + return congestion.MinInitialPacketSize + } +} diff --git a/transport/tuic/congestion_v2/clock.go b/transport/tuic/congestion_v2/clock.go new file mode 100644 index 00000000..405fae70 --- /dev/null +++ b/transport/tuic/congestion_v2/clock.go @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..1266edbc --- /dev/null +++ b/transport/tuic/congestion_v2/minmax_go120.go @@ -0,0 +1,19 @@ +//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 new file mode 100644 index 00000000..65b06726 --- /dev/null +++ b/transport/tuic/congestion_v2/minmax_go121.go @@ -0,0 +1,13 @@ +//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 new file mode 100644 index 00000000..ecaf3d11 --- /dev/null +++ b/transport/tuic/congestion_v2/pacer.go @@ -0,0 +1,74 @@ +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.InitialPacketSizeIPv4, + maxDatagramSize: congestion.InitialPacketSizeIPv4, + 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 new file mode 100644 index 00000000..119d36f6 --- /dev/null +++ b/transport/tuic/congestion_v2/packet_number_indexed_queue.go @@ -0,0 +1,199 @@ +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 new file mode 100644 index 00000000..e110c00f --- /dev/null +++ b/transport/tuic/congestion_v2/ringbuffer.go @@ -0,0 +1,118 @@ +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 new file mode 100644 index 00000000..2421b48b --- /dev/null +++ b/transport/tuic/congestion_v2/windowed_filter.go @@ -0,0 +1,162 @@ +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/conn.go b/transport/tuic/conn.go deleted file mode 100644 index 7ecc3f0d..00000000 --- a/transport/tuic/conn.go +++ /dev/null @@ -1,256 +0,0 @@ -package tuic - -import ( - "fmt" - "net" - "net/netip" - "sync" - "sync/atomic" - "time" - - "github.com/metacubex/quic-go" - - N "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/common/pool" - "github.com/Dreamacro/clash/transport/tuic/congestion" -) - -const ( - DefaultStreamReceiveWindow = 15728640 // 15 MB/s - DefaultConnectionReceiveWindow = 67108864 // 64 MB/s -) - -func SetCongestionController(quicConn quic.Connection, cc string) { - switch cc { - case "cubic": - quicConn.SetCongestionControl( - congestion.NewCubicSender( - congestion.DefaultClock{}, - congestion.GetInitialPacketSize(quicConn.RemoteAddr()), - false, - nil, - ), - ) - case "new_reno": - quicConn.SetCongestionControl( - congestion.NewCubicSender( - congestion.DefaultClock{}, - congestion.GetInitialPacketSize(quicConn.RemoteAddr()), - true, - nil, - ), - ) - case "bbr": - quicConn.SetCongestionControl( - congestion.NewBBRSender( - congestion.DefaultClock{}, - congestion.GetInitialPacketSize(quicConn.RemoteAddr()), - congestion.InitialCongestionWindow*congestion.InitialMaxDatagramSize, - congestion.DefaultBBRMaxCongestionWindow*congestion.InitialMaxDatagramSize, - ), - ) - } -} - -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{} - -type quicStreamPacketConn struct { - connId uint32 - quicConn quic.Connection - inputConn *N.BufferedConn - - udpRelayMode string - 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) WriteTo(p []byte, addr net.Addr) (n int, err error) { - if len(p) > q.maxUdpRelayPacketSize { - return 0, fmt.Errorf("udp packet too large(%d > %d)", len(p), 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) - }() - } - addr.String() - buf := pool.GetBuffer() - defer pool.PutBuffer(buf) - addrPort, err := netip.ParseAddrPort(addr.String()) - if err != nil { - return - } - err = NewPacket(q.connId, uint16(len(p)), NewAddressAddrPort(addrPort), p).WriteTo(buf) - if err != nil { - return - } - switch q.udpRelayMode { - case "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 - err = q.quicConn.SendMessage(buf.Bytes()) - if err != nil { - return - } - } - n = len(p) - - return -} - -func (q *quicStreamPacketConn) LocalAddr() net.Addr { - return q.quicConn.LocalAddr() -} - -var _ net.PacketConn = &quicStreamPacketConn{} diff --git a/transport/tuic/pool_client.go b/transport/tuic/pool_client.go index fe06c2f3..e492fba7 100644 --- a/transport/tuic/pool_client.go +++ b/transport/tuic/pool_client.go @@ -8,34 +8,38 @@ import ( "sync" "time" - "github.com/Dreamacro/clash/common/generics/list" - N "github.com/Dreamacro/clash/common/net" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "github.com/metacubex/mihomo/common/generics/list" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/quic-go" ) type dialResult struct { - pc net.PacketConn - addr net.Addr - err error + transport *quic.Transport + addr net.Addr + err error } type PoolClient struct { - *ClientOption - - newClientOption *ClientOption - dialResultMap map[C.Dialer]dialResult - dialResultMutex *sync.Mutex - tcpClients *list.List[*Client] - tcpClientsMutex *sync.Mutex - udpClients *list.List[*Client] - udpClientsMutex *sync.Mutex + 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) { - conn, err := t.getClient(false, dialer).DialContextWithDialer(ctx, metadata, dialer, dialFn) + 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, dialFn) + conn, err = t.newClient(false, dialer).DialContextWithDialer(ctx, metadata, dialer, newDialFn) } if err != nil { return nil, err @@ -44,9 +48,12 @@ func (t *PoolClient) DialContextWithDialer(ctx context.Context, metadata *C.Meta } func (t *PoolClient) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.PacketConn, error) { - pc, err := t.getClient(true, dialer).ListenPacketWithDialer(ctx, metadata, dialer, dialFn) + 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, dialFn) + pc, err = t.newClient(true, dialer).ListenPacketWithDialer(ctx, metadata, dialer, newDialFn) } if err != nil { return nil, err @@ -54,40 +61,44 @@ func (t *PoolClient) ListenPacketWithDialer(ctx context.Context, metadata *C.Met return N.NewRefPacketConn(pc, t), nil } -func (t *PoolClient) dial(ctx context.Context, dialer C.Dialer, dialFn DialFunc) (pc net.PacketConn, addr net.Addr, err error) { +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.pc, dr.addr, dr.err + return dr.transport, dr.addr, dr.err } - pc, addr, err = dialFn(ctx, dialer) + transport, addr, err = dialFn(ctx, dialer) if err != nil { return nil, nil, err } - dr.pc, dr.addr, dr.err = pc, addr, 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 pc, 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 { - pc := t.dialResultMap[key].pc - if pc != nil { - _ = pc.Close() + transport := t.dialResultMap[key].transport + if transport != nil { + _ = transport.Close() } delete(t.dialResultMap, key) } } -func (t *PoolClient) newClient(udp bool, dialer C.Dialer) *Client { +func (t *PoolClient) newClient(udp bool, dialer C.Dialer) (client Client) { clients := t.tcpClients clientsMutex := t.tcpClientsMutex if udp { @@ -98,22 +109,26 @@ func (t *PoolClient) newClient(udp bool, dialer C.Dialer) *Client { clientsMutex.Lock() defer clientsMutex.Unlock() - client := NewClient(t.newClientOption, udp) - client.dialerRef = dialer - client.lastVisited = time.Now() + 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 { +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 + var bestClient Client func() { clientsMutex.Lock() @@ -126,11 +141,11 @@ func (t *PoolClient) getClient(udp bool, dialer C.Dialer) *Client { it = next continue } - if client.dialerRef == dialer { + if client.DialerRef() == dialer { if bestClient == nil { bestClient = client } else { - if client.openStreams.Load() < bestClient.openStreams.Load() { + if client.OpenStreams() < bestClient.OpenStreams() { bestClient = client } } @@ -140,7 +155,7 @@ func (t *PoolClient) getClient(udp bool, dialer C.Dialer) *Client { }() for it := clients.Front(); it != nil; { client := it.Value - if client != bestClient && client.openStreams.Load() == 0 && time.Now().Sub(client.lastVisited) > 30*time.Minute { + if client != bestClient && client.OpenStreams() == 0 && time.Now().Sub(client.LastVisited()) > 30*time.Minute { client.Close() next := it.Next() clients.Remove(it) @@ -153,25 +168,40 @@ func (t *PoolClient) getClient(udp bool, dialer C.Dialer) *Client { if bestClient == nil { return t.newClient(udp, dialer) } else { - bestClient.lastVisited = time.Now() + bestClient.SetLastVisited(time.Now()) return bestClient } } -func NewPoolClient(clientOption *ClientOption) *PoolClient { +func NewPoolClientV4(clientOption *ClientOptionV4) *PoolClient { p := &PoolClient{ - ClientOption: clientOption, dialResultMap: make(map[C.Dialer]dialResult), dialResultMutex: &sync.Mutex{}, - tcpClients: list.New[*Client](), + tcpClients: list.New[Client](), tcpClientsMutex: &sync.Mutex{}, - udpClients: list.New[*Client](), + udpClients: list.New[Client](), udpClientsMutex: &sync.Mutex{}, } newClientOption := *clientOption - p.newClientOption = &newClientOption + p.newClientOptionV4 = &newClientOption runtime.SetFinalizer(p, closeClientPool) - log.Debugln("New Tuic PoolClient at %p", p) + 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 } diff --git a/transport/tuic/server.go b/transport/tuic/server.go index 2830b324..a273e462 100644 --- a/transport/tuic/server.go +++ b/transport/tuic/server.go @@ -2,50 +2,43 @@ package tuic import ( "bufio" - "bytes" "context" "crypto/tls" - "fmt" "net" - "sync" - "sync/atomic" "time" - "github.com/gofrs/uuid" - "github.com/metacubex/quic-go" + "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/utils" + 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" - N "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/common/pool" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + "github.com/gofrs/uuid/v5" + "github.com/metacubex/quic-go" ) type ServerOption struct { - HandleTcpFn func(conn net.Conn, addr socks5.Addr) error - HandleUdpFn func(addr socks5.Addr, packet C.UDPPacket) error + 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 *tls.Config QuicConfig *quic.Config - Tokens [][32]byte + 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 - listener quic.EarlyListener -} - -func NewServer(option *ServerOption, pc net.PacketConn) (*Server, error) { - listener, err := quic.ListenEarly(pc, option.TlsConfig, option.QuicConfig) - if err != nil { - return nil, err - } - return &Server{ - ServerOption: option, - listener: listener, - }, err + optionV4 *v4.ServerOption + optionV5 *v5.ServerOption + listener *quic.EarlyListener } func (s *Server) Serve() error { @@ -54,16 +47,17 @@ func (s *Server) Serve() error { if err != nil { return err } - SetCongestionController(conn, s.CongestionController) - uuid, err := uuid.NewV4() - if err != nil { - return err - } + common.SetCongestionController(conn, s.CongestionController, s.CWND) h := &serverHandler{ Server: s, quicConn: conn, - uuid: uuid, - authCh: make(chan struct{}), + 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() } @@ -75,24 +69,14 @@ func (s *Server) Close() error { type serverHandler struct { *Server - quicConn quic.Connection + quicConn quic.EarlyConnection uuid uuid.UUID - authCh chan struct{} - authOk bool - authOnce sync.Once - - udpInputMap sync.Map + v4Handler common.ServerHandler + v5Handler common.ServerHandler } func (s *serverHandler) handle() { - time.AfterFunc(s.AuthenticationTimeout, func() { - s.authOnce.Do(func() { - _ = s.quicConn.CloseWithError(AuthenticationTimeout, "") - s.authOk = false - close(s.authCh) - }) - }) go func() { _ = s.handleUniStream() }() @@ -102,63 +86,56 @@ func (s *serverHandler) handle() { 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.ReceiveMessage() + message, err = s.quicConn.ReceiveMessage(context.Background()) if err != nil { return err } go func() (err error) { - buffer := bytes.NewBuffer(message) - packet, err := ReadPacket(buffer) - if err != nil { - return + 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 s.parsePacket(packet, "native") + return }() } } -func (s *serverHandler) parsePacket(packet Packet, udpRelayMode string) (err error) { - <-s.authCh - if !s.authOk { - return - } - var assocId uint32 - - assocId = packet.ASSOC_ID - - v, _ := s.udpInputMap.LoadOrStore(assocId, &atomic.Bool{}) - writeClosed := v.(*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: s.genServerAssocIdAddr(assocId, s.quicConn.RemoteAddr()), - }) -} - -func (s *serverHandler) genServerAssocIdAddr(assocId uint32, addr net.Addr) net.Addr { - return &ServerAssocIdAddr{assocId: fmt.Sprintf("tuic-%s-%d", s.uuid.String(), assocId), addr: addr} -} - func (s *serverHandler) handleStream() (err error) { for { var quicStream quic.Stream @@ -167,40 +144,30 @@ func (s *serverHandler) handleStream() (err error) { return err } go func() (err error) { - stream := &quicStreamConn{ - Stream: quicStream, - lAddr: s.quicConn.LocalAddr(), - rAddr: s.quicConn.RemoteAddr(), - } + stream := common.NewQuicStreamConn( + quicStream, + s.quicConn.LocalAddr(), + s.quicConn.RemoteAddr(), + nil, + ) conn := N.NewBufferedConn(stream) - connect, err := ReadConnect(conn) - if err != nil { - return err - } - <-s.authCh - if !s.authOk { - 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(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 }() } @@ -218,103 +185,54 @@ func (s *serverHandler) handleUniStream() (err error) { stream.CancelRead(0) }() reader := bufio.NewReader(stream) - commandHead, err := ReadCommandHead(reader) + verBytes, err := reader.Peek(1) if err != nil { - return + return err } - switch commandHead.TYPE { - case AuthenticateType: - var authenticate Authenticate - authenticate, err = ReadAuthenticateWithHead(commandHead, reader) - if err != nil { - return + + switch verBytes[0] { + case v4.VER: + if s.v4Handler != nil { + return s.v4Handler.HandleUniStream(reader) } - ok := false - for _, tkn := range s.Tokens { - if authenticate.TKN == tkn { - ok = true - break - } + case v5.VER: + if s.v5Handler != nil { + return s.v5Handler.HandleUniStream(reader) } - s.authOnce.Do(func() { - if !ok { - _ = s.quicConn.CloseWithError(AuthenticationFailed, "") - } - s.authOk = ok - close(s.authCh) - }) - case PacketType: - var packet Packet - packet, err = ReadPacketWithHead(commandHead, reader) - if err != nil { - return - } - return s.parsePacket(packet, "quic") - case DissociateType: - var disassociate Dissociate - disassociate, err = ReadDissociateWithHead(commandHead, reader) - if err != nil { - return - } - if v, loaded := s.udpInputMap.LoadAndDelete(disassociate.ASSOC_ID); loaded { - writeClosed := v.(*atomic.Bool) - writeClosed.Store(true) - } - case HeartbeatType: - var heartbeat Heartbeat - heartbeat, err = ReadHeartbeatWithHead(commandHead, reader) - if err != nil { - return - } - heartbeat.BytesLen() } return }() } } -type ServerAssocIdAddr struct { - assocId string - addr net.Addr +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 } - -func (a ServerAssocIdAddr) Network() string { - return "ServerAssocIdAddr" -} - -func (a ServerAssocIdAddr) String() string { - return a.assocId -} - -func (a ServerAssocIdAddr) RawAddr() net.Addr { - return a.addr -} - -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{} -var _ C.UDPPacketInAddr = &serverUDPPacket{} diff --git a/transport/tuic/tuic.go b/transport/tuic/tuic.go new file mode 100644 index 00000000..02aaa3ad --- /dev/null +++ b/transport/tuic/tuic.go @@ -0,0 +1,40 @@ +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/client.go b/transport/tuic/v4/client.go similarity index 52% rename from transport/tuic/client.go rename to transport/tuic/v4/client.go index d3f511df..0bc8f9bb 100644 --- a/transport/tuic/client.go +++ b/transport/tuic/v4/client.go @@ -1,4 +1,4 @@ -package tuic +package v4 import ( "bufio" @@ -6,40 +6,38 @@ import ( "context" "crypto/tls" "errors" - "math/rand" "net" "runtime" "sync" "sync/atomic" "time" + "unsafe" + + atomic2 "github.com/metacubex/mihomo/common/atomic" + "github.com/metacubex/mihomo/common/buf" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/pool" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/quic-go" - - N "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/common/pool" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + "github.com/puzpuzpuz/xsync/v2" + "github.com/zhangyunhao116/fastrand" ) -var ( - ClientClosed = errors.New("tuic: client closed") - TooManyOpenStreams = errors.New("tuic: too many open streams") -) - -type DialFunc func(ctx context.Context, dialer C.Dialer) (pc net.PacketConn, addr net.Addr, err error) - type ClientOption struct { TlsConfig *tls.Config QuicConfig *quic.Config - Host string Token [32]byte - UdpRelayMode string + UdpRelayMode common.UdpRelayMode CongestionController string ReduceRtt bool RequestTimeout time.Duration MaxUdpRelayPacketSize int FastOpen bool MaxOpenStreams int64 + CWND int } type clientImpl struct { @@ -52,34 +50,50 @@ type clientImpl struct { openStreams atomic.Int64 closed atomic.Bool - udpInputMap sync.Map + udpInputMap *xsync.MapOf[uint32, net.Conn] // only ready for PoolClient dialerRef C.Dialer - lastVisited time.Time + lastVisited atomic2.TypedValue[time.Time] } -func (t *clientImpl) getQuicConn(ctx context.Context, dialer C.Dialer, dialFn DialFunc) (quic.Connection, error) { +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 } - pc, addr, err := dialFn(ctx, dialer) + transport, addr, err := dialFn(ctx, dialer) if err != nil { return nil, err } var quicConn quic.Connection if t.ReduceRtt { - quicConn, err = quic.DialEarlyContext(ctx, pc, addr, t.Host, t.TlsConfig, t.QuicConfig) + quicConn, err = transport.DialEarly(ctx, addr, t.TlsConfig, t.QuicConfig) } else { - quicConn, err = quic.DialContext(ctx, pc, addr, t.Host, t.TlsConfig, t.QuicConfig) + quicConn, err = transport.Dial(ctx, addr, t.TlsConfig, t.QuicConfig) } if err != nil { return nil, err } - SetCongestionController(quicConn, t.CongestionController) + common.SetCongestionController(quicConn, t.CongestionController, t.CWND) go func() { _ = t.sendAuthentication(quicConn) @@ -87,7 +101,12 @@ func (t *clientImpl) getQuicConn(ctx context.Context, dialer C.Dialer, dialFn Di if t.udp { go func() { - _ = t.parseUDP(quicConn) + switch t.UdpRelayMode { + case common.QUIC: + _ = t.handleUniStream(quicConn) + default: // native + _ = t.handleMessage(quicConn) + } }() } @@ -121,80 +140,102 @@ func (t *clientImpl) sendAuthentication(quicConn quic.Connection) (err error) { return nil } -func (t *clientImpl) parseUDP(quicConn quic.Connection) (err error) { +func (t *clientImpl) handleUniStream(quicConn quic.Connection) (err error) { defer func() { t.deferQuicConn(quicConn, err) }() - switch t.UdpRelayMode { - case "quic": - 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() - } + 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) - packet, err := ReadPacket(reader) + } + 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 } - 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 - }() - } - default: // native - for { - var message []byte - message, err = quicConn.ReceiveMessage() - 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() - } + 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() } } - }() - buffer := bytes.NewBuffer(message) - packet, err := ReadPacket(buffer) + } + } + return + }() + } +} + +func (t *clientImpl) handleMessage(quicConn quic.Connection) (err error) { + defer func() { + t.deferQuicConn(quicConn, err) + }() + for { + var message []byte + message, err = quicConn.ReceiveMessage(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 } - assocId = packet.ASSOC_ID - if val, ok := t.udpInputMap.Load(assocId); ok { - if conn, ok := val.(net.Conn); ok { - _, _ = conn.Write(message) + 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 - }() - } + } + return + }() } } @@ -223,11 +264,10 @@ func (t *clientImpl) forceClose(quicConn quic.Connection, err error) { if quicConn != nil { _ = quicConn.CloseWithError(ProtocolError, errStr) } - udpInputMap := &t.udpInputMap - udpInputMap.Range(func(key, value any) bool { - if conn, ok := value.(net.Conn); ok { - _ = conn.Close() - } + udpInputMap := t.udpInputMap + udpInputMap.Range(func(key uint32, value net.Conn) bool { + conn := value + _ = conn.Close() udpInputMap.Delete(key) return true }) @@ -236,11 +276,11 @@ func (t *clientImpl) forceClose(quicConn quic.Connection, err error) { func (t *clientImpl) Close() { t.closed.Store(true) if t.openStreams.Load() == 0 { - t.forceClose(nil, ClientClosed) + t.forceClose(nil, common.ClientClosed) } } -func (t *clientImpl) DialContextWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.Conn, error) { +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 @@ -248,9 +288,9 @@ func (t *clientImpl) DialContextWithDialer(ctx context.Context, metadata *C.Meta openStreams := t.openStreams.Add(1) if openStreams >= t.MaxOpenStreams { t.openStreams.Add(-1) - return nil, TooManyOpenStreams + return nil, common.TooManyOpenStreams } - stream, err := func() (stream *quicStreamConn, err error) { + stream, err := func() (stream net.Conn, err error) { defer func() { t.deferQuicConn(quicConn, err) }() @@ -264,19 +304,19 @@ func (t *clientImpl) DialContextWithDialer(ctx context.Context, metadata *C.Meta if err != nil { return nil, err } - stream = &quicStreamConn{ - Stream: quicStream, - lAddr: quicConn.LocalAddr(), - rAddr: quicConn.RemoteAddr(), - closeDeferFn: func() { + 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, ClientClosed) + t.forceClose(quicConn, common.ClientClosed) } }) }, - } + ) _, err = buf.WriteTo(stream) if err != nil { _ = stream.Close() @@ -288,7 +328,8 @@ func (t *clientImpl) DialContextWithDialer(ctx context.Context, metadata *C.Meta return nil, err } - conn := &earlyConn{BufferedConn: N.NewBufferedConn(stream), RequestTimeout: t.RequestTimeout} + bufConn := N.NewBufferedConn(stream) + conn := &earlyConn{ExtendedConn: bufConn, bufConn: bufConn, RequestTimeout: t.RequestTimeout} if !t.FastOpen { err = conn.Response() if err != nil { @@ -299,9 +340,10 @@ func (t *clientImpl) DialContextWithDialer(ctx context.Context, metadata *C.Meta } type earlyConn struct { - *N.BufferedConn - resOnce sync.Once - resErr error + N.ExtendedConn // only expose standard N.ExtendedConn function to outside + bufConn *N.BufferedConn + resOnce sync.Once + resErr error RequestTimeout time.Duration } @@ -310,7 +352,7 @@ func (conn *earlyConn) response() error { if conn.RequestTimeout > 0 { _ = conn.SetReadDeadline(time.Now().Add(conn.RequestTimeout)) } - response, err := ReadResponse(conn) + response, err := ReadResponse(conn.bufConn) if err != nil { _ = conn.Close() return err @@ -335,10 +377,30 @@ func (conn *earlyConn) Read(b []byte) (n int, err error) { if err != nil { return 0, err } - return conn.BufferedConn.Read(b) + return conn.bufConn.Read(b) } -func (t *clientImpl) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.PacketConn, error) { +func (conn *earlyConn) ReadBuffer(buffer *buf.Buffer) (err error) { + err = conn.Response() + if err != nil { + return err + } + return conn.bufConn.ReadBuffer(buffer) +} + +func (conn *earlyConn) Upstream() any { + return conn.bufConn +} + +func (conn *earlyConn) ReaderReplaceable() bool { + return atomic.LoadUint32((*uint32)(unsafe.Pointer(&conn.resOnce))) == 1 && conn.resErr == nil +} + +func (conn *earlyConn) WriterReplaceable() bool { + return true +} + +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 @@ -346,13 +408,13 @@ func (t *clientImpl) ListenPacketWithDialer(ctx context.Context, metadata *C.Met openStreams := t.openStreams.Add(1) if openStreams >= t.MaxOpenStreams { t.openStreams.Add(-1) - return nil, TooManyOpenStreams + return nil, common.TooManyOpenStreams } pipe1, pipe2 := net.Pipe() var connId uint32 for { - connId = rand.Uint32() + connId = fastrand.Uint32() _, loaded := t.udpInputMap.LoadOrStore(connId, pipe1) if !loaded { break @@ -370,7 +432,7 @@ func (t *clientImpl) ListenPacketWithDialer(ctx context.Context, metadata *C.Met time.AfterFunc(C.DefaultUDPTimeout, func() { openStreams := t.openStreams.Add(-1) if openStreams == 0 && t.closed.Load() { - t.forceClose(quicConn, ClientClosed) + t.forceClose(quicConn, common.ClientClosed) } }) }, @@ -382,7 +444,7 @@ 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 DialFunc) (net.Conn, error) { +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 @@ -390,7 +452,7 @@ func (t *Client) DialContextWithDialer(ctx context.Context, metadata *C.Metadata return N.NewRefConn(conn, t), err } -func (t *Client) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadata, dialer C.Dialer, dialFn DialFunc) (net.PacketConn, error) { +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 @@ -399,21 +461,23 @@ func (t *Client) ListenPacketWithDialer(ctx context.Context, metadata *C.Metadat } func (t *Client) forceClose() { - t.clientImpl.forceClose(nil, ClientClosed) + t.clientImpl.forceClose(nil, common.ClientClosed) } -func NewClient(clientOption *ClientOption, udp bool) *Client { +func NewClient(clientOption *ClientOption, udp bool, dialerRef C.Dialer) *Client { ci := &clientImpl{ ClientOption: clientOption, udp: udp, + dialerRef: dialerRef, + udpInputMap: xsync.NewIntegerMapOf[uint32, net.Conn](), } c := &Client{ci} runtime.SetFinalizer(c, closeClient) - log.Debugln("New Tuic Client at %p", c) + log.Debugln("New TuicV4 Client at %p", c) return c } func closeClient(client *Client) { - log.Debugln("Close Tuic Client at %p", 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 new file mode 100644 index 00000000..5bd6504c --- /dev/null +++ b/transport/tuic/v4/packet.go @@ -0,0 +1,178 @@ +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.ErrMessageTooLarge(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.SendMessage(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/protocol.go b/transport/tuic/v4/protocol.go similarity index 91% rename from transport/tuic/protocol.go rename to transport/tuic/v4/protocol.go index a54c8e54..29536742 100644 --- a/transport/tuic/protocol.go +++ b/transport/tuic/v4/protocol.go @@ -1,4 +1,4 @@ -package tuic +package v4 import ( "encoding/binary" @@ -11,8 +11,8 @@ import ( "github.com/metacubex/quic-go" "lukechampine.com/blake3" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/socks5" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/transport/socks5" ) type BufferedReader interface { @@ -36,6 +36,8 @@ const ( ResponseType = CommandType(0xff) ) +const VER byte = 0x04 + func (c CommandType) String() string { switch c { case AuthenticateType: @@ -66,7 +68,7 @@ type CommandHead struct { func NewCommandHead(TYPE CommandType) CommandHead { return CommandHead{ - VER: 0x04, + VER: VER, TYPE: TYPE, } } @@ -114,9 +116,6 @@ func NewAuthenticate(TKN [32]byte) Authenticate { func ReadAuthenticateWithHead(head CommandHead, reader BufferedReader) (c Authenticate, err error) { c.CommandHead = head - if err != nil { - return - } if c.CommandHead.TYPE != AuthenticateType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return @@ -170,9 +169,6 @@ func NewConnect(ADDR Address) Connect { func ReadConnectWithHead(head CommandHead, reader BufferedReader) (c Connect, err error) { c.CommandHead = head - if err != nil { - return - } if c.CommandHead.TYPE != ConnectType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return @@ -228,9 +224,6 @@ func NewPacket(ASSOC_ID uint32, LEN uint16, ADDR Address, DATA []byte) Packet { func ReadPacketWithHead(head CommandHead, reader BufferedReader) (c Packet, err error) { c.CommandHead = head - if err != nil { - return - } if c.CommandHead.TYPE != PacketType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return @@ -291,6 +284,8 @@ 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 @@ -305,9 +300,6 @@ func NewDissociate(ASSOC_ID uint32) Dissociate { func ReadDissociateWithHead(head CommandHead, reader BufferedReader) (c Dissociate, err error) { c.CommandHead = head - if err != nil { - return - } if c.CommandHead.TYPE != DissociateType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return @@ -465,26 +457,43 @@ func NewAddress(metadata *C.Metadata) Address { copy(addr[1:], metadata.Host) } - port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) - return Address{ TYPE: addrType, ADDR: addr, - PORT: uint16(port), + 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 - if addrPort.Addr().Is4() { + port := addrPort.Port() + addr := addrPort.Addr().Unmap() + if addr.Is4() { addrType = AtypIPv4 } else { addrType = AtypIPv6 } return Address{ TYPE: addrType, - ADDR: addrPort.Addr().AsSlice(), - PORT: addrPort.Port(), + ADDR: addr.AsSlice(), + PORT: port, } } diff --git a/transport/tuic/v4/server.go b/transport/tuic/v4/server.go new file mode 100644 index 00000000..c4e4d735 --- /dev/null +++ b/transport/tuic/v4/server.go @@ -0,0 +1,218 @@ +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/v2" +) + +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.NewIntegerMapOf[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 new file mode 100644 index 00000000..e37d60fc --- /dev/null +++ b/transport/tuic/v5/client.go @@ -0,0 +1,420 @@ +package v5 + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "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" + 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/puzpuzpuz/xsync/v2" + "github.com/zhangyunhao116/fastrand" +) + +type ClientOption struct { + TlsConfig *tls.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.ReceiveMessage(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 := net.Pipe() + var connId uint16 + for { + connId = uint16(fastrand.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.NewIntegerMapOf[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 new file mode 100644 index 00000000..4e07a1c7 --- /dev/null +++ b/transport/tuic/v5/frag.go @@ -0,0 +1,114 @@ +package v5 + +import ( + "bytes" + "sync" + + "github.com/metacubex/mihomo/common/cache" + + "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 +var MaxFragSize = 1200 - PacketOverHead + +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.SendMessage(data) + if err != nil { + return + } + packet.ADDR.TYPE = AtypNone // avoid "fragment 2/2: address in non-first fragment" + } + return +} + +type deFragger struct { + lru *cache.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 = cache.New( + cache.WithAge[uint16, *packetBag](10), + cache.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 new file mode 100644 index 00000000..8ab45068 --- /dev/null +++ b/transport/tuic/v5/packet.go @@ -0,0 +1,207 @@ +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/zhangyunhao116/fastrand" +) + +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.ErrMessageTooLarge(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(fastrand.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.SendMessage(data) + } + + var tooLarge quic.ErrMessageTooLarge + if errors.As(err, &tooLarge) { + err = fragWriteNative(q.quicConn, packet, buf, int(tooLarge)-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 new file mode 100644 index 00000000..de51a080 --- /dev/null +++ b/transport/tuic/v5/protocol.go @@ -0,0 +1,583 @@ +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 socks5.AtypIPv4: + addrType = AtypIPv4 + addr = metadata.DstIP.AsSlice() + case socks5.AtypIPv6: + addrType = AtypIPv6 + addr = metadata.DstIP.AsSlice() + case socks5.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 new file mode 100644 index 00000000..c8170f62 --- /dev/null +++ b/transport/tuic/v5/server.go @@ -0,0 +1,229 @@ +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/v2" +) + +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.NewIntegerMapOf[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/websocket.go b/transport/v2ray-plugin/websocket.go index 7c2c8a88..1c7056d6 100644 --- a/transport/v2ray-plugin/websocket.go +++ b/transport/v2ray-plugin/websocket.go @@ -1,38 +1,41 @@ package obfs import ( + "context" "crypto/tls" "net" "net/http" - tlsC "github.com/Dreamacro/clash/component/tls" - "github.com/Dreamacro/clash/transport/vmess" + "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 + Host string + Port string + Path string + Headers map[string]string + TLS bool + SkipCertVerify bool + Fingerprint string + Mux bool + V2rayHttpUpgrade bool } // NewV2rayObfs return a HTTPObfs -func NewV2rayObfs(conn net.Conn, option *Option) (net.Conn, error) { +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, - Headers: header, + Host: option.Host, + Port: option.Port, + Path: option.Path, + V2rayHttpUpgrade: option.V2rayHttpUpgrade, + Headers: header, } if option.TLS { @@ -42,13 +45,10 @@ func NewV2rayObfs(conn net.Conn, option *Option) (net.Conn, error) { InsecureSkipVerify: option.SkipCertVerify, NextProtos: []string{"http/1.1"}, } - if len(option.Fingerprint) == 0 { - config.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } else { - var err error - if config.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil { - return nil, err - } + var err error + config.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + if err != nil { + return nil, err } if host := config.Headers.Get("Host"); host != "" { @@ -57,7 +57,7 @@ func NewV2rayObfs(conn net.Conn, option *Option) (net.Conn, error) { } var err error - conn, err = vmess.StreamWebsocketConn(conn, config) + conn, err = vmess.StreamWebsocketConn(ctx, conn, config) if err != nil { return nil, err } diff --git a/transport/vless/config.pb.go b/transport/vless/config.pb.go index 1407e4d7..14fcc5f9 100644 --- a/transport/vless/config.pb.go +++ b/transport/vless/config.pb.go @@ -108,7 +108,7 @@ func file_transport_vless_config_proto_rawDescGZIP() []byte { var file_transport_vless_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_transport_vless_config_proto_goTypes = []interface{}{ - (*Addons)(nil), // 0: clash.transport.vless.Addons + (*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 diff --git a/transport/vless/config.proto b/transport/vless/config.proto index 80900230..44cad479 100644 --- a/transport/vless/config.proto +++ b/transport/vless/config.proto @@ -1,9 +1,9 @@ syntax = "proto3"; -package clash.transport.vless; -option csharp_namespace = "Clash.Transport.Vless"; -option go_package = "github.com/Dreamacro/clash/transport/vless"; -option java_package = "com.clash.transport.vless"; +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 { diff --git a/transport/vless/conn.go b/transport/vless/conn.go index aceda463..02224892 100644 --- a/transport/vless/conn.go +++ b/transport/vless/conn.go @@ -3,103 +3,100 @@ package vless import ( "encoding/binary" "errors" - "fmt" "io" "net" "sync" - "time" - "github.com/Dreamacro/clash/common/buf" - N "github.com/Dreamacro/clash/common/net" + "github.com/metacubex/mihomo/common/buf" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/transport/vless/vision" - "github.com/gofrs/uuid" - xtls "github.com/xtls/go" + "github.com/gofrs/uuid/v5" "google.golang.org/protobuf/proto" ) type Conn struct { - N.ExtendedConn + N.ExtendedWriter + N.ExtendedReader + net.Conn dst *DstAddr id *uuid.UUID addons *Addons received bool - handshake chan struct{} handshakeMutex sync.Mutex + needHandshake bool err error } func (vc *Conn) Read(b []byte) (int, error) { if vc.received { - return vc.ExtendedConn.Read(b) + return vc.ExtendedReader.Read(b) } if err := vc.recvResponse(); err != nil { return 0, err } vc.received = true - return vc.ExtendedConn.Read(b) + return vc.ExtendedReader.Read(b) } func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { if vc.received { - return vc.ExtendedConn.ReadBuffer(buffer) + return vc.ExtendedReader.ReadBuffer(buffer) } if err := vc.recvResponse(); err != nil { return err } vc.received = true - return vc.ExtendedConn.ReadBuffer(buffer) + return vc.ExtendedReader.ReadBuffer(buffer) } func (vc *Conn) Write(p []byte) (int, error) { - select { - case <-vc.handshake: - default: - if vc.sendRequest(p) { + 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 } - return len(p), vc.err - } - if vc.err != nil { - return 0, vc.err } + vc.handshakeMutex.Unlock() } - return vc.ExtendedConn.Write(p) + + return vc.ExtendedWriter.Write(p) } func (vc *Conn) WriteBuffer(buffer *buf.Buffer) error { - select { - case <-vc.handshake: - default: - if vc.sendRequest(buffer.Bytes()) { - return vc.err - } - if vc.err != nil { - return vc.err + 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.ExtendedConn.WriteBuffer(buffer) + + return vc.ExtendedWriter.WriteBuffer(buffer) } func (vc *Conn) sendRequest(p []byte) bool { - vc.handshakeMutex.Lock() - defer vc.handshakeMutex.Unlock() - - select { - case <-vc.handshake: - // The handshake has been completed before. - // So return false to remind the caller. - return false - default: - } - defer close(vc.handshake) - - requestLen := 1 // protocol version - requestLen += 16 // UUID - requestLen += 1 // addons length var addonsBytes []byte if vc.addons != nil { addonsBytes, vc.err = proto.Marshal(vc.addons) @@ -107,19 +104,27 @@ func (vc *Conn) sendRequest(p []byte) bool { return true } } - 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.StackNewSize(requestLen) - defer buf.KeepAlive(_buffer) - buffer := buf.Dup(_buffer) - defer buffer.Release() + 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 @@ -146,74 +151,63 @@ func (vc *Conn) sendRequest(p []byte) bool { buf.Must(buf.Error(buffer.Write(p))) - _, vc.err = vc.ExtendedConn.Write(buffer.Bytes()) + _, vc.err = vc.ExtendedWriter.Write(buffer.Bytes()) return true } func (vc *Conn) recvResponse() error { - var buf [1]byte - _, vc.err = io.ReadFull(vc.ExtendedConn, buf[:]) + var buffer [2]byte + _, vc.err = io.ReadFull(vc.ExtendedReader, buffer[:]) if vc.err != nil { return vc.err } - if buf[0] != Version { + if buffer[0] != Version { return errors.New("unexpected response version") } - _, vc.err = io.ReadFull(vc.ExtendedConn, buf[:]) - if vc.err != nil { - return vc.err - } - - length := int64(buf[0]) + length := int64(buffer[1]) if length != 0 { // addon data length > 0 - io.CopyN(io.Discard, vc.ExtendedConn, length) // just discard + io.CopyN(io.Discard, vc.ExtendedReader, length) // just discard } return nil } func (vc *Conn) Upstream() any { - return vc.ExtendedConn + 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) (*Conn, error) { +func newConn(conn net.Conn, client *Client, dst *DstAddr) (net.Conn, error) { c := &Conn{ - ExtendedConn: N.NewExtendedConn(conn), - id: client.uuid, - dst: dst, - handshake: make(chan struct{}), + ExtendedReader: N.NewExtendedReader(conn), + ExtendedWriter: N.NewExtendedWriter(conn), + Conn: conn, + id: client.uuid, + dst: dst, + needHandshake: true, } - if !dst.UDP && client.Addons != nil { + if client.Addons != nil { switch client.Addons.Flow { - case XRO, XRD, XRS: - if xtlsConn, ok := conn.(*xtls.Conn); ok { - xtlsConn.RPRX = true - xtlsConn.SHOW = client.XTLSShow - xtlsConn.MARK = "XTLS" - if client.Addons.Flow == XRS { - client.Addons.Flow = XRD - } - - if client.Addons.Flow == XRD { - xtlsConn.DirectMode = true - } - c.addons = client.Addons - } else { - return nil, fmt.Errorf("failed to use %s, maybe \"security\" is not \"xtls\"", 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 } } - go func() { - select { - case <-c.handshake: - case <-time.After(200 * time.Millisecond): - c.sendRequest(nil) - } - }() return c, nil } diff --git a/transport/vless/vision/conn.go b/transport/vless/vision/conn.go new file mode 100644 index 00000000..79c77835 --- /dev/null +++ b/transport/vless/vision/conn.go @@ -0,0 +1,302 @@ +package vision + +import ( + "bytes" + "crypto/subtle" + gotls "crypto/tls" + "encoding/binary" + "fmt" + "io" + "net" + + "github.com/metacubex/mihomo/common/buf" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/log" + + "github.com/gofrs/uuid/v5" + utls "github.com/sagernet/utls" +) + +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.ReadFrom(vc.input) + if err != nil { + return err + } + if vc.input.Len() == 0 { + needReturn = true + vc.input = nil + } else { // buffer is full + return nil + } + } + if vc.rawInput != nil { + _, err := buffer.ReadFrom(vc.rawInput) + if err != nil { + 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 { + buffer := buf.New() + defer buffer.Release() + buffer.Write(p) + err := vc.WriteBuffer(buffer) + if err != nil { + return 0, err + } + return len(p), nil + } + 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 *utls.UConn: + if underlying.ConnectionState().Version != utls.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) 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 new file mode 100644 index 00000000..55b5663f --- /dev/null +++ b/transport/vless/vision/filter.go @@ -0,0 +1,90 @@ +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 new file mode 100644 index 00000000..e5f9dc85 --- /dev/null +++ b/transport/vless/vision/padding.go @@ -0,0 +1,81 @@ +package vision + +import ( + "bytes" + "encoding/binary" + + "github.com/metacubex/mihomo/common/buf" + "github.com/metacubex/mihomo/log" + + "github.com/gofrs/uuid/v5" + "github.com/zhangyunhao116/fastrand" +) + +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 = fastrand.Int31n(500) + 900 - contentLen + } else { + paddingLen = fastrand.Int31n(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 = fastrand.Int31n(500) + 900 - contentLen + } else { + paddingLen = fastrand.Int31n(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 new file mode 100644 index 00000000..09299b23 --- /dev/null +++ b/transport/vless/vision/vision.go @@ -0,0 +1,70 @@ +// 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/sagernet/sing/common" + utls "github.com/sagernet/utls" +) + +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 *utls.UConn: + //log.Debugln("type *utls.UConn") + c.Conn = underlying.NetConn() + c.tlsConn = underlying + t = reflect.TypeOf(underlying.Conn).Elem() + p = unsafe.Pointer(underlying.Conn) + case *tlsC.UConn: + //log.Debugln("type *tlsC.UConn") + c.Conn = underlying.NetConn() + c.tlsConn = underlying.UConn + 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 index 4b101703..ce07cdb4 100644 --- a/transport/vless/vless.go +++ b/transport/vless/vless.go @@ -3,15 +3,16 @@ package vless import ( "net" - "github.com/Dreamacro/clash/common/utils" + "github.com/metacubex/mihomo/common/utils" - "github.com/gofrs/uuid" + "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 ) @@ -41,9 +42,8 @@ type DstAddr struct { // Client is vless connection generator type Client struct { - uuid *uuid.UUID - Addons *Addons - XTLSShow bool + uuid *uuid.UUID + Addons *Addons } // StreamConn return a Conn with net.Conn and DstAddr @@ -52,15 +52,14 @@ func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) { } // NewClient return Client instance -func NewClient(uuidStr string, addons *Addons, xtlsShow bool) (*Client, error) { +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, - XTLSShow: xtlsShow, + uuid: &uid, + Addons: addons, }, nil } diff --git a/transport/vless/xtls.go b/transport/vless/xtls.go deleted file mode 100644 index a1aea44f..00000000 --- a/transport/vless/xtls.go +++ /dev/null @@ -1,41 +0,0 @@ -package vless - -import ( - "context" - "net" - - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" - xtls "github.com/xtls/go" -) - -type XTLSConfig struct { - Host string - SkipCertVerify bool - Fingerprint string - NextProtos []string -} - -func StreamXTLSConn(conn net.Conn, cfg *XTLSConfig) (net.Conn, error) { - xtlsConfig := &xtls.Config{ - ServerName: cfg.Host, - InsecureSkipVerify: cfg.SkipCertVerify, - NextProtos: cfg.NextProtos, - } - if len(cfg.Fingerprint) == 0 { - xtlsConfig = tlsC.GetGlobalXTLSConfig(xtlsConfig) - } else { - var err error - if xtlsConfig, err = tlsC.GetSpecifiedFingerprintXTLSConfig(xtlsConfig, cfg.Fingerprint); err != nil { - return nil, err - } - } - - xtlsConn := xtls.Client(conn, xtlsConfig) - - // fix xtls handshake not timeout - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() - err := xtlsConn.HandshakeContext(ctx) - return xtlsConn, err -} diff --git a/transport/vmess/aead.go b/transport/vmess/aead.go index d4fbf2d9..89ec6a3b 100644 --- a/transport/vmess/aead.go +++ b/transport/vmess/aead.go @@ -7,7 +7,7 @@ import ( "io" "sync" - "github.com/Dreamacro/clash/common/pool" + "github.com/metacubex/mihomo/common/pool" ) type aeadWriter struct { diff --git a/transport/vmess/chunk.go b/transport/vmess/chunk.go index ab1adb6d..f52fc82c 100644 --- a/transport/vmess/chunk.go +++ b/transport/vmess/chunk.go @@ -5,7 +5,7 @@ import ( "errors" "io" - "github.com/Dreamacro/clash/common/pool" + "github.com/metacubex/mihomo/common/pool" ) const ( diff --git a/transport/vmess/conn.go b/transport/vmess/conn.go index cc3155ee..292137ab 100644 --- a/transport/vmess/conn.go +++ b/transport/vmess/conn.go @@ -11,17 +11,13 @@ import ( "errors" "hash/fnv" "io" - "math/rand" "net" "time" + "github.com/zhangyunhao116/fastrand" "golang.org/x/crypto/chacha20poly1305" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - // Conn wrapper a net.Conn with vmess protocol type Conn struct { net.Conn @@ -76,7 +72,7 @@ func (vc *Conn) sendRequest() error { buf.WriteByte(vc.respV) buf.WriteByte(OptionChunkStream) - p := rand.Intn(16) + p := fastrand.Intn(16) // P Sec Reserve Cmd buf.WriteByte(byte(p<<4) | byte(vc.security)) buf.WriteByte(0) @@ -94,7 +90,7 @@ func (vc *Conn) sendRequest() error { // padding if p > 0 { padding := make([]byte, p) - rand.Read(padding) + fastrand.Read(padding) buf.Write(padding) } @@ -200,7 +196,7 @@ func hashTimestamp(t time.Time) []byte { // 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) + fastrand.Read(randBytes) reqBodyIV := make([]byte, 16) reqBodyKey := make([]byte, 16) copy(reqBodyIV[:], randBytes[:16]) diff --git a/transport/vmess/h2.go b/transport/vmess/h2.go index d4e81607..f91c2766 100644 --- a/transport/vmess/h2.go +++ b/transport/vmess/h2.go @@ -1,12 +1,13 @@ package vmess import ( + "context" "io" - "math/rand" "net" "net/http" "net/url" + "github.com/zhangyunhao116/fastrand" "golang.org/x/net/http2" ) @@ -26,7 +27,7 @@ type H2Config struct { func (hc *h2Conn) establishConn() error { preader, pwriter := io.Pipe() - host := hc.cfg.Hosts[rand.Intn(len(hc.cfg.Hosts))] + host := hc.cfg.Hosts[fastrand.Intn(len(hc.cfg.Hosts))] path := hc.cfg.Path // TODO: connect use VMess Host instead of H2 Host req := http.Request{ @@ -84,10 +85,16 @@ func (hc *h2Conn) Write(b []byte) (int, error) { } func (hc *h2Conn) Close() error { - if err := hc.pwriter.Close(); err != nil { - return err + if hc.pwriter != nil { + if err := hc.pwriter.Close(); err != nil { + return err + } } - if err := hc.ClientConn.Shutdown(hc.res.Request.Context()); err != nil { + 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() diff --git a/transport/vmess/http.go b/transport/vmess/http.go index 1c09e215..782e7eb2 100644 --- a/transport/vmess/http.go +++ b/transport/vmess/http.go @@ -4,10 +4,13 @@ import ( "bufio" "bytes" "fmt" - "math/rand" "net" "net/http" "net/textproto" + + "github.com/metacubex/mihomo/common/util" + + "github.com/zhangyunhao116/fastrand" ) type httpConn struct { @@ -51,16 +54,16 @@ func (hc *httpConn) Write(b []byte) (int, error) { return hc.Conn.Write(b) } - path := hc.cfg.Path[rand.Intn(len(hc.cfg.Path))] + path := hc.cfg.Path[fastrand.Intn(len(hc.cfg.Path))] host := hc.cfg.Host if header := hc.cfg.Headers["Host"]; len(header) != 0 { - host = header[rand.Intn(len(header))] + host = header[fastrand.Intn(len(header))] } u := fmt.Sprintf("http://%s%s", host, path) - req, _ := http.NewRequest("GET", u, bytes.NewBuffer(b)) + req, _ := http.NewRequest(util.EmptyOr(hc.cfg.Method, http.MethodGet), u, bytes.NewBuffer(b)) for key, list := range hc.cfg.Headers { - req.Header.Set(key, list[rand.Intn(len(list))]) + req.Header.Set(key, list[fastrand.Intn(len(list))]) } req.ContentLength = int64(len(b)) if err := req.Write(hc.Conn); err != nil { diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go index 711c342d..bdaa8ccc 100644 --- a/transport/vmess/tls.go +++ b/transport/vmess/tls.go @@ -3,10 +3,11 @@ package vmess import ( "context" "crypto/tls" + "errors" "net" - tlsC "github.com/Dreamacro/clash/component/tls" - C "github.com/Dreamacro/clash/constant" + "github.com/metacubex/mihomo/component/ca" + tlsC "github.com/metacubex/mihomo/component/tls" ) type TLSConfig struct { @@ -15,44 +16,44 @@ type TLSConfig struct { FingerPrint string ClientFingerprint string NextProtos []string + Reality *tlsC.RealityConfig } -func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { +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, } - if len(cfg.FingerPrint) == 0 { - tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } else { - var err error - if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, cfg.FingerPrint); err != nil { - return nil, err - } + var err error + tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, cfg.FingerPrint) + if err != nil { + return nil, err } if len(cfg.ClientFingerprint) != 0 { - utlsConn, valid := GetUtlsConnWithClientFingerprint(conn, cfg.ClientFingerprint, tlsConfig) - if valid { - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() - - err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx) - return utlsConn, err + if cfg.Reality == nil { + utlsConn, valid := GetUTLSConn(conn, cfg.ClientFingerprint, tlsConfig) + if valid { + err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx) + return utlsConn, err + } + } else { + return tlsC.GetRealityConn(ctx, conn, cfg.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) - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) - defer cancel() - - err := tlsConn.HandshakeContext(ctx) + err = tlsConn.HandshakeContext(ctx) return tlsConn, err } -func GetUtlsConnWithClientFingerprint(conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config) (net.Conn, bool) { +func GetUTLSConn(conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config) (net.Conn, bool) { if fingerprint, exists := tlsC.GetFingerprint(ClientFingerprint); exists { utlsConn := tlsC.UClient(conn, tlsConfig, fingerprint) diff --git a/transport/vmess/user.go b/transport/vmess/user.go index c098389e..091df0a8 100644 --- a/transport/vmess/user.go +++ b/transport/vmess/user.go @@ -4,7 +4,7 @@ import ( "bytes" "crypto/md5" - "github.com/gofrs/uuid" + "github.com/gofrs/uuid/v5" ) // ID cmdKey length diff --git a/transport/vmess/vmess.go b/transport/vmess/vmess.go index d7c8edb4..7c587c6a 100644 --- a/transport/vmess/vmess.go +++ b/transport/vmess/vmess.go @@ -2,12 +2,13 @@ package vmess import ( "fmt" - "github.com/Dreamacro/clash/common/utils" - "math/rand" "net" "runtime" - "github.com/gofrs/uuid" + "github.com/metacubex/mihomo/common/utils" + + "github.com/gofrs/uuid/v5" + "github.com/zhangyunhao116/fastrand" ) // Version of vmess @@ -77,7 +78,7 @@ type Config struct { // StreamConn return a Conn with net.Conn and DstAddr func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) { - r := rand.Intn(len(c.user)) + r := fastrand.Intn(len(c.user)) return newConn(conn, c.user[r], dst, c.security, c.isAead) } diff --git a/transport/vmess/websocket.go b/transport/vmess/websocket.go index 71dabbdd..b30bb8aa 100644 --- a/transport/vmess/websocket.go +++ b/transport/vmess/websocket.go @@ -1,40 +1,40 @@ package vmess import ( + "bufio" "bytes" "context" + "crypto/sha1" "crypto/tls" "encoding/base64" "encoding/binary" "errors" "fmt" - "io" - "math/rand" "net" "net/http" "net/url" "strconv" "strings" - "sync" "time" - "github.com/Dreamacro/clash/common/buf" - N "github.com/Dreamacro/clash/common/net" - tlsC "github.com/Dreamacro/clash/component/tls" - "github.com/gorilla/websocket" + "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/zhangyunhao116/fastrand" ) type websocketConn struct { - conn *websocket.Conn - reader io.Reader - remoteAddr net.Addr + net.Conn + state ws.State + reader *wsutil.Reader + controlHandler wsutil.FrameHandlerFunc rawWriter N.ExtendedWriter - - // https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency - rMux sync.Mutex - wMux sync.Mutex } type websocketWithEarlyDataConn struct { @@ -58,35 +58,52 @@ type WebsocketConfig struct { MaxEarlyData int EarlyDataHeaderName string ClientFingerprint string + V2rayHttpUpgrade bool } // Read implements net.Conn.Read() -func (wsc *websocketConn) Read(b []byte) (int, error) { - wsc.rMux.Lock() - defer wsc.rMux.Unlock() +// modify from gobwas/ws/wsutil.readData +func (wsc *websocketConn) Read(b []byte) (n int, err error) { + var header ws.Header for { - reader, err := wsc.getReader() - if err != nil { - return 0, err + 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 } - - nBytes, err := reader.Read(b) - if err == io.EOF { - wsc.reader = 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 } - return nBytes, err } } // Write implements io.Writer. -func (wsc *websocketConn) Write(b []byte) (int, error) { - wsc.wMux.Lock() - defer wsc.wMux.Unlock() - if err := wsc.conn.WriteMessage(websocket.BinaryMessage, b); err != nil { - return 0, err +func (wsc *websocketConn) Write(b []byte) (n int, err error) { + err = wsutil.WriteMessage(wsc.Conn, wsc.state, ws.OpBinary, b) + if err != nil { + return } - return len(b), nil + n = len(b) + return } func (wsc *websocketConn) WriteBuffer(buffer *buf.Buffer) error { @@ -104,11 +121,17 @@ func (wsc *websocketConn) WriteBuffer(buffer *buf.Buffer) error { var headerLen int headerLen += 1 // FIN / RSV / OPCODE headerLen += payloadBitLength - headerLen += 4 // MASK KEY + if wsc.state.ClientSide() { + headerLen += 4 // MASK KEY + } header := buffer.ExtendHeader(headerLen) - header[0] = websocket.BinaryMessage | 1<<7 - header[1] = 1 << 7 + 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) @@ -120,12 +143,12 @@ func (wsc *websocketConn) WriteBuffer(buffer *buf.Buffer) error { binary.BigEndian.PutUint64(header[2:], uint64(dataLen)) } - maskKey := rand.Uint32() - binary.LittleEndian.PutUint32(header[1+payloadBitLength:], maskKey) - N.MaskWebSocket(maskKey, data) + if wsc.state.ClientSide() { + maskKey := fastrand.Uint32() + binary.LittleEndian.PutUint32(header[1+payloadBitLength:], maskKey) + N.MaskWebSocket(maskKey, data) + } - wsc.wMux.Lock() - defer wsc.wMux.Unlock() return wsc.rawWriter.WriteBuffer(buffer) } @@ -134,76 +157,33 @@ func (wsc *websocketConn) FrontHeadroom() int { } func (wsc *websocketConn) Upstream() any { - return wsc.conn.UnderlyingConn() + return wsc.Conn } func (wsc *websocketConn) Close() error { - var e []string - if err := wsc.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second*5)); err != nil { - e = append(e, err.Error()) - } - if err := wsc.conn.Close(); err != nil { - e = append(e, err.Error()) - } - if len(e) > 0 { - return fmt.Errorf("failed to close connection: %s", strings.Join(e, ",")) - } + _ = 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 (wsc *websocketConn) getReader() (io.Reader, error) { - if wsc.reader != nil { - return wsc.reader, nil - } - - _, reader, err := wsc.conn.NextReader() - if err != nil { - return nil, err - } - wsc.reader = reader - return reader, nil -} - -func (wsc *websocketConn) LocalAddr() net.Addr { - return wsc.conn.LocalAddr() -} - -func (wsc *websocketConn) RemoteAddr() net.Addr { - return wsc.remoteAddr -} - -func (wsc *websocketConn) SetDeadline(t time.Time) error { - if err := wsc.SetReadDeadline(t); err != nil { - return err - } - return wsc.SetWriteDeadline(t) -} - -func (wsc *websocketConn) SetReadDeadline(t time.Time) error { - return wsc.conn.SetReadDeadline(t) -} - -func (wsc *websocketConn) SetWriteDeadline(t time.Time) error { - return wsc.conn.SetWriteDeadline(t) -} - func (wsedc *websocketWithEarlyDataConn) Dial(earlyData []byte) error { base64DataBuf := &bytes.Buffer{} base64EarlyDataEncoder := base64.NewEncoder(base64.RawURLEncoding, base64DataBuf) earlyDataBuf := bytes.NewBuffer(earlyData) if _, err := base64EarlyDataEncoder.Write(earlyDataBuf.Next(wsedc.config.MaxEarlyData)); err != nil { - return errors.New("failed to encode early data: " + err.Error()) + return fmt.Errorf("failed to encode early data: %w", err) } if errc := base64EarlyDataEncoder.Close(); errc != nil { - return errors.New("failed to encode early data tail: " + errc.Error()) + return fmt.Errorf("failed to encode early data tail: %w", errc) } var err error - if wsedc.Conn, err = streamWebsocketConn(wsedc.underlay, wsedc.config, base64DataBuf); err != nil { + if wsedc.Conn, err = streamWebsocketConn(wsedc.ctx, wsedc.underlay, wsedc.config, base64DataBuf); err != nil { wsedc.Close() - return errors.New("failed to dial WebSocket: " + err.Error()) + return fmt.Errorf("failed to dial WebSocket: %w", err) } wsedc.dialed <- true @@ -301,15 +281,27 @@ func (wsedc *websocketWithEarlyDataConn) SetWriteDeadline(t time.Time) error { return wsedc.Conn.SetWriteDeadline(t) } -func (wsedc *websocketWithEarlyDataConn) LazyHeadroom() bool { - return wsedc.Conn == nil +func (wsedc *websocketWithEarlyDataConn) FrontHeadroom() int { + return 14 } 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 + 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) { @@ -321,82 +313,150 @@ func streamWebsocketWithEarlyDataConn(conn net.Conn, c *WebsocketConfig) (net.Co underlay: conn, config: c, } - return conn, nil + // 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(conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buffer) (net.Conn, error) { - - dialer := &websocket.Dialer{ - NetDial: func(network, addr string) (net.Conn, error) { - return conn, nil - }, - ReadBufferSize: 4 * 1024, - WriteBufferSize: 4 * 1024, - HandshakeTimeout: time.Second * 8, - } - - scheme := "ws" - if c.TLS { - scheme = "wss" - dialer.TLSClientConfig = c.TLSConfig - if len(c.ClientFingerprint) != 0 { - if fingerprint, exists := tlsC.GetFingerprint(c.ClientFingerprint); exists { - dialer.NetDialTLSContext = func(_ context.Context, _, addr string) (net.Conn, error) { - utlsConn := tlsC.UClient(conn, c.TLSConfig, fingerprint) - - if err := utlsConn.(*tlsC.UConn).WebsocketHandshake(); err != nil { - return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) - } - return utlsConn, nil - } - } - } - } - +func streamWebsocketConn(ctx context.Context, conn net.Conn, c *WebsocketConfig, earlyData *bytes.Buffer) (net.Conn, 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: scheme, + Scheme: "ws", Host: net.JoinHostPort(c.Host, c.Port), Path: u.Path, RawQuery: u.RawQuery, } - headers := http.Header{} - if c.Headers != nil { - for k := range c.Headers { - headers.Add(k, c.Headers.Get(k)) + if 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 = uri.Host + } + + if len(c.ClientFingerprint) != 0 { + if fingerprint, exists := tlsC.GetFingerprint(c.ClientFingerprint); exists { + utlsConn := tlsC.UClient(conn, config, fingerprint) + if err = utlsConn.BuildWebsocketHandshakeState(); err != nil { + return nil, fmt.Errorf("parse url %s error: %w", c.Path, err) + } + conn = utlsConn + } + } else { + conn = tls.Client(conn, config) + } + + if tlsConn, ok := conn.(interface { + HandshakeContext(ctx context.Context) error + }); ok { + if err = tlsConn.HandshakeContext(ctx); err != nil { + return nil, err + } + } + } + + 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 = fastrand.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 += earlyData.String() + uri.Path += earlyDataString } else { - headers.Set(c.EarlyDataHeaderName, earlyData.String()) + request.Header.Set(c.EarlyDataHeaderName, earlyDataString) } } - wsConn, resp, err := dialer.Dial(uri.String(), headers) + if ctx.Done() != nil { + done := N.SetupContextForConn(ctx, conn) + defer done(&err) + } + + err = request.Write(conn) if err != nil { - reason := err.Error() - if resp != nil { - reason = resp.Status - } - return nil, fmt.Errorf("dial %s error: %s", uri.Host, reason) + return nil, err + } + bufferedConn := N.NewBufferedConn(conn) + 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) } - return &websocketConn{ - conn: wsConn, - rawWriter: N.NewExtendedWriter(wsConn.UnderlyingConn()), - remoteAddr: conn.RemoteAddr(), - }, nil + 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(conn, ws.StateClientSide) + // websocketConn can't correct handle ReadDeadline + // so call N.NewDeadlineConn to add a safe wrapper + return N.NewDeadlineConn(conn), nil } -func StreamWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) { +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 { @@ -413,5 +473,89 @@ func StreamWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) { return streamWebsocketWithEarlyDataConn(conn, c) } - return streamWebsocketConn(conn, c, nil) + 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 { + 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 { + conn = N.NewCachedConn(conn, edBuf) + } + + return conn, nil } diff --git a/tunnel/connection.go b/tunnel/connection.go index bd8d1b63..33cc4e8d 100644 --- a/tunnel/connection.go +++ b/tunnel/connection.go @@ -6,15 +6,12 @@ import ( "net/netip" "time" - N "github.com/Dreamacro/clash/common/net" - "github.com/Dreamacro/clash/common/pool" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" ) func handleUDPToRemote(packet C.UDPPacket, pc C.PacketConn, metadata *C.Metadata) error { - defer packet.Drop() - addr := metadata.UDPAddr() if addr == nil { return errors.New("udp addr invalid") @@ -29,33 +26,46 @@ func handleUDPToRemote(packet C.UDPPacket, pc C.PacketConn, metadata *C.Metadata return nil } -func handleUDPToLocal(packet C.UDPPacket, pc net.PacketConn, key string, oAddr, fAddr netip.Addr) { - buf := pool.Get(pool.UDPBufferSize) +func handleUDPToLocal(writeBack C.WriteBack, pc N.EnhancePacketConn, key string, oAddrPort netip.AddrPort, fAddr netip.Addr) { defer func() { _ = pc.Close() closeAllLocalCoon(key) natTable.Delete(key) - _ = pool.Put(buf) }() for { _ = pc.SetReadDeadline(time.Now().Add(udpTimeout)) - n, from, err := pc.ReadFrom(buf) + data, put, from, err := pc.WaitReadFrom() if err != nil { return } - fromUDPAddr := from.(*net.UDPAddr) - fromUDPAddr = &(*fromUDPAddr) // make a copy - if fromAddr, ok := netip.AddrFromSlice(fromUDPAddr.IP); ok { - if fAddr.IsValid() && (oAddr.Unmap() == fromAddr.Unmap()) { - fromUDPAddr.IP = fAddr.Unmap().AsSlice() - } else { - fromUDPAddr.IP = fromAddr.Unmap().AsSlice() + 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 = packet.WriteBack(buf[:n], fromUDPAddr) + _, err = writeBack.WriteBack(data, fromUDPAddr) + if put != nil { + put() + } if err != nil { return } @@ -63,12 +73,9 @@ func handleUDPToLocal(packet C.UDPPacket, pc net.PacketConn, key string, oAddr, } func closeAllLocalCoon(lAddr string) { - natTable.RangeLocalConn(lAddr, func(key, value any) bool { - conn, ok := value.(*net.UDPConn) - if !ok || conn == nil { - log.Debugln("Value %#v unknown value when closing TProxy local conn...", conn) - return true - } + 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 diff --git a/tunnel/statistic/manager.go b/tunnel/statistic/manager.go index e67d3871..8e962dae 100644 --- a/tunnel/statistic/manager.go +++ b/tunnel/statistic/manager.go @@ -1,45 +1,65 @@ package statistic import ( - "sync" + "os" "time" - "go.uber.org/atomic" + "github.com/metacubex/mihomo/common/atomic" + + "github.com/puzpuzpuz/xsync/v2" + "github.com/shirou/gopsutil/v3/process" ) var DefaultManager *Manager func init() { DefaultManager = &Manager{ + connections: xsync.NewMapOf[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 sync.Map - uploadTemp *atomic.Int64 - downloadTemp *atomic.Int64 - uploadBlip *atomic.Int64 - downloadBlip *atomic.Int64 - uploadTotal *atomic.Int64 - downloadTotal *atomic.Int64 + 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) { +func (m *Manager) Join(c Tracker) { m.connections.Store(c.ID(), c) } -func (m *Manager) Leave(c tracker) { +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) @@ -54,20 +74,33 @@ 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 { - connections := []tracker{} - m.connections.Range(func(key, value any) bool { - connections = append(connections, value.(tracker)) + 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) @@ -89,7 +122,8 @@ func (m *Manager) handle() { } type Snapshot struct { - DownloadTotal int64 `json:"downloadTotal"` - UploadTotal int64 `json:"uploadTotal"` - Connections []tracker `json:"connections"` + DownloadTotal int64 `json:"downloadTotal"` + UploadTotal int64 `json:"uploadTotal"` + Connections []*TrackerInfo `json:"connections"` + Memory uint64 `json:"memory"` } diff --git a/tunnel/statistic/tracker.go b/tunnel/statistic/tracker.go index 97dd7316..0bf7995d 100644 --- a/tunnel/statistic/tracker.go +++ b/tunnel/statistic/tracker.go @@ -1,77 +1,112 @@ package statistic import ( + "io" "net" + "net/netip" "time" - "github.com/Dreamacro/clash/common/buf" - N "github.com/Dreamacro/clash/common/net" - C "github.com/Dreamacro/clash/constant" + "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" - "go.uber.org/atomic" + "github.com/gofrs/uuid/v5" ) -type tracker interface { +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 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 - extendedReader N.ExtendedReader - extendedWriter N.ExtendedWriter + *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) - tt.manager.PushDownloaded(download) + 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.extendedReader.ReadBuffer(buffer) + err = tt.Conn.ReadBuffer(buffer) download := int64(buffer.Len()) - tt.manager.PushDownloaded(download) + 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) - tt.manager.PushUploaded(upload) + 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.extendedWriter.WriteBuffer(buffer) - tt.manager.PushUploaded(upload) + 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() @@ -81,35 +116,53 @@ func (tt *tcpTracker) Upstream() any { return tt.Conn } -func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule) *tcpTracker { - uuid, _ := uuid.NewV4() - if conn != nil { - if tcpAddr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { - metadata.RemoteDst = tcpAddr.IP.String() +func parseRemoteDestination(addr net.Addr, conn C.Connection) string { + if addr == nil && conn != nil { + return conn.RemoteDestination() + } + if addrPort, err := netip.ParseAddrPort(addr.String()); err == nil && addrPort.Addr().IsValid() { + return addrPort.Addr().String() + } else { + if conn != nil { + return conn.RemoteDestination() } else { - metadata.RemoteDst = conn.RemoteDestination() + return "" } } +} + +func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule, uploadTotal int64, downloadTotal int64, pushToManager bool) *tcpTracker { + if conn != nil { + metadata.RemoteDst = parseRemoteDestination(conn.RemoteAddr(), conn) + } t := &tcpTracker{ Conn: conn, manager: manager, - trackerInfo: &trackerInfo{ - UUID: uuid, + TrackerInfo: &TrackerInfo{ + UUID: utils.NewUUIDV4(), Start: time.Now(), Metadata: metadata, Chain: conn.Chains(), Rule: "", - UploadTotal: atomic.NewInt64(0), - DownloadTotal: atomic.NewInt64(0), + UploadTotal: atomic.NewInt64(uploadTotal), + DownloadTotal: atomic.NewInt64(downloadTotal), }, - extendedReader: N.NewExtendedReader(conn), - extendedWriter: N.NewExtendedWriter(conn), + 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() + t.TrackerInfo.Rule = rule.RuleType().String() + t.TrackerInfo.RulePayload = rule.Payload() } manager.Join(t) @@ -118,26 +171,46 @@ func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.R type udpTracker struct { C.PacketConn `json:"-"` - *trackerInfo + *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) - ut.manager.PushDownloaded(download) + 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) - ut.manager.PushUploaded(upload) + if ut.pushToManager { + ut.manager.PushUploaded(upload) + } ut.UploadTotal.Add(upload) return n, err } @@ -147,27 +220,40 @@ func (ut *udpTracker) Close() error { return ut.PacketConn.Close() } -func NewUDPTracker(conn C.PacketConn, manager *Manager, metadata *C.Metadata, rule C.Rule) *udpTracker { - uuid, _ := uuid.NewV4() - metadata.RemoteDst = conn.RemoteDestination() +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: uuid, + TrackerInfo: &TrackerInfo{ + UUID: utils.NewUUIDV4(), Start: time.Now(), Metadata: metadata, Chain: conn.Chains(), Rule: "", - UploadTotal: atomic.NewInt64(0), - DownloadTotal: atomic.NewInt64(0), + 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() + ut.TrackerInfo.Rule = rule.RuleType().String() + ut.TrackerInfo.RulePayload = rule.Payload() } manager.Join(ut) diff --git a/tunnel/status.go b/tunnel/status.go new file mode 100644 index 00000000..d81dd45e --- /dev/null +++ b/tunnel/status.go @@ -0,0 +1,92 @@ +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 +) + +// 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 mode") + } + *s = status + return nil +} + +// 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 +} + +// MarshalJSON serialize Status +func (s TunnelStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +// MarshalYAML serialize TunnelMode with yaml +func (s TunnelStatus) MarshalYAML() (any, error) { + return 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 index 695f2945..596e26d7 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -7,24 +7,25 @@ import ( "net/netip" "path/filepath" "runtime" - "strconv" "sync" "time" "github.com/jpillora/backoff" - "github.com/Dreamacro/clash/component/nat" - P "github.com/Dreamacro/clash/component/process" - "github.com/Dreamacro/clash/component/resolver" - "github.com/Dreamacro/clash/component/sniffer" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/constant/provider" - icontext "github.com/Dreamacro/clash/context" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel/statistic" + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/nat" + P "github.com/metacubex/mihomo/component/process" + "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/component/sniffer" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/provider" + icontext "github.com/metacubex/mihomo/context" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/tunnel/statistic" ) var ( + status = newAtomicStatus(Suspend) tcpQueue = make(chan C.ConnContext, 200) udpQueue = make(chan C.PacketAdapter, 200) natTable = nat.New() @@ -48,6 +49,43 @@ var ( fakeIPRange netip.Prefix ) +type tunnel struct{} + +var Tunnel C.Tunnel = tunnel{} + +func (t tunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) { + connCtx := icontext.NewConnContext(conn, metadata) + handleTCPConn(connCtx) +} + +func (t tunnel) HandleUDPPacket(packet C.UDPPacket, metadata *C.Metadata) { + packetAdapter := C.NewPacketAdapter(packet, metadata) + select { + case udpQueue <- packetAdapter: + default: + } +} + +func (t tunnel) NatTable() C.NatTable { + return natTable +} + +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 } @@ -73,11 +111,13 @@ func init() { } // TCPIn return fan-in queue +// Deprecated: using Tunnel instead func TCPIn() chan<- C.ConnContext { return tcpQueue } // UDPIn return fan-in udp queue +// Deprecated: using Tunnel instead func UDPIn() chan<- C.PacketAdapter { return udpQueue } @@ -110,6 +150,20 @@ 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 @@ -157,6 +211,11 @@ func SetFindProcessMode(mode P.FindProcessMode) { findProcessMode = mode } +func isHandle(t C.Type) bool { + status := status.Load() + return status == Running || (status == Inner && t == C.INNER) +} + // processUDP starts a loop to handle udp packet func processUDP() { queue := udpQueue @@ -200,19 +259,24 @@ func preHandleMetadata(metadata *C.Metadata) error { if resolver.FakeIPEnabled() { metadata.DstIP = netip.Addr{} metadata.DNSMode = C.DNSFakeIP - } else if node := resolver.DefaultHosts.Search(host); node != nil { + } else if node, ok := resolver.DefaultHosts.Search(host, false); ok { // redir-host should lookup the hosts - metadata.DstIP = node.Data() + 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(ctx C.PlainContext, metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { +func resolveMetadata(metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { if metadata.SpecialProxy != "" { var exist bool proxy, exist = proxies[metadata.SpecialProxy] @@ -235,8 +299,14 @@ func resolveMetadata(ctx C.PlainContext, metadata *C.Metadata) (proxy C.Proxy, r } 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 } @@ -248,14 +318,20 @@ func handleUDPConn(packet C.PacketAdapter) { } if err := preHandleMetadata(metadata); err != nil { + packet.Drop() log.Debugln("[Metadata PreHandle] error: %s", err) return } + if sniffer.Dispatcher.Enable() && sniffingEnable { + sniffer.Dispatcher.UDPSniff(packet) + } + // local resolve UDP dns if !metadata.Resolved() { ip, err := resolver.ResolveIP(context.Background(), metadata.Host) if err != nil { + packet.Drop() return } metadata.DstIP = ip @@ -264,8 +340,11 @@ func handleUDPConn(packet C.PacketAdapter) { key := packet.LocalAddr().String() handle := func() bool { - pc := natTable.Get(key) + pc, proxy := natTable.Get(key) if pc != nil { + if proxy != nil { + proxy.UpdateWriteBack(packet) + } _ = handleUDPToRemote(packet, pc, metadata) return true } @@ -273,13 +352,15 @@ func handleUDPConn(packet C.PacketAdapter) { } if handle() { + packet.Drop() return } - lockKey := key + "-lock" - cond, loaded := natTable.GetOrCreateLock(lockKey) + cond, loaded := natTable.GetOrCreateLock(key) go func() { + defer packet.Drop() + if loaded { cond.L.Lock() cond.Wait() @@ -289,12 +370,12 @@ func handleUDPConn(packet C.PacketAdapter) { } defer func() { - natTable.Delete(lockKey) + natTable.DeleteLock(key) cond.Broadcast() }() pCtx := icontext.NewPacketConnContext(metadata) - proxy, rule, err := resolveMetadata(pCtx, metadata) + proxy, rule, err := resolveMetadata(metadata) if err != nil { log.Warnln("[UDP] Parse metadata failed: %s", err.Error()) return @@ -322,7 +403,7 @@ func handleUDPConn(packet C.PacketAdapter) { } pCtx.InjectPacketConn(rawPc) - pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule) + pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule, 0, 0, true) switch true { case metadata.SpecialProxy != "": @@ -341,16 +422,22 @@ func handleUDPConn(packet C.PacketAdapter) { log.Infoln("[UDP] %s --> %s doesn't match any rule using DIRECT", metadata.SourceDetail(), metadata.RemoteAddress()) } - oAddr := metadata.DstIP - natTable.Set(key, pc) + oAddrPort := metadata.AddrPort() + writeBackProxy := nat.NewWriteBackProxy(packet) + natTable.Set(key, pc, writeBackProxy) - go handleUDPToLocal(packet, pc, key, oAddr, fAddr) + go handleUDPToLocal(writeBackProxy, pc, key, oAddrPort, fAddr) handle() }() } func handleTCPConn(connCtx C.ConnContext) { + if !isHandle(connCtx.Metadata().Type) { + _ = connCtx.Conn().Close() + return + } + defer func(conn net.Conn) { _ = conn.Close() }(connCtx.Conn()) @@ -361,16 +448,42 @@ func handleTCPConn(connCtx C.ConnContext) { 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 sniffer.Dispatcher.Enable() && sniffingEnable { + // 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 sniffer.Dispatcher.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 } - if sniffer.Dispatcher.Enable() && sniffingEnable { - sniffer.Dispatcher.TCPSniff(connCtx.Conn(), metadata) + 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(connCtx, metadata) + proxy, rule, err := resolveMetadata(metadata) if err != nil { log.Warnln("[Metadata] parse failed: %s", err.Error()) return @@ -378,8 +491,8 @@ func handleTCPConn(connCtx C.ConnContext) { dialMetadata := metadata if len(metadata.Host) > 0 { - if node := resolver.DefaultHosts.Search(metadata.Host); node != nil { - if dstIp := node.Data(); !FakeIPRange().Contains(dstIp) { + 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() @@ -387,10 +500,41 @@ func handleTCPConn(connCtx C.ConnContext) { } } + var peekBytes []byte + var peekLen int + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) defer cancel() - remoteConn, err := retry(ctx, func(ctx context.Context) (C.Conn, error) { - return proxy.DialContext(ctx, dialMetadata) + 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) { if rule == nil { log.Warnln( @@ -408,7 +552,7 @@ func handleTCPConn(connCtx C.ConnContext) { return } - remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule) + remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule, 0, int64(peekLen), true) defer func(remoteConn C.Conn) { _ = remoteConn.Close() }(remoteConn) @@ -434,6 +578,10 @@ func handleTCPConn(connCtx C.ConnContext) { ) } + _ = conn.SetReadDeadline(time.Now()) // stop unfinished peek + peekMutex.Lock() + defer peekMutex.Unlock() + _ = conn.SetReadDeadline(time.Time{}) // reset handleSocket(connCtx, remoteConn) } @@ -445,12 +593,12 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { configMux.RLock() defer configMux.RUnlock() var ( - resolved bool - processFound bool + resolved bool + attemptProcessLookup = true ) - if node := resolver.DefaultHosts.Search(metadata.Host); node != nil { - metadata.DstIP = node.Data() + if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok { + metadata.DstIP, _ = node.RandIP() resolved = true } @@ -470,16 +618,15 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { }() } - if !findProcessMode.Off() && !processFound && (findProcessMode.Always() || rule.ShouldFindProcess()) { - srcPort, err := strconv.ParseUint(metadata.SrcPort, 10, 16) - uid, path, err := P.FindProcessName(metadata.NetWork.String(), metadata.SrcIP, int(srcPort)) + if attemptProcessLookup && !findProcessMode.Off() && (findProcessMode.Always() || rule.ShouldFindProcess()) { + attemptProcessLookup = false + uid, path, err := P.FindProcessName(metadata.NetWork.String(), metadata.SrcIP, int(metadata.SrcPort)) if err != nil { log.Debugln("[Process] find process %s: %v", metadata.String(), err) } else { metadata.Process = filepath.Base(path) metadata.ProcessPath = path metadata.Uid = uid - processFound = true } }