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.
-[](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_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 @@
-
-
-
-
-
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 @@
-
-
-
-
-
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
}
}