diff --git a/.github/genReleaseNote.sh b/.github/genReleaseNote.sh index 0425061d..ab617fd0 100755 --- a/.github/genReleaseNote.sh +++ b/.github/genReleaseNote.sh @@ -18,15 +18,15 @@ if [ -z "$version_range" ]; then fi echo "## What's Changed" > release.md -git log --pretty=format:"* %s by @%an" --grep="^feat" -i $version_range | sort -f | uniq >> release.md +git log --pretty=format:"* %h %s by @%an" --grep="^feat" -i $version_range | sort -f | uniq >> release.md echo "" >> release.md echo "## BUG & Fix" >> release.md -git log --pretty=format:"* %s by @%an" --grep="^fix" -i $version_range | sort -f | uniq >> release.md +git log --pretty=format:"* %h %s by @%an" --grep="^fix" -i $version_range | sort -f | uniq >> release.md echo "" >> release.md echo "## Maintenance" >> release.md -git log --pretty=format:"* %s by @%an" --grep="^chore\|^docs\|^refactor" -i $version_range | sort -f | uniq >> release.md +git log --pretty=format:"* %h %s by @%an" --grep="^chore\|^docs\|^refactor" -i $version_range | sort -f | uniq >> release.md echo "" >> release.md -echo "**Full Changelog**: https://github.com/MetaCubeX/Clash.Meta/compare/$version_range" >> release.md +echo "**Full Changelog**: https://github.com/MetaCubeX/mihomo/compare/$version_range" >> release.md diff --git a/.github/mihomo.service b/.github/mihomo.service index a884059f..f34b6a6a 100644 --- a/.github/mihomo.service +++ b/.github/mihomo.service @@ -6,8 +6,8 @@ After=network.target NetworkManager.service systemd-networkd.service iwd.service Type=simple LimitNPROC=500 LimitNOFILE=1000000 -CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH -AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE Restart=always ExecStartPre=/usr/bin/sleep 2s ExecStart=/usr/bin/mihomo -d /etc/mihomo diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bad84cd1..56c7b66d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,10 @@ name: Build on: workflow_dispatch: + inputs: + version: + description: "Tag version to release" + required: true push: paths-ignore: - "docs/**" @@ -13,9 +17,8 @@ on: pull_request_target: branches: - Alpha - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true env: @@ -34,11 +37,13 @@ jobs: - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, test: test } - { goos: linux, goarch: amd64, goamd64: v3, output: amd64 } - { goos: linux, goarch: arm64, output: arm64 } + - { goos: linux, goarch: arm, goarm: '5', output: armv5 } + - { goos: linux, goarch: arm, goarm: '6', output: armv6 } - { goos: linux, goarch: arm, goarm: '7', output: armv7 } - - { goos: linux, goarch: mips, mips: hardfloat, output: mips-hardfloat } - - { goos: linux, goarch: mips, mips: softfloat, output: mips-softfloat } - - { goos: linux, goarch: mipsle, mips: hardfloat, output: mipsle-hardfloat } - - { goos: linux, goarch: mipsle, mips: softfloat, output: mipsle-softfloat } + - { goos: linux, goarch: mips, gomips: hardfloat, output: mips-hardfloat } + - { goos: linux, goarch: mips, gomips: softfloat, output: mips-softfloat } + - { goos: linux, goarch: mipsle, gomips: hardfloat, output: mipsle-hardfloat } + - { goos: linux, goarch: mipsle, gomips: softfloat, output: mipsle-softfloat } - { goos: linux, goarch: mips64, output: mips64 } - { goos: linux, goarch: mips64le, output: mips64le } - { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1' } @@ -62,11 +67,29 @@ jobs: - { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 } - { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 } + # Go 1.22 with special patch can work on Windows 7 + # https://github.com/MetaCubeX/go/commits/release-branch.go1.22/ + - { goos: windows, goarch: '386', output: '386-go122', goversion: '1.22' } + - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go122, goversion: '1.22' } + - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go122, goversion: '1.22' } + + # Go 1.21 can revert commit `9e4385` to work on Windows 7 + # https://github.com/golang/go/issues/64622#issuecomment-1847475161 + # (OR we can just use golang1.21.4 which unneeded any patch) + - { goos: windows, goarch: '386', output: '386-go121', goversion: '1.21' } + - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go121, goversion: '1.21' } + - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go121, goversion: '1.21' } + # Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016. - { goos: windows, goarch: '386', output: '386-go120', goversion: '1.20' } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' } - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } + # Go 1.22 is the last release that will run on macOS 10.15 Catalina. Go 1.23 will require macOS 11 Big Sur or later. + - { goos: darwin, goarch: arm64, output: arm64-go122, goversion: '1.22' } + - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go122, goversion: '1.22' } + - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go122, goversion: '1.22' } + # Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later. - { goos: darwin, goarch: arm64, output: arm64-go120, goversion: '1.20' } - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' } @@ -81,39 +104,75 @@ jobs: - uses: actions/checkout@v4 - name: Set up Go - if: ${{ matrix.jobs.goversion == '' && matrix.jobs.goarch != 'loong64' }} + if: ${{ matrix.jobs.goversion == '' && matrix.jobs.abi != '1' }} uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' - name: Set up Go - if: ${{ matrix.jobs.goversion != '' && matrix.jobs.goarch != 'loong64' }} + if: ${{ matrix.jobs.goversion != '' && matrix.jobs.abi != '1' }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.jobs.goversion }} - - name: Set up Go1.21 loongarch abi1 + - name: Set up Go1.23 loongarch abi1 if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }} run: | - wget -q https://github.com/xishang0128/loongarch64-golang/releases/download/1.21.5/go1.21.5.linux-amd64-abi1.tar.gz - sudo tar zxf go1.21.5.linux-amd64-abi1.tar.gz -C /usr/local + wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.23.0/go1.23.0.linux-amd64-abi1.tar.gz + sudo tar zxf go1.23.0.linux-amd64-abi1.tar.gz -C /usr/local echo "/usr/local/go/bin" >> $GITHUB_PATH - - name: Set up Go1.21 loongarch abi2 - if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '2' }} + # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 + # this patch file only works on golang1.23.x + # that means after golang1.24 release it must be changed + # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/ + # revert: + # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" + # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" + # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" + # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" + - name: Revert Golang1.23 commit for Windows7/8 + if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }} run: | - wget -q https://github.com/xishang0128/loongarch64-golang/releases/download/1.21.5/go1.21.5.linux-amd64-abi2.tar.gz - sudo tar zxf go1.21.5.linux-amd64-abi2.tar.gz -C /usr/local - echo "/usr/local/go/bin" >> $GITHUB_PATH + cd $(go env GOROOT) + curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1 + curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1 + curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1 + curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1 + + # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 + # this patch file only works on golang1.22.x + # that means after golang1.23 release it must be changed + # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/ + # revert: + # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" + # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" + # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" + # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" + - name: Revert Golang1.22 commit for Windows7/8 + if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.22' }} + run: | + cd $(go env GOROOT) + curl https://github.com/MetaCubeX/go/commit/9779155f18b6556a034f7bb79fb7fb2aad1e26a9.diff | patch --verbose -p 1 + curl https://github.com/MetaCubeX/go/commit/ef0606261340e608017860b423ffae5c1ce78239.diff | patch --verbose -p 1 + curl https://github.com/MetaCubeX/go/commit/7f83badcb925a7e743188041cb6e561fc9b5b642.diff | patch --verbose -p 1 + curl https://github.com/MetaCubeX/go/commit/83ff9782e024cb328b690cbf0da4e7848a327f4f.diff | patch --verbose -p 1 + + # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 + - name: Revert Golang1.21 commit for Windows7/8 + if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.21' }} + run: | + cd $(go env GOROOT) + curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1 - name: Set variables - if: ${{github.ref_name=='Alpha'}} - run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} + run: echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV shell: bash - name: Set variables - if: ${{github.ref_name=='' || github.ref_type=='tag'}} - run: echo "VERSION=$(git describe --tags)" >> $GITHUB_ENV + if: ${{ github.event_name != 'workflow_dispatch' && github.ref_name == 'Alpha' }} + run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV shell: bash - name: Set Time Variable @@ -121,13 +180,14 @@ jobs: echo "BUILDTIME=$(date)" >> $GITHUB_ENV echo "CGO_ENABLED=0" >> $GITHUB_ENV echo "BUILDTAG=-extldflags --static" >> $GITHUB_ENV + echo "GOTOOLCHAIN=local" >> $GITHUB_ENV - name: Setup NDK if: ${{ matrix.jobs.goos == 'android' }} uses: nttld/setup-ndk@v1 id: setup-ndk with: - ndk-version: r26c + ndk-version: r28-beta1 - name: Set NDK path if: ${{ matrix.jobs.goos == 'android' }} @@ -140,16 +200,24 @@ jobs: if: ${{ matrix.jobs.test == 'test' }} run: | go test ./... + echo "---test with_gvisor---" + go test ./... -tags "with_gvisor" -count=1 + + - name: Update CA + run: | + sudo apt-get install ca-certificates + sudo update-ca-certificates + cp -f /etc/ssl/certs/ca-certificates.crt component/ca/ca-certificates.crt - name: Build core env: GOOS: ${{matrix.jobs.goos}} GOARCH: ${{matrix.jobs.goarch}} GOAMD64: ${{matrix.jobs.goamd64}} - GOARM: ${{matrix.jobs.arm}} - GOMIPS: ${{matrix.jobs.mips}} + GOARM: ${{matrix.jobs.goarm}} + GOMIPS: ${{matrix.jobs.gomips}} run: | - echo $CGO_ENABLED + go env go build -v -tags "with_gvisor" -trimpath -ldflags "${BUILDTAG} -X 'github.com/metacubex/mihomo/constant.Version=${VERSION}' -X 'github.com/metacubex/mihomo/constant.BuildTime=${BUILDTIME}' -w -s -buildid=" if [ "${{matrix.jobs.goos}}" = "windows" ]; then cp mihomo.exe mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}.exe @@ -166,9 +234,18 @@ jobs: sudo apt-get install dpkg if [ "${{matrix.jobs.abi}}" = "1" ]; then ARCH=loongarch64 + elif [ "${{matrix.jobs.goarm}}" = "7" ]; then + ARCH=armhf + elif [ "${{matrix.jobs.goarch}}" = "arm" ]; then + ARCH=armel else ARCH=${{matrix.jobs.goarch}} fi + PackageVersion=$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$' | sed 's/v//g' ) + if [ $(git branch | awk -F ' ' '{print $2}') = "Alpha" ]; then + PackageVersion="$(echo "${PackageVersion}" | awk -F '.' '{$NF = $NF + 1; print}' OFS='.')-${VERSION}" + fi + mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo @@ -186,7 +263,7 @@ jobs: cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN/control <> $GITHUB_ENV - git fetch --tags - echo "PREVERSION=$(git describe --tags --abbrev=0 HEAD^)" >> $GITHUB_ENV + - name: Get tags + run: | + echo "CURRENTVERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV + git fetch --tags + echo "PREVERSION=$(git describe --tags --abbrev=0 HEAD)" >> $GITHUB_ENV - - name: Generate release notes - run: | - cp ./.github/genReleaseNote.sh ./ - bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION} - rm ./genReleaseNote.sh + - name: Force push Alpha branch to Meta + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git fetch origin Alpha:Alpha + git push origin Alpha:Meta --force + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/download-artifact@v4 - with: - path: bin/ - merge-multiple: true + - name: Tag the commit on Alpha + run: | + git checkout Alpha + git tag ${{ github.event.inputs.version }} + git push origin ${{ github.event.inputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Display structure of downloaded files - run: ls -R - working-directory: bin - - - name: Upload Release - uses: softprops/action-gh-release@v1 - if: ${{ success() }} - with: - tag_name: ${{ github.ref_name }} - files: bin/* - generate_release_notes: true - body_path: release.md + - name: Generate release notes + run: | + cp ./.github/genReleaseNote.sh ./ + bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION} + rm ./genReleaseNote.sh + + - uses: actions/download-artifact@v4 + with: + path: bin/ + merge-multiple: true + + - name: Display structure of downloaded files + run: ls -R + working-directory: bin + + - name: Upload Release + uses: softprops/action-gh-release@v2 + if: ${{ success() }} + with: + tag_name: ${{ github.event.inputs.version }} + files: bin/* + body_path: release.md Docker: if: ${{ !startsWith(github.event_name, 'pull_request') }} @@ -352,20 +447,35 @@ jobs: uses: docker/setup-buildx-action@v3 with: version: latest - + # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - name: Extract Docker metadata - id: meta + if: ${{ github.event_name != 'workflow_dispatch' }} + id: meta_alpha uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ github.repository }} - + images: '${{ env.REGISTRY }}/${{ github.repository }}' + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} + id: meta_release + uses: docker/metadata-action@v5 + with: + images: '${{ env.REGISTRY }}/${{ github.repository }}' + tags: | + ${{ github.event.inputs.version }} + flavor: | + latest=true + labels: org.opencontainers.image.version=${{ github.event.inputs.version }} + - name: Show files run: | ls . ls bin/ - + - name: login to docker REGISTRY uses: docker/login-action@v3 with: @@ -376,7 +486,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - id: build-and-push + if: ${{ github.event_name != 'workflow_dispatch' }} uses: docker/build-push-action@v5 with: context: . @@ -387,5 +497,20 @@ jobs: linux/amd64 linux/arm64 linux/arm/v7 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta_alpha.outputs.tags }} + labels: ${{ steps.meta_alpha.outputs.labels }} + + - name: Build and push Docker image + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: ${{ github.event_name != 'pull_request' }} + platforms: | + linux/386 + linux/amd64 + linux/arm64 + linux/arm/v7 + tags: ${{ steps.meta_release.outputs.tags }} + labels: ${{ steps.meta_release.outputs.labels }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 64b33cf7..67d4a6e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ WORKDIR /mihomo COPY bin/ bin/ RUN FILE_NAME=`sh file-name.sh` && echo $FILE_NAME && \ FILE_NAME=`ls bin/ | egrep "$FILE_NAME.gz"|awk NR==1` && echo $FILE_NAME && \ - mv bin/$FILE_NAME mihomo.gz && gzip -d mihomo.gz && echo "$FILE_NAME" > /mihomo-config/test + mv bin/$FILE_NAME mihomo.gz && gzip -d mihomo.gz && chmod +x mihomo && echo "$FILE_NAME" > /mihomo-config/test FROM alpine:latest LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/mihomo" @@ -23,5 +23,4 @@ VOLUME ["/root/.config/mihomo/"] 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 59bec41e..36c640d5 100644 --- a/Makefile +++ b/Makefile @@ -163,7 +163,3 @@ clean: CLANG ?= clang-14 CFLAGS := -O2 -g -Wall -Werror $(CFLAGS) -ebpf: export BPF_CLANG := $(CLANG) -ebpf: export BPF_CFLAGS := $(CFLAGS) -ebpf: - cd component/ebpf/ && go generate ./... diff --git a/README.md b/README.md index 975f1268..d38fbedd 100644 --- a/README.md +++ b/README.md @@ -98,4 +98,3 @@ API. This software is released under the GPL-3.0 license. -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FMetaCubeX%2Fmihomo.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FMetaCubeX%2Fmihomo?ref=badge_large) diff --git a/adapter/adapter.go b/adapter/adapter.go index dbf3db6e..31706a2e 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -2,6 +2,7 @@ package adapter import ( "context" + "crypto/tls" "encoding/json" "fmt" "net" @@ -9,13 +10,16 @@ import ( "net/netip" "net/url" "strconv" + "strings" "time" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/queue" "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" "github.com/puzpuzpuz/xsync/v3" ) @@ -37,6 +41,11 @@ type Proxy struct { extra *xsync.MapOf[string, *internalProxyState] } +// Adapter implements C.Proxy +func (p *Proxy) Adapter() C.ProxyAdapter { + return p.ProxyAdapter +} + // AliveForTestUrl implements C.Proxy func (p *Proxy) AliveForTestUrl(url string) bool { if state, ok := p.extra.Load(url); ok { @@ -154,8 +163,17 @@ func (p *Proxy) MarshalJSON() ([]byte, error) { mapping["alive"] = p.alive.Load() mapping["name"] = p.Name() mapping["udp"] = p.SupportUDP() - mapping["xudp"] = p.SupportXUDP() - mapping["tfo"] = p.SupportTFO() + mapping["uot"] = p.SupportUOT() + + proxyInfo := p.ProxyInfo() + mapping["xudp"] = proxyInfo.XUDP + mapping["tfo"] = proxyInfo.TFO + mapping["mptcp"] = proxyInfo.MPTCP + mapping["smux"] = proxyInfo.SMUX + mapping["interface"] = proxyInfo.Interface + mapping["dialer-proxy"] = proxyInfo.DialerProxy + mapping["routing-mark"] = proxyInfo.RoutingMark + return json.Marshal(mapping) } @@ -230,6 +248,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}), } client := http.Client{ @@ -252,10 +271,18 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In if unifiedDelay { second := time.Now() - resp, err = client.Do(req) - if err == nil { + var ignoredErr error + var secondResp *http.Response + secondResp, ignoredErr = client.Do(req) + if ignoredErr == nil { + resp = secondResp _ = resp.Body.Close() start = second + } else { + if strings.HasPrefix(url, "http://") { + log.Errorln("%s failed to get the second response from %s: %v", p.Name(), url, ignoredErr) + log.Warnln("It is recommended to use HTTPS for provider.health-check.url and group.url to ensure better reliability. Due to some proxy providers hijacking test addresses and not being compatible with repeated HEAD requests, using HTTP may result in failed tests.") + } } } diff --git a/adapter/inbound/addition.go b/adapter/inbound/addition.go index c38c1aa1..894910aa 100644 --- a/adapter/inbound/addition.go +++ b/adapter/inbound/addition.go @@ -47,7 +47,7 @@ func WithDstAddr(addr net.Addr) Addition { func WithSrcAddr(addr net.Addr) Addition { return func(metadata *C.Metadata) { m := C.Metadata{} - if err := m.SetRemoteAddr(addr);err ==nil{ + if err := m.SetRemoteAddr(addr); err == nil { metadata.SrcIP = m.DstIP metadata.SrcPort = m.DstPort } @@ -57,7 +57,7 @@ func WithSrcAddr(addr net.Addr) Addition { func WithInAddr(addr net.Addr) Addition { return func(metadata *C.Metadata) { m := C.Metadata{} - if err := m.SetRemoteAddr(addr);err ==nil{ + if err := m.SetRemoteAddr(addr); err == nil { metadata.InIP = m.DstIP metadata.InPort = m.DstPort } @@ -69,3 +69,5 @@ func WithDSCP(dscp uint8) Addition { metadata.DSCP = dscp } } + +func Placeholder(metadata *C.Metadata) {} diff --git a/adapter/inbound/auth.go b/adapter/inbound/auth.go index 984c9bd6..83172746 100644 --- a/adapter/inbound/auth.go +++ b/adapter/inbound/auth.go @@ -34,12 +34,5 @@ func SkipAuthRemoteAddress(addr string) bool { } func skipAuth(addr netip.Addr) bool { - if addr.IsValid() { - for _, prefix := range skipAuthPrefixes { - if prefix.Contains(addr.Unmap()) { - return true - } - } - } - return false + return prefixesContains(skipAuthPrefixes, addr) } diff --git a/adapter/inbound/http.go b/adapter/inbound/http.go index 8f912fbe..f7d45399 100644 --- a/adapter/inbound/http.go +++ b/adapter/inbound/http.go @@ -14,7 +14,7 @@ func NewHTTP(target socks5.Addr, srcConn net.Conn, conn net.Conn, additions ...A metadata.Type = C.HTTP metadata.RawSrcAddr = srcConn.RemoteAddr() metadata.RawDstAddr = srcConn.LocalAddr() - ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(conn.LocalAddr())) + ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(srcConn.LocalAddr())) ApplyAdditions(metadata, additions...) return conn, metadata } diff --git a/adapter/inbound/https.go b/adapter/inbound/https.go index 55f6731a..24b30804 100644 --- a/adapter/inbound/https.go +++ b/adapter/inbound/https.go @@ -11,6 +11,8 @@ import ( func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) { metadata := parseHTTPAddr(request) metadata.Type = C.HTTPS + metadata.RawSrcAddr = conn.RemoteAddr() + metadata.RawDstAddr = conn.LocalAddr() ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) ApplyAdditions(metadata, additions...) return conn, metadata diff --git a/adapter/inbound/ipfilter.go b/adapter/inbound/ipfilter.go index 7fa218c1..872d0c85 100644 --- a/adapter/inbound/ipfilter.go +++ b/adapter/inbound/ipfilter.go @@ -31,27 +31,17 @@ func IsRemoteAddrDisAllowed(addr net.Addr) bool { if err := m.SetRemoteAddr(addr); err != nil { return false } - return isAllowed(m.AddrPort().Addr().Unmap()) && !isDisAllowed(m.AddrPort().Addr().Unmap()) + ipAddr := m.AddrPort().Addr() + if ipAddr.IsValid() { + return isAllowed(ipAddr) && !isDisAllowed(ipAddr) + } + return false } func isAllowed(addr netip.Addr) bool { - if addr.IsValid() { - for _, prefix := range lanAllowedIPs { - if prefix.Contains(addr) { - return true - } - } - } - return false + return prefixesContains(lanAllowedIPs, addr) } func isDisAllowed(addr netip.Addr) bool { - if addr.IsValid() { - for _, prefix := range lanDisAllowedIPs { - if prefix.Contains(addr) { - return true - } - } - } - return false + return prefixesContains(lanDisAllowedIPs, addr) } diff --git a/adapter/inbound/listen.go b/adapter/inbound/listen.go index 18dc1bc2..ad944006 100644 --- a/adapter/inbound/listen.go +++ b/adapter/inbound/listen.go @@ -2,7 +2,12 @@ package inbound import ( "context" + "fmt" "net" + "net/netip" + "sync" + + "github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/tfo-go" ) @@ -11,20 +16,68 @@ var ( lc = tfo.ListenConfig{ DisableTFO: true, } + mutex sync.RWMutex ) func SetTfo(open bool) { + mutex.Lock() + defer mutex.Unlock() lc.DisableTFO = !open } +func Tfo() bool { + mutex.RLock() + defer mutex.RUnlock() + return !lc.DisableTFO +} + func SetMPTCP(open bool) { + mutex.Lock() + defer mutex.Unlock() setMultiPathTCP(&lc.ListenConfig, open) } +func MPTCP() bool { + mutex.RLock() + defer mutex.RUnlock() + return getMultiPathTCP(&lc.ListenConfig) +} + func ListenContext(ctx context.Context, network, address string) (net.Listener, error) { + switch network { // like net.Resolver.internetAddrList but filter domain to avoid call net.Resolver.lookupIPAddr + case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6": + if host, port, err := net.SplitHostPort(address); err == nil { + switch host { + case "localhost": + switch network { + case "tcp6", "udp6", "ip6": + address = net.JoinHostPort("::1", port) + default: + address = net.JoinHostPort("127.0.0.1", port) + } + case "": // internetAddrList can handle this special case + break + default: + if _, err := netip.ParseAddr(host); err != nil { // not ip + return nil, fmt.Errorf("invalid network address: %s", address) + } + } + } + } + + mutex.RLock() + defer mutex.RUnlock() return lc.Listen(ctx, network, address) } func Listen(network, address string) (net.Listener, error) { return ListenContext(context.Background(), network, address) } + +func init() { + keepalive.SetDisableKeepAliveCallback.Register(func(b bool) { + mutex.Lock() + defer mutex.Unlock() + keepalive.SetNetListenConfig(&lc.ListenConfig) + }) +} diff --git a/adapter/inbound/listen_notwindows.go b/adapter/inbound/listen_notwindows.go new file mode 100644 index 00000000..8fdfb7b8 --- /dev/null +++ b/adapter/inbound/listen_notwindows.go @@ -0,0 +1,14 @@ +//go:build !windows + +package inbound + +import ( + "net" + "os" +) + +const SupportNamedPipe = false + +func ListenNamedPipe(path string) (net.Listener, error) { + return nil, os.ErrInvalid +} diff --git a/adapter/inbound/listen_windows.go b/adapter/inbound/listen_windows.go new file mode 100644 index 00000000..d19239da --- /dev/null +++ b/adapter/inbound/listen_windows.go @@ -0,0 +1,32 @@ +package inbound + +import ( + "net" + "os" + + "github.com/metacubex/wireguard-go/ipc/namedpipe" + "golang.org/x/sys/windows" +) + +const SupportNamedPipe = true + +// windowsSDDL is the Security Descriptor set on the namedpipe. +// It provides read/write access to all users and the local system. +const windowsSDDL = "D:PAI(A;OICI;GWGR;;;BU)(A;OICI;GWGR;;;SY)" + +func ListenNamedPipe(path string) (net.Listener, error) { + sddl := os.Getenv("LISTEN_NAMEDPIPE_SDDL") + if sddl == "" { + sddl = windowsSDDL + } + securityDescriptor, err := windows.SecurityDescriptorFromString(sddl) + if err != nil { + return nil, err + } + namedpipeLC := namedpipe.ListenConfig{ + SecurityDescriptor: securityDescriptor, + InputBufferSize: 256 * 1024, + OutputBufferSize: 256 * 1024, + } + return namedpipeLC.Listen(path) +} diff --git a/adapter/inbound/mptcp_go120.go b/adapter/inbound/mptcp_go120.go index f9b22533..faae6ec5 100644 --- a/adapter/inbound/mptcp_go120.go +++ b/adapter/inbound/mptcp_go120.go @@ -8,3 +8,7 @@ const multipathTCPAvailable = false func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) { } + +func getMultiPathTCP(listenConfig *net.ListenConfig) bool { + return false +} diff --git a/adapter/inbound/mptcp_go121.go b/adapter/inbound/mptcp_go121.go index 6b35d1a8..9163878a 100644 --- a/adapter/inbound/mptcp_go121.go +++ b/adapter/inbound/mptcp_go121.go @@ -9,3 +9,7 @@ const multipathTCPAvailable = true func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) { listenConfig.SetMultipathTCP(open) } + +func getMultiPathTCP(listenConfig *net.ListenConfig) bool { + return listenConfig.MultipathTCP() +} diff --git a/adapter/inbound/util.go b/adapter/inbound/util.go index 743337fc..3bcd2808 100644 --- a/adapter/inbound/util.go +++ b/adapter/inbound/util.go @@ -61,3 +61,19 @@ func parseHTTPAddr(request *http.Request) *C.Metadata { return metadata } + +func prefixesContains(prefixes []netip.Prefix, addr netip.Addr) bool { + if len(prefixes) == 0 { + return false + } + if !addr.IsValid() { + return false + } + addr = addr.Unmap().WithZone("") // netip.Prefix.Contains returns false if ip has an IPv6 zone + for _, prefix := range prefixes { + if prefix.Contains(addr) { + return true + } + } + return false +} diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go index ae8c4651..dd226f74 100644 --- a/adapter/outbound/base.go +++ b/adapter/outbound/base.go @@ -85,14 +85,15 @@ func (b *Base) SupportUDP() bool { return b.udp } -// SupportXUDP implements C.ProxyAdapter -func (b *Base) SupportXUDP() bool { - return b.xudp -} - -// SupportTFO implements C.ProxyAdapter -func (b *Base) SupportTFO() bool { - return b.tfo +// ProxyInfo implements C.ProxyAdapter +func (b *Base) ProxyInfo() (info C.ProxyInfo) { + info.XUDP = b.xudp + info.TFO = b.tfo + info.MPTCP = b.mpTcp + info.SMUX = false + info.Interface = b.iface + info.RoutingMark = b.rmark + return } // IsL3Protocol implements C.ProxyAdapter diff --git a/adapter/outbound/direct.go b/adapter/outbound/direct.go index 7def7b20..dbde5593 100644 --- a/adapter/outbound/direct.go +++ b/adapter/outbound/direct.go @@ -3,18 +3,15 @@ package outbound import ( "context" "errors" - "fmt" - "net/netip" - - N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/loopback" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" ) type Direct struct { *Base - loopBack *loopBackDetector + loopBack *loopback.Detector } type DirectOption struct { @@ -24,38 +21,41 @@ type DirectOption struct { // DialContext implements C.ProxyAdapter func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { - if d.loopBack.CheckConn(metadata.SourceAddrPort()) { - return nil, fmt.Errorf("reject loopback connection to: %s", metadata.RemoteAddress()) + if err := d.loopBack.CheckConn(metadata); err != nil { + return nil, err } - opts = append(opts, dialer.WithResolver(resolver.DefaultResolver)) + opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver)) c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...) if err != nil { return nil, err } - N.TCPKeepAlive(c) return d.loopBack.NewConn(NewConn(c, d)), nil } // ListenPacketContext implements C.ProxyAdapter func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { - if d.loopBack.CheckPacketConn(metadata.SourceAddrPort()) { - return nil, fmt.Errorf("reject loopback connection to: %s", metadata.RemoteAddress()) + if err := d.loopBack.CheckPacketConn(metadata); err != nil { + return nil, err } // net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr if !metadata.Resolved() { - ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DefaultResolver) + ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver) if err != nil { return nil, errors.New("can't resolve ip") } metadata.DstIP = ip } - pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort)) + pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", metadata.AddrPort()) if err != nil { return nil, err } return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil } +func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool { + return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver +} + func NewDirectWithOption(option DirectOption) *Direct { return &Direct{ Base: &Base{ @@ -68,7 +68,7 @@ func NewDirectWithOption(option DirectOption) *Direct { rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), }, - loopBack: newLoopBackDetector(), + loopBack: loopback.NewDetector(), } } @@ -80,7 +80,7 @@ func NewDirect() *Direct { udp: true, prefer: C.DualStack, }, - loopBack: newLoopBackDetector(), + loopBack: loopback.NewDetector(), } } @@ -92,6 +92,6 @@ func NewCompatible() *Direct { udp: true, prefer: C.DualStack, }, - loopBack: newLoopBackDetector(), + loopBack: loopback.NewDetector(), } } diff --git a/adapter/outbound/direct_loopback_detect.go b/adapter/outbound/direct_loopback_detect.go deleted file mode 100644 index 410d5a2f..00000000 --- a/adapter/outbound/direct_loopback_detect.go +++ /dev/null @@ -1,68 +0,0 @@ -package outbound - -import ( - "net/netip" - - "github.com/metacubex/mihomo/common/callback" - C "github.com/metacubex/mihomo/constant" - - "github.com/puzpuzpuz/xsync/v3" -) - -type loopBackDetector struct { - connMap *xsync.MapOf[netip.AddrPort, struct{}] - packetConnMap *xsync.MapOf[netip.AddrPort, struct{}] -} - -func newLoopBackDetector() *loopBackDetector { - return &loopBackDetector{ - connMap: xsync.NewMapOf[netip.AddrPort, struct{}](), - packetConnMap: xsync.NewMapOf[netip.AddrPort, struct{}](), - } -} - -func (l *loopBackDetector) NewConn(conn C.Conn) C.Conn { - metadata := C.Metadata{} - if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { - return conn - } - connAddr := metadata.AddrPort() - if !connAddr.IsValid() { - return conn - } - l.connMap.Store(connAddr, struct{}{}) - return callback.NewCloseCallbackConn(conn, func() { - l.connMap.Delete(connAddr) - }) -} - -func (l *loopBackDetector) NewPacketConn(conn C.PacketConn) C.PacketConn { - metadata := C.Metadata{} - if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { - return conn - } - connAddr := metadata.AddrPort() - if !connAddr.IsValid() { - return conn - } - l.packetConnMap.Store(connAddr, struct{}{}) - return callback.NewCloseCallbackPacketConn(conn, func() { - l.packetConnMap.Delete(connAddr) - }) -} - -func (l *loopBackDetector) CheckConn(connAddr netip.AddrPort) bool { - if !connAddr.IsValid() { - return false - } - _, ok := l.connMap.Load(connAddr) - return ok -} - -func (l *loopBackDetector) CheckPacketConn(connAddr netip.AddrPort) bool { - if !connAddr.IsValid() { - return false - } - _, ok := l.packetConnMap.Load(connAddr) - return ok -} diff --git a/adapter/outbound/dns.go b/adapter/outbound/dns.go index 21a5b2b7..8686b288 100644 --- a/adapter/outbound/dns.go +++ b/adapter/outbound/dns.go @@ -89,14 +89,14 @@ func (d *dnsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { return len(p), nil } - ctx, cancel := context.WithTimeout(d.ctx, resolver.DefaultDnsRelayTimeout) - defer cancel() - buf := pool.Get(resolver.SafeDnsPacketSize) put := func() { _ = pool.Put(buf) } copy(buf, p) // avoid p be changed after WriteTo returned go func() { // don't block the WriteTo function + ctx, cancel := context.WithTimeout(d.ctx, resolver.DefaultDnsRelayTimeout) + defer cancel() + buf, err = resolver.RelayDnsPacket(ctx, buf[:len(p)], buf) if err != nil { put() diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go index b837e49a..54e05f9d 100644 --- a/adapter/outbound/http.go +++ b/adapter/outbound/http.go @@ -7,13 +7,11 @@ import ( "encoding/base64" "errors" "fmt" - "io" "net" "net/http" "strconv" - N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" @@ -76,7 +74,6 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad if err != nil { return nil, fmt.Errorf("%s connect error: %w", h.addr, err) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) @@ -95,6 +92,13 @@ func (h *Http) SupportWithDialer() C.NetWork { return C.TCP } +// ProxyInfo implements C.ProxyAdapter +func (h *Http) ProxyInfo() C.ProxyInfo { + info := h.Base.ProxyInfo() + info.DialerProxy = h.option.DialerProxy + return info +} + func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { addr := metadata.RemoteAddress() HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n" diff --git a/adapter/outbound/hysteria.go b/adapter/outbound/hysteria.go index dacffd10..b0edab02 100644 --- a/adapter/outbound/hysteria.go +++ b/adapter/outbound/hysteria.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/netip" + "runtime" "strconv" "time" @@ -14,6 +15,7 @@ import ( "github.com/metacubex/quic-go/congestion" M "github.com/sagernet/sing/common/metadata" + 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" @@ -43,6 +45,8 @@ type Hysteria struct { option *HysteriaOption client *core.Client + + closeCh chan struct{} // for test } func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { @@ -51,7 +55,7 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts . return nil, err } - return NewConn(tcpConn, h), nil + return NewConn(CN.NewRefConn(tcpConn, h), h), nil } func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { @@ -59,13 +63,13 @@ func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata if err != nil { return nil, err } - return newPacketConn(&hyPacketConn{udpConn}, h), nil + return newPacketConn(CN.NewRefPacketConn(&hyPacketConn{udpConn}, h), 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) { + hyDialer: func(network string, rAddr net.Addr) (net.PacketConn, error) { var err error var cDialer C.Dialer = dialer.NewDialer(h.Base.DialOptions(opts...)...) if len(h.option.DialerProxy) > 0 { @@ -74,7 +78,7 @@ func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.Pack return nil, err } } - rAddrPort, _ := netip.ParseAddrPort(h.Addr()) + rAddrPort, _ := netip.ParseAddrPort(rAddr.String()) return cDialer.ListenPacket(ctx, network, "", rAddrPort) }, remoteAddr: func(addr string) (net.Addr, error) { @@ -83,6 +87,13 @@ func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.Pack } } +// ProxyInfo implements C.ProxyAdapter +func (h *Hysteria) ProxyInfo() C.ProxyInfo { + info := h.Base.ProxyInfo() + info.DialerProxy = h.option.DialerProxy + return info +} + type HysteriaOption struct { BasicOption Name string `proxy:"name"` @@ -127,11 +138,7 @@ func (c *HysteriaOption) Speed() (uint64, uint64, error) { } func NewHysteria(option HysteriaOption) (*Hysteria, error) { - clientTransport := &transport.ClientTransport{ - Dialer: &net.Dialer{ - Timeout: 8 * time.Second, - }, - } + clientTransport := &transport.ClientTransport{} addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) ports := option.Ports @@ -218,7 +225,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { if err != nil { return nil, fmt.Errorf("hysteria %s create error: %w", addr, err) } - return &Hysteria{ + outbound := &Hysteria{ Base: &Base{ name: option.Name, addr: addr, @@ -231,7 +238,19 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { }, option: &option, client: client, - }, nil + } + runtime.SetFinalizer(outbound, closeHysteria) + + return outbound, nil +} + +func closeHysteria(h *Hysteria) { + if h.client != nil { + _ = h.client.Close() + } + if h.closeCh != nil { + close(h.closeCh) + } } type hyPacketConn struct { @@ -268,7 +287,7 @@ func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { } type hyDialerWithContext struct { - hyDialer func(network string) (net.PacketConn, error) + hyDialer func(network string, rAddr net.Addr) (net.PacketConn, error) ctx context.Context remoteAddr func(host string) (net.Addr, error) } @@ -278,7 +297,7 @@ func (h *hyDialerWithContext) ListenPacket(rAddr net.Addr) (net.PacketConn, erro if addrPort, err := netip.ParseAddrPort(rAddr.String()); err == nil { network = dialer.ParseNetwork(network, addrPort.Addr()) } - return h.hyDialer(network) + return h.hyDialer(network, rAddr) } func (h *hyDialerWithContext) Context() context.Context { diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index 0ad7c214..e7a9adae 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -21,8 +21,8 @@ import ( "github.com/metacubex/sing-quic/hysteria2" + "github.com/metacubex/randv2" M "github.com/sagernet/sing/common/metadata" - "github.com/zhangyunhao116/fastrand" ) func init() { @@ -38,6 +38,8 @@ type Hysteria2 struct { option *Hysteria2Option client *hysteria2.Client dialer proxydialer.SingDialer + + closeCh chan struct{} // for test } type Hysteria2Option struct { @@ -89,6 +91,16 @@ func closeHysteria2(h *Hysteria2) { if h.client != nil { _ = h.client.CloseWithError(errors.New("proxy removed")) } + if h.closeCh != nil { + close(h.closeCh) + } +} + +// ProxyInfo implements C.ProxyAdapter +func (h *Hysteria2) ProxyInfo() C.ProxyInfo { + info := h.Base.ProxyInfo() + info.DialerProxy = h.option.DialerProxy + return info } func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { @@ -165,7 +177,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { }) if len(serverAddress) > 0 { clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) { - return resolveUDPAddrWithPrefer(ctx, "udp", serverAddress[fastrand.Intn(len(serverAddress))], C.NewDNSPrefer(option.IPVersion)) + return resolveUDPAddrWithPrefer(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion)) } if option.HopInterval == 0 { diff --git a/adapter/outbound/hysteria2_test.go b/adapter/outbound/hysteria2_test.go new file mode 100644 index 00000000..de7d8227 --- /dev/null +++ b/adapter/outbound/hysteria2_test.go @@ -0,0 +1,38 @@ +package outbound + +import ( + "context" + "runtime" + "testing" + "time" +) + +func TestHysteria2GC(t *testing.T) { + option := Hysteria2Option{} + option.Server = "127.0.0.1" + option.Ports = "200,204,401-429,501-503" + option.HopInterval = 30 + option.Password = "password" + option.Obfs = "salamander" + option.ObfsPassword = "password" + option.SNI = "example.com" + option.ALPN = []string{"h3"} + hy, err := NewHysteria2(option) + if err != nil { + t.Error(err) + return + } + closeCh := make(chan struct{}) + hy.closeCh = closeCh + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + hy = nil + runtime.GC() + select { + case <-closeCh: + return + case <-ctx.Done(): + t.Error("timeout not GC") + } +} diff --git a/adapter/outbound/hysteria_test.go b/adapter/outbound/hysteria_test.go new file mode 100644 index 00000000..f2297c60 --- /dev/null +++ b/adapter/outbound/hysteria_test.go @@ -0,0 +1,39 @@ +package outbound + +import ( + "context" + "runtime" + "testing" + "time" +) + +func TestHysteriaGC(t *testing.T) { + option := HysteriaOption{} + option.Server = "127.0.0.1" + option.Ports = "200,204,401-429,501-503" + option.Protocol = "udp" + option.Up = "1Mbps" + option.Down = "1Mbps" + option.HopInterval = 30 + option.Obfs = "salamander" + option.SNI = "example.com" + option.ALPN = []string{"h3"} + hy, err := NewHysteria(option) + if err != nil { + t.Error(err) + return + } + closeCh := make(chan struct{}) + hy.closeCh = closeCh + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + hy = nil + runtime.GC() + select { + case <-closeCh: + return + case <-ctx.Done(): + t.Error("timeout not GC") + } +} diff --git a/adapter/outbound/mieru.go b/adapter/outbound/mieru.go new file mode 100644 index 00000000..7aab2f58 --- /dev/null +++ b/adapter/outbound/mieru.go @@ -0,0 +1,281 @@ +package outbound + +import ( + "context" + "fmt" + "net" + "runtime" + "strconv" + "sync" + + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/proxydialer" + C "github.com/metacubex/mihomo/constant" + + mieruclient "github.com/enfein/mieru/v3/apis/client" + mierumodel "github.com/enfein/mieru/v3/apis/model" + mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb" + "google.golang.org/protobuf/proto" +) + +type Mieru struct { + *Base + option *MieruOption + client mieruclient.Client + mu sync.Mutex +} + +type MieruOption struct { + BasicOption + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port,omitempty"` + PortRange string `proxy:"port-range,omitempty"` + Transport string `proxy:"transport"` + UserName string `proxy:"username"` + Password string `proxy:"password"` + Multiplexing string `proxy:"multiplexing,omitempty"` +} + +// DialContext implements C.ProxyAdapter +func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { + if err := m.ensureClientIsRunning(opts...); err != nil { + return nil, err + } + addr := metadataToMieruNetAddrSpec(metadata) + c, err := m.client.DialContext(ctx, addr) + if err != nil { + return nil, fmt.Errorf("dial to %s failed: %w", addr, err) + } + return NewConn(c, m), nil +} + +// ProxyInfo implements C.ProxyAdapter +func (m *Mieru) ProxyInfo() C.ProxyInfo { + info := m.Base.ProxyInfo() + info.DialerProxy = m.option.DialerProxy + return info +} + +func (m *Mieru) ensureClientIsRunning(opts ...dialer.Option) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.client.IsRunning() { + return nil + } + + // Create a dialer and add it to the client config, before starting the client. + var dialer C.Dialer = dialer.NewDialer(m.Base.DialOptions(opts...)...) + var err error + if len(m.option.DialerProxy) > 0 { + dialer, err = proxydialer.NewByName(m.option.DialerProxy, dialer) + if err != nil { + return err + } + } + config, err := m.client.Load() + if err != nil { + return err + } + config.Dialer = dialer + if err := m.client.Store(config); err != nil { + return err + } + + if err := m.client.Start(); err != nil { + return fmt.Errorf("failed to start mieru client: %w", err) + } + return nil +} + +func NewMieru(option MieruOption) (*Mieru, error) { + config, err := buildMieruClientConfig(option) + if err != nil { + return nil, fmt.Errorf("failed to build mieru client config: %w", err) + } + c := mieruclient.NewClient() + if err := c.Store(config); err != nil { + return nil, fmt.Errorf("failed to store mieru client config: %w", err) + } + // Client is started lazily on the first use. + + var addr string + if option.Port != 0 { + addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + } else { + beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange) + addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort)) + } + outbound := &Mieru{ + Base: &Base{ + name: option.Name, + addr: addr, + iface: option.Interface, + tp: C.Mieru, + udp: false, + xudp: false, + rmark: option.RoutingMark, + prefer: C.NewDNSPrefer(option.IPVersion), + }, + option: &option, + client: c, + } + runtime.SetFinalizer(outbound, closeMieru) + return outbound, nil +} + +func closeMieru(m *Mieru) { + m.mu.Lock() + defer m.mu.Unlock() + if m.client != nil && m.client.IsRunning() { + m.client.Stop() + } +} + +func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec { + if metadata.Host != "" { + return mierumodel.NetAddrSpec{ + AddrSpec: mierumodel.AddrSpec{ + FQDN: metadata.Host, + Port: int(metadata.DstPort), + }, + Net: "tcp", + } + } else { + return mierumodel.NetAddrSpec{ + AddrSpec: mierumodel.AddrSpec{ + IP: metadata.DstIP.AsSlice(), + Port: int(metadata.DstPort), + }, + Net: "tcp", + } + } +} + +func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, error) { + if err := validateMieruOption(option); err != nil { + return nil, fmt.Errorf("failed to validate mieru option: %w", err) + } + + transportProtocol := mierupb.TransportProtocol_TCP.Enum() + var server *mierupb.ServerEndpoint + if net.ParseIP(option.Server) != nil { + // server is an IP address + if option.PortRange != "" { + server = &mierupb.ServerEndpoint{ + IpAddress: proto.String(option.Server), + PortBindings: []*mierupb.PortBinding{ + { + PortRange: proto.String(option.PortRange), + Protocol: transportProtocol, + }, + }, + } + } else { + server = &mierupb.ServerEndpoint{ + IpAddress: proto.String(option.Server), + PortBindings: []*mierupb.PortBinding{ + { + Port: proto.Int32(int32(option.Port)), + Protocol: transportProtocol, + }, + }, + } + } + } else { + // server is a domain name + if option.PortRange != "" { + server = &mierupb.ServerEndpoint{ + DomainName: proto.String(option.Server), + PortBindings: []*mierupb.PortBinding{ + { + PortRange: proto.String(option.PortRange), + Protocol: transportProtocol, + }, + }, + } + } else { + server = &mierupb.ServerEndpoint{ + DomainName: proto.String(option.Server), + PortBindings: []*mierupb.PortBinding{ + { + Port: proto.Int32(int32(option.Port)), + Protocol: transportProtocol, + }, + }, + } + } + } + config := &mieruclient.ClientConfig{ + Profile: &mierupb.ClientProfile{ + ProfileName: proto.String(option.Name), + User: &mierupb.User{ + Name: proto.String(option.UserName), + Password: proto.String(option.Password), + }, + Servers: []*mierupb.ServerEndpoint{server}, + }, + } + if multiplexing, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; ok { + config.Profile.Multiplexing = &mierupb.MultiplexingConfig{ + Level: mierupb.MultiplexingLevel(multiplexing).Enum(), + } + } + return config, nil +} + +func validateMieruOption(option MieruOption) error { + if option.Name == "" { + return fmt.Errorf("name is empty") + } + if option.Server == "" { + return fmt.Errorf("server is empty") + } + if option.Port == 0 && option.PortRange == "" { + return fmt.Errorf("either port or port-range must be set") + } + if option.Port != 0 && option.PortRange != "" { + return fmt.Errorf("port and port-range cannot be set at the same time") + } + if option.Port != 0 && (option.Port < 1 || option.Port > 65535) { + return fmt.Errorf("port must be between 1 and 65535") + } + if option.PortRange != "" { + begin, end, err := beginAndEndPortFromPortRange(option.PortRange) + if err != nil { + return fmt.Errorf("invalid port-range format") + } + if begin < 1 || begin > 65535 { + return fmt.Errorf("begin port must be between 1 and 65535") + } + if end < 1 || end > 65535 { + return fmt.Errorf("end port must be between 1 and 65535") + } + if begin > end { + return fmt.Errorf("begin port must be less than or equal to end port") + } + } + + if option.Transport != "TCP" { + return fmt.Errorf("transport must be TCP") + } + if option.UserName == "" { + return fmt.Errorf("username is empty") + } + if option.Password == "" { + return fmt.Errorf("password is empty") + } + if option.Multiplexing != "" { + if _, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; !ok { + return fmt.Errorf("invalid multiplexing level: %s", option.Multiplexing) + } + } + return nil +} + +func beginAndEndPortFromPortRange(portRange string) (int, int, error) { + var begin, end int + _, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end) + return begin, end, err +} diff --git a/adapter/outbound/mieru_test.go b/adapter/outbound/mieru_test.go new file mode 100644 index 00000000..086b7910 --- /dev/null +++ b/adapter/outbound/mieru_test.go @@ -0,0 +1,92 @@ +package outbound + +import "testing" + +func TestNewMieru(t *testing.T) { + testCases := []struct { + option MieruOption + wantBaseAddr string + }{ + { + option: MieruOption{ + Name: "test", + Server: "1.2.3.4", + Port: 10000, + Transport: "TCP", + UserName: "test", + Password: "test", + }, + wantBaseAddr: "1.2.3.4:10000", + }, + { + option: MieruOption{ + Name: "test", + Server: "2001:db8::1", + PortRange: "10001-10002", + Transport: "TCP", + UserName: "test", + Password: "test", + }, + wantBaseAddr: "[2001:db8::1]:10001", + }, + { + option: MieruOption{ + Name: "test", + Server: "example.com", + Port: 10003, + Transport: "TCP", + UserName: "test", + Password: "test", + }, + wantBaseAddr: "example.com:10003", + }, + } + + for _, testCase := range testCases { + mieru, err := NewMieru(testCase.option) + if err != nil { + t.Error(err) + } + if mieru.addr != testCase.wantBaseAddr { + t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr) + } + } +} + +func TestBeginAndEndPortFromPortRange(t *testing.T) { + testCases := []struct { + input string + begin int + end int + hasErr bool + }{ + {"1-10", 1, 10, false}, + {"1000-2000", 1000, 2000, false}, + {"65535-65535", 65535, 65535, false}, + {"1", 0, 0, true}, + {"1-", 0, 0, true}, + {"-10", 0, 0, true}, + {"a-b", 0, 0, true}, + {"1-b", 0, 0, true}, + {"a-10", 0, 0, true}, + } + + for _, testCase := range testCases { + begin, end, err := beginAndEndPortFromPortRange(testCase.input) + if testCase.hasErr { + if err == nil { + t.Errorf("beginAndEndPortFromPortRange(%s) should return an error", testCase.input) + } + } else { + if err != nil { + t.Errorf("beginAndEndPortFromPortRange(%s) should not return an error, but got %v", testCase.input, err) + } + if begin != testCase.begin { + t.Errorf("beginAndEndPortFromPortRange(%s) begin port mismatch, got %d, want %d", testCase.input, begin, testCase.begin) + } + if end != testCase.end { + t.Errorf("beginAndEndPortFromPortRange(%s) end port mismatch, got %d, want %d", testCase.input, end, testCase.end) + } + } + } +} diff --git a/adapter/outbound/reject.go b/adapter/outbound/reject.go index 62f4aaa5..a8e2100d 100644 --- a/adapter/outbound/reject.go +++ b/adapter/outbound/reject.go @@ -37,7 +37,7 @@ func NewRejectWithOption(option RejectOption) *Reject { return &Reject{ Base: &Base{ name: option.Name, - tp: C.Direct, + tp: C.Reject, udp: true, }, } diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 98932f0c..94a7763d 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -149,7 +149,6 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) @@ -166,12 +165,6 @@ 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 { @@ -179,6 +172,12 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial } return ss.ListenPacketOnStreamConn(ctx, tcpConn, metadata) } + if len(ss.option.DialerProxy) > 0 { + dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer) + if err != nil { + return nil, err + } + } addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer) if err != nil { return nil, err @@ -197,6 +196,13 @@ func (ss *ShadowSocks) SupportWithDialer() C.NetWork { return C.ALLNet } +// ProxyInfo implements C.ProxyAdapter +func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo { + info := ss.Base.ProxyInfo() + info.DialerProxy = ss.option.DialerProxy + return info +} + // ListenPacketOnStreamConn implements C.ProxyAdapter func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { if ss.option.UDPOverTCP { @@ -230,7 +236,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { Password: option.Password, }) if err != nil { - return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) + return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err) } var v2rayOption *v2rayObfs.Option @@ -273,6 +279,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { if opts.TLS { v2rayOption.TLS = true v2rayOption.SkipCertVerify = opts.SkipCertVerify + v2rayOption.Fingerprint = opts.Fingerprint } } else if option.Plugin == shadowtls.Mode { obfsMode = shadowtls.Mode diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go index 07d78047..7012b30b 100644 --- a/adapter/outbound/shadowsocksr.go +++ b/adapter/outbound/shadowsocksr.go @@ -80,7 +80,6 @@ func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dia if err != nil { return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) @@ -123,6 +122,13 @@ func (ssr *ShadowSocksR) SupportWithDialer() C.NetWork { return C.ALLNet } +// ProxyInfo implements C.ProxyAdapter +func (ssr *ShadowSocksR) ProxyInfo() C.ProxyInfo { + info := ssr.Base.ProxyInfo() + info.DialerProxy = ssr.option.DialerProxy + return info +} + func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { // SSR protocol compatibility // https://github.com/metacubex/mihomo/pull/2056 @@ -135,7 +141,7 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { password := option.Password coreCiph, err := core.PickCipher(cipher, nil, password) if err != nil { - return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err) + return nil, fmt.Errorf("ssr %s cipher: %s initialize error: %w", addr, cipher, err) } var ( ivSize int diff --git a/adapter/outbound/singmux.go b/adapter/outbound/singmux.go index 67267744..26a8d26e 100644 --- a/adapter/outbound/singmux.go +++ b/adapter/outbound/singmux.go @@ -97,6 +97,12 @@ func (s *SingMux) SupportUOT() bool { return true } +func (s *SingMux) ProxyInfo() C.ProxyInfo { + info := s.ProxyAdapter.ProxyInfo() + info.SMUX = true + return info +} + func closeSingMux(s *SingMux) { _ = s.client.Close() } diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go index 76ed4be9..f4b94787 100644 --- a/adapter/outbound/snell.go +++ b/adapter/outbound/snell.go @@ -6,7 +6,6 @@ import ( "net" "strconv" - N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" @@ -94,7 +93,6 @@ func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta if err != nil { return nil, fmt.Errorf("%s connect error: %w", s.addr, err) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) @@ -122,7 +120,6 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met if err != nil { return nil, err } - N.TCPKeepAlive(c) c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) err = snell.WriteUDPHeader(c, s.version) @@ -144,6 +141,13 @@ func (s *Snell) SupportUOT() bool { return true } +// ProxyInfo implements C.ProxyAdapter +func (s *Snell) ProxyInfo() C.ProxyInfo { + info := s.Base.ProxyInfo() + info.DialerProxy = s.option.DialerProxy + return info +} + func NewSnell(option SnellOption) (*Snell, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) psk := []byte(option.Psk) @@ -208,7 +212,6 @@ func NewSnell(option SnellOption) (*Snell, error) { return nil, err } - 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 c17ee6a7..b8dd3b39 100644 --- a/adapter/outbound/socks5.go +++ b/adapter/outbound/socks5.go @@ -10,7 +10,6 @@ import ( "net/netip" "strconv" - N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" @@ -82,7 +81,6 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) @@ -128,7 +126,6 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, safeConnClose(c, err) }(c) - N.TCPKeepAlive(c) var user *socks5.User if ss.user != "" { user = &socks5.User{ @@ -174,6 +171,13 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil } +// ProxyInfo implements C.ProxyAdapter +func (ss *Socks5) ProxyInfo() C.ProxyInfo { + info := ss.Base.ProxyInfo() + info.DialerProxy = ss.option.DialerProxy + return info +} + func NewSocks5(option Socks5Option) (*Socks5, error) { var tlsConfig *tls.Config if option.TLS { diff --git a/adapter/outbound/ssh.go b/adapter/outbound/ssh.go index a0efabca..d746842d 100644 --- a/adapter/outbound/ssh.go +++ b/adapter/outbound/ssh.go @@ -17,7 +17,7 @@ import ( "github.com/metacubex/mihomo/component/proxydialer" C "github.com/metacubex/mihomo/constant" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" "golang.org/x/crypto/ssh" ) @@ -77,7 +77,6 @@ func (s *sshClient) connect(ctx context.Context, cDialer C.Dialer, addr string) if err != nil { return nil, err } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) @@ -122,6 +121,13 @@ func closeSsh(s *Ssh) { _ = s.client.Close() } +// ProxyInfo implements C.ProxyAdapter +func (s *Ssh) ProxyInfo() C.ProxyInfo { + info := s.Base.ProxyInfo() + info.DialerProxy = s.option.DialerProxy + return info +} + func NewSsh(option SshOption) (*Ssh, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) @@ -180,10 +186,10 @@ func NewSsh(option SshOption) (*Ssh, error) { } version := "SSH-2.0-OpenSSH_" - if fastrand.Intn(2) == 0 { - version += "7." + strconv.Itoa(fastrand.Intn(10)) + if randv2.IntN(2) == 0 { + version += "7." + strconv.Itoa(randv2.IntN(10)) } else { - version += "8." + strconv.Itoa(fastrand.Intn(9)) + version += "8." + strconv.Itoa(randv2.IntN(9)) } config.ClientVersion = version diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index b14761a4..b6265439 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -3,18 +3,19 @@ package outbound import ( "context" "crypto/tls" + "errors" "fmt" "net" "net/http" "strconv" - N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/gun" + "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/trojan" ) @@ -29,6 +30,8 @@ type Trojan struct { transport *gun.TransportWrap realityConfig *tlsC.RealityConfig + + ssCipher core.Cipher } type TrojanOption struct { @@ -46,9 +49,17 @@ type TrojanOption struct { RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"` + SSOpts TrojanSSOption `proxy:"ss-opts,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } +// TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 +type TrojanSSOption struct { + Enabled bool `proxy:"enabled,omitempty"` + Method string `proxy:"method,omitempty"` + Password string `proxy:"password,omitempty"` +} + func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error) { if t.option.Network == "ws" { host, port, _ := net.SplitHostPort(t.addr) @@ -95,6 +106,10 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } + if t.ssCipher != nil { + c = t.ssCipher.StreamConn(c) + } + if metadata.NetWork == C.UDP { err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) return c, err @@ -112,6 +127,10 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ... return nil, err } + if t.ssCipher != nil { + c = t.ssCipher.StreamConn(c) + } + if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil { c.Close() return nil, err @@ -134,7 +153,6 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) @@ -161,6 +179,11 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, defer func(c net.Conn) { safeConnClose(c, err) }(c) + + if t.ssCipher != nil { + c = t.ssCipher.StreamConn(c) + } + err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) if err != nil { return nil, err @@ -187,12 +210,15 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me defer func(c net.Conn) { safeConnClose(c, err) }(c) - N.TCPKeepAlive(c) c, err = t.plainStream(ctx, c) if err != nil { return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } + if t.ssCipher != nil { + c = t.ssCipher.StreamConn(c) + } + err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) if err != nil { return nil, err @@ -218,6 +244,13 @@ func (t *Trojan) SupportUOT() bool { return true } +// ProxyInfo implements C.ProxyAdapter +func (t *Trojan) ProxyInfo() C.ProxyInfo { + info := t.Base.ProxyInfo() + info.DialerProxy = t.option.DialerProxy + return info +} + func NewTrojan(option TrojanOption) (*Trojan, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) @@ -257,6 +290,20 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { } tOption.Reality = t.realityConfig + if option.SSOpts.Enabled { + if option.SSOpts.Password == "" { + return nil, errors.New("empty password") + } + if option.SSOpts.Method == "" { + option.SSOpts.Method = "AES-128-GCM" + } + ciph, err := core.PickCipher(option.SSOpts.Method, nil, option.SSOpts.Password) + if err != nil { + return nil, err + } + t.ssCipher = ciph + } + if option.Network == "grpc" { dialFn := func(network, addr string) (net.Conn, error) { var err error @@ -271,7 +318,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { if err != nil { return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) } - N.TCPKeepAlive(c) return c, nil } diff --git a/adapter/outbound/tuic.go b/adapter/outbound/tuic.go index 666e72fa..eaacb817 100644 --- a/adapter/outbound/tuic.go +++ b/adapter/outbound/tuic.go @@ -146,6 +146,13 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport * return } +// ProxyInfo implements C.ProxyAdapter +func (t *Tuic) ProxyInfo() C.ProxyInfo { + info := t.Base.ProxyInfo() + info.DialerProxy = t.option.DialerProxy + return info +} + func NewTuic(option TuicOption) (*Tuic, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) serverName := option.Server diff --git a/adapter/outbound/util.go b/adapter/outbound/util.go index 2c85c7c8..9f0636a6 100644 --- a/adapter/outbound/util.go +++ b/adapter/outbound/util.go @@ -55,7 +55,7 @@ func resolveUDPAddr(ctx context.Context, network, address string) (*net.UDPAddr, return nil, err } - ip, err := resolver.ResolveProxyServerHost(ctx, host) + ip, err := resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver) if err != nil { return nil, err } @@ -71,12 +71,12 @@ func resolveUDPAddrWithPrefer(ctx context.Context, network, address string, pref var fallback netip.Addr switch prefer { case C.IPv4Only: - ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host) + ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver) case C.IPv6Only: - ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host) + ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver) case C.IPv6Prefer: var ips []netip.Addr - ips, err = resolver.LookupIPProxyServerHost(ctx, host) + ips, err = resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver) if err == nil { for _, addr := range ips { if addr.Is6() { @@ -92,7 +92,7 @@ func resolveUDPAddrWithPrefer(ctx context.Context, network, address string, pref default: // C.IPv4Prefer, C.DualStack and other var ips []netip.Addr - ips, err = resolver.LookupIPProxyServerHost(ctx, host) + ips, err = resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver) if err == nil { for _, addr := range ips { if addr.Is4() { diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index 43b4aa21..7ab61ff6 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -262,7 +262,6 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) @@ -327,7 +326,6 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) @@ -381,6 +379,13 @@ func (v *Vless) SupportUOT() bool { return true } +// ProxyInfo implements C.ProxyAdapter +func (v *Vless) ProxyInfo() C.ProxyInfo { + info := v.Base.ProxyInfo() + info.DialerProxy = v.option.DialerProxy + return info +} + func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr { var addrType byte var addr []byte @@ -505,17 +510,14 @@ func NewVless(option VlessOption) (*Vless, error) { var addons *vless.Addons if option.Network != "ws" && len(option.Flow) >= 16 { option.Flow = option.Flow[:16] - switch option.Flow { - 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: + if option.Flow != vless.XRV { return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) } + + log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV) + addons = &vless.Addons{ + Flow: option.Flow, + } } switch option.PacketEncoding { @@ -577,7 +579,6 @@ func NewVless(option VlessOption) (*Vless, error) { if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } - N.TCPKeepAlive(c) return c, nil } diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index c1c981ce..f7de4e8e 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -179,6 +179,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M tlsOpts := mihomoVMess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, + FingerPrint: v.option.Fingerprint, NextProtos: []string{"h2"}, ClientFingerprint: v.option.ClientFingerprint, Reality: v.realityConfig, @@ -208,6 +209,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M tlsOpts := &mihomoVMess.TLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, + FingerPrint: v.option.Fingerprint, ClientFingerprint: v.option.ClientFingerprint, Reality: v.realityConfig, NextProtos: v.option.ALPN, @@ -310,7 +312,6 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) @@ -371,7 +372,6 @@ 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()) } - N.TCPKeepAlive(c) defer func(c net.Conn) { safeConnClose(c, err) }(c) @@ -388,6 +388,13 @@ func (v *Vmess) SupportWithDialer() C.NetWork { return C.ALLNet } +// ProxyInfo implements C.ProxyAdapter +func (v *Vmess) ProxyInfo() C.ProxyInfo { + info := v.Base.ProxyInfo() + info.DialerProxy = v.option.DialerProxy + return info +} + // ListenPacketOnStreamConn implements C.ProxyAdapter func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr @@ -471,7 +478,6 @@ func NewVmess(option VmessOption) (*Vmess, error) { if err != nil { return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) } - N.TCPKeepAlive(c) return c, nil } diff --git a/adapter/outbound/wireguard.go b/adapter/outbound/wireguard.go index 7c021c87..2834d324 100644 --- a/adapter/outbound/wireguard.go +++ b/adapter/outbound/wireguard.go @@ -12,7 +12,9 @@ import ( "strconv" "strings" "sync" + "time" + "github.com/metacubex/mihomo/common/atomic" CN "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" @@ -22,54 +24,86 @@ import ( "github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/log" + amnezia "github.com/metacubex/amneziawg-go/device" wireguard "github.com/metacubex/sing-wireguard" + "github.com/metacubex/wireguard-go/device" - "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/debug" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" - "github.com/sagernet/wireguard-go/device" ) +type wireguardGoDevice interface { + Close() + IpcSet(uapiConf string) error +} + type WireGuard struct { *Base bind *wireguard.ClientBind - device *device.Device + device wireguardGoDevice tunDevice wireguard.Device dialer proxydialer.SingDialer - startOnce sync.Once - startErr error - resolver *dns.Resolver + resolver resolver.Resolver refP *refProxyAdapter + + initOk atomic.Bool + initMutex sync.Mutex + initErr error + option WireGuardOption + connectAddr M.Socksaddr + localPrefixes []netip.Prefix + + serverAddrMap map[M.Socksaddr]netip.AddrPort + serverAddrTime atomic.TypedValue[time.Time] + serverAddrMutex sync.Mutex + + closeCh chan struct{} // for test } type WireGuardOption struct { BasicOption WireGuardPeerOption Name string `proxy:"name"` + Ip string `proxy:"ip,omitempty"` + Ipv6 string `proxy:"ipv6,omitempty"` PrivateKey string `proxy:"private-key"` Workers int `proxy:"workers,omitempty"` MTU int `proxy:"mtu,omitempty"` UDP bool `proxy:"udp,omitempty"` PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"` + AmneziaWGOption *AmneziaWGOption `proxy:"amnezia-wg-option,omitempty"` + Peers []WireGuardPeerOption `proxy:"peers,omitempty"` RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"` Dns []string `proxy:"dns,omitempty"` + + RefreshServerIPInterval int `proxy:"refresh-server-ip-interval,omitempty"` } type WireGuardPeerOption struct { Server string `proxy:"server"` Port int `proxy:"port"` - 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"` } +type AmneziaWGOption struct { + JC int `proxy:"jc"` + JMin int `proxy:"jmin"` + JMax int `proxy:"jmax"` + S1 int `proxy:"s1"` + S2 int `proxy:"s2"` + H1 uint32 `proxy:"h1"` + H2 uint32 `proxy:"h2"` + H3 uint32 `proxy:"h3"` + H4 uint32 `proxy:"h4"` +} + type wgSingErrorHandler struct { name string } @@ -98,7 +132,7 @@ func (option WireGuardPeerOption) Addr() M.Socksaddr { return M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)) } -func (option WireGuardPeerOption) Prefixes() ([]netip.Prefix, error) { +func (option WireGuardOption) Prefixes() ([]netip.Prefix, error) { localPrefixes := make([]netip.Prefix, 0, 2) if len(option.Ip) > 0 { if !strings.Contains(option.Ip, "/") { @@ -149,147 +183,103 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { 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() + outbound.connectAddr = option.Peers[0].Addr() } else { - connectAddr = option.Addr() + outbound.connectAddr = option.Addr() } } - outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, outbound.dialer, isConnect, connectAddr, reserved) + outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, outbound.dialer, isConnect, outbound.connectAddr.AddrPort(), reserved) - var localPrefixes []netip.Prefix + var err error + outbound.localPrefixes, err = option.Prefixes() + if err != nil { + return nil, err + } - var privateKey string { bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey) if err != nil { return nil, E.Cause(err, "decode private key") } - privateKey = hex.EncodeToString(bytes) + option.PrivateKey = hex.EncodeToString(bytes) } - ipcConf := "private_key=" + privateKey - 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 len(option.Peers) > 0 { + for i := range option.Peers { + peer := &option.Peers[i] // we need modify option here + bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode public key for peer ", i) } + peer.PublicKey = hex.EncodeToString(bytes) + if peer.PreSharedKey != "" { bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey) if err != nil { return nil, E.Cause(err, "decode pre shared key for peer ", i) } - preSharedKey = hex.EncodeToString(bytes) - } - destination := peer.Addr() - ipcConf += "\npublic_key=" + peerPublicKey - ipcConf += "\nendpoint=" + destination.String() - if preSharedKey != "" { - ipcConf += "\npreshared_key=" + preSharedKey + peer.PreSharedKey = hex.EncodeToString(bytes) } + 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) + option.PublicKey = hex.EncodeToString(bytes) } if option.PreSharedKey != "" { bytes, err := base64.StdEncoding.DecodeString(option.PreSharedKey) if err != nil { return nil, E.Cause(err, "decode pre shared key") } - 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" + option.PreSharedKey = hex.EncodeToString(bytes) } } + outbound.option = option - if option.PersistentKeepalive != 0 { - ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", option.PersistentKeepalive) - } mtu := option.MTU if mtu == 0 { mtu = 1408 } - if len(localPrefixes) == 0 { + if len(outbound.localPrefixes) == 0 { return nil, E.New("missing local address") } - var err error - outbound.tunDevice, err = wireguard.NewStackDevice(localPrefixes, uint32(mtu)) + outbound.tunDevice, err = wireguard.NewStackDevice(outbound.localPrefixes, uint32(mtu)) if err != nil { return nil, E.Cause(err, "create WireGuard device") } - outbound.device = device.NewDevice(context.Background(), outbound.tunDevice, outbound.bind, &device.Logger{ + logger := &device.Logger{ Verbosef: func(format string, args ...interface{}) { log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) }, Errorf: func(format string, args ...interface{}) { log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) }, - }, option.Workers) - if debug.Enabled { - 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") + if option.AmneziaWGOption != nil { + outbound.bind.SetParseReserved(false) // AmneziaWG don't need parse reserved + outbound.device = amnezia.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers) + } else { + outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers) } - //err = outbound.tunDevice.Start() var has6 bool - for _, address := range localPrefixes { + for _, address := range outbound.localPrefixes { if !address.Addr().Unmap().Is4() { has6 = true break @@ -315,22 +305,204 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { return outbound, nil } +func (w *WireGuard) resolve(ctx context.Context, address M.Socksaddr) (netip.AddrPort, error) { + if address.Addr.IsValid() { + return address.AddrPort(), nil + } + udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", address.String(), w.prefer) + if err != nil { + return netip.AddrPort{}, err + } + // net.ResolveUDPAddr maybe return 4in6 address, so unmap at here + addrPort := udpAddr.AddrPort() + return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()), nil +} + +func (w *WireGuard) init(ctx context.Context) error { + err := w.init0(ctx) + if err != nil { + return err + } + w.updateServerAddr(ctx) + return nil +} + +func (w *WireGuard) init0(ctx context.Context) error { + if w.initOk.Load() { + return nil + } + w.initMutex.Lock() + defer w.initMutex.Unlock() + // double check like sync.Once + if w.initOk.Load() { + return nil + } + if w.initErr != nil { + return w.initErr + } + + w.bind.ResetReservedForEndpoint() + w.serverAddrMap = make(map[M.Socksaddr]netip.AddrPort) + ipcConf, err := w.genIpcConf(ctx, false) + if err != nil { + // !!! do not set initErr here !!! + // let us can retry domain resolve in next time + return err + } + + if debug.Enabled { + log.SingLogger.Trace(fmt.Sprintf("[WG](%s) created wireguard ipc conf: \n %s", w.option.Name, ipcConf)) + } + err = w.device.IpcSet(ipcConf) + if err != nil { + w.initErr = E.Cause(err, "setup wireguard") + return w.initErr + } + w.serverAddrTime.Store(time.Now()) + + err = w.tunDevice.Start() + if err != nil { + w.initErr = err + return w.initErr + } + + w.initOk.Store(true) + return nil +} + +func (w *WireGuard) updateServerAddr(ctx context.Context) { + if w.option.RefreshServerIPInterval != 0 && time.Since(w.serverAddrTime.Load()) > time.Second*time.Duration(w.option.RefreshServerIPInterval) { + if w.serverAddrMutex.TryLock() { + defer w.serverAddrMutex.Unlock() + ipcConf, err := w.genIpcConf(ctx, true) + if err != nil { + log.Warnln("[WG](%s)UpdateServerAddr failed to generate wireguard ipc conf: %s", w.option.Name, err) + return + } + err = w.device.IpcSet(ipcConf) + if err != nil { + log.Warnln("[WG](%s)UpdateServerAddr failed to update wireguard ipc conf: %s", w.option.Name, err) + return + } + w.serverAddrTime.Store(time.Now()) + } + } +} + +func (w *WireGuard) genIpcConf(ctx context.Context, updateOnly bool) (string, error) { + ipcConf := "" + if !updateOnly { + ipcConf += "private_key=" + w.option.PrivateKey + "\n" + if w.option.AmneziaWGOption != nil { + ipcConf += "jc=" + strconv.Itoa(w.option.AmneziaWGOption.JC) + "\n" + ipcConf += "jmin=" + strconv.Itoa(w.option.AmneziaWGOption.JMin) + "\n" + ipcConf += "jmax=" + strconv.Itoa(w.option.AmneziaWGOption.JMax) + "\n" + ipcConf += "s1=" + strconv.Itoa(w.option.AmneziaWGOption.S1) + "\n" + ipcConf += "s2=" + strconv.Itoa(w.option.AmneziaWGOption.S2) + "\n" + ipcConf += "h1=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H1), 10) + "\n" + ipcConf += "h2=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H2), 10) + "\n" + ipcConf += "h3=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H3), 10) + "\n" + ipcConf += "h4=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H4), 10) + "\n" + } + } + if len(w.option.Peers) > 0 { + for i, peer := range w.option.Peers { + peerAddr := peer.Addr() + destination, err := w.resolve(ctx, peerAddr) + if err != nil { + return "", E.Cause(err, "resolve endpoint domain for peer ", i) + } + if w.serverAddrMap[peerAddr] != destination { + w.serverAddrMap[peerAddr] = destination + } else if updateOnly { + continue + } + + if len(w.option.Peers) == 1 { // must call SetConnectAddr if isConnect == true + w.bind.SetConnectAddr(destination) + } + ipcConf += "public_key=" + peer.PublicKey + "\n" + if updateOnly { + ipcConf += "update_only=true\n" + } + ipcConf += "endpoint=" + destination.String() + "\n" + if len(peer.Reserved) > 0 { + var reserved [3]uint8 + copy(reserved[:], w.option.Reserved) + w.bind.SetReservedForEndpoint(destination, reserved) + } + if updateOnly { + continue + } + if peer.PreSharedKey != "" { + ipcConf += "preshared_key=" + peer.PreSharedKey + "\n" + } + for _, allowedIP := range peer.AllowedIPs { + ipcConf += "allowed_ip=" + allowedIP + "\n" + } + if w.option.PersistentKeepalive != 0 { + ipcConf += fmt.Sprintf("persistent_keepalive_interval=%d\n", w.option.PersistentKeepalive) + } + } + } else { + destination, err := w.resolve(ctx, w.connectAddr) + if err != nil { + return "", E.Cause(err, "resolve endpoint domain") + } + if w.serverAddrMap[w.connectAddr] != destination { + w.serverAddrMap[w.connectAddr] = destination + } else if updateOnly { + return "", nil + } + w.bind.SetConnectAddr(destination) // must call SetConnectAddr if isConnect == true + ipcConf += "public_key=" + w.option.PublicKey + "\n" + if updateOnly { + ipcConf += "update_only=true\n" + } + ipcConf += "endpoint=" + destination.String() + "\n" + if updateOnly { + return ipcConf, nil + } + if w.option.PreSharedKey != "" { + ipcConf += "preshared_key=" + w.option.PreSharedKey + "\n" + } + var has4, has6 bool + for _, address := range w.localPrefixes { + if address.Addr().Is4() { + has4 = true + } else { + has6 = true + } + } + if has4 { + ipcConf += "allowed_ip=0.0.0.0/0\n" + } + if has6 { + ipcConf += "allowed_ip=::/0\n" + } + + if w.option.PersistentKeepalive != 0 { + ipcConf += fmt.Sprintf("persistent_keepalive_interval=%d\n", w.option.PersistentKeepalive) + } + } + return ipcConf, nil +} + func closeWireGuard(w *WireGuard) { if w.device != nil { w.device.Close() } - _ = common.Close(w.tunDevice) + if w.closeCh != nil { + close(w.closeCh) + } } func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { options := w.Base.DialOptions(opts...) w.dialer.SetDialer(dialer.NewDialer(options...)) var conn net.Conn - w.startOnce.Do(func() { - w.startErr = w.tunDevice.Start() - }) - if w.startErr != nil { - return nil, w.startErr + if err = w.init(ctx); err != nil { + return nil, err } if !metadata.Resolved() || w.resolver != nil { r := resolver.DefaultResolver @@ -358,13 +530,7 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat options := w.Base.DialOptions(opts...) w.dialer.SetDialer(dialer.NewDialer(options...)) var pc net.PacketConn - w.startOnce.Do(func() { - w.startErr = w.tunDevice.Start() - }) - if w.startErr != nil { - return nil, w.startErr - } - if err != nil { + if err = w.init(ctx); err != nil { return nil, err } if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" { @@ -445,18 +611,11 @@ func (r *refProxyAdapter) SupportUDP() bool { return false } -func (r *refProxyAdapter) SupportXUDP() bool { +func (r *refProxyAdapter) ProxyInfo() C.ProxyInfo { if r.proxyAdapter != nil { - return r.proxyAdapter.SupportXUDP() + return r.proxyAdapter.ProxyInfo() } - return false -} - -func (r *refProxyAdapter) SupportTFO() bool { - if r.proxyAdapter != nil { - return r.proxyAdapter.SupportTFO() - } - return false + return C.ProxyInfo{} } func (r *refProxyAdapter) MarshalJSON() ([]byte, error) { diff --git a/adapter/outbound/wireguard_test.go b/adapter/outbound/wireguard_test.go new file mode 100644 index 00000000..2248bb7b --- /dev/null +++ b/adapter/outbound/wireguard_test.go @@ -0,0 +1,45 @@ +//go:build with_gvisor + +package outbound + +import ( + "context" + "runtime" + "testing" + "time" +) + +func TestWireGuardGC(t *testing.T) { + option := WireGuardOption{} + option.Server = "162.159.192.1" + option.Port = 2408 + option.PrivateKey = "iOx7749AdqH3IqluG7+0YbGKd0m1mcEXAfGRzpy9rG8=" + option.PublicKey = "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=" + option.Ip = "172.16.0.2" + option.Ipv6 = "2606:4700:110:8d29:be92:3a6a:f4:c437" + option.Reserved = []uint8{51, 69, 125} + wg, err := NewWireGuard(option) + if err != nil { + t.Error(err) + } + closeCh := make(chan struct{}) + wg.closeCh = closeCh + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + err = wg.init(ctx) + if err != nil { + t.Error(err) + return + } + // must do a small sleep before test GC + // because it maybe deadlocks if w.device.Close call too fast after w.device.Start + time.Sleep(10 * time.Millisecond) + wg = nil + runtime.GC() + select { + case <-closeCh: + return + case <-ctx.Done(): + t.Error("timeout not GC") + } +} diff --git a/adapter/outboundgroup/groupbase.go b/adapter/outboundgroup/groupbase.go index b39ee3a6..69fbb617 100644 --- a/adapter/outboundgroup/groupbase.go +++ b/adapter/outboundgroup/groupbase.go @@ -48,7 +48,7 @@ type GroupBaseOption struct { func NewGroupBase(opt GroupBaseOption) *GroupBase { var excludeFilterReg *regexp2.Regexp if opt.excludeFilter != "" { - excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, 0) + excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, regexp2.None) } var excludeTypeArray []string if opt.excludeType != "" { @@ -58,7 +58,7 @@ func NewGroupBase(opt GroupBaseOption) *GroupBase { var filterRegs []*regexp2.Regexp if opt.filter != "" { for _, filter := range strings.Split(opt.filter, "`") { - filterReg := regexp2.MustCompile(filter, 0) + filterReg := regexp2.MustCompile(filter, regexp2.None) filterRegs = append(filterRegs, filterReg) } } @@ -126,7 +126,7 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { for _, filterReg := range gb.filterRegs { for _, p := range proxies { name := p.Name() - if mat, _ := filterReg.FindStringMatch(name); mat != nil { + if mat, _ := filterReg.MatchString(name); mat { if _, ok := proxiesSet[name]; !ok { proxiesSet[name] = struct{}{} newProxies = append(newProxies, p) @@ -150,7 +150,7 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { for _, filterReg := range gb.filterRegs { for _, p := range proxies { name := p.Name() - if mat, _ := filterReg.FindStringMatch(name); mat != nil { + if mat, _ := filterReg.MatchString(name); mat { if _, ok := proxiesSet[name]; !ok { proxiesSet[name] = struct{}{} newProxies = append(newProxies, p) @@ -191,7 +191,7 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { var newProxies []C.Proxy for _, p := range proxies { name := p.Name() - if mat, _ := gb.excludeFilterReg.FindStringMatch(name); mat != nil { + if mat, _ := gb.excludeFilterReg.MatchString(name); mat { continue } newProxies = append(newProxies, p) diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go index 4cb0db00..048cc34c 100644 --- a/adapter/outboundgroup/loadbalance.go +++ b/adapter/outboundgroup/loadbalance.go @@ -204,8 +204,7 @@ func strategyStickySessions(url string) strategyFn { for i := 1; i < maxRetry; i++ { proxy := proxies[nowIdx] if proxy.AliveForTestUrl(url) { - if nowIdx != idx { - lruCache.Delete(key) + if !has || nowIdx != idx { lruCache.Set(key, nowIdx) } @@ -215,7 +214,6 @@ func strategyStickySessions(url string) strategyFn { } } - lruCache.Delete(key) lruCache.Set(key, 0) return proxies[0] } diff --git a/adapter/outboundgroup/parser.go b/adapter/outboundgroup/parser.go index 74947587..b073c4bb 100644 --- a/adapter/outboundgroup/parser.go +++ b/adapter/outboundgroup/parser.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/dlclark/regexp2" + "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/common/structure" @@ -67,10 +69,28 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide } if groupOption.IncludeAllProviders { - groupOption.Use = append(groupOption.Use, AllProviders...) + groupOption.Use = AllProviders } if groupOption.IncludeAllProxies { - groupOption.Proxies = append(groupOption.Proxies, AllProxies...) + if groupOption.Filter != "" { + var filterRegs []*regexp2.Regexp + for _, filter := range strings.Split(groupOption.Filter, "`") { + filterReg := regexp2.MustCompile(filter, regexp2.None) + filterRegs = append(filterRegs, filterReg) + } + for _, p := range AllProxies { + for _, filterReg := range filterRegs { + if mat, _ := filterReg.MatchString(p); mat { + groupOption.Proxies = append(groupOption.Proxies, p) + } + } + } + } else { + groupOption.Proxies = append(groupOption.Proxies, AllProxies...) + } + if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { + groupOption.Proxies = []string{"COMPATIBLE"} + } } if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { @@ -88,6 +108,29 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide } groupOption.ExpectedStatus = status + if len(groupOption.Use) != 0 { + PDs, err := getProviders(providersMap, groupOption.Use) + if err != nil { + return nil, fmt.Errorf("%s: %w", groupName, err) + } + + // if test URL is empty, use the first health check URL of providers + if groupOption.URL == "" { + for _, pd := range PDs { + if pd.HealthCheckURL() != "" { + groupOption.URL = pd.HealthCheckURL() + break + } + } + if groupOption.URL == "" { + groupOption.URL = C.DefaultTestURL + } + } else { + addTestUrlToProviders(PDs, groupOption.URL, expectedStatus, groupOption.Filter, uint(groupOption.Interval)) + } + providers = append(providers, PDs...) + } + if len(groupOption.Proxies) != 0 { ps, err := getProxies(proxyMap, groupOption.Proxies) if err != nil { @@ -98,14 +141,15 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider) } - // select don't need health check + if groupOption.URL == "" { + groupOption.URL = C.DefaultTestURL + } + + // select don't need auto health check if groupOption.Type != "select" && groupOption.Type != "relay" { if groupOption.Interval == 0 { groupOption.Interval = 300 } - if groupOption.URL == "" { - groupOption.URL = C.DefaultTestURL - } } hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.TestTimeout), uint(groupOption.Interval), groupOption.Lazy, expectedStatus) @@ -115,34 +159,10 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide return nil, fmt.Errorf("%s: %w", groupName, err) } - providers = append(providers, pd) + providers = append([]types.ProxyProvider{pd}, providers...) providersMap[groupName] = pd } - if len(groupOption.Use) != 0 { - list, err := getProviders(providersMap, groupOption.Use) - if err != nil { - return nil, fmt.Errorf("%s: %w", groupName, err) - } - - if groupOption.URL == "" { - for _, p := range list { - if p.HealthCheckURL() != "" { - groupOption.URL = p.HealthCheckURL() - } - break - } - - if groupOption.URL == "" { - groupOption.URL = C.DefaultTestURL - } - } - - // different proxy groups use different test URL - addTestUrlToProviders(list, groupOption.URL, expectedStatus, groupOption.Filter, uint(groupOption.Interval)) - providers = append(providers, list...) - } - var group C.ProxyAdapter switch groupOption.Type { case "url-test": diff --git a/adapter/outboundgroup/relay.go b/adapter/outboundgroup/relay.go index 07fbcd95..29aa9c6a 100644 --- a/adapter/outboundgroup/relay.go +++ b/adapter/outboundgroup/relay.go @@ -9,6 +9,7 @@ import ( "github.com/metacubex/mihomo/component/proxydialer" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/log" ) type Relay struct { @@ -149,6 +150,7 @@ func (r *Relay) Addr() string { } func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay { + log.Warnln("The group [%s] with relay type is deprecated, please using dialer-proxy instead", option.Name) return &Relay{ GroupBase: NewGroupBase(GroupBaseOption{ outbound.BaseOption{ diff --git a/adapter/outboundgroup/util.go b/adapter/outboundgroup/util.go index 84216377..66b2510c 100644 --- a/adapter/outboundgroup/util.go +++ b/adapter/outboundgroup/util.go @@ -4,3 +4,7 @@ type SelectAble interface { Set(string) error ForceSet(name string) } + +var _ SelectAble = (*Fallback)(nil) +var _ SelectAble = (*URLTest)(nil) +var _ SelectAble = (*Selector)(nil) diff --git a/adapter/parser.go b/adapter/parser.go index c64ee13a..ce4e91d5 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -141,6 +141,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { break } proxy, err = outbound.NewSsh(*sshOption) + case "mieru": + mieruOption := &outbound.MieruOption{} + err = decoder.Decode(mapping, mieruOption) + if err != nil { + break + } + proxy, err = outbound.NewMieru(*mieruOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } diff --git a/adapter/provider/healthcheck.go b/adapter/provider/healthcheck.go index bfbcf8d9..8737ff96 100644 --- a/adapter/provider/healthcheck.go +++ b/adapter/provider/healthcheck.go @@ -27,6 +27,8 @@ type extraOption struct { } type HealthCheck struct { + ctx context.Context + ctxCancel context.CancelFunc url string extra map[string]*extraOption mu sync.Mutex @@ -36,7 +38,6 @@ type HealthCheck struct { lazy bool expectedStatus utils.IntRanges[uint16] lastTouch atomic.TypedValue[time.Time] - done chan struct{} singleDo *singledo.Single[struct{}] timeout time.Duration } @@ -59,7 +60,7 @@ func (hc *HealthCheck) process() { } else { log.Debugln("Skip once health check because we are lazy") } - case <-hc.done: + case <-hc.ctx.Done(): ticker.Stop() hc.stop() return @@ -146,7 +147,7 @@ func (hc *HealthCheck) check() { _, _, _ = hc.singleDo.Do(func() (struct{}, error) { id := utils.NewUUIDV4().String() log.Debugln("Start New Health Checking {%s}", id) - b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10)) + b, _ := batch.New[bool](hc.ctx, batch.WithConcurrencyNum[bool](10)) // execute default health check option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus} @@ -181,21 +182,21 @@ func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *ex filters = append(filters, filter) } - filterReg = regexp2.MustCompile(strings.Join(filters, "|"), 0) + filterReg = regexp2.MustCompile(strings.Join(filters, "|"), regexp2.None) } } for _, proxy := range hc.proxies { // skip proxies that do not require health check if filterReg != nil { - if match, _ := filterReg.FindStringMatch(proxy.Name()); match == nil { + if match, _ := filterReg.MatchString(proxy.Name()); !match { continue } } p := proxy b.Go(p.Name(), func() (bool, error) { - ctx, cancel := context.WithTimeout(context.Background(), hc.timeout) + ctx, cancel := context.WithTimeout(hc.ctx, hc.timeout) defer cancel() log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid) _, _ = p.URLTest(ctx, url, expectedStatus) @@ -206,7 +207,7 @@ func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *ex } func (hc *HealthCheck) close() { - hc.done <- struct{}{} + hc.ctxCancel() } func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck { @@ -217,8 +218,11 @@ func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, if timeout == 0 { timeout = 5000 } + ctx, cancel := context.WithCancel(context.Background()) return &HealthCheck{ + ctx: ctx, + ctxCancel: cancel, proxies: proxies, url: url, timeout: time.Duration(timeout) * time.Millisecond, @@ -226,7 +230,6 @@ func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, 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 2e436669..cd5c3ff7 100644 --- a/adapter/provider/parser.go +++ b/adapter/provider/parser.go @@ -1,6 +1,7 @@ package provider import ( + "encoding" "errors" "fmt" "time" @@ -9,8 +10,9 @@ import ( "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" types "github.com/metacubex/mihomo/constant/provider" + + "github.com/dlclark/regexp2" ) var ( @@ -27,8 +29,20 @@ type healthCheckSchema struct { ExpectedStatus string `provider:"expected-status,omitempty"` } +type OverrideProxyNameSchema struct { + // matching expression for regex replacement + Pattern *regexp2.Regexp `provider:"pattern"` + // the new content after regex matching + Target string `provider:"target"` +} + +var _ encoding.TextUnmarshaler = (*regexp2.Regexp)(nil) // ensure *regexp2.Regexp can decode direct by structure package + type OverrideSchema struct { + TFO *bool `provider:"tfo,omitempty"` + MPTcp *bool `provider:"mptcp,omitempty"` UDP *bool `provider:"udp,omitempty"` + UDPOverTCP *bool `provider:"udp-over-tcp,omitempty"` Up *string `provider:"up,omitempty"` Down *string `provider:"down,omitempty"` DialerProxy *string `provider:"dialer-proxy,omitempty"` @@ -38,20 +52,26 @@ type OverrideSchema struct { IPVersion *string `provider:"ip-version,omitempty"` AdditionalPrefix *string `provider:"additional-prefix,omitempty"` AdditionalSuffix *string `provider:"additional-suffix,omitempty"` + + ProxyName []OverrideProxyNameSchema `provider:"proxy-name,omitempty"` } type proxyProviderSchema struct { - Type string `provider:"type"` - Path string `provider:"path,omitempty"` - URL string `provider:"url,omitempty"` - 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"` + Type string `provider:"type"` + Path string `provider:"path,omitempty"` + URL string `provider:"url,omitempty"` + Proxy string `provider:"proxy,omitempty"` + Interval int `provider:"interval,omitempty"` + Filter string `provider:"filter,omitempty"` + ExcludeFilter string `provider:"exclude-filter,omitempty"` + ExcludeType string `provider:"exclude-type,omitempty"` + DialerProxy string `provider:"dialer-proxy,omitempty"` + SizeLimit int64 `provider:"size-limit,omitempty"` + Payload []map[string]any `provider:"payload,omitempty"` - HealthCheck healthCheckSchema `provider:"health-check,omitempty"` - Override OverrideSchema `provider:"override,omitempty"` + HealthCheck healthCheckSchema `provider:"health-check,omitempty"` + Override OverrideSchema `provider:"override,omitempty"` + Header map[string][]string `provider:"header,omitempty"` } func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) { @@ -80,32 +100,32 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide } hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, uint(schema.HealthCheck.TestTimeout), hcInterval, schema.HealthCheck.Lazy, expectedStatus) + parser, err := NewProxiesParser(schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override) + if err != nil { + return nil, err + } + var vehicle types.Vehicle switch schema.Type { case "file": path := C.Path.Resolve(schema.Path) vehicle = resource.NewFileVehicle(path) case "http": + path := C.Path.GetPathByHash("proxies", schema.URL) if schema.Path != "" { - path := C.Path.Resolve(schema.Path) - if !features.CMFA && !C.Path.IsSafePath(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) } + vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit) + case "inline": + return NewInlineProvider(name, schema.Payload, parser, hc) default: return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) } interval := time.Duration(uint(schema.Interval)) * time.Second - filter := schema.Filter - excludeFilter := schema.ExcludeFilter - excludeType := schema.ExcludeType - dialerProxy := schema.DialerProxy - override := schema.Override - return NewProxySetProvider(name, interval, filter, excludeFilter, excludeType, dialerProxy, override, vehicle, hc) + return NewProxySetProvider(name, interval, parser, vehicle, hc) } diff --git a/adapter/provider/patch_android.go b/adapter/provider/patch_android.go deleted file mode 100644 index e9042bda..00000000 --- a/adapter/provider/patch_android.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build android && cmfa - -package provider - -import ( - "time" -) - -var ( - suspended bool -) - -type UpdatableProvider interface { - UpdatedAt() time.Time -} - -func (pp *proxySetProvider) UpdatedAt() time.Time { - return pp.Fetcher.UpdatedAt -} - -func (pp *proxySetProvider) Close() error { - pp.healthCheck.close() - pp.Fetcher.Destroy() - - return nil -} - -func (cp *compatibleProvider) Close() error { - cp.healthCheck.close() - - return nil -} - -func Suspend(s bool) { - suspended = s -} diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go index 2715a309..64d83b08 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -1,11 +1,11 @@ package provider import ( - "context" "encoding/json" "errors" "fmt" "net/http" + "reflect" "runtime" "strings" "time" @@ -13,11 +13,10 @@ import ( "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/profile/cachefile" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" types "github.com/metacubex/mihomo/constant/provider" - "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel/statistic" "github.com/dlclark/regexp2" @@ -32,125 +31,118 @@ type ProxySchema struct { Proxies []map[string]any `yaml:"proxies"` } +type providerForApi struct { + Name string `json:"name"` + Type string `json:"type"` + VehicleType string `json:"vehicleType"` + Proxies []C.Proxy `json:"proxies"` + TestUrl string `json:"testUrl"` + ExpectedStatus string `json:"expectedStatus"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` + SubscriptionInfo *SubscriptionInfo `json:"subscriptionInfo,omitempty"` +} + +type baseProvider struct { + name string + proxies []C.Proxy + healthCheck *HealthCheck + version uint32 +} + +func (bp *baseProvider) Name() string { + return bp.name +} + +func (bp *baseProvider) Version() uint32 { + return bp.version +} + +func (bp *baseProvider) HealthCheck() { + bp.healthCheck.check() +} + +func (bp *baseProvider) Type() types.ProviderType { + return types.Proxy +} + +func (bp *baseProvider) Proxies() []C.Proxy { + return bp.proxies +} + +func (bp *baseProvider) Count() int { + return len(bp.proxies) +} + +func (bp *baseProvider) Touch() { + bp.healthCheck.touch() +} + +func (bp *baseProvider) HealthCheckURL() string { + return bp.healthCheck.url +} + +func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { + bp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval) +} + +func (bp *baseProvider) setProxies(proxies []C.Proxy) { + bp.proxies = proxies + bp.healthCheck.setProxy(proxies) + if bp.healthCheck.auto() { + go bp.healthCheck.check() + } +} + +func (bp *baseProvider) Close() error { + bp.healthCheck.close() + return nil +} + // ProxySetProvider for auto gc type ProxySetProvider struct { *proxySetProvider } type proxySetProvider struct { + baseProvider *resource.Fetcher[[]C.Proxy] - proxies []C.Proxy - healthCheck *HealthCheck - version uint32 subscriptionInfo *SubscriptionInfo } func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]any{ - "name": pp.Name(), - "type": pp.Type().String(), - "vehicleType": pp.VehicleType().String(), - "proxies": pp.Proxies(), - "testUrl": pp.healthCheck.url, - "expectedStatus": pp.healthCheck.expectedStatus.String(), - "updatedAt": pp.UpdatedAt, - "subscriptionInfo": pp.subscriptionInfo, + return json.Marshal(providerForApi{ + Name: pp.Name(), + Type: pp.Type().String(), + VehicleType: pp.VehicleType().String(), + Proxies: pp.Proxies(), + TestUrl: pp.healthCheck.url, + ExpectedStatus: pp.healthCheck.expectedStatus.String(), + UpdatedAt: pp.UpdatedAt(), + SubscriptionInfo: pp.subscriptionInfo, }) } -func (pp *proxySetProvider) Version() uint32 { - return pp.version -} - func (pp *proxySetProvider) Name() string { return pp.Fetcher.Name() } -func (pp *proxySetProvider) HealthCheck() { - pp.healthCheck.check() -} - func (pp *proxySetProvider) Update() error { - elm, same, err := pp.Fetcher.Update() - if err == nil && !same { - pp.OnUpdate(elm) - } + _, _, err := pp.Fetcher.Update() return err } func (pp *proxySetProvider) Initial() error { - elm, err := pp.Fetcher.Initial() + _, err := pp.Fetcher.Initial() if err != nil { return err } - pp.OnUpdate(elm) - pp.getSubscriptionInfo() + if subscriptionInfo := cachefile.Cache().GetSubscriptionInfo(pp.Name()); subscriptionInfo != "" { + pp.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo) + } pp.closeAllConnections() return nil } -func (pp *proxySetProvider) Type() types.ProviderType { - return types.Proxy -} - -func (pp *proxySetProvider) Proxies() []C.Proxy { - return pp.proxies -} - -func (pp *proxySetProvider) Touch() { - pp.healthCheck.touch() -} - -func (pp *proxySetProvider) HealthCheckURL() string { - return pp.healthCheck.url -} - -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() { - go pp.healthCheck.check() - } -} - -func (pp *proxySetProvider) getSubscriptionInfo() { - if pp.VehicleType() != types.HTTP { - return - } - go func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), - http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) - if err != nil { - return - } - defer resp.Body.Close() - - userInfoStr := strings.TrimSpace(resp.Header.Get("subscription-userinfo")) - if userInfoStr == "" { - resp2, err := mihomoHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), - http.MethodGet, http.Header{"User-Agent": {"Quantumultx"}}, nil) - if err != nil { - return - } - defer resp2.Body.Close() - userInfoStr = strings.TrimSpace(resp2.Header.Get("subscription-userinfo")) - if userInfoStr == "" { - return - } - } - pp.subscriptionInfo, err = NewSubscriptionInfo(userInfoStr) - if err != nil { - log.Warnln("[Provider] get subscription-userinfo: %e", err) - } - }() -} - func (pp *proxySetProvider) closeAllConnections() { statistic.DefaultManager.Range(func(c statistic.Tracker) bool { for _, chain := range c.Chains() { @@ -163,81 +155,134 @@ func (pp *proxySetProvider) closeAllConnections() { }) } -func stopProxyProvider(pd *ProxySetProvider) { - pd.healthCheck.close() - _ = pd.Fetcher.Destroy() +func (pp *proxySetProvider) Close() error { + _ = pp.baseProvider.Close() + return pp.Fetcher.Close() } -func NewProxySetProvider(name string, interval time.Duration, filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema, 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) - } - var excludeTypeArray []string - if excludeType != "" { - excludeTypeArray = strings.Split(excludeType, "|") - } - - var filterRegs []*regexp2.Regexp - for _, filter := range strings.Split(filter, "`") { - filterReg, err := regexp2.Compile(filter, 0) - if err != nil { - return nil, fmt.Errorf("invalid filter regex: %w", err) - } - filterRegs = append(filterRegs, filterReg) - } - +func NewProxySetProvider(name string, interval time.Duration, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { if hc.auto() { go hc.process() } pd := &proxySetProvider{ - proxies: []C.Proxy{}, - healthCheck: hc, + baseProvider: baseProvider{ + name: name, + proxies: []C.Proxy{}, + healthCheck: hc, + }, } - fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, excludeTypeArray, filterRegs, excludeFilterReg, dialerProxy, override), proxiesOnUpdate(pd)) + fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, proxiesOnUpdate(pd)) pd.Fetcher = fetcher + if httpVehicle, ok := vehicle.(*resource.HTTPVehicle); ok { + httpVehicle.SetInRead(func(resp *http.Response) { + if subscriptionInfo := resp.Header.Get("subscription-userinfo"); subscriptionInfo != "" { + cachefile.Cache().SetSubscriptionInfo(name, subscriptionInfo) + pd.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo) + } + }) + } + wrapper := &ProxySetProvider{pd} - runtime.SetFinalizer(wrapper, stopProxyProvider) + runtime.SetFinalizer(wrapper, (*ProxySetProvider).Close) return wrapper, nil } +func (pp *ProxySetProvider) Close() error { + runtime.SetFinalizer(pp, nil) + return pp.proxySetProvider.Close() +} + +// InlineProvider for auto gc +type InlineProvider struct { + *inlineProvider +} + +type inlineProvider struct { + baseProvider + updateAt time.Time +} + +func (ip *inlineProvider) MarshalJSON() ([]byte, error) { + return json.Marshal(providerForApi{ + Name: ip.Name(), + Type: ip.Type().String(), + VehicleType: ip.VehicleType().String(), + Proxies: ip.Proxies(), + TestUrl: ip.healthCheck.url, + ExpectedStatus: ip.healthCheck.expectedStatus.String(), + UpdatedAt: ip.updateAt, + }) +} + +func (ip *inlineProvider) VehicleType() types.VehicleType { + return types.Inline +} + +func (ip *inlineProvider) Initial() error { + return nil +} + +func (ip *inlineProvider) Update() error { + // make api update happy + ip.updateAt = time.Now() + return nil +} + +func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) { + if hc.auto() { + go hc.process() + } + + ps := ProxySchema{Proxies: payload} + buf, err := yaml.Marshal(ps) + if err != nil { + return nil, err + } + proxies, err := parser(buf) + if err != nil { + return nil, err + } + + ip := &inlineProvider{ + baseProvider: baseProvider{ + name: name, + proxies: proxies, + healthCheck: hc, + }, + updateAt: time.Now(), + } + wrapper := &InlineProvider{ip} + runtime.SetFinalizer(wrapper, (*InlineProvider).Close) + return wrapper, nil +} + +func (ip *InlineProvider) Close() error { + runtime.SetFinalizer(ip, nil) + return ip.baseProvider.Close() +} + // CompatibleProvider for auto gc type CompatibleProvider struct { *compatibleProvider } type compatibleProvider struct { - name string - healthCheck *HealthCheck - proxies []C.Proxy - version uint32 + baseProvider } func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]any{ - "name": cp.Name(), - "type": cp.Type().String(), - "vehicleType": cp.VehicleType().String(), - "proxies": cp.Proxies(), - "testUrl": cp.healthCheck.url, - "expectedStatus": cp.healthCheck.expectedStatus.String(), + return json.Marshal(providerForApi{ + Name: cp.Name(), + Type: cp.Type().String(), + VehicleType: cp.VehicleType().String(), + Proxies: cp.Proxies(), + TestUrl: cp.healthCheck.url, + ExpectedStatus: cp.healthCheck.expectedStatus.String(), }) } -func (cp *compatibleProvider) Version() uint32 { - return cp.version -} - -func (cp *compatibleProvider) Name() string { - return cp.name -} - -func (cp *compatibleProvider) HealthCheck() { - cp.healthCheck.check() -} - func (cp *compatibleProvider) Update() error { return nil } @@ -253,30 +298,6 @@ func (cp *compatibleProvider) VehicleType() types.VehicleType { return types.Compatible } -func (cp *compatibleProvider) Type() types.ProviderType { - return types.Proxy -} - -func (cp *compatibleProvider) Proxies() []C.Proxy { - return cp.proxies -} - -func (cp *compatibleProvider) Touch() { - cp.healthCheck.touch() -} - -func (cp *compatibleProvider) HealthCheckURL() string { - return cp.healthCheck.url -} - -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() -} - func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) { if len(proxies) == 0 { return nil, errors.New("provider need one proxy at least") @@ -287,25 +308,49 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co } pd := &compatibleProvider{ - name: name, - proxies: proxies, - healthCheck: hc, + baseProvider: baseProvider{ + name: name, + proxies: proxies, + healthCheck: hc, + }, } wrapper := &CompatibleProvider{pd} - runtime.SetFinalizer(wrapper, stopCompatibleProvider) + runtime.SetFinalizer(wrapper, (*CompatibleProvider).Close) return wrapper, nil } +func (cp *CompatibleProvider) Close() error { + runtime.SetFinalizer(cp, nil) + return cp.compatibleProvider.Close() +} + func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) { return func(elm []C.Proxy) { pd.setProxies(elm) pd.version += 1 - pd.getSubscriptionInfo() } } -func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray []string, filterRegs []*regexp2.Regexp, excludeFilterReg *regexp2.Regexp, dialerProxy string, override OverrideSchema) resource.Parser[[]C.Proxy] { +func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) { + excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None) + if err != nil { + return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) + } + var excludeTypeArray []string + if excludeType != "" { + excludeTypeArray = strings.Split(excludeType, "|") + } + + var filterRegs []*regexp2.Regexp + for _, filter := range strings.Split(filter, "`") { + filterReg, err := regexp2.Compile(filter, regexp2.None) + if err != nil { + return nil, fmt.Errorf("invalid filter regex: %w", err) + } + filterRegs = append(filterRegs, filterReg) + } + return func(buf []byte) ([]C.Proxy, error) { schema := &ProxySchema{} @@ -356,12 +401,12 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray continue } if len(excludeFilter) > 0 { - if mat, _ := excludeFilterReg.FindStringMatch(name); mat != nil { + if mat, _ := excludeFilterReg.MatchString(name); mat { continue } } if len(filter) > 0 { - if mat, _ := filterReg.FindStringMatch(name); mat == nil { + if mat, _ := filterReg.MatchString(name); !mat { continue } } @@ -373,37 +418,33 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray mapping["dialer-proxy"] = dialerProxy } - if override.UDP != nil { - mapping["udp"] = *override.UDP - } - if override.Up != nil { - mapping["up"] = *override.Up - } - if override.Down != nil { - mapping["down"] = *override.Down - } - if override.DialerProxy != nil { - mapping["dialer-proxy"] = *override.DialerProxy - } - if override.SkipCertVerify != nil { - mapping["skip-cert-verify"] = *override.SkipCertVerify - } - if override.Interface != nil { - mapping["interface-name"] = *override.Interface - } - if override.RoutingMark != nil { - mapping["routing-mark"] = *override.RoutingMark - } - if override.IPVersion != nil { - mapping["ip-version"] = *override.IPVersion - } - if override.AdditionalPrefix != nil { - name := mapping["name"].(string) - mapping["name"] = *override.AdditionalPrefix + name - } - if override.AdditionalSuffix != nil { - name := mapping["name"].(string) - mapping["name"] = name + *override.AdditionalSuffix + val := reflect.ValueOf(override) + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + if field.IsNil() { + continue + } + fieldName := strings.Split(val.Type().Field(i).Tag.Get("provider"), ",")[0] + switch fieldName { + case "additional-prefix": + name := mapping["name"].(string) + mapping["name"] = *field.Interface().(*string) + name + case "additional-suffix": + name := mapping["name"].(string) + mapping["name"] = name + *field.Interface().(*string) + case "proxy-name": + // Iterate through all naming replacement rules and perform the replacements. + for _, expr := range override.ProxyName { + name := mapping["name"].(string) + newName, err := expr.Pattern.Replace(name, expr.Target, 0, -1) + if err != nil { + return nil, fmt.Errorf("proxy name replace error: %w", err) + } + mapping["name"] = newName + } + default: + mapping[fieldName] = field.Elem().Interface() + } } proxy, err := adapter.ParseProxy(mapping) @@ -424,5 +465,5 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray } return proxies, nil - } + }, nil } diff --git a/adapter/provider/subscription_info.go b/adapter/provider/subscription_info.go index 3a9e2d72..2ec8537d 100644 --- a/adapter/provider/subscription_info.go +++ b/adapter/provider/subscription_info.go @@ -1,8 +1,11 @@ package provider import ( + "fmt" "strconv" "strings" + + "github.com/metacubex/mihomo/log" ) type SubscriptionInfo struct { @@ -12,28 +15,44 @@ type SubscriptionInfo struct { Expire int64 } -func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo, err error) { - userinfo = strings.ToLower(userinfo) - userinfo = strings.ReplaceAll(userinfo, " ", "") +func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo) { + userinfo = strings.ReplaceAll(strings.ToLower(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": - if value == "" { - si.Expire = 0 - } else { - si.Expire, err = strconv.ParseInt(value, 10, 64) - } + name, value, ok := strings.Cut(field, "=") + if !ok { + continue } + + intValue, err := parseValue(value) if err != nil { - return + log.Warnln("[Provider] get subscription-userinfo: %e", err) + continue + } + + switch name { + case "upload": + si.Upload = intValue + case "download": + si.Download = intValue + case "total": + si.Total = intValue + case "expire": + si.Expire = intValue } } - return + return si +} + +func parseValue(value string) (int64, error) { + if intValue, err := strconv.ParseInt(value, 10, 64); err == nil { + return intValue, nil + } + + if floatValue, err := strconv.ParseFloat(value, 64); err == nil { + return int64(floatValue), nil + } + + return 0, fmt.Errorf("failed to parse value '%s'", value) } diff --git a/common/arc/arc.go b/common/arc/arc.go index da78b1c1..8d44a180 100644 --- a/common/arc/arc.go +++ b/common/arc/arc.go @@ -33,15 +33,8 @@ type ARC[K comparable, V any] struct { // New returns a new Adaptive Replacement Cache (ARC). func New[K comparable, V any](options ...Option[K, V]) *ARC[K, V] { - arc := &ARC[K, V]{ - p: 0, - t1: list.New[*entry[K, V]](), - b1: list.New[*entry[K, V]](), - t2: list.New[*entry[K, V]](), - b2: list.New[*entry[K, V]](), - len: 0, - cache: make(map[K]*entry[K, V]), - } + arc := &ARC[K, V]{} + arc.Clear() for _, option := range options { option(arc) @@ -49,6 +42,19 @@ func New[K comparable, V any](options ...Option[K, V]) *ARC[K, V] { return arc } +func (a *ARC[K, V]) Clear() { + a.mutex.Lock() + defer a.mutex.Unlock() + + a.p = 0 + a.t1 = list.New[*entry[K, V]]() + a.b1 = list.New[*entry[K, V]]() + a.t2 = list.New[*entry[K, V]]() + a.b2 = list.New[*entry[K, V]]() + a.len = 0 + a.cache = make(map[K]*entry[K, V]) +} + // Set inserts a new key-value pair into the cache. // This optimizes future access to this entry (side effect). func (a *ARC[K, V]) Set(key K, value V) { diff --git a/common/atomic/value.go b/common/atomic/value.go index cbc6c5b8..82d40076 100644 --- a/common/atomic/value.go +++ b/common/atomic/value.go @@ -15,28 +15,39 @@ type TypedValue[T any] struct { value atomic.Value } +// tValue is a struct with determined type to resolve atomic.Value usages with interface types +// https://github.com/golang/go/issues/22550 +// +// The intention to have an atomic value store for errors. However, running this code panics: +// panic: sync/atomic: store of inconsistently typed value into Value +// This is because atomic.Value requires that the underlying concrete type be the same (which is a reasonable expectation for its implementation). +// When going through the atomic.Value.Store method call, the fact that both these are of the error interface is lost. +type tValue[T any] struct { + value T +} + func (t *TypedValue[T]) Load() T { value := t.value.Load() if value == nil { return DefaultValue[T]() } - return value.(T) + return value.(tValue[T]).value } func (t *TypedValue[T]) Store(value T) { - t.value.Store(value) + t.value.Store(tValue[T]{value}) } func (t *TypedValue[T]) Swap(new T) T { - old := t.value.Swap(new) + old := t.value.Swap(tValue[T]{new}) if old == nil { return DefaultValue[T]() } - return old.(T) + return old.(tValue[T]).value } func (t *TypedValue[T]) CompareAndSwap(old, new T) bool { - return t.value.CompareAndSwap(old, new) + return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) } func (t *TypedValue[T]) MarshalJSON() ([]byte, error) { diff --git a/common/convert/converter.go b/common/convert/converter.go index 809aa94f..19886d25 100644 --- a/common/convert/converter.go +++ b/common/convert/converter.go @@ -330,15 +330,38 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { vmess["h2-opts"] = h2Opts - case "ws": + case "ws", "httpupgrade": headers := make(map[string]any) wsOpts := make(map[string]any) - wsOpts["path"] = []string{"/"} + wsOpts["path"] = "/" if host, ok := values["host"]; ok && host != "" { headers["Host"] = host.(string) } if path, ok := values["path"]; ok && path != "" { - wsOpts["path"] = path.(string) + path := path.(string) + pathURL, err := url.Parse(path) + if err == nil { + query := pathURL.Query() + if earlyData := query.Get("ed"); earlyData != "" { + med, err := strconv.Atoi(earlyData) + if err == nil { + switch network { + case "ws": + wsOpts["max-early-data"] = med + wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol" + case "httpupgrade": + wsOpts["v2ray-http-upgrade-fast-open"] = true + } + query.Del("ed") + pathURL.RawQuery = query.Encode() + path = pathURL.String() + } + } + if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" { + wsOpts["early-data-header-name"] = earlyDataHeader + } + } + wsOpts["path"] = path } wsOpts["headers"] = headers vmess["ws-opts"] = wsOpts diff --git a/common/convert/util.go b/common/convert/util.go index a715b556..ab006374 100644 --- a/common/convert/util.go +++ b/common/convert/util.go @@ -8,8 +8,8 @@ import ( "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/randv2" "github.com/metacubex/sing-shadowsocks/shadowimpl" - "github.com/zhangyunhao116/fastrand" ) var hostsSuffix = []string{ @@ -302,11 +302,11 @@ func RandHost() string { prefix += string(buf[6:8]) + "-" prefix += string(buf[len(buf)-8:]) - return prefix + hostsSuffix[fastrand.Intn(hostsLen)] + return prefix + hostsSuffix[randv2.IntN(hostsLen)] } func RandUserAgent() string { - return userAgents[fastrand.Intn(uaLen)] + return userAgents[randv2.IntN(uaLen)] } func SetUserAgent(header http.Header) { diff --git a/common/convert/v.go b/common/convert/v.go index 2d8cf732..4102ab75 100644 --- a/common/convert/v.go +++ b/common/convert/v.go @@ -100,7 +100,7 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m h2Opts["headers"] = headers proxy["h2-opts"] = h2Opts - case "ws": + case "ws", "httpupgrade": headers := make(map[string]any) wsOpts := make(map[string]any) headers["User-Agent"] = RandUserAgent() @@ -113,7 +113,13 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m if err != nil { return fmt.Errorf("bad WebSocket max early data size: %v", err) } - wsOpts["max-early-data"] = med + switch network { + case "ws": + wsOpts["max-early-data"] = med + wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol" + case "httpupgrade": + wsOpts["v2ray-http-upgrade-fast-open"] = true + } } if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" { wsOpts["early-data-header-name"] = earlyDataHeader diff --git a/common/lru/lrucache.go b/common/lru/lrucache.go index 6f32ed18..4afe8e0c 100644 --- a/common/lru/lrucache.go +++ b/common/lru/lrucache.go @@ -68,10 +68,8 @@ type LruCache[K comparable, V any] struct { // New creates an LruCache func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { - lc := &LruCache[K, V]{ - lru: list.New[*entry[K, V]](), - cache: make(map[K]*list.Element[*entry[K, V]]), - } + lc := &LruCache[K, V]{} + lc.Clear() for _, option := range options { option(lc) @@ -80,6 +78,14 @@ func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { return lc } +func (c *LruCache[K, V]) Clear() { + c.mu.Lock() + defer c.mu.Unlock() + + c.lru = list.New[*entry[K, V]]() + c.cache = make(map[K]*list.Element[*entry[K, V]]) +} + // Get returns any representation of a cached response and a bool // set to true if the key was found. func (c *LruCache[K, V]) Get(key K) (V, bool) { @@ -223,6 +229,10 @@ func (c *LruCache[K, V]) Delete(key K) { c.mu.Lock() defer c.mu.Unlock() + c.delete(key) +} + +func (c *LruCache[K, V]) delete(key K) { if le, ok := c.cache[key]; ok { c.deleteElement(le) } @@ -246,13 +256,32 @@ func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) { } } -func (c *LruCache[K, V]) Clear() error { +// Compute either sets the computed new value for the key or deletes +// the value for the key. When the delete result of the valueFn function +// is set to true, the value will be deleted, if it exists. When delete +// is set to false, the value is updated to the newValue. +// The ok result indicates whether value was computed and stored, thus, is +// present in the map. The actual result contains the new value in cases where +// the value was computed and stored. +func (c *LruCache[K, V]) Compute( + key K, + valueFn func(oldValue V, loaded bool) (newValue V, delete bool), +) (actual V, ok bool) { c.mu.Lock() defer c.mu.Unlock() - c.cache = make(map[K]*list.Element[*entry[K, V]]) - - return nil + if el := c.get(key); el != nil { + actual, ok = el.value, true + } + if newValue, del := valueFn(actual, ok); del { + if ok { // data not in cache, so needn't delete + c.delete(key) + } + return lo.Empty[V](), false + } else { + c.set(key, newValue) + return newValue, true + } } type entry[K comparable, V any] struct { diff --git a/common/net/tcpip.go b/common/net/tcpip.go index 0499e54c..a84e7e4c 100644 --- a/common/net/tcpip.go +++ b/common/net/tcpip.go @@ -4,11 +4,8 @@ import ( "fmt" "net" "strings" - "time" ) -var KeepAliveInterval = 15 * time.Second - func SplitNetworkType(s string) (string, string, error) { var ( shecme string @@ -47,10 +44,3 @@ 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/nnip/netip.go b/common/nnip/netip.go index e4566138..b987bb45 100644 --- a/common/nnip/netip.go +++ b/common/nnip/netip.go @@ -51,3 +51,23 @@ func UnMasked(p netip.Prefix) netip.Addr { } return addr } + +// PrefixCompare returns an integer comparing two prefixes. +// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2. +// modify from https://github.com/golang/go/issues/61642#issuecomment-1848587909 +func PrefixCompare(p, p2 netip.Prefix) int { + // compare by validity, address family and prefix base address + if c := p.Masked().Addr().Compare(p2.Masked().Addr()); c != 0 { + return c + } + // compare by prefix length + f1, f2 := p.Bits(), p2.Bits() + if f1 < f2 { + return -1 + } + if f1 > f2 { + return 1 + } + // compare by prefix address + return p.Addr().Compare(p2.Addr()) +} diff --git a/common/pool/alloc_test.go b/common/pool/alloc_test.go index 0ac72ee0..c76ff26a 100644 --- a/common/pool/alloc_test.go +++ b/common/pool/alloc_test.go @@ -3,8 +3,8 @@ package pool import ( "testing" + "github.com/metacubex/randv2" "github.com/stretchr/testify/assert" - "github.com/zhangyunhao116/fastrand" ) func TestAllocGet(t *testing.T) { @@ -43,6 +43,6 @@ func TestAllocPutThenGet(t *testing.T) { func BenchmarkMSB(b *testing.B) { for i := 0; i < b.N; i++ { - msb(fastrand.Int()) + msb(randv2.Int()) } } diff --git a/common/queue/queue.go b/common/queue/queue.go index cb58e2f5..d1b6beeb 100644 --- a/common/queue/queue.go +++ b/common/queue/queue.go @@ -59,8 +59,8 @@ func (q *Queue[T]) Copy() []T { // Len returns the number of items in this queue. func (q *Queue[T]) Len() int64 { - q.lock.Lock() - defer q.lock.Unlock() + q.lock.RLock() + defer q.lock.RUnlock() return int64(len(q.items)) } diff --git a/common/singleflight/singleflight.go b/common/singleflight/singleflight.go new file mode 100644 index 00000000..e31c4eb6 --- /dev/null +++ b/common/singleflight/singleflight.go @@ -0,0 +1,224 @@ +// copy and modify from "golang.org/x/sync/singleflight" + +// Copyright 2013 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 singleflight provides a duplicate function call suppression +// mechanism. +package singleflight + +import ( + "bytes" + "errors" + "fmt" + "runtime" + "runtime/debug" + "sync" +) + +// errGoexit indicates the runtime.Goexit was called in +// the user given function. +var errGoexit = errors.New("runtime.Goexit was called") + +// A panicError is an arbitrary value recovered from a panic +// with the stack trace during the execution of given function. +type panicError struct { + value interface{} + stack []byte +} + +// Error implements error interface. +func (p *panicError) Error() string { + return fmt.Sprintf("%v\n\n%s", p.value, p.stack) +} + +func (p *panicError) Unwrap() error { + err, ok := p.value.(error) + if !ok { + return nil + } + + return err +} + +func newPanicError(v interface{}) error { + stack := debug.Stack() + + // The first line of the stack trace is of the form "goroutine N [status]:" + // but by the time the panic reaches Do the goroutine may no longer exist + // and its status will have changed. Trim out the misleading line. + if line := bytes.IndexByte(stack[:], '\n'); line >= 0 { + stack = stack[line+1:] + } + return &panicError{value: v, stack: stack} +} + +// call is an in-flight or completed singleflight.Do call +type call[T any] struct { + wg sync.WaitGroup + + // These fields are written once before the WaitGroup is done + // and are only read after the WaitGroup is done. + val T + err error + + // These fields are read and written with the singleflight + // mutex held before the WaitGroup is done, and are read but + // not written after the WaitGroup is done. + dups int + chans []chan<- Result[T] +} + +// Group represents a class of work and forms a namespace in +// which units of work can be executed with duplicate suppression. +type Group[T any] struct { + mu sync.Mutex // protects m + m map[string]*call[T] // lazily initialized + + StoreResult bool +} + +// Result holds the results of Do, so they can be passed +// on a channel. +type Result[T any] struct { + Val T + Err error + Shared bool +} + +// Do executes and returns the results of the given function, making +// sure that only one execution is in-flight for a given key at a +// time. If a duplicate comes in, the duplicate caller waits for the +// original to complete and receives the same results. +// The return value shared indicates whether v was given to multiple callers. +func (g *Group[T]) Do(key string, fn func() (T, error)) (v T, err error, shared bool) { + g.mu.Lock() + if g.m == nil { + g.m = make(map[string]*call[T]) + } + if c, ok := g.m[key]; ok { + c.dups++ + g.mu.Unlock() + c.wg.Wait() + + if e, ok := c.err.(*panicError); ok { + panic(e) + } else if c.err == errGoexit { + runtime.Goexit() + } + return c.val, c.err, true + } + c := new(call[T]) + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + g.doCall(c, key, fn) + return c.val, c.err, c.dups > 0 +} + +// DoChan is like Do but returns a channel that will receive the +// results when they are ready. +// +// The returned channel will not be closed. +func (g *Group[T]) DoChan(key string, fn func() (T, error)) <-chan Result[T] { + ch := make(chan Result[T], 1) + g.mu.Lock() + if g.m == nil { + g.m = make(map[string]*call[T]) + } + if c, ok := g.m[key]; ok { + c.dups++ + c.chans = append(c.chans, ch) + g.mu.Unlock() + return ch + } + c := &call[T]{chans: []chan<- Result[T]{ch}} + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + go g.doCall(c, key, fn) + + return ch +} + +// doCall handles the single call for a key. +func (g *Group[T]) doCall(c *call[T], key string, fn func() (T, error)) { + normalReturn := false + recovered := false + + // use double-defer to distinguish panic from runtime.Goexit, + // more details see https://golang.org/cl/134395 + defer func() { + // the given function invoked runtime.Goexit + if !normalReturn && !recovered { + c.err = errGoexit + } + + g.mu.Lock() + defer g.mu.Unlock() + c.wg.Done() + if g.m[key] == c && !g.StoreResult { + delete(g.m, key) + } + + if e, ok := c.err.(*panicError); ok { + // In order to prevent the waiting channels from being blocked forever, + // needs to ensure that this panic cannot be recovered. + if len(c.chans) > 0 { + go panic(e) + select {} // Keep this goroutine around so that it will appear in the crash dump. + } else { + panic(e) + } + } else if c.err == errGoexit { + // Already in the process of goexit, no need to call again + } else { + // Normal return + for _, ch := range c.chans { + ch <- Result[T]{c.val, c.err, c.dups > 0} + } + } + }() + + func() { + defer func() { + if !normalReturn { + // Ideally, we would wait to take a stack trace until we've determined + // whether this is a panic or a runtime.Goexit. + // + // Unfortunately, the only way we can distinguish the two is to see + // whether the recover stopped the goroutine from terminating, and by + // the time we know that, the part of the stack trace relevant to the + // panic has been discarded. + if r := recover(); r != nil { + c.err = newPanicError(r) + } + } + }() + + c.val, c.err = fn() + normalReturn = true + }() + + if !normalReturn { + recovered = true + } +} + +// Forget tells the singleflight to forget about a key. Future calls +// to Do for this key will call the function rather than waiting for +// an earlier call to complete. +func (g *Group[T]) Forget(key string) { + g.mu.Lock() + delete(g.m, key) + g.mu.Unlock() +} + +func (g *Group[T]) Reset() { + g.mu.Lock() + g.m = nil + g.mu.Unlock() +} diff --git a/common/structure/structure.go b/common/structure/structure.go index fde22309..5bafc178 100644 --- a/common/structure/structure.go +++ b/common/structure/structure.go @@ -3,6 +3,7 @@ package structure // references: https://github.com/mitchellh/mapstructure import ( + "encoding" "encoding/base64" "fmt" "reflect" @@ -85,36 +86,62 @@ func (d *Decoder) Decode(src map[string]any, dst any) error { return nil } -func (d *Decoder) decode(name string, data any, val reflect.Value) error { - kind := val.Kind() - switch { - case isInt(kind): - return d.decodeInt(name, data, val) - case isUint(kind): - return d.decodeUint(name, data, val) - case isFloat(kind): - return d.decodeFloat(name, data, val) +// isNil returns true if the input is nil or a typed nil pointer. +func isNil(input any) bool { + if input == nil { + return true } - switch kind { - case reflect.Pointer: - if val.IsNil() { + val := reflect.ValueOf(input) + return val.Kind() == reflect.Pointer && val.IsNil() +} + +func (d *Decoder) decode(name string, data any, val reflect.Value) error { + if isNil(data) { + // If the data is nil, then we don't set anything + // Maybe we should set to zero value? + return nil + } + if !reflect.ValueOf(data).IsValid() { + // If the input value is invalid, then we just set the value + // to be the zero value. + val.Set(reflect.Zero(val.Type())) + return nil + } + for { + kind := val.Kind() + if kind == reflect.Pointer && val.IsNil() { val.Set(reflect.New(val.Type().Elem())) } - return d.decode(name, data, val.Elem()) - case reflect.String: - return d.decodeString(name, data, val) - case reflect.Bool: - return d.decodeBool(name, data, val) - case reflect.Slice: - return d.decodeSlice(name, data, val) - case reflect.Map: - return d.decodeMap(name, data, val) - case reflect.Interface: - return d.setInterface(name, data, val) - case reflect.Struct: - return d.decodeStruct(name, data, val) - default: - return fmt.Errorf("type %s not support", val.Kind().String()) + if ok, err := d.decodeTextUnmarshaller(name, data, val); ok { + return err + } + switch { + case isInt(kind): + return d.decodeInt(name, data, val) + case isUint(kind): + return d.decodeUint(name, data, val) + case isFloat(kind): + return d.decodeFloat(name, data, val) + } + switch kind { + case reflect.Pointer: + val = val.Elem() + continue + case reflect.String: + return d.decodeString(name, data, val) + case reflect.Bool: + return d.decodeBool(name, data, val) + case reflect.Slice: + return d.decodeSlice(name, data, val) + case reflect.Map: + return d.decodeMap(name, data, val) + case reflect.Interface: + return d.setInterface(name, data, val) + case reflect.Struct: + return d.decodeStruct(name, data, val) + default: + return fmt.Errorf("type %s not support", val.Kind().String()) + } } } @@ -553,3 +580,25 @@ func (d *Decoder) setInterface(name string, data any, val reflect.Value) (err er val.Set(dataVal) return nil } + +func (d *Decoder) decodeTextUnmarshaller(name string, data any, val reflect.Value) (bool, error) { + if !val.CanAddr() { + return false, nil + } + valAddr := val.Addr() + if !valAddr.CanInterface() { + return false, nil + } + unmarshaller, ok := valAddr.Interface().(encoding.TextUnmarshaler) + if !ok { + return false, nil + } + var str string + if err := d.decodeString(name, data, reflect.Indirect(reflect.ValueOf(&str))); err != nil { + return false, err + } + if err := unmarshaller.UnmarshalText([]byte(str)); err != nil { + return true, fmt.Errorf("cannot parse '%s' as %s: %s", name, val.Type(), err) + } + return true, nil +} diff --git a/common/structure/structure_test.go b/common/structure/structure_test.go index 9f31d3d1..266b828f 100644 --- a/common/structure/structure_test.go +++ b/common/structure/structure_test.go @@ -1,6 +1,7 @@ package structure import ( + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -179,3 +180,108 @@ func TestStructure_SliceNilValueComplex(t *testing.T) { err = decoder.Decode(rawMap, ss) assert.NotNil(t, err) } + +func TestStructure_SliceCap(t *testing.T) { + rawMap := map[string]any{ + "foo": []string{}, + } + + s := &struct { + Foo []string `test:"foo,omitempty"` + Bar []string `test:"bar,omitempty"` + }{} + + err := decoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.NotNil(t, s.Foo) // structure's Decode will ensure value not nil when input has value even it was set an empty array + assert.Nil(t, s.Bar) +} + +func TestStructure_Base64(t *testing.T) { + rawMap := map[string]any{ + "foo": "AQID", + } + + s := &struct { + Foo []byte `test:"foo"` + }{} + + err := decoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.Equal(t, []byte{1, 2, 3}, s.Foo) +} + +func TestStructure_Pointer(t *testing.T) { + rawMap := map[string]any{ + "foo": "foo", + } + + s := &struct { + Foo *string `test:"foo,omitempty"` + Bar *string `test:"bar,omitempty"` + }{} + + err := decoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.NotNil(t, s.Foo) + assert.Equal(t, "foo", *s.Foo) + assert.Nil(t, s.Bar) +} + +type num struct { + a int +} + +func (n *num) UnmarshalText(text []byte) (err error) { + n.a, err = strconv.Atoi(string(text)) + return +} + +func TestStructure_TextUnmarshaller(t *testing.T) { + rawMap := map[string]any{ + "num": "255", + "num_p": "127", + } + + s := &struct { + Num num `test:"num"` + NumP *num `test:"num_p"` + }{} + + err := decoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.Equal(t, 255, s.Num.a) + assert.NotNil(t, s.NumP) + assert.Equal(t, s.NumP.a, 127) + + // test WeaklyTypedInput + rawMap["num"] = 256 + err = decoder.Decode(rawMap, s) + assert.NotNilf(t, err, "should throw error: %#v", s) + err = weakTypeDecoder.Decode(rawMap, s) + assert.Nil(t, err) + assert.Equal(t, 256, s.Num.a) + + // test invalid input + rawMap["num_p"] = "abc" + err = decoder.Decode(rawMap, s) + assert.NotNilf(t, err, "should throw error: %#v", s) +} + +func TestStructure_Null(t *testing.T) { + rawMap := map[string]any{ + "opt": map[string]any{ + "bar": nil, + }, + } + + s := struct { + Opt struct { + Bar string `test:"bar,optional"` + } `test:"opt,optional"` + }{} + + err := decoder.Decode(rawMap, &s) + assert.Nil(t, err) + assert.Equal(t, s.Opt.Bar, "") +} diff --git a/common/utils/callback.go b/common/utils/callback.go new file mode 100644 index 00000000..ad734c0f --- /dev/null +++ b/common/utils/callback.go @@ -0,0 +1,50 @@ +package utils + +import ( + "io" + "sync" + + list "github.com/bahlo/generic-list-go" +) + +type Callback[T any] struct { + list list.List[func(T)] + mutex sync.RWMutex +} + +func NewCallback[T any]() *Callback[T] { + return &Callback[T]{} +} + +func (c *Callback[T]) Register(item func(T)) io.Closer { + c.mutex.Lock() + defer c.mutex.Unlock() + element := c.list.PushBack(item) + return &callbackCloser[T]{ + element: element, + callback: c, + } +} + +func (c *Callback[T]) Emit(item T) { + c.mutex.RLock() + defer c.mutex.RUnlock() + for element := c.list.Front(); element != nil; element = element.Next() { + go element.Value(item) + } +} + +type callbackCloser[T any] struct { + element *list.Element[func(T)] + callback *Callback[T] + once sync.Once +} + +func (c *callbackCloser[T]) Close() error { + c.once.Do(func() { + c.callback.mutex.Lock() + defer c.callback.mutex.Unlock() + c.callback.list.Remove(c.element) + }) + return nil +} diff --git a/common/utils/hash.go b/common/utils/hash.go new file mode 100644 index 00000000..6957e2c3 --- /dev/null +++ b/common/utils/hash.go @@ -0,0 +1,62 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" + "errors" +) + +// HashType warps hash array inside struct +// someday can change to other hash algorithm simply +type HashType struct { + md5 [md5.Size]byte // MD5 +} + +func MakeHash(data []byte) HashType { + return HashType{md5.Sum(data)} +} + +func (h HashType) Equal(hash HashType) bool { + return h.md5 == hash.md5 +} + +func (h HashType) Bytes() []byte { + return h.md5[:] +} + +func (h HashType) String() string { + return hex.EncodeToString(h.Bytes()) +} + +func (h HashType) MarshalText() ([]byte, error) { + return []byte(h.String()), nil +} + +func (h *HashType) UnmarshalText(data []byte) error { + if hex.DecodedLen(len(data)) != md5.Size { + return errors.New("invalid hash length") + } + _, err := hex.Decode(h.md5[:], data) + return err +} + +func (h HashType) MarshalBinary() ([]byte, error) { + return h.md5[:], nil +} + +func (h *HashType) UnmarshalBinary(data []byte) error { + if len(data) != md5.Size { + return errors.New("invalid hash length") + } + copy(h.md5[:], data) + return nil +} + +func (h HashType) Len() int { + return len(h.md5) +} + +func (h HashType) IsValid() bool { + var zero HashType + return h != zero +} diff --git a/common/utils/uuid.go b/common/utils/uuid.go index f559b471..d9a8b789 100644 --- a/common/utils/uuid.go +++ b/common/utils/uuid.go @@ -2,19 +2,39 @@ package utils import ( "github.com/gofrs/uuid/v5" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) -type fastRandReader struct{} +type unsafeRandReader struct{} -func (r fastRandReader) Read(p []byte) (int, error) { - return fastrand.Read(p) +func (r unsafeRandReader) Read(p []byte) (n int, err error) { + // modify from https://github.com/golang/go/blob/587c3847da81aa7cfc3b3db2677c8586c94df13a/src/runtime/rand.go#L70-L89 + // Inspired by wyrand. + n = len(p) + v := randv2.Uint64() + for len(p) > 0 { + v ^= 0xa0761d6478bd642f + v *= 0xe7037ed1a0b428db + size := 8 + if len(p) < 8 { + size = len(p) + } + for i := 0; i < size; i++ { + p[i] ^= byte(v >> (8 * i)) + } + p = p[size:] + v = v>>32 | v<<32 + } + + return } -var UnsafeUUIDGenerator = uuid.NewGenWithOptions(uuid.WithRandomReader(fastRandReader{})) +var UnsafeRandReader = unsafeRandReader{} + +var UnsafeUUIDGenerator = uuid.NewGenWithOptions(uuid.WithRandomReader(UnsafeRandReader)) func NewUUIDV1() uuid.UUID { - u, _ := UnsafeUUIDGenerator.NewV1() // fastrand.Read wouldn't cause error, so ignore err is safe + u, _ := UnsafeUUIDGenerator.NewV1() // unsafeRandReader wouldn't cause error, so ignore err is safe return u } @@ -23,7 +43,7 @@ func NewUUIDV3(ns uuid.UUID, name string) uuid.UUID { } func NewUUIDV4() uuid.UUID { - u, _ := UnsafeUUIDGenerator.NewV4() // fastrand.Read wouldn't cause error, so ignore err is safe + u, _ := UnsafeUUIDGenerator.NewV4() // unsafeRandReader wouldn't cause error, so ignore err is safe return u } @@ -32,12 +52,12 @@ func NewUUIDV5(ns uuid.UUID, name string) uuid.UUID { } func NewUUIDV6() uuid.UUID { - u, _ := UnsafeUUIDGenerator.NewV6() // fastrand.Read wouldn't cause error, so ignore err is safe + u, _ := UnsafeUUIDGenerator.NewV6() // unsafeRandReader 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 + u, _ := UnsafeUUIDGenerator.NewV7() // unsafeRandReader wouldn't cause error, so ignore err is safe return u } diff --git a/component/auth/auth.go b/component/auth/auth.go index b52fa135..176b21d7 100644 --- a/component/auth/auth.go +++ b/component/auth/auth.go @@ -5,6 +5,11 @@ type Authenticator interface { Users() []string } +type AuthStore interface { + Authenticator() Authenticator + SetAuthenticator(Authenticator) +} + type AuthUser struct { User string Pass string diff --git a/component/ca/ca-certificates.crt b/component/ca/ca-certificates.crt new file mode 100644 index 00000000..e69de29b diff --git a/component/ca/config.go b/component/ca/config.go index 03fb007c..9d002db6 100644 --- a/component/ca/config.go +++ b/component/ca/config.go @@ -5,12 +5,16 @@ import ( "crypto/sha256" "crypto/tls" "crypto/x509" + _ "embed" "encoding/hex" "errors" "fmt" "os" + "strconv" "strings" "sync" + + C "github.com/metacubex/mihomo/constant" ) var trustCerts []*x509.Certificate @@ -18,6 +22,11 @@ var globalCertPool *x509.CertPool var mutex sync.RWMutex var errNotMatch = errors.New("certificate fingerprints do not match") +//go:embed ca-certificates.crt +var _CaCertificates []byte +var DisableEmbedCa, _ = strconv.ParseBool(os.Getenv("DISABLE_EMBED_CA")) +var DisableSystemCa, _ = strconv.ParseBool(os.Getenv("DISABLE_SYSTEM_CA")) + func AddCertificate(certificate string) error { mutex.Lock() defer mutex.Unlock() @@ -34,13 +43,20 @@ func AddCertificate(certificate string) error { func initializeCertPool() { var err error - globalCertPool, err = x509.SystemCertPool() - if err != nil { + if DisableSystemCa { globalCertPool = x509.NewCertPool() + } else { + globalCertPool, err = x509.SystemCertPool() + if err != nil { + globalCertPool = x509.NewCertPool() + } } for _, cert := range trustCerts { globalCertPool.AddCert(cert) } + if !DisableEmbedCa { + globalCertPool.AppendCertsFromPEM(_CaCertificates) + } } func ResetCertificate() { @@ -51,9 +67,6 @@ func ResetCertificate() { } func getCertPool() *x509.CertPool { - if len(trustCerts) == 0 { - return nil - } if globalCertPool == nil { mutex.Lock() defer mutex.Unlock() @@ -103,7 +116,7 @@ func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, cu var certificate []byte var err error if len(customCA) > 0 { - certificate, err = os.ReadFile(customCA) + certificate, err = os.ReadFile(C.Path.Resolve(customCA)) if err != nil { return nil, fmt.Errorf("load ca error: %w", err) } diff --git a/component/ca/fix_windows.go b/component/ca/fix_windows.go new file mode 100644 index 00000000..42511791 --- /dev/null +++ b/component/ca/fix_windows.go @@ -0,0 +1,14 @@ +package ca + +import ( + "github.com/metacubex/mihomo/constant/features" +) + +func init() { + // crypto/x509: certificate validation in Windows fails to validate IP in SAN + // https://github.com/golang/go/issues/37176 + // As far as I can tell this is still the case on most older versions of Windows (but seems to be fixed in 10) + if features.WindowsMajorVersion < 10 && len(_CaCertificates) > 0 { + DisableSystemCa = true + } +} diff --git a/component/cidr/ipcidr_set.go b/component/cidr/ipcidr_set.go index 0cb55e36..4bde9671 100644 --- a/component/cidr/ipcidr_set.go +++ b/component/cidr/ipcidr_set.go @@ -43,12 +43,20 @@ func (set *IpCidrSet) IsContainForString(ipString string) bool { } func (set *IpCidrSet) IsContain(ip netip.Addr) bool { - return set.toIPSet().Contains(ip.WithZone("")) + return set.ToIPSet().Contains(ip.WithZone("")) +} + +// MatchIp implements C.IpMatcher +func (set *IpCidrSet) MatchIp(ip netip.Addr) bool { + if set.IsEmpty() { + return false + } + return set.IsContain(ip) } func (set *IpCidrSet) Merge() error { var b netipx.IPSetBuilder - b.AddSet(set.toIPSet()) + b.AddSet(set.ToIPSet()) i, err := b.IPSet() if err != nil { return err @@ -57,7 +65,23 @@ func (set *IpCidrSet) Merge() error { return nil } -func (set *IpCidrSet) toIPSet() *netipx.IPSet { +func (set *IpCidrSet) IsEmpty() bool { + return set == nil || len(set.rr) == 0 +} + +func (set *IpCidrSet) Foreach(f func(prefix netip.Prefix) bool) { + for _, r := range set.rr { + for _, prefix := range r.Prefixes() { + if !f(prefix) { + return + } + } + } +} + +// ToIPSet not safe convert to *netipx.IPSet +// be careful, must be used after Merge +func (set *IpCidrSet) ToIPSet() *netipx.IPSet { return (*netipx.IPSet)(unsafe.Pointer(set)) } diff --git a/component/cidr/ipcidr_set_bin.go b/component/cidr/ipcidr_set_bin.go new file mode 100644 index 00000000..f6a03488 --- /dev/null +++ b/component/cidr/ipcidr_set_bin.go @@ -0,0 +1,77 @@ +package cidr + +import ( + "encoding/binary" + "errors" + "io" + "net/netip" + + "go4.org/netipx" +) + +func (ss *IpCidrSet) WriteBin(w io.Writer) (err error) { + // version + _, err = w.Write([]byte{1}) + if err != nil { + return err + } + + // rr + err = binary.Write(w, binary.BigEndian, int64(len(ss.rr))) + if err != nil { + return err + } + for _, r := range ss.rr { + err = binary.Write(w, binary.BigEndian, r.From().As16()) + if err != nil { + return err + } + err = binary.Write(w, binary.BigEndian, r.To().As16()) + if err != nil { + return err + } + } + + return nil +} + +func ReadIpCidrSet(r io.Reader) (ss *IpCidrSet, err error) { + // version + version := make([]byte, 1) + _, err = io.ReadFull(r, version) + if err != nil { + return nil, err + } + if version[0] != 1 { + return nil, errors.New("version is invalid") + } + + ss = NewIpCidrSet() + var length int64 + + // rr + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 1 { + return nil, errors.New("length is invalid") + } + ss.rr = make([]netipx.IPRange, length) + for i := int64(0); i < length; i++ { + var a16 [16]byte + err = binary.Read(r, binary.BigEndian, &a16) + if err != nil { + return nil, err + } + from := netip.AddrFrom16(a16).Unmap() + err = binary.Read(r, binary.BigEndian, &a16) + if err != nil { + return nil, err + } + to := netip.AddrFrom16(a16).Unmap() + ss.rr[i] = netipx.IPRangeFrom(from, to) + } + + return ss, nil +} diff --git a/component/dhcp/conn.go b/component/dhcp/conn.go index ff26275b..61c801bd 100644 --- a/component/dhcp/conn.go +++ b/component/dhcp/conn.go @@ -3,6 +3,7 @@ package dhcp import ( "context" "net" + "net/netip" "runtime" "github.com/metacubex/mihomo/component/dialer" @@ -24,5 +25,5 @@ func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, er options = append(options, dialer.WithFallbackBind(true)) } - return dialer.ListenPacket(ctx, "udp4", listenAddr, options...) + return dialer.ListenPacket(ctx, "udp4", listenAddr, netip.AddrPortFrom(netip.AddrFrom4([4]byte{255, 255, 255, 255}), 67), options...) } diff --git a/component/dialer/bind.go b/component/dialer/bind.go index 72df8c72..de6aacd3 100644 --- a/component/dialer/bind.go +++ b/component/dialer/bind.go @@ -14,6 +14,7 @@ func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination if err != nil { return nil, err } + destination = destination.Unmap() var addr netip.Prefix switch network { @@ -23,7 +24,7 @@ func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination addr, err = ifaceObj.PickIPv6Addr(destination) default: if destination.IsValid() { - if destination.Is4() || destination.Is4In6() { + if destination.Is4() { addr, err = ifaceObj.PickIPv4Addr(destination) } else { addr, err = ifaceObj.PickIPv6Addr(destination) @@ -74,7 +75,7 @@ func fallbackBindIfaceToDialer(ifaceName string, dialer *net.Dialer, network str return nil } -func fallbackBindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) { +func fallbackBindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string, rAddrPort netip.AddrPort) (string, error) { _, port, err := net.SplitHostPort(address) if err != nil { port = "0" diff --git a/component/dialer/bind_darwin.go b/component/dialer/bind_darwin.go index f83b86f8..fdea24bf 100644 --- a/component/dialer/bind_darwin.go +++ b/component/dialer/bind_darwin.go @@ -46,7 +46,7 @@ func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.A return nil } -func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) { +func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { ifaceObj, err := iface.ResolveInterface(ifaceName) if err != nil { return "", err diff --git a/component/dialer/bind_linux.go b/component/dialer/bind_linux.go index 1ec98f3d..79cf735b 100644 --- a/component/dialer/bind_linux.go +++ b/component/dialer/bind_linux.go @@ -35,7 +35,7 @@ func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.A return nil } -func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) { +func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { addControlToListenConfig(lc, bindControl(ifaceName)) return address, nil diff --git a/component/dialer/bind_others.go b/component/dialer/bind_others.go index 44181610..b7db962a 100644 --- a/component/dialer/bind_others.go +++ b/component/dialer/bind_others.go @@ -11,8 +11,8 @@ func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, des return fallbackBindIfaceToDialer(ifaceName, dialer, network, destination) } -func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, network, address string) (string, error) { - return fallbackBindIfaceToListenConfig(ifaceName, lc, network, address) +func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, network, address string, rAddrPort netip.AddrPort) (string, error) { + return fallbackBindIfaceToListenConfig(ifaceName, lc, network, address, rAddrPort) } func ParseNetwork(network string, addr netip.Addr) string { diff --git a/component/dialer/bind_windows.go b/component/dialer/bind_windows.go index 120f1657..98a076d0 100644 --- a/component/dialer/bind_windows.go +++ b/component/dialer/bind_windows.go @@ -36,7 +36,7 @@ func bind6(handle syscall.Handle, ifaceIdx int) error { return err } -func bindControl(ifaceIdx int) controlFn { +func bindControl(ifaceIdx int, rAddrPort netip.AddrPort) controlFn { return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { addrPort, err := netip.ParseAddrPort(address) if err == nil && !addrPort.Addr().IsGlobalUnicast() { @@ -55,7 +55,7 @@ func bindControl(ifaceIdx int) controlFn { 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 { + if (!addrPort.Addr().IsValid() || addrPort.Addr().IsUnspecified()) && bind6err != nil && rAddrPort.Addr().Unmap().Is4() { // try bind ipv6, if failed, ignore. it's a workaround for windows disable interface ipv6 if bind4err != nil { innerErr = fmt.Errorf("%w (%s)", bind6err, bind4err) @@ -76,23 +76,23 @@ func bindControl(ifaceIdx int) controlFn { } } -func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error { +func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, destination netip.Addr) error { ifaceObj, err := iface.ResolveInterface(ifaceName) if err != nil { return err } - addControlToDialer(dialer, bindControl(ifaceObj.Index)) + addControlToDialer(dialer, bindControl(ifaceObj.Index, netip.AddrPortFrom(destination, 0))) return nil } -func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) { +func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { ifaceObj, err := iface.ResolveInterface(ifaceName) if err != nil { return "", err } - addControlToListenConfig(lc, bindControl(ifaceObj.Index)) + addControlToListenConfig(lc, bindControl(ifaceObj.Index, rAddrPort)) return address, nil } diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go index 12e7c960..4fd051ef 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -12,8 +12,8 @@ import ( "sync" "time" + "github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/mihomo/component/resolver" - "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/log" ) @@ -78,30 +78,30 @@ func DialContext(ctx context.Context, network, address string, options ...Option } } -func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) { - if features.CMFA && DefaultSocketHook != nil { - return listenPacketHooked(ctx, network, address) - } - +func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort, options ...Option) (net.PacketConn, error) { cfg := applyOptions(options...) lc := &net.ListenConfig{} - if cfg.interfaceName != "" { - bind := bindIfaceToListenConfig - if cfg.fallbackBind { - bind = fallbackBindIfaceToListenConfig - } - addr, err := bind(cfg.interfaceName, lc, network, address) - if err != nil { - return nil, err - } - address = addr - } if cfg.addrReuse { addrReuseToListenConfig(lc) } - if cfg.routingMark != 0 { - bindMarkToListenConfig(cfg.routingMark, lc, network, address) + if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA) + socketHookToListenConfig(lc) + } else { + if cfg.interfaceName != "" { + bind := bindIfaceToListenConfig + if cfg.fallbackBind { + bind = fallbackBindIfaceToListenConfig + } + addr, err := bind(cfg.interfaceName, lc, network, address, rAddrPort) + if err != nil { + return nil, err + } + address = addr + } + if cfg.routingMark != 0 { + bindMarkToListenConfig(cfg.routingMark, lc, network, address) + } } return lc.ListenPacket(ctx, network, address) @@ -127,17 +127,11 @@ func GetTcpConcurrent() bool { } func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) { - if features.CMFA && DefaultSocketHook != nil { - return dialContextHooked(ctx, network, destination, port) - } - var address string if IP4PEnable { - NewDestination, NewPort := lookupIP4P(destination.String(), port) - address = net.JoinHostPort(NewDestination, NewPort) - } else { - address = net.JoinHostPort(destination.String(), port) + destination, port = lookupIP4P(destination, port) } + address = net.JoinHostPort(destination.String(), port) netDialer := opt.netDialer switch netDialer.(type) { @@ -151,24 +145,31 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po } dialer := netDialer.(*net.Dialer) - if opt.interfaceName != "" { - bind := bindIfaceToDialer - if opt.fallbackBind { - bind = fallbackBindIfaceToDialer - } - if err := bind(opt.interfaceName, dialer, network, destination); err != nil { - return nil, err - } - } - if opt.routingMark != 0 { - bindMarkToDialer(opt.routingMark, dialer, network, destination) - } + keepalive.SetNetDialer(dialer) if opt.mpTcp { setMultiPathTCP(dialer) } - if opt.tfo { - return dialTFO(ctx, *dialer, network, address) + + if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA) + socketHookToToDialer(dialer) + } else { + if opt.interfaceName != "" { + bind := bindIfaceToDialer + if opt.fallbackBind { + bind = fallbackBindIfaceToDialer + } + if err := bind(opt.interfaceName, dialer, network, destination); err != nil { + return nil, err + } + } + if opt.routingMark != 0 { + bindMarkToDialer(opt.routingMark, dialer, network, destination) + } + if opt.tfo && !DisableTFO { + return dialTFO(ctx, *dialer, network, address) + } } + return dialer.DialContext(ctx, network, address) } @@ -339,26 +340,18 @@ func parseAddr(ctx context.Context, network, address string, preferResolver reso return nil, "-1", err } + if preferResolver == nil { + preferResolver = resolver.ProxyServerHostResolver + } + var ips []netip.Addr switch network { case "tcp4", "udp4": - if preferResolver == nil { - ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host) - } else { - ips, err = resolver.LookupIPv4WithResolver(ctx, host, preferResolver) - } + 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) - } + 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) - } + ips, err = resolver.LookupIPWithResolver(ctx, host, preferResolver) } if err != nil { return nil, "-1", fmt.Errorf("dns resolve failed: %w", err) @@ -380,12 +373,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) { - opt := WithOption(d.Opt) + opt := d.Opt // make a copy if rAddrPort.Addr().Unmap().IsLoopback() { // avoid "The requested address is not valid in its context." - opt = WithInterface("") + WithInterface("")(&opt) } - return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, opt) + return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, rAddrPort, WithOption(opt)) } func NewDialer(options ...Option) Dialer { @@ -399,13 +392,13 @@ func GetIP4PEnable(enableIP4PConvert bool) { // kanged from https://github.com/heiher/frp/blob/ip4p/client/ip4p.go -func lookupIP4P(addr string, port string) (string, string) { - ip := net.ParseIP(addr) +func lookupIP4P(addr netip.Addr, port string) (netip.Addr, string) { + ip := addr.AsSlice() if ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x00 && ip[3] == 0x00 { - addr = net.IPv4(ip[12], ip[13], ip[14], ip[15]).String() + addr = netip.AddrFrom4([4]byte{ip[12], ip[13], ip[14], ip[15]}) port = strconv.Itoa(int(ip[10])<<8 + int(ip[11])) - log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr, port)) + log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr.String(), port)) return addr, port } return addr, port diff --git a/component/dialer/patch_android.go b/component/dialer/patch_android.go deleted file mode 100644 index 7c33a6c0..00000000 --- a/component/dialer/patch_android.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build android && cmfa - -package dialer - -import ( - "context" - "net" - "net/netip" - "syscall" -) - -type SocketControl func(network, address string, conn syscall.RawConn) error - -var DefaultSocketHook SocketControl - -func dialContextHooked(ctx context.Context, network string, destination netip.Addr, port string) (net.Conn, error) { - dialer := &net.Dialer{ - Control: DefaultSocketHook, - } - - conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port)) - if err != nil { - return nil, err - } - - if t, ok := conn.(*net.TCPConn); ok { - t.SetKeepAlive(false) - } - - return conn, nil -} - -func listenPacketHooked(ctx context.Context, network, address string) (net.PacketConn, error) { - lc := &net.ListenConfig{ - Control: DefaultSocketHook, - } - - return lc.ListenPacket(ctx, network, address) -} diff --git a/component/dialer/patch_common.go b/component/dialer/patch_common.go deleted file mode 100644 index bad0ef48..00000000 --- a/component/dialer/patch_common.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build !(android && cmfa) - -package dialer - -import ( - "context" - "net" - "net/netip" - "syscall" -) - -type SocketControl func(network, address string, conn syscall.RawConn) error - -var DefaultSocketHook SocketControl - -func dialContextHooked(ctx context.Context, network string, destination netip.Addr, port string) (net.Conn, error) { - return nil, nil -} - -func listenPacketHooked(ctx context.Context, network, address string) (net.PacketConn, error) { - return nil, nil -} diff --git a/component/dialer/resolver.go b/component/dialer/resolver.go deleted file mode 100644 index ea38a90e..00000000 --- a/component/dialer/resolver.go +++ /dev/null @@ -1,29 +0,0 @@ -package dialer - -import ( - "context" - "net" - "net/netip" -) - -func init() { - // We must use this DialContext to query DNS - // when using net default resolver. - net.DefaultResolver.PreferGo = true - net.DefaultResolver.Dial = resolverDialContext -} - -func resolverDialContext(ctx context.Context, network, address string) (net.Conn, error) { - d := &net.Dialer{} - - interfaceName := DefaultInterface.Load() - - if interfaceName != "" { - dstIP, err := netip.ParseAddr(address) - if err == nil { - _ = bindIfaceToDialer(interfaceName, d, network, dstIP) - } - } - - return d.DialContext(ctx, network, address) -} diff --git a/component/dialer/socket_hook.go b/component/dialer/socket_hook.go new file mode 100644 index 00000000..f8605525 --- /dev/null +++ b/component/dialer/socket_hook.go @@ -0,0 +1,27 @@ +package dialer + +import ( + "context" + "net" + "syscall" +) + +// SocketControl +// never change type traits because it's used in CMFA +type SocketControl func(network, address string, conn syscall.RawConn) error + +// DefaultSocketHook +// never change type traits because it's used in CMFA +var DefaultSocketHook SocketControl + +func socketHookToToDialer(dialer *net.Dialer) { + addControlToDialer(dialer, func(ctx context.Context, network, address string, c syscall.RawConn) error { + return DefaultSocketHook(network, address, c) + }) +} + +func socketHookToListenConfig(lc *net.ListenConfig) { + addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error { + return DefaultSocketHook(network, address, c) + }) +} diff --git a/component/dialer/tfo.go b/component/dialer/tfo.go index 228e9689..76fe94d0 100644 --- a/component/dialer/tfo.go +++ b/component/dialer/tfo.go @@ -9,6 +9,8 @@ import ( "github.com/metacubex/tfo-go" ) +var DisableTFO = false + type tfoConn struct { net.Conn closed bool diff --git a/component/dialer/tfo_windows.go b/component/dialer/tfo_windows.go new file mode 100644 index 00000000..63266118 --- /dev/null +++ b/component/dialer/tfo_windows.go @@ -0,0 +1,11 @@ +package dialer + +import "github.com/metacubex/mihomo/constant/features" + +func init() { + // According to MSDN, this option is available since Windows 10, 1607 + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms738596(v=vs.85).aspx + if features.WindowsMajorVersion < 10 || (features.WindowsMajorVersion == 10 && features.WindowsBuildNumber < 14393) { + DisableTFO = true + } +} diff --git a/component/ebpf/bpf/bpf_endian.h b/component/ebpf/bpf/bpf_endian.h deleted file mode 100644 index ec9db4fe..00000000 --- a/component/ebpf/bpf/bpf_endian.h +++ /dev/null @@ -1,99 +0,0 @@ -/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ -#ifndef __BPF_ENDIAN__ -#define __BPF_ENDIAN__ - -/* - * Isolate byte #n and put it into byte #m, for __u##b type. - * E.g., moving byte #6 (nnnnnnnn) into byte #1 (mmmmmmmm) for __u64: - * 1) xxxxxxxx nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx - * 2) nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx 00000000 - * 3) 00000000 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn - * 4) 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn 00000000 - */ -#define ___bpf_mvb(x, b, n, m) ((__u##b)(x) << (b-(n+1)*8) >> (b-8) << (m*8)) - -#define ___bpf_swab16(x) ((__u16)( \ - ___bpf_mvb(x, 16, 0, 1) | \ - ___bpf_mvb(x, 16, 1, 0))) - -#define ___bpf_swab32(x) ((__u32)( \ - ___bpf_mvb(x, 32, 0, 3) | \ - ___bpf_mvb(x, 32, 1, 2) | \ - ___bpf_mvb(x, 32, 2, 1) | \ - ___bpf_mvb(x, 32, 3, 0))) - -#define ___bpf_swab64(x) ((__u64)( \ - ___bpf_mvb(x, 64, 0, 7) | \ - ___bpf_mvb(x, 64, 1, 6) | \ - ___bpf_mvb(x, 64, 2, 5) | \ - ___bpf_mvb(x, 64, 3, 4) | \ - ___bpf_mvb(x, 64, 4, 3) | \ - ___bpf_mvb(x, 64, 5, 2) | \ - ___bpf_mvb(x, 64, 6, 1) | \ - ___bpf_mvb(x, 64, 7, 0))) - -/* LLVM's BPF target selects the endianness of the CPU - * it compiles on, or the user specifies (bpfel/bpfeb), - * respectively. The used __BYTE_ORDER__ is defined by - * the compiler, we cannot rely on __BYTE_ORDER from - * libc headers, since it doesn't reflect the actual - * requested byte order. - * - * Note, LLVM's BPF target has different __builtin_bswapX() - * semantics. It does map to BPF_ALU | BPF_END | BPF_TO_BE - * in bpfel and bpfeb case, which means below, that we map - * to cpu_to_be16(). We could use it unconditionally in BPF - * case, but better not rely on it, so that this header here - * can be used from application and BPF program side, which - * use different targets. - */ -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ -# define __bpf_ntohs(x) __builtin_bswap16(x) -# define __bpf_htons(x) __builtin_bswap16(x) -# define __bpf_constant_ntohs(x) ___bpf_swab16(x) -# define __bpf_constant_htons(x) ___bpf_swab16(x) -# define __bpf_ntohl(x) __builtin_bswap32(x) -# define __bpf_htonl(x) __builtin_bswap32(x) -# define __bpf_constant_ntohl(x) ___bpf_swab32(x) -# define __bpf_constant_htonl(x) ___bpf_swab32(x) -# define __bpf_be64_to_cpu(x) __builtin_bswap64(x) -# define __bpf_cpu_to_be64(x) __builtin_bswap64(x) -# define __bpf_constant_be64_to_cpu(x) ___bpf_swab64(x) -# define __bpf_constant_cpu_to_be64(x) ___bpf_swab64(x) -#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -# define __bpf_ntohs(x) (x) -# define __bpf_htons(x) (x) -# define __bpf_constant_ntohs(x) (x) -# define __bpf_constant_htons(x) (x) -# define __bpf_ntohl(x) (x) -# define __bpf_htonl(x) (x) -# define __bpf_constant_ntohl(x) (x) -# define __bpf_constant_htonl(x) (x) -# define __bpf_be64_to_cpu(x) (x) -# define __bpf_cpu_to_be64(x) (x) -# define __bpf_constant_be64_to_cpu(x) (x) -# define __bpf_constant_cpu_to_be64(x) (x) -#else -# error "Fix your compiler's __BYTE_ORDER__?!" -#endif - -#define bpf_htons(x) \ - (__builtin_constant_p(x) ? \ - __bpf_constant_htons(x) : __bpf_htons(x)) -#define bpf_ntohs(x) \ - (__builtin_constant_p(x) ? \ - __bpf_constant_ntohs(x) : __bpf_ntohs(x)) -#define bpf_htonl(x) \ - (__builtin_constant_p(x) ? \ - __bpf_constant_htonl(x) : __bpf_htonl(x)) -#define bpf_ntohl(x) \ - (__builtin_constant_p(x) ? \ - __bpf_constant_ntohl(x) : __bpf_ntohl(x)) -#define bpf_cpu_to_be64(x) \ - (__builtin_constant_p(x) ? \ - __bpf_constant_cpu_to_be64(x) : __bpf_cpu_to_be64(x)) -#define bpf_be64_to_cpu(x) \ - (__builtin_constant_p(x) ? \ - __bpf_constant_be64_to_cpu(x) : __bpf_be64_to_cpu(x)) - -#endif /* __BPF_ENDIAN__ */ diff --git a/component/ebpf/bpf/bpf_helper_defs.h b/component/ebpf/bpf/bpf_helper_defs.h deleted file mode 100644 index 8a4edf61..00000000 --- a/component/ebpf/bpf/bpf_helper_defs.h +++ /dev/null @@ -1,4139 +0,0 @@ -/* This is auto-generated file. See bpf_doc.py for details. */ - -/* Forward declarations of BPF structs */ -struct bpf_fib_lookup; -struct bpf_sk_lookup; -struct bpf_perf_event_data; -struct bpf_perf_event_value; -struct bpf_pidns_info; -struct bpf_redir_neigh; -struct bpf_sock; -struct bpf_sock_addr; -struct bpf_sock_ops; -struct bpf_sock_tuple; -struct bpf_spin_lock; -struct bpf_sysctl; -struct bpf_tcp_sock; -struct bpf_tunnel_key; -struct bpf_xfrm_state; -struct linux_binprm; -struct pt_regs; -struct sk_reuseport_md; -struct sockaddr; -struct tcphdr; -struct seq_file; -struct tcp6_sock; -struct tcp_sock; -struct tcp_timewait_sock; -struct tcp_request_sock; -struct udp6_sock; -struct unix_sock; -struct task_struct; -struct __sk_buff; -struct sk_msg_md; -struct xdp_md; -struct path; -struct btf_ptr; -struct inode; -struct socket; -struct file; -struct bpf_timer; - -/* - * bpf_map_lookup_elem - * - * Perform a lookup in *map* for an entry associated to *key*. - * - * Returns - * Map value associated to *key*, or **NULL** if no entry was - * found. - */ -static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1; - -/* - * bpf_map_update_elem - * - * Add or update the value of the entry associated to *key* in - * *map* with *value*. *flags* is one of: - * - * **BPF_NOEXIST** - * The entry for *key* must not exist in the map. - * **BPF_EXIST** - * The entry for *key* must already exist in the map. - * **BPF_ANY** - * No condition on the existence of the entry for *key*. - * - * Flag value **BPF_NOEXIST** cannot be used for maps of types - * **BPF_MAP_TYPE_ARRAY** or **BPF_MAP_TYPE_PERCPU_ARRAY** (all - * elements always exist), the helper would return an error. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_map_update_elem)(void *map, const void *key, const void *value, __u64 flags) = (void *) 2; - -/* - * bpf_map_delete_elem - * - * Delete entry with *key* from *map*. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_map_delete_elem)(void *map, const void *key) = (void *) 3; - -/* - * bpf_probe_read - * - * For tracing programs, safely attempt to read *size* bytes from - * kernel space address *unsafe_ptr* and store the data in *dst*. - * - * Generally, use **bpf_probe_read_user**\ () or - * **bpf_probe_read_kernel**\ () instead. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_probe_read)(void *dst, __u32 size, const void *unsafe_ptr) = (void *) 4; - -/* - * bpf_ktime_get_ns - * - * Return the time elapsed since system boot, in nanoseconds. - * Does not include time the system was suspended. - * See: **clock_gettime**\ (**CLOCK_MONOTONIC**) - * - * Returns - * Current *ktime*. - */ -static __u64 (*bpf_ktime_get_ns)(void) = (void *) 5; - -/* - * bpf_trace_printk - * - * This helper is a "printk()-like" facility for debugging. It - * prints a message defined by format *fmt* (of size *fmt_size*) - * to file *\/sys/kernel/debug/tracing/trace* from DebugFS, if - * available. It can take up to three additional **u64** - * arguments (as an eBPF helpers, the total number of arguments is - * limited to five). - * - * Each time the helper is called, it appends a line to the trace. - * Lines are discarded while *\/sys/kernel/debug/tracing/trace* is - * open, use *\/sys/kernel/debug/tracing/trace_pipe* to avoid this. - * The format of the trace is customizable, and the exact output - * one will get depends on the options set in - * *\/sys/kernel/debug/tracing/trace_options* (see also the - * *README* file under the same directory). However, it usually - * defaults to something like: - * - * :: - * - * telnet-470 [001] .N.. 419421.045894: 0x00000001: - * - * In the above: - * - * * ``telnet`` is the name of the current task. - * * ``470`` is the PID of the current task. - * * ``001`` is the CPU number on which the task is - * running. - * * In ``.N..``, each character refers to a set of - * options (whether irqs are enabled, scheduling - * options, whether hard/softirqs are running, level of - * preempt_disabled respectively). **N** means that - * **TIF_NEED_RESCHED** and **PREEMPT_NEED_RESCHED** - * are set. - * * ``419421.045894`` is a timestamp. - * * ``0x00000001`` is a fake value used by BPF for the - * instruction pointer register. - * * ```` is the message formatted with - * *fmt*. - * - * The conversion specifiers supported by *fmt* are similar, but - * more limited than for printk(). They are **%d**, **%i**, - * **%u**, **%x**, **%ld**, **%li**, **%lu**, **%lx**, **%lld**, - * **%lli**, **%llu**, **%llx**, **%p**, **%s**. No modifier (size - * of field, padding with zeroes, etc.) is available, and the - * helper will return **-EINVAL** (but print nothing) if it - * encounters an unknown specifier. - * - * Also, note that **bpf_trace_printk**\ () is slow, and should - * only be used for debugging purposes. For this reason, a notice - * block (spanning several lines) is printed to kernel logs and - * states that the helper should not be used "for production use" - * the first time this helper is used (or more precisely, when - * **trace_printk**\ () buffers are allocated). For passing values - * to user space, perf events should be preferred. - * - * Returns - * The number of bytes written to the buffer, or a negative error - * in case of failure. - */ -static long (*bpf_trace_printk)(const char *fmt, __u32 fmt_size, ...) = (void *) 6; - -/* - * bpf_get_prandom_u32 - * - * Get a pseudo-random number. - * - * From a security point of view, this helper uses its own - * pseudo-random internal state, and cannot be used to infer the - * seed of other random functions in the kernel. However, it is - * essential to note that the generator used by the helper is not - * cryptographically secure. - * - * Returns - * A random 32-bit unsigned value. - */ -static __u32 (*bpf_get_prandom_u32)(void) = (void *) 7; - -/* - * bpf_get_smp_processor_id - * - * Get the SMP (symmetric multiprocessing) processor id. Note that - * all programs run with migration disabled, which means that the - * SMP processor id is stable during all the execution of the - * program. - * - * Returns - * The SMP id of the processor running the program. - */ -static __u32 (*bpf_get_smp_processor_id)(void) = (void *) 8; - -/* - * bpf_skb_store_bytes - * - * Store *len* bytes from address *from* into the packet - * associated to *skb*, at *offset*. *flags* are a combination of - * **BPF_F_RECOMPUTE_CSUM** (automatically recompute the - * checksum for the packet after storing the bytes) and - * **BPF_F_INVALIDATE_HASH** (set *skb*\ **->hash**, *skb*\ - * **->swhash** and *skb*\ **->l4hash** to 0). - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_store_bytes)(struct __sk_buff *skb, __u32 offset, const void *from, __u32 len, __u64 flags) = (void *) 9; - -/* - * bpf_l3_csum_replace - * - * Recompute the layer 3 (e.g. IP) checksum for the packet - * associated to *skb*. Computation is incremental, so the helper - * must know the former value of the header field that was - * modified (*from*), the new value of this field (*to*), and the - * number of bytes (2 or 4) for this field, stored in *size*. - * Alternatively, it is possible to store the difference between - * the previous and the new values of the header field in *to*, by - * setting *from* and *size* to 0. For both methods, *offset* - * indicates the location of the IP checksum within the packet. - * - * This helper works in combination with **bpf_csum_diff**\ (), - * which does not update the checksum in-place, but offers more - * flexibility and can handle sizes larger than 2 or 4 for the - * checksum to update. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_l3_csum_replace)(struct __sk_buff *skb, __u32 offset, __u64 from, __u64 to, __u64 size) = (void *) 10; - -/* - * bpf_l4_csum_replace - * - * Recompute the layer 4 (e.g. TCP, UDP or ICMP) checksum for the - * packet associated to *skb*. Computation is incremental, so the - * helper must know the former value of the header field that was - * modified (*from*), the new value of this field (*to*), and the - * number of bytes (2 or 4) for this field, stored on the lowest - * four bits of *flags*. Alternatively, it is possible to store - * the difference between the previous and the new values of the - * header field in *to*, by setting *from* and the four lowest - * bits of *flags* to 0. For both methods, *offset* indicates the - * location of the IP checksum within the packet. In addition to - * the size of the field, *flags* can be added (bitwise OR) actual - * flags. With **BPF_F_MARK_MANGLED_0**, a null checksum is left - * untouched (unless **BPF_F_MARK_ENFORCE** is added as well), and - * for updates resulting in a null checksum the value is set to - * **CSUM_MANGLED_0** instead. Flag **BPF_F_PSEUDO_HDR** indicates - * the checksum is to be computed against a pseudo-header. - * - * This helper works in combination with **bpf_csum_diff**\ (), - * which does not update the checksum in-place, but offers more - * flexibility and can handle sizes larger than 2 or 4 for the - * checksum to update. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_l4_csum_replace)(struct __sk_buff *skb, __u32 offset, __u64 from, __u64 to, __u64 flags) = (void *) 11; - -/* - * bpf_tail_call - * - * This special helper is used to trigger a "tail call", or in - * other words, to jump into another eBPF program. The same stack - * frame is used (but values on stack and in registers for the - * caller are not accessible to the callee). This mechanism allows - * for program chaining, either for raising the maximum number of - * available eBPF instructions, or to execute given programs in - * conditional blocks. For security reasons, there is an upper - * limit to the number of successive tail calls that can be - * performed. - * - * Upon call of this helper, the program attempts to jump into a - * program referenced at index *index* in *prog_array_map*, a - * special map of type **BPF_MAP_TYPE_PROG_ARRAY**, and passes - * *ctx*, a pointer to the context. - * - * If the call succeeds, the kernel immediately runs the first - * instruction of the new program. This is not a function call, - * and it never returns to the previous program. If the call - * fails, then the helper has no effect, and the caller continues - * to run its subsequent instructions. A call can fail if the - * destination program for the jump does not exist (i.e. *index* - * is superior to the number of entries in *prog_array_map*), or - * if the maximum number of tail calls has been reached for this - * chain of programs. This limit is defined in the kernel by the - * macro **MAX_TAIL_CALL_CNT** (not accessible to user space), - * which is currently set to 33. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_tail_call)(void *ctx, void *prog_array_map, __u32 index) = (void *) 12; - -/* - * bpf_clone_redirect - * - * Clone and redirect the packet associated to *skb* to another - * net device of index *ifindex*. Both ingress and egress - * interfaces can be used for redirection. The **BPF_F_INGRESS** - * value in *flags* is used to make the distinction (ingress path - * is selected if the flag is present, egress path otherwise). - * This is the only flag supported for now. - * - * In comparison with **bpf_redirect**\ () helper, - * **bpf_clone_redirect**\ () has the associated cost of - * duplicating the packet buffer, but this can be executed out of - * the eBPF program. Conversely, **bpf_redirect**\ () is more - * efficient, but it is handled through an action code where the - * redirection happens only after the eBPF program has returned. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_clone_redirect)(struct __sk_buff *skb, __u32 ifindex, __u64 flags) = (void *) 13; - -/* - * bpf_get_current_pid_tgid - * - * - * Returns - * A 64-bit integer containing the current tgid and pid, and - * created as such: - * *current_task*\ **->tgid << 32 \|** - * *current_task*\ **->pid**. - */ -static __u64 (*bpf_get_current_pid_tgid)(void) = (void *) 14; - -/* - * bpf_get_current_uid_gid - * - * - * Returns - * A 64-bit integer containing the current GID and UID, and - * created as such: *current_gid* **<< 32 \|** *current_uid*. - */ -static __u64 (*bpf_get_current_uid_gid)(void) = (void *) 15; - -/* - * bpf_get_current_comm - * - * Copy the **comm** attribute of the current task into *buf* of - * *size_of_buf*. The **comm** attribute contains the name of - * the executable (excluding the path) for the current task. The - * *size_of_buf* must be strictly positive. On success, the - * helper makes sure that the *buf* is NUL-terminated. On failure, - * it is filled with zeroes. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_get_current_comm)(void *buf, __u32 size_of_buf) = (void *) 16; - -/* - * bpf_get_cgroup_classid - * - * Retrieve the classid for the current task, i.e. for the net_cls - * cgroup to which *skb* belongs. - * - * This helper can be used on TC egress path, but not on ingress. - * - * The net_cls cgroup provides an interface to tag network packets - * based on a user-provided identifier for all traffic coming from - * the tasks belonging to the related cgroup. See also the related - * kernel documentation, available from the Linux sources in file - * *Documentation/admin-guide/cgroup-v1/net_cls.rst*. - * - * The Linux kernel has two versions for cgroups: there are - * cgroups v1 and cgroups v2. Both are available to users, who can - * use a mixture of them, but note that the net_cls cgroup is for - * cgroup v1 only. This makes it incompatible with BPF programs - * run on cgroups, which is a cgroup-v2-only feature (a socket can - * only hold data for one version of cgroups at a time). - * - * This helper is only available is the kernel was compiled with - * the **CONFIG_CGROUP_NET_CLASSID** configuration option set to - * "**y**" or to "**m**". - * - * Returns - * The classid, or 0 for the default unconfigured classid. - */ -static __u32 (*bpf_get_cgroup_classid)(struct __sk_buff *skb) = (void *) 17; - -/* - * bpf_skb_vlan_push - * - * Push a *vlan_tci* (VLAN tag control information) of protocol - * *vlan_proto* to the packet associated to *skb*, then update - * the checksum. Note that if *vlan_proto* is different from - * **ETH_P_8021Q** and **ETH_P_8021AD**, it is considered to - * be **ETH_P_8021Q**. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_vlan_push)(struct __sk_buff *skb, __be16 vlan_proto, __u16 vlan_tci) = (void *) 18; - -/* - * bpf_skb_vlan_pop - * - * Pop a VLAN header from the packet associated to *skb*. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_vlan_pop)(struct __sk_buff *skb) = (void *) 19; - -/* - * bpf_skb_get_tunnel_key - * - * Get tunnel metadata. This helper takes a pointer *key* to an - * empty **struct bpf_tunnel_key** of **size**, that will be - * filled with tunnel metadata for the packet associated to *skb*. - * The *flags* can be set to **BPF_F_TUNINFO_IPV6**, which - * indicates that the tunnel is based on IPv6 protocol instead of - * IPv4. - * - * The **struct bpf_tunnel_key** is an object that generalizes the - * principal parameters used by various tunneling protocols into a - * single struct. This way, it can be used to easily make a - * decision based on the contents of the encapsulation header, - * "summarized" in this struct. In particular, it holds the IP - * address of the remote end (IPv4 or IPv6, depending on the case) - * in *key*\ **->remote_ipv4** or *key*\ **->remote_ipv6**. Also, - * this struct exposes the *key*\ **->tunnel_id**, which is - * generally mapped to a VNI (Virtual Network Identifier), making - * it programmable together with the **bpf_skb_set_tunnel_key**\ - * () helper. - * - * Let's imagine that the following code is part of a program - * attached to the TC ingress interface, on one end of a GRE - * tunnel, and is supposed to filter out all messages coming from - * remote ends with IPv4 address other than 10.0.0.1: - * - * :: - * - * int ret; - * struct bpf_tunnel_key key = {}; - * - * ret = bpf_skb_get_tunnel_key(skb, &key, sizeof(key), 0); - * if (ret < 0) - * return TC_ACT_SHOT; // drop packet - * - * if (key.remote_ipv4 != 0x0a000001) - * return TC_ACT_SHOT; // drop packet - * - * return TC_ACT_OK; // accept packet - * - * This interface can also be used with all encapsulation devices - * that can operate in "collect metadata" mode: instead of having - * one network device per specific configuration, the "collect - * metadata" mode only requires a single device where the - * configuration can be extracted from this helper. - * - * This can be used together with various tunnels such as VXLan, - * Geneve, GRE or IP in IP (IPIP). - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_get_tunnel_key)(struct __sk_buff *skb, struct bpf_tunnel_key *key, __u32 size, __u64 flags) = (void *) 20; - -/* - * bpf_skb_set_tunnel_key - * - * Populate tunnel metadata for packet associated to *skb.* The - * tunnel metadata is set to the contents of *key*, of *size*. The - * *flags* can be set to a combination of the following values: - * - * **BPF_F_TUNINFO_IPV6** - * Indicate that the tunnel is based on IPv6 protocol - * instead of IPv4. - * **BPF_F_ZERO_CSUM_TX** - * For IPv4 packets, add a flag to tunnel metadata - * indicating that checksum computation should be skipped - * and checksum set to zeroes. - * **BPF_F_DONT_FRAGMENT** - * Add a flag to tunnel metadata indicating that the - * packet should not be fragmented. - * **BPF_F_SEQ_NUMBER** - * Add a flag to tunnel metadata indicating that a - * sequence number should be added to tunnel header before - * sending the packet. This flag was added for GRE - * encapsulation, but might be used with other protocols - * as well in the future. - * - * Here is a typical usage on the transmit path: - * - * :: - * - * struct bpf_tunnel_key key; - * populate key ... - * bpf_skb_set_tunnel_key(skb, &key, sizeof(key), 0); - * bpf_clone_redirect(skb, vxlan_dev_ifindex, 0); - * - * See also the description of the **bpf_skb_get_tunnel_key**\ () - * helper for additional information. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_set_tunnel_key)(struct __sk_buff *skb, struct bpf_tunnel_key *key, __u32 size, __u64 flags) = (void *) 21; - -/* - * bpf_perf_event_read - * - * Read the value of a perf event counter. This helper relies on a - * *map* of type **BPF_MAP_TYPE_PERF_EVENT_ARRAY**. The nature of - * the perf event counter is selected when *map* is updated with - * perf event file descriptors. The *map* is an array whose size - * is the number of available CPUs, and each cell contains a value - * relative to one CPU. The value to retrieve is indicated by - * *flags*, that contains the index of the CPU to look up, masked - * with **BPF_F_INDEX_MASK**. Alternatively, *flags* can be set to - * **BPF_F_CURRENT_CPU** to indicate that the value for the - * current CPU should be retrieved. - * - * Note that before Linux 4.13, only hardware perf event can be - * retrieved. - * - * Also, be aware that the newer helper - * **bpf_perf_event_read_value**\ () is recommended over - * **bpf_perf_event_read**\ () in general. The latter has some ABI - * quirks where error and counter value are used as a return code - * (which is wrong to do since ranges may overlap). This issue is - * fixed with **bpf_perf_event_read_value**\ (), which at the same - * time provides more features over the **bpf_perf_event_read**\ - * () interface. Please refer to the description of - * **bpf_perf_event_read_value**\ () for details. - * - * Returns - * The value of the perf event counter read from the map, or a - * negative error code in case of failure. - */ -static __u64 (*bpf_perf_event_read)(void *map, __u64 flags) = (void *) 22; - -/* - * bpf_redirect - * - * Redirect the packet to another net device of index *ifindex*. - * This helper is somewhat similar to **bpf_clone_redirect**\ - * (), except that the packet is not cloned, which provides - * increased performance. - * - * Except for XDP, both ingress and egress interfaces can be used - * for redirection. The **BPF_F_INGRESS** value in *flags* is used - * to make the distinction (ingress path is selected if the flag - * is present, egress path otherwise). Currently, XDP only - * supports redirection to the egress interface, and accepts no - * flag at all. - * - * The same effect can also be attained with the more generic - * **bpf_redirect_map**\ (), which uses a BPF map to store the - * redirect target instead of providing it directly to the helper. - * - * Returns - * For XDP, the helper returns **XDP_REDIRECT** on success or - * **XDP_ABORTED** on error. For other program types, the values - * are **TC_ACT_REDIRECT** on success or **TC_ACT_SHOT** on - * error. - */ -static long (*bpf_redirect)(__u32 ifindex, __u64 flags) = (void *) 23; - -/* - * bpf_get_route_realm - * - * Retrieve the realm or the route, that is to say the - * **tclassid** field of the destination for the *skb*. The - * identifier retrieved is a user-provided tag, similar to the - * one used with the net_cls cgroup (see description for - * **bpf_get_cgroup_classid**\ () helper), but here this tag is - * held by a route (a destination entry), not by a task. - * - * Retrieving this identifier works with the clsact TC egress hook - * (see also **tc-bpf(8)**), or alternatively on conventional - * classful egress qdiscs, but not on TC ingress path. In case of - * clsact TC egress hook, this has the advantage that, internally, - * the destination entry has not been dropped yet in the transmit - * path. Therefore, the destination entry does not need to be - * artificially held via **netif_keep_dst**\ () for a classful - * qdisc until the *skb* is freed. - * - * This helper is available only if the kernel was compiled with - * **CONFIG_IP_ROUTE_CLASSID** configuration option. - * - * Returns - * The realm of the route for the packet associated to *skb*, or 0 - * if none was found. - */ -static __u32 (*bpf_get_route_realm)(struct __sk_buff *skb) = (void *) 24; - -/* - * bpf_perf_event_output - * - * Write raw *data* blob into a special BPF perf event held by - * *map* of type **BPF_MAP_TYPE_PERF_EVENT_ARRAY**. This perf - * event must have the following attributes: **PERF_SAMPLE_RAW** - * as **sample_type**, **PERF_TYPE_SOFTWARE** as **type**, and - * **PERF_COUNT_SW_BPF_OUTPUT** as **config**. - * - * The *flags* are used to indicate the index in *map* for which - * the value must be put, masked with **BPF_F_INDEX_MASK**. - * Alternatively, *flags* can be set to **BPF_F_CURRENT_CPU** - * to indicate that the index of the current CPU core should be - * used. - * - * The value to write, of *size*, is passed through eBPF stack and - * pointed by *data*. - * - * The context of the program *ctx* needs also be passed to the - * helper. - * - * On user space, a program willing to read the values needs to - * call **perf_event_open**\ () on the perf event (either for - * one or for all CPUs) and to store the file descriptor into the - * *map*. This must be done before the eBPF program can send data - * into it. An example is available in file - * *samples/bpf/trace_output_user.c* in the Linux kernel source - * tree (the eBPF program counterpart is in - * *samples/bpf/trace_output_kern.c*). - * - * **bpf_perf_event_output**\ () achieves better performance - * than **bpf_trace_printk**\ () for sharing data with user - * space, and is much better suitable for streaming data from eBPF - * programs. - * - * Note that this helper is not restricted to tracing use cases - * and can be used with programs attached to TC or XDP as well, - * where it allows for passing data to user space listeners. Data - * can be: - * - * * Only custom structs, - * * Only the packet payload, or - * * A combination of both. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_perf_event_output)(void *ctx, void *map, __u64 flags, void *data, __u64 size) = (void *) 25; - -/* - * bpf_skb_load_bytes - * - * This helper was provided as an easy way to load data from a - * packet. It can be used to load *len* bytes from *offset* from - * the packet associated to *skb*, into the buffer pointed by - * *to*. - * - * Since Linux 4.7, usage of this helper has mostly been replaced - * by "direct packet access", enabling packet data to be - * manipulated with *skb*\ **->data** and *skb*\ **->data_end** - * pointing respectively to the first byte of packet data and to - * the byte after the last byte of packet data. However, it - * remains useful if one wishes to read large quantities of data - * at once from a packet into the eBPF stack. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_load_bytes)(const void *skb, __u32 offset, void *to, __u32 len) = (void *) 26; - -/* - * bpf_get_stackid - * - * Walk a user or a kernel stack and return its id. To achieve - * this, the helper needs *ctx*, which is a pointer to the context - * on which the tracing program is executed, and a pointer to a - * *map* of type **BPF_MAP_TYPE_STACK_TRACE**. - * - * The last argument, *flags*, holds the number of stack frames to - * skip (from 0 to 255), masked with - * **BPF_F_SKIP_FIELD_MASK**. The next bits can be used to set - * a combination of the following flags: - * - * **BPF_F_USER_STACK** - * Collect a user space stack instead of a kernel stack. - * **BPF_F_FAST_STACK_CMP** - * Compare stacks by hash only. - * **BPF_F_REUSE_STACKID** - * If two different stacks hash into the same *stackid*, - * discard the old one. - * - * The stack id retrieved is a 32 bit long integer handle which - * can be further combined with other data (including other stack - * ids) and used as a key into maps. This can be useful for - * generating a variety of graphs (such as flame graphs or off-cpu - * graphs). - * - * For walking a stack, this helper is an improvement over - * **bpf_probe_read**\ (), which can be used with unrolled loops - * but is not efficient and consumes a lot of eBPF instructions. - * Instead, **bpf_get_stackid**\ () can collect up to - * **PERF_MAX_STACK_DEPTH** both kernel and user frames. Note that - * this limit can be controlled with the **sysctl** program, and - * that it should be manually increased in order to profile long - * user stacks (such as stacks for Java programs). To do so, use: - * - * :: - * - * # sysctl kernel.perf_event_max_stack= - * - * Returns - * The positive or null stack id on success, or a negative error - * in case of failure. - */ -static long (*bpf_get_stackid)(void *ctx, void *map, __u64 flags) = (void *) 27; - -/* - * bpf_csum_diff - * - * Compute a checksum difference, from the raw buffer pointed by - * *from*, of length *from_size* (that must be a multiple of 4), - * towards the raw buffer pointed by *to*, of size *to_size* - * (same remark). An optional *seed* can be added to the value - * (this can be cascaded, the seed may come from a previous call - * to the helper). - * - * This is flexible enough to be used in several ways: - * - * * With *from_size* == 0, *to_size* > 0 and *seed* set to - * checksum, it can be used when pushing new data. - * * With *from_size* > 0, *to_size* == 0 and *seed* set to - * checksum, it can be used when removing data from a packet. - * * With *from_size* > 0, *to_size* > 0 and *seed* set to 0, it - * can be used to compute a diff. Note that *from_size* and - * *to_size* do not need to be equal. - * - * This helper can be used in combination with - * **bpf_l3_csum_replace**\ () and **bpf_l4_csum_replace**\ (), to - * which one can feed in the difference computed with - * **bpf_csum_diff**\ (). - * - * Returns - * The checksum result, or a negative error code in case of - * failure. - */ -static __s64 (*bpf_csum_diff)(__be32 *from, __u32 from_size, __be32 *to, __u32 to_size, __wsum seed) = (void *) 28; - -/* - * bpf_skb_get_tunnel_opt - * - * Retrieve tunnel options metadata for the packet associated to - * *skb*, and store the raw tunnel option data to the buffer *opt* - * of *size*. - * - * This helper can be used with encapsulation devices that can - * operate in "collect metadata" mode (please refer to the related - * note in the description of **bpf_skb_get_tunnel_key**\ () for - * more details). A particular example where this can be used is - * in combination with the Geneve encapsulation protocol, where it - * allows for pushing (with **bpf_skb_get_tunnel_opt**\ () helper) - * and retrieving arbitrary TLVs (Type-Length-Value headers) from - * the eBPF program. This allows for full customization of these - * headers. - * - * Returns - * The size of the option data retrieved. - */ -static long (*bpf_skb_get_tunnel_opt)(struct __sk_buff *skb, void *opt, __u32 size) = (void *) 29; - -/* - * bpf_skb_set_tunnel_opt - * - * Set tunnel options metadata for the packet associated to *skb* - * to the option data contained in the raw buffer *opt* of *size*. - * - * See also the description of the **bpf_skb_get_tunnel_opt**\ () - * helper for additional information. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_set_tunnel_opt)(struct __sk_buff *skb, void *opt, __u32 size) = (void *) 30; - -/* - * bpf_skb_change_proto - * - * Change the protocol of the *skb* to *proto*. Currently - * supported are transition from IPv4 to IPv6, and from IPv6 to - * IPv4. The helper takes care of the groundwork for the - * transition, including resizing the socket buffer. The eBPF - * program is expected to fill the new headers, if any, via - * **skb_store_bytes**\ () and to recompute the checksums with - * **bpf_l3_csum_replace**\ () and **bpf_l4_csum_replace**\ - * (). The main case for this helper is to perform NAT64 - * operations out of an eBPF program. - * - * Internally, the GSO type is marked as dodgy so that headers are - * checked and segments are recalculated by the GSO/GRO engine. - * The size for GSO target is adapted as well. - * - * All values for *flags* are reserved for future usage, and must - * be left at zero. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_change_proto)(struct __sk_buff *skb, __be16 proto, __u64 flags) = (void *) 31; - -/* - * bpf_skb_change_type - * - * Change the packet type for the packet associated to *skb*. This - * comes down to setting *skb*\ **->pkt_type** to *type*, except - * the eBPF program does not have a write access to *skb*\ - * **->pkt_type** beside this helper. Using a helper here allows - * for graceful handling of errors. - * - * The major use case is to change incoming *skb*s to - * **PACKET_HOST** in a programmatic way instead of having to - * recirculate via **redirect**\ (..., **BPF_F_INGRESS**), for - * example. - * - * Note that *type* only allows certain values. At this time, they - * are: - * - * **PACKET_HOST** - * Packet is for us. - * **PACKET_BROADCAST** - * Send packet to all. - * **PACKET_MULTICAST** - * Send packet to group. - * **PACKET_OTHERHOST** - * Send packet to someone else. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_change_type)(struct __sk_buff *skb, __u32 type) = (void *) 32; - -/* - * bpf_skb_under_cgroup - * - * Check whether *skb* is a descendant of the cgroup2 held by - * *map* of type **BPF_MAP_TYPE_CGROUP_ARRAY**, at *index*. - * - * Returns - * The return value depends on the result of the test, and can be: - * - * * 0, if the *skb* failed the cgroup2 descendant test. - * * 1, if the *skb* succeeded the cgroup2 descendant test. - * * A negative error code, if an error occurred. - */ -static long (*bpf_skb_under_cgroup)(struct __sk_buff *skb, void *map, __u32 index) = (void *) 33; - -/* - * bpf_get_hash_recalc - * - * Retrieve the hash of the packet, *skb*\ **->hash**. If it is - * not set, in particular if the hash was cleared due to mangling, - * recompute this hash. Later accesses to the hash can be done - * directly with *skb*\ **->hash**. - * - * Calling **bpf_set_hash_invalid**\ (), changing a packet - * prototype with **bpf_skb_change_proto**\ (), or calling - * **bpf_skb_store_bytes**\ () with the - * **BPF_F_INVALIDATE_HASH** are actions susceptible to clear - * the hash and to trigger a new computation for the next call to - * **bpf_get_hash_recalc**\ (). - * - * Returns - * The 32-bit hash. - */ -static __u32 (*bpf_get_hash_recalc)(struct __sk_buff *skb) = (void *) 34; - -/* - * bpf_get_current_task - * - * - * Returns - * A pointer to the current task struct. - */ -static __u64 (*bpf_get_current_task)(void) = (void *) 35; - -/* - * bpf_probe_write_user - * - * Attempt in a safe way to write *len* bytes from the buffer - * *src* to *dst* in memory. It only works for threads that are in - * user context, and *dst* must be a valid user space address. - * - * This helper should not be used to implement any kind of - * security mechanism because of TOC-TOU attacks, but rather to - * debug, divert, and manipulate execution of semi-cooperative - * processes. - * - * Keep in mind that this feature is meant for experiments, and it - * has a risk of crashing the system and running programs. - * Therefore, when an eBPF program using this helper is attached, - * a warning including PID and process name is printed to kernel - * logs. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_probe_write_user)(void *dst, const void *src, __u32 len) = (void *) 36; - -/* - * bpf_current_task_under_cgroup - * - * Check whether the probe is being run is the context of a given - * subset of the cgroup2 hierarchy. The cgroup2 to test is held by - * *map* of type **BPF_MAP_TYPE_CGROUP_ARRAY**, at *index*. - * - * Returns - * The return value depends on the result of the test, and can be: - * - * * 0, if current task belongs to the cgroup2. - * * 1, if current task does not belong to the cgroup2. - * * A negative error code, if an error occurred. - */ -static long (*bpf_current_task_under_cgroup)(void *map, __u32 index) = (void *) 37; - -/* - * bpf_skb_change_tail - * - * Resize (trim or grow) the packet associated to *skb* to the - * new *len*. The *flags* are reserved for future usage, and must - * be left at zero. - * - * The basic idea is that the helper performs the needed work to - * change the size of the packet, then the eBPF program rewrites - * the rest via helpers like **bpf_skb_store_bytes**\ (), - * **bpf_l3_csum_replace**\ (), **bpf_l3_csum_replace**\ () - * and others. This helper is a slow path utility intended for - * replies with control messages. And because it is targeted for - * slow path, the helper itself can afford to be slow: it - * implicitly linearizes, unclones and drops offloads from the - * *skb*. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_change_tail)(struct __sk_buff *skb, __u32 len, __u64 flags) = (void *) 38; - -/* - * bpf_skb_pull_data - * - * Pull in non-linear data in case the *skb* is non-linear and not - * all of *len* are part of the linear section. Make *len* bytes - * from *skb* readable and writable. If a zero value is passed for - * *len*, then the whole length of the *skb* is pulled. - * - * This helper is only needed for reading and writing with direct - * packet access. - * - * For direct packet access, testing that offsets to access - * are within packet boundaries (test on *skb*\ **->data_end**) is - * susceptible to fail if offsets are invalid, or if the requested - * data is in non-linear parts of the *skb*. On failure the - * program can just bail out, or in the case of a non-linear - * buffer, use a helper to make the data available. The - * **bpf_skb_load_bytes**\ () helper is a first solution to access - * the data. Another one consists in using **bpf_skb_pull_data** - * to pull in once the non-linear parts, then retesting and - * eventually access the data. - * - * At the same time, this also makes sure the *skb* is uncloned, - * which is a necessary condition for direct write. As this needs - * to be an invariant for the write part only, the verifier - * detects writes and adds a prologue that is calling - * **bpf_skb_pull_data()** to effectively unclone the *skb* from - * the very beginning in case it is indeed cloned. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_pull_data)(struct __sk_buff *skb, __u32 len) = (void *) 39; - -/* - * bpf_csum_update - * - * Add the checksum *csum* into *skb*\ **->csum** in case the - * driver has supplied a checksum for the entire packet into that - * field. Return an error otherwise. This helper is intended to be - * used in combination with **bpf_csum_diff**\ (), in particular - * when the checksum needs to be updated after data has been - * written into the packet through direct packet access. - * - * Returns - * The checksum on success, or a negative error code in case of - * failure. - */ -static __s64 (*bpf_csum_update)(struct __sk_buff *skb, __wsum csum) = (void *) 40; - -/* - * bpf_set_hash_invalid - * - * Invalidate the current *skb*\ **->hash**. It can be used after - * mangling on headers through direct packet access, in order to - * indicate that the hash is outdated and to trigger a - * recalculation the next time the kernel tries to access this - * hash or when the **bpf_get_hash_recalc**\ () helper is called. - * - */ -static void (*bpf_set_hash_invalid)(struct __sk_buff *skb) = (void *) 41; - -/* - * bpf_get_numa_node_id - * - * Return the id of the current NUMA node. The primary use case - * for this helper is the selection of sockets for the local NUMA - * node, when the program is attached to sockets using the - * **SO_ATTACH_REUSEPORT_EBPF** option (see also **socket(7)**), - * but the helper is also available to other eBPF program types, - * similarly to **bpf_get_smp_processor_id**\ (). - * - * Returns - * The id of current NUMA node. - */ -static long (*bpf_get_numa_node_id)(void) = (void *) 42; - -/* - * bpf_skb_change_head - * - * Grows headroom of packet associated to *skb* and adjusts the - * offset of the MAC header accordingly, adding *len* bytes of - * space. It automatically extends and reallocates memory as - * required. - * - * This helper can be used on a layer 3 *skb* to push a MAC header - * for redirection into a layer 2 device. - * - * All values for *flags* are reserved for future usage, and must - * be left at zero. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_change_head)(struct __sk_buff *skb, __u32 len, __u64 flags) = (void *) 43; - -/* - * bpf_xdp_adjust_head - * - * Adjust (move) *xdp_md*\ **->data** by *delta* bytes. Note that - * it is possible to use a negative value for *delta*. This helper - * can be used to prepare the packet for pushing or popping - * headers. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_xdp_adjust_head)(struct xdp_md *xdp_md, int delta) = (void *) 44; - -/* - * bpf_probe_read_str - * - * Copy a NUL terminated string from an unsafe kernel address - * *unsafe_ptr* to *dst*. See **bpf_probe_read_kernel_str**\ () for - * more details. - * - * Generally, use **bpf_probe_read_user_str**\ () or - * **bpf_probe_read_kernel_str**\ () instead. - * - * Returns - * On success, the strictly positive length of the string, - * including the trailing NUL character. On error, a negative - * value. - */ -static long (*bpf_probe_read_str)(void *dst, __u32 size, const void *unsafe_ptr) = (void *) 45; - -/* - * bpf_get_socket_cookie - * - * If the **struct sk_buff** pointed by *skb* has a known socket, - * retrieve the cookie (generated by the kernel) of this socket. - * If no cookie has been set yet, generate a new cookie. Once - * generated, the socket cookie remains stable for the life of the - * socket. This helper can be useful for monitoring per socket - * networking traffic statistics as it provides a global socket - * identifier that can be assumed unique. - * - * Returns - * A 8-byte long unique number on success, or 0 if the socket - * field is missing inside *skb*. - */ -static __u64 (*bpf_get_socket_cookie)(void *ctx) = (void *) 46; - -/* - * bpf_get_socket_uid - * - * - * Returns - * The owner UID of the socket associated to *skb*. If the socket - * is **NULL**, or if it is not a full socket (i.e. if it is a - * time-wait or a request socket instead), **overflowuid** value - * is returned (note that **overflowuid** might also be the actual - * UID value for the socket). - */ -static __u32 (*bpf_get_socket_uid)(struct __sk_buff *skb) = (void *) 47; - -/* - * bpf_set_hash - * - * Set the full hash for *skb* (set the field *skb*\ **->hash**) - * to value *hash*. - * - * Returns - * 0 - */ -static long (*bpf_set_hash)(struct __sk_buff *skb, __u32 hash) = (void *) 48; - -/* - * bpf_setsockopt - * - * Emulate a call to **setsockopt()** on the socket associated to - * *bpf_socket*, which must be a full socket. The *level* at - * which the option resides and the name *optname* of the option - * must be specified, see **setsockopt(2)** for more information. - * The option value of length *optlen* is pointed by *optval*. - * - * *bpf_socket* should be one of the following: - * - * * **struct bpf_sock_ops** for **BPF_PROG_TYPE_SOCK_OPS**. - * * **struct bpf_sock_addr** for **BPF_CGROUP_INET4_CONNECT** - * and **BPF_CGROUP_INET6_CONNECT**. - * - * This helper actually implements a subset of **setsockopt()**. - * It supports the following *level*\ s: - * - * * **SOL_SOCKET**, which supports the following *optname*\ s: - * **SO_RCVBUF**, **SO_SNDBUF**, **SO_MAX_PACING_RATE**, - * **SO_PRIORITY**, **SO_RCVLOWAT**, **SO_MARK**, - * **SO_BINDTODEVICE**, **SO_KEEPALIVE**. - * * **IPPROTO_TCP**, which supports the following *optname*\ s: - * **TCP_CONGESTION**, **TCP_BPF_IW**, - * **TCP_BPF_SNDCWND_CLAMP**, **TCP_SAVE_SYN**, - * **TCP_KEEPIDLE**, **TCP_KEEPINTVL**, **TCP_KEEPCNT**, - * **TCP_SYNCNT**, **TCP_USER_TIMEOUT**, **TCP_NOTSENT_LOWAT**. - * * **IPPROTO_IP**, which supports *optname* **IP_TOS**. - * * **IPPROTO_IPV6**, which supports *optname* **IPV6_TCLASS**. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_setsockopt)(void *bpf_socket, int level, int optname, void *optval, int optlen) = (void *) 49; - -/* - * bpf_skb_adjust_room - * - * Grow or shrink the room for data in the packet associated to - * *skb* by *len_diff*, and according to the selected *mode*. - * - * By default, the helper will reset any offloaded checksum - * indicator of the skb to CHECKSUM_NONE. This can be avoided - * by the following flag: - * - * * **BPF_F_ADJ_ROOM_NO_CSUM_RESET**: Do not reset offloaded - * checksum data of the skb to CHECKSUM_NONE. - * - * There are two supported modes at this time: - * - * * **BPF_ADJ_ROOM_MAC**: Adjust room at the mac layer - * (room space is added or removed below the layer 2 header). - * - * * **BPF_ADJ_ROOM_NET**: Adjust room at the network layer - * (room space is added or removed below the layer 3 header). - * - * The following flags are supported at this time: - * - * * **BPF_F_ADJ_ROOM_FIXED_GSO**: Do not adjust gso_size. - * Adjusting mss in this way is not allowed for datagrams. - * - * * **BPF_F_ADJ_ROOM_ENCAP_L3_IPV4**, - * **BPF_F_ADJ_ROOM_ENCAP_L3_IPV6**: - * Any new space is reserved to hold a tunnel header. - * Configure skb offsets and other fields accordingly. - * - * * **BPF_F_ADJ_ROOM_ENCAP_L4_GRE**, - * **BPF_F_ADJ_ROOM_ENCAP_L4_UDP**: - * Use with ENCAP_L3 flags to further specify the tunnel type. - * - * * **BPF_F_ADJ_ROOM_ENCAP_L2**\ (*len*): - * Use with ENCAP_L3/L4 flags to further specify the tunnel - * type; *len* is the length of the inner MAC header. - * - * * **BPF_F_ADJ_ROOM_ENCAP_L2_ETH**: - * Use with BPF_F_ADJ_ROOM_ENCAP_L2 flag to further specify the - * L2 type as Ethernet. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_adjust_room)(struct __sk_buff *skb, __s32 len_diff, __u32 mode, __u64 flags) = (void *) 50; - -/* - * bpf_redirect_map - * - * Redirect the packet to the endpoint referenced by *map* at - * index *key*. Depending on its type, this *map* can contain - * references to net devices (for forwarding packets through other - * ports), or to CPUs (for redirecting XDP frames to another CPU; - * but this is only implemented for native XDP (with driver - * support) as of this writing). - * - * The lower two bits of *flags* are used as the return code if - * the map lookup fails. This is so that the return value can be - * one of the XDP program return codes up to **XDP_TX**, as chosen - * by the caller. The higher bits of *flags* can be set to - * BPF_F_BROADCAST or BPF_F_EXCLUDE_INGRESS as defined below. - * - * With BPF_F_BROADCAST the packet will be broadcasted to all the - * interfaces in the map, with BPF_F_EXCLUDE_INGRESS the ingress - * interface will be excluded when do broadcasting. - * - * See also **bpf_redirect**\ (), which only supports redirecting - * to an ifindex, but doesn't require a map to do so. - * - * Returns - * **XDP_REDIRECT** on success, or the value of the two lower bits - * of the *flags* argument on error. - */ -static long (*bpf_redirect_map)(void *map, __u32 key, __u64 flags) = (void *) 51; - -/* - * bpf_sk_redirect_map - * - * Redirect the packet to the socket referenced by *map* (of type - * **BPF_MAP_TYPE_SOCKMAP**) at index *key*. Both ingress and - * egress interfaces can be used for redirection. The - * **BPF_F_INGRESS** value in *flags* is used to make the - * distinction (ingress path is selected if the flag is present, - * egress path otherwise). This is the only flag supported for now. - * - * Returns - * **SK_PASS** on success, or **SK_DROP** on error. - */ -static long (*bpf_sk_redirect_map)(struct __sk_buff *skb, void *map, __u32 key, __u64 flags) = (void *) 52; - -/* - * bpf_sock_map_update - * - * Add an entry to, or update a *map* referencing sockets. The - * *skops* is used as a new value for the entry associated to - * *key*. *flags* is one of: - * - * **BPF_NOEXIST** - * The entry for *key* must not exist in the map. - * **BPF_EXIST** - * The entry for *key* must already exist in the map. - * **BPF_ANY** - * No condition on the existence of the entry for *key*. - * - * If the *map* has eBPF programs (parser and verdict), those will - * be inherited by the socket being added. If the socket is - * already attached to eBPF programs, this results in an error. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_sock_map_update)(struct bpf_sock_ops *skops, void *map, void *key, __u64 flags) = (void *) 53; - -/* - * bpf_xdp_adjust_meta - * - * Adjust the address pointed by *xdp_md*\ **->data_meta** by - * *delta* (which can be positive or negative). Note that this - * operation modifies the address stored in *xdp_md*\ **->data**, - * so the latter must be loaded only after the helper has been - * called. - * - * The use of *xdp_md*\ **->data_meta** is optional and programs - * are not required to use it. The rationale is that when the - * packet is processed with XDP (e.g. as DoS filter), it is - * possible to push further meta data along with it before passing - * to the stack, and to give the guarantee that an ingress eBPF - * program attached as a TC classifier on the same device can pick - * this up for further post-processing. Since TC works with socket - * buffers, it remains possible to set from XDP the **mark** or - * **priority** pointers, or other pointers for the socket buffer. - * Having this scratch space generic and programmable allows for - * more flexibility as the user is free to store whatever meta - * data they need. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_xdp_adjust_meta)(struct xdp_md *xdp_md, int delta) = (void *) 54; - -/* - * bpf_perf_event_read_value - * - * Read the value of a perf event counter, and store it into *buf* - * of size *buf_size*. This helper relies on a *map* of type - * **BPF_MAP_TYPE_PERF_EVENT_ARRAY**. The nature of the perf event - * counter is selected when *map* is updated with perf event file - * descriptors. The *map* is an array whose size is the number of - * available CPUs, and each cell contains a value relative to one - * CPU. The value to retrieve is indicated by *flags*, that - * contains the index of the CPU to look up, masked with - * **BPF_F_INDEX_MASK**. Alternatively, *flags* can be set to - * **BPF_F_CURRENT_CPU** to indicate that the value for the - * current CPU should be retrieved. - * - * This helper behaves in a way close to - * **bpf_perf_event_read**\ () helper, save that instead of - * just returning the value observed, it fills the *buf* - * structure. This allows for additional data to be retrieved: in - * particular, the enabled and running times (in *buf*\ - * **->enabled** and *buf*\ **->running**, respectively) are - * copied. In general, **bpf_perf_event_read_value**\ () is - * recommended over **bpf_perf_event_read**\ (), which has some - * ABI issues and provides fewer functionalities. - * - * These values are interesting, because hardware PMU (Performance - * Monitoring Unit) counters are limited resources. When there are - * more PMU based perf events opened than available counters, - * kernel will multiplex these events so each event gets certain - * percentage (but not all) of the PMU time. In case that - * multiplexing happens, the number of samples or counter value - * will not reflect the case compared to when no multiplexing - * occurs. This makes comparison between different runs difficult. - * Typically, the counter value should be normalized before - * comparing to other experiments. The usual normalization is done - * as follows. - * - * :: - * - * normalized_counter = counter * t_enabled / t_running - * - * Where t_enabled is the time enabled for event and t_running is - * the time running for event since last normalization. The - * enabled and running times are accumulated since the perf event - * open. To achieve scaling factor between two invocations of an - * eBPF program, users can use CPU id as the key (which is - * typical for perf array usage model) to remember the previous - * value and do the calculation inside the eBPF program. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_perf_event_read_value)(void *map, __u64 flags, struct bpf_perf_event_value *buf, __u32 buf_size) = (void *) 55; - -/* - * bpf_perf_prog_read_value - * - * For en eBPF program attached to a perf event, retrieve the - * value of the event counter associated to *ctx* and store it in - * the structure pointed by *buf* and of size *buf_size*. Enabled - * and running times are also stored in the structure (see - * description of helper **bpf_perf_event_read_value**\ () for - * more details). - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_perf_prog_read_value)(struct bpf_perf_event_data *ctx, struct bpf_perf_event_value *buf, __u32 buf_size) = (void *) 56; - -/* - * bpf_getsockopt - * - * Emulate a call to **getsockopt()** on the socket associated to - * *bpf_socket*, which must be a full socket. The *level* at - * which the option resides and the name *optname* of the option - * must be specified, see **getsockopt(2)** for more information. - * The retrieved value is stored in the structure pointed by - * *opval* and of length *optlen*. - * - * *bpf_socket* should be one of the following: - * - * * **struct bpf_sock_ops** for **BPF_PROG_TYPE_SOCK_OPS**. - * * **struct bpf_sock_addr** for **BPF_CGROUP_INET4_CONNECT** - * and **BPF_CGROUP_INET6_CONNECT**. - * - * This helper actually implements a subset of **getsockopt()**. - * It supports the following *level*\ s: - * - * * **IPPROTO_TCP**, which supports *optname* - * **TCP_CONGESTION**. - * * **IPPROTO_IP**, which supports *optname* **IP_TOS**. - * * **IPPROTO_IPV6**, which supports *optname* **IPV6_TCLASS**. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_getsockopt)(void *bpf_socket, int level, int optname, void *optval, int optlen) = (void *) 57; - -/* - * bpf_override_return - * - * Used for error injection, this helper uses kprobes to override - * the return value of the probed function, and to set it to *rc*. - * The first argument is the context *regs* on which the kprobe - * works. - * - * This helper works by setting the PC (program counter) - * to an override function which is run in place of the original - * probed function. This means the probed function is not run at - * all. The replacement function just returns with the required - * value. - * - * This helper has security implications, and thus is subject to - * restrictions. It is only available if the kernel was compiled - * with the **CONFIG_BPF_KPROBE_OVERRIDE** configuration - * option, and in this case it only works on functions tagged with - * **ALLOW_ERROR_INJECTION** in the kernel code. - * - * Also, the helper is only available for the architectures having - * the CONFIG_FUNCTION_ERROR_INJECTION option. As of this writing, - * x86 architecture is the only one to support this feature. - * - * Returns - * 0 - */ -static long (*bpf_override_return)(struct pt_regs *regs, __u64 rc) = (void *) 58; - -/* - * bpf_sock_ops_cb_flags_set - * - * Attempt to set the value of the **bpf_sock_ops_cb_flags** field - * for the full TCP socket associated to *bpf_sock_ops* to - * *argval*. - * - * The primary use of this field is to determine if there should - * be calls to eBPF programs of type - * **BPF_PROG_TYPE_SOCK_OPS** at various points in the TCP - * code. A program of the same type can change its value, per - * connection and as necessary, when the connection is - * established. This field is directly accessible for reading, but - * this helper must be used for updates in order to return an - * error if an eBPF program tries to set a callback that is not - * supported in the current kernel. - * - * *argval* is a flag array which can combine these flags: - * - * * **BPF_SOCK_OPS_RTO_CB_FLAG** (retransmission time out) - * * **BPF_SOCK_OPS_RETRANS_CB_FLAG** (retransmission) - * * **BPF_SOCK_OPS_STATE_CB_FLAG** (TCP state change) - * * **BPF_SOCK_OPS_RTT_CB_FLAG** (every RTT) - * - * Therefore, this function can be used to clear a callback flag by - * setting the appropriate bit to zero. e.g. to disable the RTO - * callback: - * - * **bpf_sock_ops_cb_flags_set(bpf_sock,** - * **bpf_sock->bpf_sock_ops_cb_flags & ~BPF_SOCK_OPS_RTO_CB_FLAG)** - * - * Here are some examples of where one could call such eBPF - * program: - * - * * When RTO fires. - * * When a packet is retransmitted. - * * When the connection terminates. - * * When a packet is sent. - * * When a packet is received. - * - * Returns - * Code **-EINVAL** if the socket is not a full TCP socket; - * otherwise, a positive number containing the bits that could not - * be set is returned (which comes down to 0 if all bits were set - * as required). - */ -static long (*bpf_sock_ops_cb_flags_set)(struct bpf_sock_ops *bpf_sock, int argval) = (void *) 59; - -/* - * bpf_msg_redirect_map - * - * This helper is used in programs implementing policies at the - * socket level. If the message *msg* is allowed to pass (i.e. if - * the verdict eBPF program returns **SK_PASS**), redirect it to - * the socket referenced by *map* (of type - * **BPF_MAP_TYPE_SOCKMAP**) at index *key*. Both ingress and - * egress interfaces can be used for redirection. The - * **BPF_F_INGRESS** value in *flags* is used to make the - * distinction (ingress path is selected if the flag is present, - * egress path otherwise). This is the only flag supported for now. - * - * Returns - * **SK_PASS** on success, or **SK_DROP** on error. - */ -static long (*bpf_msg_redirect_map)(struct sk_msg_md *msg, void *map, __u32 key, __u64 flags) = (void *) 60; - -/* - * bpf_msg_apply_bytes - * - * For socket policies, apply the verdict of the eBPF program to - * the next *bytes* (number of bytes) of message *msg*. - * - * For example, this helper can be used in the following cases: - * - * * A single **sendmsg**\ () or **sendfile**\ () system call - * contains multiple logical messages that the eBPF program is - * supposed to read and for which it should apply a verdict. - * * An eBPF program only cares to read the first *bytes* of a - * *msg*. If the message has a large payload, then setting up - * and calling the eBPF program repeatedly for all bytes, even - * though the verdict is already known, would create unnecessary - * overhead. - * - * When called from within an eBPF program, the helper sets a - * counter internal to the BPF infrastructure, that is used to - * apply the last verdict to the next *bytes*. If *bytes* is - * smaller than the current data being processed from a - * **sendmsg**\ () or **sendfile**\ () system call, the first - * *bytes* will be sent and the eBPF program will be re-run with - * the pointer for start of data pointing to byte number *bytes* - * **+ 1**. If *bytes* is larger than the current data being - * processed, then the eBPF verdict will be applied to multiple - * **sendmsg**\ () or **sendfile**\ () calls until *bytes* are - * consumed. - * - * Note that if a socket closes with the internal counter holding - * a non-zero value, this is not a problem because data is not - * being buffered for *bytes* and is sent as it is received. - * - * Returns - * 0 - */ -static long (*bpf_msg_apply_bytes)(struct sk_msg_md *msg, __u32 bytes) = (void *) 61; - -/* - * bpf_msg_cork_bytes - * - * For socket policies, prevent the execution of the verdict eBPF - * program for message *msg* until *bytes* (byte number) have been - * accumulated. - * - * This can be used when one needs a specific number of bytes - * before a verdict can be assigned, even if the data spans - * multiple **sendmsg**\ () or **sendfile**\ () calls. The extreme - * case would be a user calling **sendmsg**\ () repeatedly with - * 1-byte long message segments. Obviously, this is bad for - * performance, but it is still valid. If the eBPF program needs - * *bytes* bytes to validate a header, this helper can be used to - * prevent the eBPF program to be called again until *bytes* have - * been accumulated. - * - * Returns - * 0 - */ -static long (*bpf_msg_cork_bytes)(struct sk_msg_md *msg, __u32 bytes) = (void *) 62; - -/* - * bpf_msg_pull_data - * - * For socket policies, pull in non-linear data from user space - * for *msg* and set pointers *msg*\ **->data** and *msg*\ - * **->data_end** to *start* and *end* bytes offsets into *msg*, - * respectively. - * - * If a program of type **BPF_PROG_TYPE_SK_MSG** is run on a - * *msg* it can only parse data that the (**data**, **data_end**) - * pointers have already consumed. For **sendmsg**\ () hooks this - * is likely the first scatterlist element. But for calls relying - * on the **sendpage** handler (e.g. **sendfile**\ ()) this will - * be the range (**0**, **0**) because the data is shared with - * user space and by default the objective is to avoid allowing - * user space to modify data while (or after) eBPF verdict is - * being decided. This helper can be used to pull in data and to - * set the start and end pointer to given values. Data will be - * copied if necessary (i.e. if data was not linear and if start - * and end pointers do not point to the same chunk). - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * All values for *flags* are reserved for future usage, and must - * be left at zero. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_msg_pull_data)(struct sk_msg_md *msg, __u32 start, __u32 end, __u64 flags) = (void *) 63; - -/* - * bpf_bind - * - * Bind the socket associated to *ctx* to the address pointed by - * *addr*, of length *addr_len*. This allows for making outgoing - * connection from the desired IP address, which can be useful for - * example when all processes inside a cgroup should use one - * single IP address on a host that has multiple IP configured. - * - * This helper works for IPv4 and IPv6, TCP and UDP sockets. The - * domain (*addr*\ **->sa_family**) must be **AF_INET** (or - * **AF_INET6**). It's advised to pass zero port (**sin_port** - * or **sin6_port**) which triggers IP_BIND_ADDRESS_NO_PORT-like - * behavior and lets the kernel efficiently pick up an unused - * port as long as 4-tuple is unique. Passing non-zero port might - * lead to degraded performance. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_bind)(struct bpf_sock_addr *ctx, struct sockaddr *addr, int addr_len) = (void *) 64; - -/* - * bpf_xdp_adjust_tail - * - * Adjust (move) *xdp_md*\ **->data_end** by *delta* bytes. It is - * possible to both shrink and grow the packet tail. - * Shrink done via *delta* being a negative integer. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_xdp_adjust_tail)(struct xdp_md *xdp_md, int delta) = (void *) 65; - -/* - * bpf_skb_get_xfrm_state - * - * Retrieve the XFRM state (IP transform framework, see also - * **ip-xfrm(8)**) at *index* in XFRM "security path" for *skb*. - * - * The retrieved value is stored in the **struct bpf_xfrm_state** - * pointed by *xfrm_state* and of length *size*. - * - * All values for *flags* are reserved for future usage, and must - * be left at zero. - * - * This helper is available only if the kernel was compiled with - * **CONFIG_XFRM** configuration option. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_get_xfrm_state)(struct __sk_buff *skb, __u32 index, struct bpf_xfrm_state *xfrm_state, __u32 size, __u64 flags) = (void *) 66; - -/* - * bpf_get_stack - * - * Return a user or a kernel stack in bpf program provided buffer. - * To achieve this, the helper needs *ctx*, which is a pointer - * to the context on which the tracing program is executed. - * To store the stacktrace, the bpf program provides *buf* with - * a nonnegative *size*. - * - * The last argument, *flags*, holds the number of stack frames to - * skip (from 0 to 255), masked with - * **BPF_F_SKIP_FIELD_MASK**. The next bits can be used to set - * the following flags: - * - * **BPF_F_USER_STACK** - * Collect a user space stack instead of a kernel stack. - * **BPF_F_USER_BUILD_ID** - * Collect buildid+offset instead of ips for user stack, - * only valid if **BPF_F_USER_STACK** is also specified. - * - * **bpf_get_stack**\ () can collect up to - * **PERF_MAX_STACK_DEPTH** both kernel and user frames, subject - * to sufficient large buffer size. Note that - * this limit can be controlled with the **sysctl** program, and - * that it should be manually increased in order to profile long - * user stacks (such as stacks for Java programs). To do so, use: - * - * :: - * - * # sysctl kernel.perf_event_max_stack= - * - * Returns - * A non-negative value equal to or less than *size* on success, - * or a negative error in case of failure. - */ -static long (*bpf_get_stack)(void *ctx, void *buf, __u32 size, __u64 flags) = (void *) 67; - -/* - * bpf_skb_load_bytes_relative - * - * This helper is similar to **bpf_skb_load_bytes**\ () in that - * it provides an easy way to load *len* bytes from *offset* - * from the packet associated to *skb*, into the buffer pointed - * by *to*. The difference to **bpf_skb_load_bytes**\ () is that - * a fifth argument *start_header* exists in order to select a - * base offset to start from. *start_header* can be one of: - * - * **BPF_HDR_START_MAC** - * Base offset to load data from is *skb*'s mac header. - * **BPF_HDR_START_NET** - * Base offset to load data from is *skb*'s network header. - * - * In general, "direct packet access" is the preferred method to - * access packet data, however, this helper is in particular useful - * in socket filters where *skb*\ **->data** does not always point - * to the start of the mac header and where "direct packet access" - * is not available. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_load_bytes_relative)(const void *skb, __u32 offset, void *to, __u32 len, __u32 start_header) = (void *) 68; - -/* - * bpf_fib_lookup - * - * Do FIB lookup in kernel tables using parameters in *params*. - * If lookup is successful and result shows packet is to be - * forwarded, the neighbor tables are searched for the nexthop. - * If successful (ie., FIB lookup shows forwarding and nexthop - * is resolved), the nexthop address is returned in ipv4_dst - * or ipv6_dst based on family, smac is set to mac address of - * egress device, dmac is set to nexthop mac address, rt_metric - * is set to metric from route (IPv4/IPv6 only), and ifindex - * is set to the device index of the nexthop from the FIB lookup. - * - * *plen* argument is the size of the passed in struct. - * *flags* argument can be a combination of one or more of the - * following values: - * - * **BPF_FIB_LOOKUP_DIRECT** - * Do a direct table lookup vs full lookup using FIB - * rules. - * **BPF_FIB_LOOKUP_OUTPUT** - * Perform lookup from an egress perspective (default is - * ingress). - * - * *ctx* is either **struct xdp_md** for XDP programs or - * **struct sk_buff** tc cls_act programs. - * - * Returns - * * < 0 if any input argument is invalid - * * 0 on success (packet is forwarded, nexthop neighbor exists) - * * > 0 one of **BPF_FIB_LKUP_RET_** codes explaining why the - * packet is not forwarded or needs assist from full stack - * - * If lookup fails with BPF_FIB_LKUP_RET_FRAG_NEEDED, then the MTU - * was exceeded and output params->mtu_result contains the MTU. - */ -static long (*bpf_fib_lookup)(void *ctx, struct bpf_fib_lookup *params, int plen, __u32 flags) = (void *) 69; - -/* - * bpf_sock_hash_update - * - * Add an entry to, or update a sockhash *map* referencing sockets. - * The *skops* is used as a new value for the entry associated to - * *key*. *flags* is one of: - * - * **BPF_NOEXIST** - * The entry for *key* must not exist in the map. - * **BPF_EXIST** - * The entry for *key* must already exist in the map. - * **BPF_ANY** - * No condition on the existence of the entry for *key*. - * - * If the *map* has eBPF programs (parser and verdict), those will - * be inherited by the socket being added. If the socket is - * already attached to eBPF programs, this results in an error. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_sock_hash_update)(struct bpf_sock_ops *skops, void *map, void *key, __u64 flags) = (void *) 70; - -/* - * bpf_msg_redirect_hash - * - * This helper is used in programs implementing policies at the - * socket level. If the message *msg* is allowed to pass (i.e. if - * the verdict eBPF program returns **SK_PASS**), redirect it to - * the socket referenced by *map* (of type - * **BPF_MAP_TYPE_SOCKHASH**) using hash *key*. Both ingress and - * egress interfaces can be used for redirection. The - * **BPF_F_INGRESS** value in *flags* is used to make the - * distinction (ingress path is selected if the flag is present, - * egress path otherwise). This is the only flag supported for now. - * - * Returns - * **SK_PASS** on success, or **SK_DROP** on error. - */ -static long (*bpf_msg_redirect_hash)(struct sk_msg_md *msg, void *map, void *key, __u64 flags) = (void *) 71; - -/* - * bpf_sk_redirect_hash - * - * This helper is used in programs implementing policies at the - * skb socket level. If the sk_buff *skb* is allowed to pass (i.e. - * if the verdict eBPF program returns **SK_PASS**), redirect it - * to the socket referenced by *map* (of type - * **BPF_MAP_TYPE_SOCKHASH**) using hash *key*. Both ingress and - * egress interfaces can be used for redirection. The - * **BPF_F_INGRESS** value in *flags* is used to make the - * distinction (ingress path is selected if the flag is present, - * egress otherwise). This is the only flag supported for now. - * - * Returns - * **SK_PASS** on success, or **SK_DROP** on error. - */ -static long (*bpf_sk_redirect_hash)(struct __sk_buff *skb, void *map, void *key, __u64 flags) = (void *) 72; - -/* - * bpf_lwt_push_encap - * - * Encapsulate the packet associated to *skb* within a Layer 3 - * protocol header. This header is provided in the buffer at - * address *hdr*, with *len* its size in bytes. *type* indicates - * the protocol of the header and can be one of: - * - * **BPF_LWT_ENCAP_SEG6** - * IPv6 encapsulation with Segment Routing Header - * (**struct ipv6_sr_hdr**). *hdr* only contains the SRH, - * the IPv6 header is computed by the kernel. - * **BPF_LWT_ENCAP_SEG6_INLINE** - * Only works if *skb* contains an IPv6 packet. Insert a - * Segment Routing Header (**struct ipv6_sr_hdr**) inside - * the IPv6 header. - * **BPF_LWT_ENCAP_IP** - * IP encapsulation (GRE/GUE/IPIP/etc). The outer header - * must be IPv4 or IPv6, followed by zero or more - * additional headers, up to **LWT_BPF_MAX_HEADROOM** - * total bytes in all prepended headers. Please note that - * if **skb_is_gso**\ (*skb*) is true, no more than two - * headers can be prepended, and the inner header, if - * present, should be either GRE or UDP/GUE. - * - * **BPF_LWT_ENCAP_SEG6**\ \* types can be called by BPF programs - * of type **BPF_PROG_TYPE_LWT_IN**; **BPF_LWT_ENCAP_IP** type can - * be called by bpf programs of types **BPF_PROG_TYPE_LWT_IN** and - * **BPF_PROG_TYPE_LWT_XMIT**. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_lwt_push_encap)(struct __sk_buff *skb, __u32 type, void *hdr, __u32 len) = (void *) 73; - -/* - * bpf_lwt_seg6_store_bytes - * - * Store *len* bytes from address *from* into the packet - * associated to *skb*, at *offset*. Only the flags, tag and TLVs - * inside the outermost IPv6 Segment Routing Header can be - * modified through this helper. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_lwt_seg6_store_bytes)(struct __sk_buff *skb, __u32 offset, const void *from, __u32 len) = (void *) 74; - -/* - * bpf_lwt_seg6_adjust_srh - * - * Adjust the size allocated to TLVs in the outermost IPv6 - * Segment Routing Header contained in the packet associated to - * *skb*, at position *offset* by *delta* bytes. Only offsets - * after the segments are accepted. *delta* can be as well - * positive (growing) as negative (shrinking). - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_lwt_seg6_adjust_srh)(struct __sk_buff *skb, __u32 offset, __s32 delta) = (void *) 75; - -/* - * bpf_lwt_seg6_action - * - * Apply an IPv6 Segment Routing action of type *action* to the - * packet associated to *skb*. Each action takes a parameter - * contained at address *param*, and of length *param_len* bytes. - * *action* can be one of: - * - * **SEG6_LOCAL_ACTION_END_X** - * End.X action: Endpoint with Layer-3 cross-connect. - * Type of *param*: **struct in6_addr**. - * **SEG6_LOCAL_ACTION_END_T** - * End.T action: Endpoint with specific IPv6 table lookup. - * Type of *param*: **int**. - * **SEG6_LOCAL_ACTION_END_B6** - * End.B6 action: Endpoint bound to an SRv6 policy. - * Type of *param*: **struct ipv6_sr_hdr**. - * **SEG6_LOCAL_ACTION_END_B6_ENCAP** - * End.B6.Encap action: Endpoint bound to an SRv6 - * encapsulation policy. - * Type of *param*: **struct ipv6_sr_hdr**. - * - * A call to this helper is susceptible to change the underlying - * packet buffer. Therefore, at load time, all checks on pointers - * previously done by the verifier are invalidated and must be - * performed again, if the helper is used in combination with - * direct packet access. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_lwt_seg6_action)(struct __sk_buff *skb, __u32 action, void *param, __u32 param_len) = (void *) 76; - -/* - * bpf_rc_repeat - * - * This helper is used in programs implementing IR decoding, to - * report a successfully decoded repeat key message. This delays - * the generation of a key up event for previously generated - * key down event. - * - * Some IR protocols like NEC have a special IR message for - * repeating last button, for when a button is held down. - * - * The *ctx* should point to the lirc sample as passed into - * the program. - * - * This helper is only available is the kernel was compiled with - * the **CONFIG_BPF_LIRC_MODE2** configuration option set to - * "**y**". - * - * Returns - * 0 - */ -static long (*bpf_rc_repeat)(void *ctx) = (void *) 77; - -/* - * bpf_rc_keydown - * - * This helper is used in programs implementing IR decoding, to - * report a successfully decoded key press with *scancode*, - * *toggle* value in the given *protocol*. The scancode will be - * translated to a keycode using the rc keymap, and reported as - * an input key down event. After a period a key up event is - * generated. This period can be extended by calling either - * **bpf_rc_keydown**\ () again with the same values, or calling - * **bpf_rc_repeat**\ (). - * - * Some protocols include a toggle bit, in case the button was - * released and pressed again between consecutive scancodes. - * - * The *ctx* should point to the lirc sample as passed into - * the program. - * - * The *protocol* is the decoded protocol number (see - * **enum rc_proto** for some predefined values). - * - * This helper is only available is the kernel was compiled with - * the **CONFIG_BPF_LIRC_MODE2** configuration option set to - * "**y**". - * - * Returns - * 0 - */ -static long (*bpf_rc_keydown)(void *ctx, __u32 protocol, __u64 scancode, __u32 toggle) = (void *) 78; - -/* - * bpf_skb_cgroup_id - * - * Return the cgroup v2 id of the socket associated with the *skb*. - * This is roughly similar to the **bpf_get_cgroup_classid**\ () - * helper for cgroup v1 by providing a tag resp. identifier that - * can be matched on or used for map lookups e.g. to implement - * policy. The cgroup v2 id of a given path in the hierarchy is - * exposed in user space through the f_handle API in order to get - * to the same 64-bit id. - * - * This helper can be used on TC egress path, but not on ingress, - * and is available only if the kernel was compiled with the - * **CONFIG_SOCK_CGROUP_DATA** configuration option. - * - * Returns - * The id is returned or 0 in case the id could not be retrieved. - */ -static __u64 (*bpf_skb_cgroup_id)(struct __sk_buff *skb) = (void *) 79; - -/* - * bpf_get_current_cgroup_id - * - * - * Returns - * A 64-bit integer containing the current cgroup id based - * on the cgroup within which the current task is running. - */ -static __u64 (*bpf_get_current_cgroup_id)(void) = (void *) 80; - -/* - * bpf_get_local_storage - * - * Get the pointer to the local storage area. - * The type and the size of the local storage is defined - * by the *map* argument. - * The *flags* meaning is specific for each map type, - * and has to be 0 for cgroup local storage. - * - * Depending on the BPF program type, a local storage area - * can be shared between multiple instances of the BPF program, - * running simultaneously. - * - * A user should care about the synchronization by himself. - * For example, by using the **BPF_ATOMIC** instructions to alter - * the shared data. - * - * Returns - * A pointer to the local storage area. - */ -static void *(*bpf_get_local_storage)(void *map, __u64 flags) = (void *) 81; - -/* - * bpf_sk_select_reuseport - * - * Select a **SO_REUSEPORT** socket from a - * **BPF_MAP_TYPE_REUSEPORT_SOCKARRAY** *map*. - * It checks the selected socket is matching the incoming - * request in the socket buffer. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_sk_select_reuseport)(struct sk_reuseport_md *reuse, void *map, void *key, __u64 flags) = (void *) 82; - -/* - * bpf_skb_ancestor_cgroup_id - * - * Return id of cgroup v2 that is ancestor of cgroup associated - * with the *skb* at the *ancestor_level*. The root cgroup is at - * *ancestor_level* zero and each step down the hierarchy - * increments the level. If *ancestor_level* == level of cgroup - * associated with *skb*, then return value will be same as that - * of **bpf_skb_cgroup_id**\ (). - * - * The helper is useful to implement policies based on cgroups - * that are upper in hierarchy than immediate cgroup associated - * with *skb*. - * - * The format of returned id and helper limitations are same as in - * **bpf_skb_cgroup_id**\ (). - * - * Returns - * The id is returned or 0 in case the id could not be retrieved. - */ -static __u64 (*bpf_skb_ancestor_cgroup_id)(struct __sk_buff *skb, int ancestor_level) = (void *) 83; - -/* - * bpf_sk_lookup_tcp - * - * Look for TCP socket matching *tuple*, optionally in a child - * network namespace *netns*. The return value must be checked, - * and if non-**NULL**, released via **bpf_sk_release**\ (). - * - * The *ctx* should point to the context of the program, such as - * the skb or socket (depending on the hook in use). This is used - * to determine the base network namespace for the lookup. - * - * *tuple_size* must be one of: - * - * **sizeof**\ (*tuple*\ **->ipv4**) - * Look for an IPv4 socket. - * **sizeof**\ (*tuple*\ **->ipv6**) - * Look for an IPv6 socket. - * - * If the *netns* is a negative signed 32-bit integer, then the - * socket lookup table in the netns associated with the *ctx* - * will be used. For the TC hooks, this is the netns of the device - * in the skb. For socket hooks, this is the netns of the socket. - * If *netns* is any other signed 32-bit value greater than or - * equal to zero then it specifies the ID of the netns relative to - * the netns associated with the *ctx*. *netns* values beyond the - * range of 32-bit integers are reserved for future use. - * - * All values for *flags* are reserved for future usage, and must - * be left at zero. - * - * This helper is available only if the kernel was compiled with - * **CONFIG_NET** configuration option. - * - * Returns - * Pointer to **struct bpf_sock**, or **NULL** in case of failure. - * For sockets with reuseport option, the **struct bpf_sock** - * result is from *reuse*\ **->socks**\ [] using the hash of the - * tuple. - */ -static struct bpf_sock *(*bpf_sk_lookup_tcp)(void *ctx, struct bpf_sock_tuple *tuple, __u32 tuple_size, __u64 netns, __u64 flags) = (void *) 84; - -/* - * bpf_sk_lookup_udp - * - * Look for UDP socket matching *tuple*, optionally in a child - * network namespace *netns*. The return value must be checked, - * and if non-**NULL**, released via **bpf_sk_release**\ (). - * - * The *ctx* should point to the context of the program, such as - * the skb or socket (depending on the hook in use). This is used - * to determine the base network namespace for the lookup. - * - * *tuple_size* must be one of: - * - * **sizeof**\ (*tuple*\ **->ipv4**) - * Look for an IPv4 socket. - * **sizeof**\ (*tuple*\ **->ipv6**) - * Look for an IPv6 socket. - * - * If the *netns* is a negative signed 32-bit integer, then the - * socket lookup table in the netns associated with the *ctx* - * will be used. For the TC hooks, this is the netns of the device - * in the skb. For socket hooks, this is the netns of the socket. - * If *netns* is any other signed 32-bit value greater than or - * equal to zero then it specifies the ID of the netns relative to - * the netns associated with the *ctx*. *netns* values beyond the - * range of 32-bit integers are reserved for future use. - * - * All values for *flags* are reserved for future usage, and must - * be left at zero. - * - * This helper is available only if the kernel was compiled with - * **CONFIG_NET** configuration option. - * - * Returns - * Pointer to **struct bpf_sock**, or **NULL** in case of failure. - * For sockets with reuseport option, the **struct bpf_sock** - * result is from *reuse*\ **->socks**\ [] using the hash of the - * tuple. - */ -static struct bpf_sock *(*bpf_sk_lookup_udp)(void *ctx, struct bpf_sock_tuple *tuple, __u32 tuple_size, __u64 netns, __u64 flags) = (void *) 85; - -/* - * bpf_sk_release - * - * Release the reference held by *sock*. *sock* must be a - * non-**NULL** pointer that was returned from - * **bpf_sk_lookup_xxx**\ (). - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_sk_release)(void *sock) = (void *) 86; - -/* - * bpf_map_push_elem - * - * Push an element *value* in *map*. *flags* is one of: - * - * **BPF_EXIST** - * If the queue/stack is full, the oldest element is - * removed to make room for this. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_map_push_elem)(void *map, const void *value, __u64 flags) = (void *) 87; - -/* - * bpf_map_pop_elem - * - * Pop an element from *map*. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_map_pop_elem)(void *map, void *value) = (void *) 88; - -/* - * bpf_map_peek_elem - * - * Get an element from *map* without removing it. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_map_peek_elem)(void *map, void *value) = (void *) 89; - -/* - * bpf_msg_push_data - * - * For socket policies, insert *len* bytes into *msg* at offset - * *start*. - * - * If a program of type **BPF_PROG_TYPE_SK_MSG** is run on a - * *msg* it may want to insert metadata or options into the *msg*. - * This can later be read and used by any of the lower layer BPF - * hooks. - * - * This helper may fail if under memory pressure (a malloc - * fails) in these cases BPF programs will get an appropriate - * error and BPF programs will need to handle them. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_msg_push_data)(struct sk_msg_md *msg, __u32 start, __u32 len, __u64 flags) = (void *) 90; - -/* - * bpf_msg_pop_data - * - * Will remove *len* bytes from a *msg* starting at byte *start*. - * This may result in **ENOMEM** errors under certain situations if - * an allocation and copy are required due to a full ring buffer. - * However, the helper will try to avoid doing the allocation - * if possible. Other errors can occur if input parameters are - * invalid either due to *start* byte not being valid part of *msg* - * payload and/or *pop* value being to large. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_msg_pop_data)(struct sk_msg_md *msg, __u32 start, __u32 len, __u64 flags) = (void *) 91; - -/* - * bpf_rc_pointer_rel - * - * This helper is used in programs implementing IR decoding, to - * report a successfully decoded pointer movement. - * - * The *ctx* should point to the lirc sample as passed into - * the program. - * - * This helper is only available is the kernel was compiled with - * the **CONFIG_BPF_LIRC_MODE2** configuration option set to - * "**y**". - * - * Returns - * 0 - */ -static long (*bpf_rc_pointer_rel)(void *ctx, __s32 rel_x, __s32 rel_y) = (void *) 92; - -/* - * bpf_spin_lock - * - * Acquire a spinlock represented by the pointer *lock*, which is - * stored as part of a value of a map. Taking the lock allows to - * safely update the rest of the fields in that value. The - * spinlock can (and must) later be released with a call to - * **bpf_spin_unlock**\ (\ *lock*\ ). - * - * Spinlocks in BPF programs come with a number of restrictions - * and constraints: - * - * * **bpf_spin_lock** objects are only allowed inside maps of - * types **BPF_MAP_TYPE_HASH** and **BPF_MAP_TYPE_ARRAY** (this - * list could be extended in the future). - * * BTF description of the map is mandatory. - * * The BPF program can take ONE lock at a time, since taking two - * or more could cause dead locks. - * * Only one **struct bpf_spin_lock** is allowed per map element. - * * When the lock is taken, calls (either BPF to BPF or helpers) - * are not allowed. - * * The **BPF_LD_ABS** and **BPF_LD_IND** instructions are not - * allowed inside a spinlock-ed region. - * * The BPF program MUST call **bpf_spin_unlock**\ () to release - * the lock, on all execution paths, before it returns. - * * The BPF program can access **struct bpf_spin_lock** only via - * the **bpf_spin_lock**\ () and **bpf_spin_unlock**\ () - * helpers. Loading or storing data into the **struct - * bpf_spin_lock** *lock*\ **;** field of a map is not allowed. - * * To use the **bpf_spin_lock**\ () helper, the BTF description - * of the map value must be a struct and have **struct - * bpf_spin_lock** *anyname*\ **;** field at the top level. - * Nested lock inside another struct is not allowed. - * * The **struct bpf_spin_lock** *lock* field in a map value must - * be aligned on a multiple of 4 bytes in that value. - * * Syscall with command **BPF_MAP_LOOKUP_ELEM** does not copy - * the **bpf_spin_lock** field to user space. - * * Syscall with command **BPF_MAP_UPDATE_ELEM**, or update from - * a BPF program, do not update the **bpf_spin_lock** field. - * * **bpf_spin_lock** cannot be on the stack or inside a - * networking packet (it can only be inside of a map values). - * * **bpf_spin_lock** is available to root only. - * * Tracing programs and socket filter programs cannot use - * **bpf_spin_lock**\ () due to insufficient preemption checks - * (but this may change in the future). - * * **bpf_spin_lock** is not allowed in inner maps of map-in-map. - * - * Returns - * 0 - */ -static long (*bpf_spin_lock)(struct bpf_spin_lock *lock) = (void *) 93; - -/* - * bpf_spin_unlock - * - * Release the *lock* previously locked by a call to - * **bpf_spin_lock**\ (\ *lock*\ ). - * - * Returns - * 0 - */ -static long (*bpf_spin_unlock)(struct bpf_spin_lock *lock) = (void *) 94; - -/* - * bpf_sk_fullsock - * - * This helper gets a **struct bpf_sock** pointer such - * that all the fields in this **bpf_sock** can be accessed. - * - * Returns - * A **struct bpf_sock** pointer on success, or **NULL** in - * case of failure. - */ -static struct bpf_sock *(*bpf_sk_fullsock)(struct bpf_sock *sk) = (void *) 95; - -/* - * bpf_tcp_sock - * - * This helper gets a **struct bpf_tcp_sock** pointer from a - * **struct bpf_sock** pointer. - * - * Returns - * A **struct bpf_tcp_sock** pointer on success, or **NULL** in - * case of failure. - */ -static struct bpf_tcp_sock *(*bpf_tcp_sock)(struct bpf_sock *sk) = (void *) 96; - -/* - * bpf_skb_ecn_set_ce - * - * Set ECN (Explicit Congestion Notification) field of IP header - * to **CE** (Congestion Encountered) if current value is **ECT** - * (ECN Capable Transport). Otherwise, do nothing. Works with IPv6 - * and IPv4. - * - * Returns - * 1 if the **CE** flag is set (either by the current helper call - * or because it was already present), 0 if it is not set. - */ -static long (*bpf_skb_ecn_set_ce)(struct __sk_buff *skb) = (void *) 97; - -/* - * bpf_get_listener_sock - * - * Return a **struct bpf_sock** pointer in **TCP_LISTEN** state. - * **bpf_sk_release**\ () is unnecessary and not allowed. - * - * Returns - * A **struct bpf_sock** pointer on success, or **NULL** in - * case of failure. - */ -static struct bpf_sock *(*bpf_get_listener_sock)(struct bpf_sock *sk) = (void *) 98; - -/* - * bpf_skc_lookup_tcp - * - * Look for TCP socket matching *tuple*, optionally in a child - * network namespace *netns*. The return value must be checked, - * and if non-**NULL**, released via **bpf_sk_release**\ (). - * - * This function is identical to **bpf_sk_lookup_tcp**\ (), except - * that it also returns timewait or request sockets. Use - * **bpf_sk_fullsock**\ () or **bpf_tcp_sock**\ () to access the - * full structure. - * - * This helper is available only if the kernel was compiled with - * **CONFIG_NET** configuration option. - * - * Returns - * Pointer to **struct bpf_sock**, or **NULL** in case of failure. - * For sockets with reuseport option, the **struct bpf_sock** - * result is from *reuse*\ **->socks**\ [] using the hash of the - * tuple. - */ -static struct bpf_sock *(*bpf_skc_lookup_tcp)(void *ctx, struct bpf_sock_tuple *tuple, __u32 tuple_size, __u64 netns, __u64 flags) = (void *) 99; - -/* - * bpf_tcp_check_syncookie - * - * Check whether *iph* and *th* contain a valid SYN cookie ACK for - * the listening socket in *sk*. - * - * *iph* points to the start of the IPv4 or IPv6 header, while - * *iph_len* contains **sizeof**\ (**struct iphdr**) or - * **sizeof**\ (**struct ip6hdr**). - * - * *th* points to the start of the TCP header, while *th_len* - * contains **sizeof**\ (**struct tcphdr**). - * - * Returns - * 0 if *iph* and *th* are a valid SYN cookie ACK, or a negative - * error otherwise. - */ -static long (*bpf_tcp_check_syncookie)(void *sk, void *iph, __u32 iph_len, struct tcphdr *th, __u32 th_len) = (void *) 100; - -/* - * bpf_sysctl_get_name - * - * Get name of sysctl in /proc/sys/ and copy it into provided by - * program buffer *buf* of size *buf_len*. - * - * The buffer is always NUL terminated, unless it's zero-sized. - * - * If *flags* is zero, full name (e.g. "net/ipv4/tcp_mem") is - * copied. Use **BPF_F_SYSCTL_BASE_NAME** flag to copy base name - * only (e.g. "tcp_mem"). - * - * Returns - * Number of character copied (not including the trailing NUL). - * - * **-E2BIG** if the buffer wasn't big enough (*buf* will contain - * truncated name in this case). - */ -static long (*bpf_sysctl_get_name)(struct bpf_sysctl *ctx, char *buf, unsigned long buf_len, __u64 flags) = (void *) 101; - -/* - * bpf_sysctl_get_current_value - * - * Get current value of sysctl as it is presented in /proc/sys - * (incl. newline, etc), and copy it as a string into provided - * by program buffer *buf* of size *buf_len*. - * - * The whole value is copied, no matter what file position user - * space issued e.g. sys_read at. - * - * The buffer is always NUL terminated, unless it's zero-sized. - * - * Returns - * Number of character copied (not including the trailing NUL). - * - * **-E2BIG** if the buffer wasn't big enough (*buf* will contain - * truncated name in this case). - * - * **-EINVAL** if current value was unavailable, e.g. because - * sysctl is uninitialized and read returns -EIO for it. - */ -static long (*bpf_sysctl_get_current_value)(struct bpf_sysctl *ctx, char *buf, unsigned long buf_len) = (void *) 102; - -/* - * bpf_sysctl_get_new_value - * - * Get new value being written by user space to sysctl (before - * the actual write happens) and copy it as a string into - * provided by program buffer *buf* of size *buf_len*. - * - * User space may write new value at file position > 0. - * - * The buffer is always NUL terminated, unless it's zero-sized. - * - * Returns - * Number of character copied (not including the trailing NUL). - * - * **-E2BIG** if the buffer wasn't big enough (*buf* will contain - * truncated name in this case). - * - * **-EINVAL** if sysctl is being read. - */ -static long (*bpf_sysctl_get_new_value)(struct bpf_sysctl *ctx, char *buf, unsigned long buf_len) = (void *) 103; - -/* - * bpf_sysctl_set_new_value - * - * Override new value being written by user space to sysctl with - * value provided by program in buffer *buf* of size *buf_len*. - * - * *buf* should contain a string in same form as provided by user - * space on sysctl write. - * - * User space may write new value at file position > 0. To override - * the whole sysctl value file position should be set to zero. - * - * Returns - * 0 on success. - * - * **-E2BIG** if the *buf_len* is too big. - * - * **-EINVAL** if sysctl is being read. - */ -static long (*bpf_sysctl_set_new_value)(struct bpf_sysctl *ctx, const char *buf, unsigned long buf_len) = (void *) 104; - -/* - * bpf_strtol - * - * Convert the initial part of the string from buffer *buf* of - * size *buf_len* to a long integer according to the given base - * and save the result in *res*. - * - * The string may begin with an arbitrary amount of white space - * (as determined by **isspace**\ (3)) followed by a single - * optional '**-**' sign. - * - * Five least significant bits of *flags* encode base, other bits - * are currently unused. - * - * Base must be either 8, 10, 16 or 0 to detect it automatically - * similar to user space **strtol**\ (3). - * - * Returns - * Number of characters consumed on success. Must be positive but - * no more than *buf_len*. - * - * **-EINVAL** if no valid digits were found or unsupported base - * was provided. - * - * **-ERANGE** if resulting value was out of range. - */ -static long (*bpf_strtol)(const char *buf, unsigned long buf_len, __u64 flags, long *res) = (void *) 105; - -/* - * bpf_strtoul - * - * Convert the initial part of the string from buffer *buf* of - * size *buf_len* to an unsigned long integer according to the - * given base and save the result in *res*. - * - * The string may begin with an arbitrary amount of white space - * (as determined by **isspace**\ (3)). - * - * Five least significant bits of *flags* encode base, other bits - * are currently unused. - * - * Base must be either 8, 10, 16 or 0 to detect it automatically - * similar to user space **strtoul**\ (3). - * - * Returns - * Number of characters consumed on success. Must be positive but - * no more than *buf_len*. - * - * **-EINVAL** if no valid digits were found or unsupported base - * was provided. - * - * **-ERANGE** if resulting value was out of range. - */ -static long (*bpf_strtoul)(const char *buf, unsigned long buf_len, __u64 flags, unsigned long *res) = (void *) 106; - -/* - * bpf_sk_storage_get - * - * Get a bpf-local-storage from a *sk*. - * - * Logically, it could be thought of getting the value from - * a *map* with *sk* as the **key**. From this - * perspective, the usage is not much different from - * **bpf_map_lookup_elem**\ (*map*, **&**\ *sk*) except this - * helper enforces the key must be a full socket and the map must - * be a **BPF_MAP_TYPE_SK_STORAGE** also. - * - * Underneath, the value is stored locally at *sk* instead of - * the *map*. The *map* is used as the bpf-local-storage - * "type". The bpf-local-storage "type" (i.e. the *map*) is - * searched against all bpf-local-storages residing at *sk*. - * - * *sk* is a kernel **struct sock** pointer for LSM program. - * *sk* is a **struct bpf_sock** pointer for other program types. - * - * An optional *flags* (**BPF_SK_STORAGE_GET_F_CREATE**) can be - * used such that a new bpf-local-storage will be - * created if one does not exist. *value* can be used - * together with **BPF_SK_STORAGE_GET_F_CREATE** to specify - * the initial value of a bpf-local-storage. If *value* is - * **NULL**, the new bpf-local-storage will be zero initialized. - * - * Returns - * A bpf-local-storage pointer is returned on success. - * - * **NULL** if not found or there was an error in adding - * a new bpf-local-storage. - */ -static void *(*bpf_sk_storage_get)(void *map, void *sk, void *value, __u64 flags) = (void *) 107; - -/* - * bpf_sk_storage_delete - * - * Delete a bpf-local-storage from a *sk*. - * - * Returns - * 0 on success. - * - * **-ENOENT** if the bpf-local-storage cannot be found. - * **-EINVAL** if sk is not a fullsock (e.g. a request_sock). - */ -static long (*bpf_sk_storage_delete)(void *map, void *sk) = (void *) 108; - -/* - * bpf_send_signal - * - * Send signal *sig* to the process of the current task. - * The signal may be delivered to any of this process's threads. - * - * Returns - * 0 on success or successfully queued. - * - * **-EBUSY** if work queue under nmi is full. - * - * **-EINVAL** if *sig* is invalid. - * - * **-EPERM** if no permission to send the *sig*. - * - * **-EAGAIN** if bpf program can try again. - */ -static long (*bpf_send_signal)(__u32 sig) = (void *) 109; - -/* - * bpf_tcp_gen_syncookie - * - * Try to issue a SYN cookie for the packet with corresponding - * IP/TCP headers, *iph* and *th*, on the listening socket in *sk*. - * - * *iph* points to the start of the IPv4 or IPv6 header, while - * *iph_len* contains **sizeof**\ (**struct iphdr**) or - * **sizeof**\ (**struct ip6hdr**). - * - * *th* points to the start of the TCP header, while *th_len* - * contains the length of the TCP header. - * - * Returns - * On success, lower 32 bits hold the generated SYN cookie in - * followed by 16 bits which hold the MSS value for that cookie, - * and the top 16 bits are unused. - * - * On failure, the returned value is one of the following: - * - * **-EINVAL** SYN cookie cannot be issued due to error - * - * **-ENOENT** SYN cookie should not be issued (no SYN flood) - * - * **-EOPNOTSUPP** kernel configuration does not enable SYN cookies - * - * **-EPROTONOSUPPORT** IP packet version is not 4 or 6 - */ -static __s64 (*bpf_tcp_gen_syncookie)(void *sk, void *iph, __u32 iph_len, struct tcphdr *th, __u32 th_len) = (void *) 110; - -/* - * bpf_skb_output - * - * Write raw *data* blob into a special BPF perf event held by - * *map* of type **BPF_MAP_TYPE_PERF_EVENT_ARRAY**. This perf - * event must have the following attributes: **PERF_SAMPLE_RAW** - * as **sample_type**, **PERF_TYPE_SOFTWARE** as **type**, and - * **PERF_COUNT_SW_BPF_OUTPUT** as **config**. - * - * The *flags* are used to indicate the index in *map* for which - * the value must be put, masked with **BPF_F_INDEX_MASK**. - * Alternatively, *flags* can be set to **BPF_F_CURRENT_CPU** - * to indicate that the index of the current CPU core should be - * used. - * - * The value to write, of *size*, is passed through eBPF stack and - * pointed by *data*. - * - * *ctx* is a pointer to in-kernel struct sk_buff. - * - * This helper is similar to **bpf_perf_event_output**\ () but - * restricted to raw_tracepoint bpf programs. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_skb_output)(void *ctx, void *map, __u64 flags, void *data, __u64 size) = (void *) 111; - -/* - * bpf_probe_read_user - * - * Safely attempt to read *size* bytes from user space address - * *unsafe_ptr* and store the data in *dst*. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_probe_read_user)(void *dst, __u32 size, const void *unsafe_ptr) = (void *) 112; - -/* - * bpf_probe_read_kernel - * - * Safely attempt to read *size* bytes from kernel space address - * *unsafe_ptr* and store the data in *dst*. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_probe_read_kernel)(void *dst, __u32 size, const void *unsafe_ptr) = (void *) 113; - -/* - * bpf_probe_read_user_str - * - * Copy a NUL terminated string from an unsafe user address - * *unsafe_ptr* to *dst*. The *size* should include the - * terminating NUL byte. In case the string length is smaller than - * *size*, the target is not padded with further NUL bytes. If the - * string length is larger than *size*, just *size*-1 bytes are - * copied and the last byte is set to NUL. - * - * On success, returns the number of bytes that were written, - * including the terminal NUL. This makes this helper useful in - * tracing programs for reading strings, and more importantly to - * get its length at runtime. See the following snippet: - * - * :: - * - * SEC("kprobe/sys_open") - * void bpf_sys_open(struct pt_regs *ctx) - * { - * char buf[PATHLEN]; // PATHLEN is defined to 256 - * int res = bpf_probe_read_user_str(buf, sizeof(buf), - * ctx->di); - * - * // Consume buf, for example push it to - * // userspace via bpf_perf_event_output(); we - * // can use res (the string length) as event - * // size, after checking its boundaries. - * } - * - * In comparison, using **bpf_probe_read_user**\ () helper here - * instead to read the string would require to estimate the length - * at compile time, and would often result in copying more memory - * than necessary. - * - * Another useful use case is when parsing individual process - * arguments or individual environment variables navigating - * *current*\ **->mm->arg_start** and *current*\ - * **->mm->env_start**: using this helper and the return value, - * one can quickly iterate at the right offset of the memory area. - * - * Returns - * On success, the strictly positive length of the output string, - * including the trailing NUL character. On error, a negative - * value. - */ -static long (*bpf_probe_read_user_str)(void *dst, __u32 size, const void *unsafe_ptr) = (void *) 114; - -/* - * bpf_probe_read_kernel_str - * - * Copy a NUL terminated string from an unsafe kernel address *unsafe_ptr* - * to *dst*. Same semantics as with **bpf_probe_read_user_str**\ () apply. - * - * Returns - * On success, the strictly positive length of the string, including - * the trailing NUL character. On error, a negative value. - */ -static long (*bpf_probe_read_kernel_str)(void *dst, __u32 size, const void *unsafe_ptr) = (void *) 115; - -/* - * bpf_tcp_send_ack - * - * Send out a tcp-ack. *tp* is the in-kernel struct **tcp_sock**. - * *rcv_nxt* is the ack_seq to be sent out. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_tcp_send_ack)(void *tp, __u32 rcv_nxt) = (void *) 116; - -/* - * bpf_send_signal_thread - * - * Send signal *sig* to the thread corresponding to the current task. - * - * Returns - * 0 on success or successfully queued. - * - * **-EBUSY** if work queue under nmi is full. - * - * **-EINVAL** if *sig* is invalid. - * - * **-EPERM** if no permission to send the *sig*. - * - * **-EAGAIN** if bpf program can try again. - */ -static long (*bpf_send_signal_thread)(__u32 sig) = (void *) 117; - -/* - * bpf_jiffies64 - * - * Obtain the 64bit jiffies - * - * Returns - * The 64 bit jiffies - */ -static __u64 (*bpf_jiffies64)(void) = (void *) 118; - -/* - * bpf_read_branch_records - * - * For an eBPF program attached to a perf event, retrieve the - * branch records (**struct perf_branch_entry**) associated to *ctx* - * and store it in the buffer pointed by *buf* up to size - * *size* bytes. - * - * Returns - * On success, number of bytes written to *buf*. On error, a - * negative value. - * - * The *flags* can be set to **BPF_F_GET_BRANCH_RECORDS_SIZE** to - * instead return the number of bytes required to store all the - * branch entries. If this flag is set, *buf* may be NULL. - * - * **-EINVAL** if arguments invalid or **size** not a multiple - * of **sizeof**\ (**struct perf_branch_entry**\ ). - * - * **-ENOENT** if architecture does not support branch records. - */ -static long (*bpf_read_branch_records)(struct bpf_perf_event_data *ctx, void *buf, __u32 size, __u64 flags) = (void *) 119; - -/* - * bpf_get_ns_current_pid_tgid - * - * Returns 0 on success, values for *pid* and *tgid* as seen from the current - * *namespace* will be returned in *nsdata*. - * - * Returns - * 0 on success, or one of the following in case of failure: - * - * **-EINVAL** if dev and inum supplied don't match dev_t and inode number - * with nsfs of current task, or if dev conversion to dev_t lost high bits. - * - * **-ENOENT** if pidns does not exists for the current task. - */ -static long (*bpf_get_ns_current_pid_tgid)(__u64 dev, __u64 ino, struct bpf_pidns_info *nsdata, __u32 size) = (void *) 120; - -/* - * bpf_xdp_output - * - * Write raw *data* blob into a special BPF perf event held by - * *map* of type **BPF_MAP_TYPE_PERF_EVENT_ARRAY**. This perf - * event must have the following attributes: **PERF_SAMPLE_RAW** - * as **sample_type**, **PERF_TYPE_SOFTWARE** as **type**, and - * **PERF_COUNT_SW_BPF_OUTPUT** as **config**. - * - * The *flags* are used to indicate the index in *map* for which - * the value must be put, masked with **BPF_F_INDEX_MASK**. - * Alternatively, *flags* can be set to **BPF_F_CURRENT_CPU** - * to indicate that the index of the current CPU core should be - * used. - * - * The value to write, of *size*, is passed through eBPF stack and - * pointed by *data*. - * - * *ctx* is a pointer to in-kernel struct xdp_buff. - * - * This helper is similar to **bpf_perf_eventoutput**\ () but - * restricted to raw_tracepoint bpf programs. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_xdp_output)(void *ctx, void *map, __u64 flags, void *data, __u64 size) = (void *) 121; - -/* - * bpf_get_netns_cookie - * - * Retrieve the cookie (generated by the kernel) of the network - * namespace the input *ctx* is associated with. The network - * namespace cookie remains stable for its lifetime and provides - * a global identifier that can be assumed unique. If *ctx* is - * NULL, then the helper returns the cookie for the initial - * network namespace. The cookie itself is very similar to that - * of **bpf_get_socket_cookie**\ () helper, but for network - * namespaces instead of sockets. - * - * Returns - * A 8-byte long opaque number. - */ -static __u64 (*bpf_get_netns_cookie)(void *ctx) = (void *) 122; - -/* - * bpf_get_current_ancestor_cgroup_id - * - * Return id of cgroup v2 that is ancestor of the cgroup associated - * with the current task at the *ancestor_level*. The root cgroup - * is at *ancestor_level* zero and each step down the hierarchy - * increments the level. If *ancestor_level* == level of cgroup - * associated with the current task, then return value will be the - * same as that of **bpf_get_current_cgroup_id**\ (). - * - * The helper is useful to implement policies based on cgroups - * that are upper in hierarchy than immediate cgroup associated - * with the current task. - * - * The format of returned id and helper limitations are same as in - * **bpf_get_current_cgroup_id**\ (). - * - * Returns - * The id is returned or 0 in case the id could not be retrieved. - */ -static __u64 (*bpf_get_current_ancestor_cgroup_id)(int ancestor_level) = (void *) 123; - -/* - * bpf_sk_assign - * - * Helper is overloaded depending on BPF program type. This - * description applies to **BPF_PROG_TYPE_SCHED_CLS** and - * **BPF_PROG_TYPE_SCHED_ACT** programs. - * - * Assign the *sk* to the *skb*. When combined with appropriate - * routing configuration to receive the packet towards the socket, - * will cause *skb* to be delivered to the specified socket. - * Subsequent redirection of *skb* via **bpf_redirect**\ (), - * **bpf_clone_redirect**\ () or other methods outside of BPF may - * interfere with successful delivery to the socket. - * - * This operation is only valid from TC ingress path. - * - * The *flags* argument must be zero. - * - * Returns - * 0 on success, or a negative error in case of failure: - * - * **-EINVAL** if specified *flags* are not supported. - * - * **-ENOENT** if the socket is unavailable for assignment. - * - * **-ENETUNREACH** if the socket is unreachable (wrong netns). - * - * **-EOPNOTSUPP** if the operation is not supported, for example - * a call from outside of TC ingress. - * - * **-ESOCKTNOSUPPORT** if the socket type is not supported - * (reuseport). - */ -static long (*bpf_sk_assign)(void *ctx, void *sk, __u64 flags) = (void *) 124; - -/* - * bpf_ktime_get_boot_ns - * - * Return the time elapsed since system boot, in nanoseconds. - * Does include the time the system was suspended. - * See: **clock_gettime**\ (**CLOCK_BOOTTIME**) - * - * Returns - * Current *ktime*. - */ -static __u64 (*bpf_ktime_get_boot_ns)(void) = (void *) 125; - -/* - * bpf_seq_printf - * - * **bpf_seq_printf**\ () uses seq_file **seq_printf**\ () to print - * out the format string. - * The *m* represents the seq_file. The *fmt* and *fmt_size* are for - * the format string itself. The *data* and *data_len* are format string - * arguments. The *data* are a **u64** array and corresponding format string - * values are stored in the array. For strings and pointers where pointees - * are accessed, only the pointer values are stored in the *data* array. - * The *data_len* is the size of *data* in bytes - must be a multiple of 8. - * - * Formats **%s**, **%p{i,I}{4,6}** requires to read kernel memory. - * Reading kernel memory may fail due to either invalid address or - * valid address but requiring a major memory fault. If reading kernel memory - * fails, the string for **%s** will be an empty string, and the ip - * address for **%p{i,I}{4,6}** will be 0. Not returning error to - * bpf program is consistent with what **bpf_trace_printk**\ () does for now. - * - * Returns - * 0 on success, or a negative error in case of failure: - * - * **-EBUSY** if per-CPU memory copy buffer is busy, can try again - * by returning 1 from bpf program. - * - * **-EINVAL** if arguments are invalid, or if *fmt* is invalid/unsupported. - * - * **-E2BIG** if *fmt* contains too many format specifiers. - * - * **-EOVERFLOW** if an overflow happened: The same object will be tried again. - */ -static long (*bpf_seq_printf)(struct seq_file *m, const char *fmt, __u32 fmt_size, const void *data, __u32 data_len) = (void *) 126; - -/* - * bpf_seq_write - * - * **bpf_seq_write**\ () uses seq_file **seq_write**\ () to write the data. - * The *m* represents the seq_file. The *data* and *len* represent the - * data to write in bytes. - * - * Returns - * 0 on success, or a negative error in case of failure: - * - * **-EOVERFLOW** if an overflow happened: The same object will be tried again. - */ -static long (*bpf_seq_write)(struct seq_file *m, const void *data, __u32 len) = (void *) 127; - -/* - * bpf_sk_cgroup_id - * - * Return the cgroup v2 id of the socket *sk*. - * - * *sk* must be a non-**NULL** pointer to a socket, e.g. one - * returned from **bpf_sk_lookup_xxx**\ (), - * **bpf_sk_fullsock**\ (), etc. The format of returned id is - * same as in **bpf_skb_cgroup_id**\ (). - * - * This helper is available only if the kernel was compiled with - * the **CONFIG_SOCK_CGROUP_DATA** configuration option. - * - * Returns - * The id is returned or 0 in case the id could not be retrieved. - */ -static __u64 (*bpf_sk_cgroup_id)(void *sk) = (void *) 128; - -/* - * bpf_sk_ancestor_cgroup_id - * - * Return id of cgroup v2 that is ancestor of cgroup associated - * with the *sk* at the *ancestor_level*. The root cgroup is at - * *ancestor_level* zero and each step down the hierarchy - * increments the level. If *ancestor_level* == level of cgroup - * associated with *sk*, then return value will be same as that - * of **bpf_sk_cgroup_id**\ (). - * - * The helper is useful to implement policies based on cgroups - * that are upper in hierarchy than immediate cgroup associated - * with *sk*. - * - * The format of returned id and helper limitations are same as in - * **bpf_sk_cgroup_id**\ (). - * - * Returns - * The id is returned or 0 in case the id could not be retrieved. - */ -static __u64 (*bpf_sk_ancestor_cgroup_id)(void *sk, int ancestor_level) = (void *) 129; - -/* - * bpf_ringbuf_output - * - * Copy *size* bytes from *data* into a ring buffer *ringbuf*. - * If **BPF_RB_NO_WAKEUP** is specified in *flags*, no notification - * of new data availability is sent. - * If **BPF_RB_FORCE_WAKEUP** is specified in *flags*, notification - * of new data availability is sent unconditionally. - * If **0** is specified in *flags*, an adaptive notification - * of new data availability is sent. - * - * An adaptive notification is a notification sent whenever the user-space - * process has caught up and consumed all available payloads. In case the user-space - * process is still processing a previous payload, then no notification is needed - * as it will process the newly added payload automatically. - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_ringbuf_output)(void *ringbuf, void *data, __u64 size, __u64 flags) = (void *) 130; - -/* - * bpf_ringbuf_reserve - * - * Reserve *size* bytes of payload in a ring buffer *ringbuf*. - * *flags* must be 0. - * - * Returns - * Valid pointer with *size* bytes of memory available; NULL, - * otherwise. - */ -static void *(*bpf_ringbuf_reserve)(void *ringbuf, __u64 size, __u64 flags) = (void *) 131; - -/* - * bpf_ringbuf_submit - * - * Submit reserved ring buffer sample, pointed to by *data*. - * If **BPF_RB_NO_WAKEUP** is specified in *flags*, no notification - * of new data availability is sent. - * If **BPF_RB_FORCE_WAKEUP** is specified in *flags*, notification - * of new data availability is sent unconditionally. - * If **0** is specified in *flags*, an adaptive notification - * of new data availability is sent. - * - * See 'bpf_ringbuf_output()' for the definition of adaptive notification. - * - * Returns - * Nothing. Always succeeds. - */ -static void (*bpf_ringbuf_submit)(void *data, __u64 flags) = (void *) 132; - -/* - * bpf_ringbuf_discard - * - * Discard reserved ring buffer sample, pointed to by *data*. - * If **BPF_RB_NO_WAKEUP** is specified in *flags*, no notification - * of new data availability is sent. - * If **BPF_RB_FORCE_WAKEUP** is specified in *flags*, notification - * of new data availability is sent unconditionally. - * If **0** is specified in *flags*, an adaptive notification - * of new data availability is sent. - * - * See 'bpf_ringbuf_output()' for the definition of adaptive notification. - * - * Returns - * Nothing. Always succeeds. - */ -static void (*bpf_ringbuf_discard)(void *data, __u64 flags) = (void *) 133; - -/* - * bpf_ringbuf_query - * - * Query various characteristics of provided ring buffer. What - * exactly is queries is determined by *flags*: - * - * * **BPF_RB_AVAIL_DATA**: Amount of data not yet consumed. - * * **BPF_RB_RING_SIZE**: The size of ring buffer. - * * **BPF_RB_CONS_POS**: Consumer position (can wrap around). - * * **BPF_RB_PROD_POS**: Producer(s) position (can wrap around). - * - * Data returned is just a momentary snapshot of actual values - * and could be inaccurate, so this facility should be used to - * power heuristics and for reporting, not to make 100% correct - * calculation. - * - * Returns - * Requested value, or 0, if *flags* are not recognized. - */ -static __u64 (*bpf_ringbuf_query)(void *ringbuf, __u64 flags) = (void *) 134; - -/* - * bpf_csum_level - * - * Change the skbs checksum level by one layer up or down, or - * reset it entirely to none in order to have the stack perform - * checksum validation. The level is applicable to the following - * protocols: TCP, UDP, GRE, SCTP, FCOE. For example, a decap of - * | ETH | IP | UDP | GUE | IP | TCP | into | ETH | IP | TCP | - * through **bpf_skb_adjust_room**\ () helper with passing in - * **BPF_F_ADJ_ROOM_NO_CSUM_RESET** flag would require one call - * to **bpf_csum_level**\ () with **BPF_CSUM_LEVEL_DEC** since - * the UDP header is removed. Similarly, an encap of the latter - * into the former could be accompanied by a helper call to - * **bpf_csum_level**\ () with **BPF_CSUM_LEVEL_INC** if the - * skb is still intended to be processed in higher layers of the - * stack instead of just egressing at tc. - * - * There are three supported level settings at this time: - * - * * **BPF_CSUM_LEVEL_INC**: Increases skb->csum_level for skbs - * with CHECKSUM_UNNECESSARY. - * * **BPF_CSUM_LEVEL_DEC**: Decreases skb->csum_level for skbs - * with CHECKSUM_UNNECESSARY. - * * **BPF_CSUM_LEVEL_RESET**: Resets skb->csum_level to 0 and - * sets CHECKSUM_NONE to force checksum validation by the stack. - * * **BPF_CSUM_LEVEL_QUERY**: No-op, returns the current - * skb->csum_level. - * - * Returns - * 0 on success, or a negative error in case of failure. In the - * case of **BPF_CSUM_LEVEL_QUERY**, the current skb->csum_level - * is returned or the error code -EACCES in case the skb is not - * subject to CHECKSUM_UNNECESSARY. - */ -static long (*bpf_csum_level)(struct __sk_buff *skb, __u64 level) = (void *) 135; - -/* - * bpf_skc_to_tcp6_sock - * - * Dynamically cast a *sk* pointer to a *tcp6_sock* pointer. - * - * Returns - * *sk* if casting is valid, or **NULL** otherwise. - */ -static struct tcp6_sock *(*bpf_skc_to_tcp6_sock)(void *sk) = (void *) 136; - -/* - * bpf_skc_to_tcp_sock - * - * Dynamically cast a *sk* pointer to a *tcp_sock* pointer. - * - * Returns - * *sk* if casting is valid, or **NULL** otherwise. - */ -static struct tcp_sock *(*bpf_skc_to_tcp_sock)(void *sk) = (void *) 137; - -/* - * bpf_skc_to_tcp_timewait_sock - * - * Dynamically cast a *sk* pointer to a *tcp_timewait_sock* pointer. - * - * Returns - * *sk* if casting is valid, or **NULL** otherwise. - */ -static struct tcp_timewait_sock *(*bpf_skc_to_tcp_timewait_sock)(void *sk) = (void *) 138; - -/* - * bpf_skc_to_tcp_request_sock - * - * Dynamically cast a *sk* pointer to a *tcp_request_sock* pointer. - * - * Returns - * *sk* if casting is valid, or **NULL** otherwise. - */ -static struct tcp_request_sock *(*bpf_skc_to_tcp_request_sock)(void *sk) = (void *) 139; - -/* - * bpf_skc_to_udp6_sock - * - * Dynamically cast a *sk* pointer to a *udp6_sock* pointer. - * - * Returns - * *sk* if casting is valid, or **NULL** otherwise. - */ -static struct udp6_sock *(*bpf_skc_to_udp6_sock)(void *sk) = (void *) 140; - -/* - * bpf_get_task_stack - * - * Return a user or a kernel stack in bpf program provided buffer. - * To achieve this, the helper needs *task*, which is a valid - * pointer to **struct task_struct**. To store the stacktrace, the - * bpf program provides *buf* with a nonnegative *size*. - * - * The last argument, *flags*, holds the number of stack frames to - * skip (from 0 to 255), masked with - * **BPF_F_SKIP_FIELD_MASK**. The next bits can be used to set - * the following flags: - * - * **BPF_F_USER_STACK** - * Collect a user space stack instead of a kernel stack. - * **BPF_F_USER_BUILD_ID** - * Collect buildid+offset instead of ips for user stack, - * only valid if **BPF_F_USER_STACK** is also specified. - * - * **bpf_get_task_stack**\ () can collect up to - * **PERF_MAX_STACK_DEPTH** both kernel and user frames, subject - * to sufficient large buffer size. Note that - * this limit can be controlled with the **sysctl** program, and - * that it should be manually increased in order to profile long - * user stacks (such as stacks for Java programs). To do so, use: - * - * :: - * - * # sysctl kernel.perf_event_max_stack= - * - * Returns - * A non-negative value equal to or less than *size* on success, - * or a negative error in case of failure. - */ -static long (*bpf_get_task_stack)(struct task_struct *task, void *buf, __u32 size, __u64 flags) = (void *) 141; - -/* - * bpf_load_hdr_opt - * - * Load header option. Support reading a particular TCP header - * option for bpf program (**BPF_PROG_TYPE_SOCK_OPS**). - * - * If *flags* is 0, it will search the option from the - * *skops*\ **->skb_data**. The comment in **struct bpf_sock_ops** - * has details on what skb_data contains under different - * *skops*\ **->op**. - * - * The first byte of the *searchby_res* specifies the - * kind that it wants to search. - * - * If the searching kind is an experimental kind - * (i.e. 253 or 254 according to RFC6994). It also - * needs to specify the "magic" which is either - * 2 bytes or 4 bytes. It then also needs to - * specify the size of the magic by using - * the 2nd byte which is "kind-length" of a TCP - * header option and the "kind-length" also - * includes the first 2 bytes "kind" and "kind-length" - * itself as a normal TCP header option also does. - * - * For example, to search experimental kind 254 with - * 2 byte magic 0xeB9F, the searchby_res should be - * [ 254, 4, 0xeB, 0x9F, 0, 0, .... 0 ]. - * - * To search for the standard window scale option (3), - * the *searchby_res* should be [ 3, 0, 0, .... 0 ]. - * Note, kind-length must be 0 for regular option. - * - * Searching for No-Op (0) and End-of-Option-List (1) are - * not supported. - * - * *len* must be at least 2 bytes which is the minimal size - * of a header option. - * - * Supported flags: - * - * * **BPF_LOAD_HDR_OPT_TCP_SYN** to search from the - * saved_syn packet or the just-received syn packet. - * - * - * Returns - * > 0 when found, the header option is copied to *searchby_res*. - * The return value is the total length copied. On failure, a - * negative error code is returned: - * - * **-EINVAL** if a parameter is invalid. - * - * **-ENOMSG** if the option is not found. - * - * **-ENOENT** if no syn packet is available when - * **BPF_LOAD_HDR_OPT_TCP_SYN** is used. - * - * **-ENOSPC** if there is not enough space. Only *len* number of - * bytes are copied. - * - * **-EFAULT** on failure to parse the header options in the - * packet. - * - * **-EPERM** if the helper cannot be used under the current - * *skops*\ **->op**. - */ -static long (*bpf_load_hdr_opt)(struct bpf_sock_ops *skops, void *searchby_res, __u32 len, __u64 flags) = (void *) 142; - -/* - * bpf_store_hdr_opt - * - * Store header option. The data will be copied - * from buffer *from* with length *len* to the TCP header. - * - * The buffer *from* should have the whole option that - * includes the kind, kind-length, and the actual - * option data. The *len* must be at least kind-length - * long. The kind-length does not have to be 4 byte - * aligned. The kernel will take care of the padding - * and setting the 4 bytes aligned value to th->doff. - * - * This helper will check for duplicated option - * by searching the same option in the outgoing skb. - * - * This helper can only be called during - * **BPF_SOCK_OPS_WRITE_HDR_OPT_CB**. - * - * - * Returns - * 0 on success, or negative error in case of failure: - * - * **-EINVAL** If param is invalid. - * - * **-ENOSPC** if there is not enough space in the header. - * Nothing has been written - * - * **-EEXIST** if the option already exists. - * - * **-EFAULT** on failrue to parse the existing header options. - * - * **-EPERM** if the helper cannot be used under the current - * *skops*\ **->op**. - */ -static long (*bpf_store_hdr_opt)(struct bpf_sock_ops *skops, const void *from, __u32 len, __u64 flags) = (void *) 143; - -/* - * bpf_reserve_hdr_opt - * - * Reserve *len* bytes for the bpf header option. The - * space will be used by **bpf_store_hdr_opt**\ () later in - * **BPF_SOCK_OPS_WRITE_HDR_OPT_CB**. - * - * If **bpf_reserve_hdr_opt**\ () is called multiple times, - * the total number of bytes will be reserved. - * - * This helper can only be called during - * **BPF_SOCK_OPS_HDR_OPT_LEN_CB**. - * - * - * Returns - * 0 on success, or negative error in case of failure: - * - * **-EINVAL** if a parameter is invalid. - * - * **-ENOSPC** if there is not enough space in the header. - * - * **-EPERM** if the helper cannot be used under the current - * *skops*\ **->op**. - */ -static long (*bpf_reserve_hdr_opt)(struct bpf_sock_ops *skops, __u32 len, __u64 flags) = (void *) 144; - -/* - * bpf_inode_storage_get - * - * Get a bpf_local_storage from an *inode*. - * - * Logically, it could be thought of as getting the value from - * a *map* with *inode* as the **key**. From this - * perspective, the usage is not much different from - * **bpf_map_lookup_elem**\ (*map*, **&**\ *inode*) except this - * helper enforces the key must be an inode and the map must also - * be a **BPF_MAP_TYPE_INODE_STORAGE**. - * - * Underneath, the value is stored locally at *inode* instead of - * the *map*. The *map* is used as the bpf-local-storage - * "type". The bpf-local-storage "type" (i.e. the *map*) is - * searched against all bpf_local_storage residing at *inode*. - * - * An optional *flags* (**BPF_LOCAL_STORAGE_GET_F_CREATE**) can be - * used such that a new bpf_local_storage will be - * created if one does not exist. *value* can be used - * together with **BPF_LOCAL_STORAGE_GET_F_CREATE** to specify - * the initial value of a bpf_local_storage. If *value* is - * **NULL**, the new bpf_local_storage will be zero initialized. - * - * Returns - * A bpf_local_storage pointer is returned on success. - * - * **NULL** if not found or there was an error in adding - * a new bpf_local_storage. - */ -static void *(*bpf_inode_storage_get)(void *map, void *inode, void *value, __u64 flags) = (void *) 145; - -/* - * bpf_inode_storage_delete - * - * Delete a bpf_local_storage from an *inode*. - * - * Returns - * 0 on success. - * - * **-ENOENT** if the bpf_local_storage cannot be found. - */ -static int (*bpf_inode_storage_delete)(void *map, void *inode) = (void *) 146; - -/* - * bpf_d_path - * - * Return full path for given **struct path** object, which - * needs to be the kernel BTF *path* object. The path is - * returned in the provided buffer *buf* of size *sz* and - * is zero terminated. - * - * - * Returns - * On success, the strictly positive length of the string, - * including the trailing NUL character. On error, a negative - * value. - */ -static long (*bpf_d_path)(struct path *path, char *buf, __u32 sz) = (void *) 147; - -/* - * bpf_copy_from_user - * - * Read *size* bytes from user space address *user_ptr* and store - * the data in *dst*. This is a wrapper of **copy_from_user**\ (). - * - * Returns - * 0 on success, or a negative error in case of failure. - */ -static long (*bpf_copy_from_user)(void *dst, __u32 size, const void *user_ptr) = (void *) 148; - -/* - * bpf_snprintf_btf - * - * Use BTF to store a string representation of *ptr*->ptr in *str*, - * using *ptr*->type_id. This value should specify the type - * that *ptr*->ptr points to. LLVM __builtin_btf_type_id(type, 1) - * can be used to look up vmlinux BTF type ids. Traversing the - * data structure using BTF, the type information and values are - * stored in the first *str_size* - 1 bytes of *str*. Safe copy of - * the pointer data is carried out to avoid kernel crashes during - * operation. Smaller types can use string space on the stack; - * larger programs can use map data to store the string - * representation. - * - * The string can be subsequently shared with userspace via - * bpf_perf_event_output() or ring buffer interfaces. - * bpf_trace_printk() is to be avoided as it places too small - * a limit on string size to be useful. - * - * *flags* is a combination of - * - * **BTF_F_COMPACT** - * no formatting around type information - * **BTF_F_NONAME** - * no struct/union member names/types - * **BTF_F_PTR_RAW** - * show raw (unobfuscated) pointer values; - * equivalent to printk specifier %px. - * **BTF_F_ZERO** - * show zero-valued struct/union members; they - * are not displayed by default - * - * - * Returns - * The number of bytes that were written (or would have been - * written if output had to be truncated due to string size), - * or a negative error in cases of failure. - */ -static long (*bpf_snprintf_btf)(char *str, __u32 str_size, struct btf_ptr *ptr, __u32 btf_ptr_size, __u64 flags) = (void *) 149; - -/* - * bpf_seq_printf_btf - * - * Use BTF to write to seq_write a string representation of - * *ptr*->ptr, using *ptr*->type_id as per bpf_snprintf_btf(). - * *flags* are identical to those used for bpf_snprintf_btf. - * - * Returns - * 0 on success or a negative error in case of failure. - */ -static long (*bpf_seq_printf_btf)(struct seq_file *m, struct btf_ptr *ptr, __u32 ptr_size, __u64 flags) = (void *) 150; - -/* - * bpf_skb_cgroup_classid - * - * See **bpf_get_cgroup_classid**\ () for the main description. - * This helper differs from **bpf_get_cgroup_classid**\ () in that - * the cgroup v1 net_cls class is retrieved only from the *skb*'s - * associated socket instead of the current process. - * - * Returns - * The id is returned or 0 in case the id could not be retrieved. - */ -static __u64 (*bpf_skb_cgroup_classid)(struct __sk_buff *skb) = (void *) 151; - -/* - * bpf_redirect_neigh - * - * Redirect the packet to another net device of index *ifindex* - * and fill in L2 addresses from neighboring subsystem. This helper - * is somewhat similar to **bpf_redirect**\ (), except that it - * populates L2 addresses as well, meaning, internally, the helper - * relies on the neighbor lookup for the L2 address of the nexthop. - * - * The helper will perform a FIB lookup based on the skb's - * networking header to get the address of the next hop, unless - * this is supplied by the caller in the *params* argument. The - * *plen* argument indicates the len of *params* and should be set - * to 0 if *params* is NULL. - * - * The *flags* argument is reserved and must be 0. The helper is - * currently only supported for tc BPF program types, and enabled - * for IPv4 and IPv6 protocols. - * - * Returns - * The helper returns **TC_ACT_REDIRECT** on success or - * **TC_ACT_SHOT** on error. - */ -static long (*bpf_redirect_neigh)(__u32 ifindex, struct bpf_redir_neigh *params, int plen, __u64 flags) = (void *) 152; - -/* - * bpf_per_cpu_ptr - * - * Take a pointer to a percpu ksym, *percpu_ptr*, and return a - * pointer to the percpu kernel variable on *cpu*. A ksym is an - * extern variable decorated with '__ksym'. For ksym, there is a - * global var (either static or global) defined of the same name - * in the kernel. The ksym is percpu if the global var is percpu. - * The returned pointer points to the global percpu var on *cpu*. - * - * bpf_per_cpu_ptr() has the same semantic as per_cpu_ptr() in the - * kernel, except that bpf_per_cpu_ptr() may return NULL. This - * happens if *cpu* is larger than nr_cpu_ids. The caller of - * bpf_per_cpu_ptr() must check the returned value. - * - * Returns - * A pointer pointing to the kernel percpu variable on *cpu*, or - * NULL, if *cpu* is invalid. - */ -static void *(*bpf_per_cpu_ptr)(const void *percpu_ptr, __u32 cpu) = (void *) 153; - -/* - * bpf_this_cpu_ptr - * - * Take a pointer to a percpu ksym, *percpu_ptr*, and return a - * pointer to the percpu kernel variable on this cpu. See the - * description of 'ksym' in **bpf_per_cpu_ptr**\ (). - * - * bpf_this_cpu_ptr() has the same semantic as this_cpu_ptr() in - * the kernel. Different from **bpf_per_cpu_ptr**\ (), it would - * never return NULL. - * - * Returns - * A pointer pointing to the kernel percpu variable on this cpu. - */ -static void *(*bpf_this_cpu_ptr)(const void *percpu_ptr) = (void *) 154; - -/* - * bpf_redirect_peer - * - * Redirect the packet to another net device of index *ifindex*. - * This helper is somewhat similar to **bpf_redirect**\ (), except - * that the redirection happens to the *ifindex*' peer device and - * the netns switch takes place from ingress to ingress without - * going through the CPU's backlog queue. - * - * The *flags* argument is reserved and must be 0. The helper is - * currently only supported for tc BPF program types at the ingress - * hook and for veth device types. The peer device must reside in a - * different network namespace. - * - * Returns - * The helper returns **TC_ACT_REDIRECT** on success or - * **TC_ACT_SHOT** on error. - */ -static long (*bpf_redirect_peer)(__u32 ifindex, __u64 flags) = (void *) 155; - -/* - * bpf_task_storage_get - * - * Get a bpf_local_storage from the *task*. - * - * Logically, it could be thought of as getting the value from - * a *map* with *task* as the **key**. From this - * perspective, the usage is not much different from - * **bpf_map_lookup_elem**\ (*map*, **&**\ *task*) except this - * helper enforces the key must be an task_struct and the map must also - * be a **BPF_MAP_TYPE_TASK_STORAGE**. - * - * Underneath, the value is stored locally at *task* instead of - * the *map*. The *map* is used as the bpf-local-storage - * "type". The bpf-local-storage "type" (i.e. the *map*) is - * searched against all bpf_local_storage residing at *task*. - * - * An optional *flags* (**BPF_LOCAL_STORAGE_GET_F_CREATE**) can be - * used such that a new bpf_local_storage will be - * created if one does not exist. *value* can be used - * together with **BPF_LOCAL_STORAGE_GET_F_CREATE** to specify - * the initial value of a bpf_local_storage. If *value* is - * **NULL**, the new bpf_local_storage will be zero initialized. - * - * Returns - * A bpf_local_storage pointer is returned on success. - * - * **NULL** if not found or there was an error in adding - * a new bpf_local_storage. - */ -static void *(*bpf_task_storage_get)(void *map, struct task_struct *task, void *value, __u64 flags) = (void *) 156; - -/* - * bpf_task_storage_delete - * - * Delete a bpf_local_storage from a *task*. - * - * Returns - * 0 on success. - * - * **-ENOENT** if the bpf_local_storage cannot be found. - */ -static long (*bpf_task_storage_delete)(void *map, struct task_struct *task) = (void *) 157; - -/* - * bpf_get_current_task_btf - * - * Return a BTF pointer to the "current" task. - * This pointer can also be used in helpers that accept an - * *ARG_PTR_TO_BTF_ID* of type *task_struct*. - * - * Returns - * Pointer to the current task. - */ -static struct task_struct *(*bpf_get_current_task_btf)(void) = (void *) 158; - -/* - * bpf_bprm_opts_set - * - * Set or clear certain options on *bprm*: - * - * **BPF_F_BPRM_SECUREEXEC** Set the secureexec bit - * which sets the **AT_SECURE** auxv for glibc. The bit - * is cleared if the flag is not specified. - * - * Returns - * **-EINVAL** if invalid *flags* are passed, zero otherwise. - */ -static long (*bpf_bprm_opts_set)(struct linux_binprm *bprm, __u64 flags) = (void *) 159; - -/* - * bpf_ktime_get_coarse_ns - * - * Return a coarse-grained version of the time elapsed since - * system boot, in nanoseconds. Does not include time the system - * was suspended. - * - * See: **clock_gettime**\ (**CLOCK_MONOTONIC_COARSE**) - * - * Returns - * Current *ktime*. - */ -static __u64 (*bpf_ktime_get_coarse_ns)(void) = (void *) 160; - -/* - * bpf_ima_inode_hash - * - * Returns the stored IMA hash of the *inode* (if it's avaialable). - * If the hash is larger than *size*, then only *size* - * bytes will be copied to *dst* - * - * Returns - * The **hash_algo** is returned on success, - * **-EOPNOTSUP** if IMA is disabled or **-EINVAL** if - * invalid arguments are passed. - */ -static long (*bpf_ima_inode_hash)(struct inode *inode, void *dst, __u32 size) = (void *) 161; - -/* - * bpf_sock_from_file - * - * If the given file represents a socket, returns the associated - * socket. - * - * Returns - * A pointer to a struct socket on success or NULL if the file is - * not a socket. - */ -static struct socket *(*bpf_sock_from_file)(struct file *file) = (void *) 162; - -/* - * bpf_check_mtu - * - * Check packet size against exceeding MTU of net device (based - * on *ifindex*). This helper will likely be used in combination - * with helpers that adjust/change the packet size. - * - * The argument *len_diff* can be used for querying with a planned - * size change. This allows to check MTU prior to changing packet - * ctx. Providing an *len_diff* adjustment that is larger than the - * actual packet size (resulting in negative packet size) will in - * principle not exceed the MTU, why it is not considered a - * failure. Other BPF-helpers are needed for performing the - * planned size change, why the responsability for catch a negative - * packet size belong in those helpers. - * - * Specifying *ifindex* zero means the MTU check is performed - * against the current net device. This is practical if this isn't - * used prior to redirect. - * - * On input *mtu_len* must be a valid pointer, else verifier will - * reject BPF program. If the value *mtu_len* is initialized to - * zero then the ctx packet size is use. When value *mtu_len* is - * provided as input this specify the L3 length that the MTU check - * is done against. Remember XDP and TC length operate at L2, but - * this value is L3 as this correlate to MTU and IP-header tot_len - * values which are L3 (similar behavior as bpf_fib_lookup). - * - * The Linux kernel route table can configure MTUs on a more - * specific per route level, which is not provided by this helper. - * For route level MTU checks use the **bpf_fib_lookup**\ () - * helper. - * - * *ctx* is either **struct xdp_md** for XDP programs or - * **struct sk_buff** for tc cls_act programs. - * - * The *flags* argument can be a combination of one or more of the - * following values: - * - * **BPF_MTU_CHK_SEGS** - * This flag will only works for *ctx* **struct sk_buff**. - * If packet context contains extra packet segment buffers - * (often knows as GSO skb), then MTU check is harder to - * check at this point, because in transmit path it is - * possible for the skb packet to get re-segmented - * (depending on net device features). This could still be - * a MTU violation, so this flag enables performing MTU - * check against segments, with a different violation - * return code to tell it apart. Check cannot use len_diff. - * - * On return *mtu_len* pointer contains the MTU value of the net - * device. Remember the net device configured MTU is the L3 size, - * which is returned here and XDP and TC length operate at L2. - * Helper take this into account for you, but remember when using - * MTU value in your BPF-code. - * - * - * Returns - * * 0 on success, and populate MTU value in *mtu_len* pointer. - * - * * < 0 if any input argument is invalid (*mtu_len* not updated) - * - * MTU violations return positive values, but also populate MTU - * value in *mtu_len* pointer, as this can be needed for - * implementing PMTU handing: - * - * * **BPF_MTU_CHK_RET_FRAG_NEEDED** - * * **BPF_MTU_CHK_RET_SEGS_TOOBIG** - */ -static long (*bpf_check_mtu)(void *ctx, __u32 ifindex, __u32 *mtu_len, __s32 len_diff, __u64 flags) = (void *) 163; - -/* - * bpf_for_each_map_elem - * - * For each element in **map**, call **callback_fn** function with - * **map**, **callback_ctx** and other map-specific parameters. - * The **callback_fn** should be a static function and - * the **callback_ctx** should be a pointer to the stack. - * The **flags** is used to control certain aspects of the helper. - * Currently, the **flags** must be 0. - * - * The following are a list of supported map types and their - * respective expected callback signatures: - * - * BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_PERCPU_HASH, - * BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH, - * BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_PERCPU_ARRAY - * - * long (\*callback_fn)(struct bpf_map \*map, const void \*key, void \*value, void \*ctx); - * - * For per_cpu maps, the map_value is the value on the cpu where the - * bpf_prog is running. - * - * If **callback_fn** return 0, the helper will continue to the next - * element. If return value is 1, the helper will skip the rest of - * elements and return. Other return values are not used now. - * - * - * Returns - * The number of traversed map elements for success, **-EINVAL** for - * invalid **flags**. - */ -static long (*bpf_for_each_map_elem)(void *map, void *callback_fn, void *callback_ctx, __u64 flags) = (void *) 164; - -/* - * bpf_snprintf - * - * Outputs a string into the **str** buffer of size **str_size** - * based on a format string stored in a read-only map pointed by - * **fmt**. - * - * Each format specifier in **fmt** corresponds to one u64 element - * in the **data** array. For strings and pointers where pointees - * are accessed, only the pointer values are stored in the *data* - * array. The *data_len* is the size of *data* in bytes - must be - * a multiple of 8. - * - * Formats **%s** and **%p{i,I}{4,6}** require to read kernel - * memory. Reading kernel memory may fail due to either invalid - * address or valid address but requiring a major memory fault. If - * reading kernel memory fails, the string for **%s** will be an - * empty string, and the ip address for **%p{i,I}{4,6}** will be 0. - * Not returning error to bpf program is consistent with what - * **bpf_trace_printk**\ () does for now. - * - * - * Returns - * The strictly positive length of the formatted string, including - * the trailing zero character. If the return value is greater than - * **str_size**, **str** contains a truncated string, guaranteed to - * be zero-terminated except when **str_size** is 0. - * - * Or **-EBUSY** if the per-CPU memory copy buffer is busy. - */ -static long (*bpf_snprintf)(char *str, __u32 str_size, const char *fmt, __u64 *data, __u32 data_len) = (void *) 165; - -/* - * bpf_sys_bpf - * - * Execute bpf syscall with given arguments. - * - * Returns - * A syscall result. - */ -static long (*bpf_sys_bpf)(__u32 cmd, void *attr, __u32 attr_size) = (void *) 166; - -/* - * bpf_btf_find_by_name_kind - * - * Find BTF type with given name and kind in vmlinux BTF or in module's BTFs. - * - * Returns - * Returns btf_id and btf_obj_fd in lower and upper 32 bits. - */ -static long (*bpf_btf_find_by_name_kind)(char *name, int name_sz, __u32 kind, int flags) = (void *) 167; - -/* - * bpf_sys_close - * - * Execute close syscall for given FD. - * - * Returns - * A syscall result. - */ -static long (*bpf_sys_close)(__u32 fd) = (void *) 168; - -/* - * bpf_timer_init - * - * Initialize the timer. - * First 4 bits of *flags* specify clockid. - * Only CLOCK_MONOTONIC, CLOCK_REALTIME, CLOCK_BOOTTIME are allowed. - * All other bits of *flags* are reserved. - * The verifier will reject the program if *timer* is not from - * the same *map*. - * - * Returns - * 0 on success. - * **-EBUSY** if *timer* is already initialized. - * **-EINVAL** if invalid *flags* are passed. - * **-EPERM** if *timer* is in a map that doesn't have any user references. - * The user space should either hold a file descriptor to a map with timers - * or pin such map in bpffs. When map is unpinned or file descriptor is - * closed all timers in the map will be cancelled and freed. - */ -static long (*bpf_timer_init)(struct bpf_timer *timer, void *map, __u64 flags) = (void *) 169; - -/* - * bpf_timer_set_callback - * - * Configure the timer to call *callback_fn* static function. - * - * Returns - * 0 on success. - * **-EINVAL** if *timer* was not initialized with bpf_timer_init() earlier. - * **-EPERM** if *timer* is in a map that doesn't have any user references. - * The user space should either hold a file descriptor to a map with timers - * or pin such map in bpffs. When map is unpinned or file descriptor is - * closed all timers in the map will be cancelled and freed. - */ -static long (*bpf_timer_set_callback)(struct bpf_timer *timer, void *callback_fn) = (void *) 170; - -/* - * bpf_timer_start - * - * Set timer expiration N nanoseconds from the current time. The - * configured callback will be invoked in soft irq context on some cpu - * and will not repeat unless another bpf_timer_start() is made. - * In such case the next invocation can migrate to a different cpu. - * Since struct bpf_timer is a field inside map element the map - * owns the timer. The bpf_timer_set_callback() will increment refcnt - * of BPF program to make sure that callback_fn code stays valid. - * When user space reference to a map reaches zero all timers - * in a map are cancelled and corresponding program's refcnts are - * decremented. This is done to make sure that Ctrl-C of a user - * process doesn't leave any timers running. If map is pinned in - * bpffs the callback_fn can re-arm itself indefinitely. - * bpf_map_update/delete_elem() helpers and user space sys_bpf commands - * cancel and free the timer in the given map element. - * The map can contain timers that invoke callback_fn-s from different - * programs. The same callback_fn can serve different timers from - * different maps if key/value layout matches across maps. - * Every bpf_timer_set_callback() can have different callback_fn. - * - * - * Returns - * 0 on success. - * **-EINVAL** if *timer* was not initialized with bpf_timer_init() earlier - * or invalid *flags* are passed. - */ -static long (*bpf_timer_start)(struct bpf_timer *timer, __u64 nsecs, __u64 flags) = (void *) 171; - -/* - * bpf_timer_cancel - * - * Cancel the timer and wait for callback_fn to finish if it was running. - * - * Returns - * 0 if the timer was not active. - * 1 if the timer was active. - * **-EINVAL** if *timer* was not initialized with bpf_timer_init() earlier. - * **-EDEADLK** if callback_fn tried to call bpf_timer_cancel() on its - * own timer which would have led to a deadlock otherwise. - */ -static long (*bpf_timer_cancel)(struct bpf_timer *timer) = (void *) 172; - -/* - * bpf_get_func_ip - * - * Get address of the traced function (for tracing and kprobe programs). - * - * Returns - * Address of the traced function. - */ -static __u64 (*bpf_get_func_ip)(void *ctx) = (void *) 173; - -/* - * bpf_get_attach_cookie - * - * Get bpf_cookie value provided (optionally) during the program - * attachment. It might be different for each individual - * attachment, even if BPF program itself is the same. - * Expects BPF program context *ctx* as a first argument. - * - * Supported for the following program types: - * - kprobe/uprobe; - * - tracepoint; - * - perf_event. - * - * Returns - * Value specified by user at BPF link creation/attachment time - * or 0, if it was not specified. - */ -static __u64 (*bpf_get_attach_cookie)(void *ctx) = (void *) 174; - -/* - * bpf_task_pt_regs - * - * Get the struct pt_regs associated with **task**. - * - * Returns - * A pointer to struct pt_regs. - */ -static long (*bpf_task_pt_regs)(struct task_struct *task) = (void *) 175; - -/* - * bpf_get_branch_snapshot - * - * Get branch trace from hardware engines like Intel LBR. The - * hardware engine is stopped shortly after the helper is - * called. Therefore, the user need to filter branch entries - * based on the actual use case. To capture branch trace - * before the trigger point of the BPF program, the helper - * should be called at the beginning of the BPF program. - * - * The data is stored as struct perf_branch_entry into output - * buffer *entries*. *size* is the size of *entries* in bytes. - * *flags* is reserved for now and must be zero. - * - * - * Returns - * On success, number of bytes written to *buf*. On error, a - * negative value. - * - * **-EINVAL** if *flags* is not zero. - * - * **-ENOENT** if architecture does not support branch records. - */ -static long (*bpf_get_branch_snapshot)(void *entries, __u32 size, __u64 flags) = (void *) 176; - -/* - * bpf_trace_vprintk - * - * Behaves like **bpf_trace_printk**\ () helper, but takes an array of u64 - * to format and can handle more format args as a result. - * - * Arguments are to be used as in **bpf_seq_printf**\ () helper. - * - * Returns - * The number of bytes written to the buffer, or a negative error - * in case of failure. - */ -static long (*bpf_trace_vprintk)(const char *fmt, __u32 fmt_size, const void *data, __u32 data_len) = (void *) 177; - -/* - * bpf_skc_to_unix_sock - * - * Dynamically cast a *sk* pointer to a *unix_sock* pointer. - * - * Returns - * *sk* if casting is valid, or **NULL** otherwise. - */ -static struct unix_sock *(*bpf_skc_to_unix_sock)(void *sk) = (void *) 178; - -/* - * bpf_kallsyms_lookup_name - * - * Get the address of a kernel symbol, returned in *res*. *res* is - * set to 0 if the symbol is not found. - * - * Returns - * On success, zero. On error, a negative value. - * - * **-EINVAL** if *flags* is not zero. - * - * **-EINVAL** if string *name* is not the same size as *name_sz*. - * - * **-ENOENT** if symbol is not found. - * - * **-EPERM** if caller does not have permission to obtain kernel address. - */ -static long (*bpf_kallsyms_lookup_name)(const char *name, int name_sz, int flags, __u64 *res) = (void *) 179; - -/* - * bpf_find_vma - * - * Find vma of *task* that contains *addr*, call *callback_fn* - * function with *task*, *vma*, and *callback_ctx*. - * The *callback_fn* should be a static function and - * the *callback_ctx* should be a pointer to the stack. - * The *flags* is used to control certain aspects of the helper. - * Currently, the *flags* must be 0. - * - * The expected callback signature is - * - * long (\*callback_fn)(struct task_struct \*task, struct vm_area_struct \*vma, void \*callback_ctx); - * - * - * Returns - * 0 on success. - * **-ENOENT** if *task->mm* is NULL, or no vma contains *addr*. - * **-EBUSY** if failed to try lock mmap_lock. - * **-EINVAL** for invalid **flags**. - */ -static long (*bpf_find_vma)(struct task_struct *task, __u64 addr, void *callback_fn, void *callback_ctx, __u64 flags) = (void *) 180; - - diff --git a/component/ebpf/bpf/bpf_helpers.h b/component/ebpf/bpf/bpf_helpers.h deleted file mode 100644 index 963b1060..00000000 --- a/component/ebpf/bpf/bpf_helpers.h +++ /dev/null @@ -1,262 +0,0 @@ -/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ -#ifndef __BPF_HELPERS__ -#define __BPF_HELPERS__ - -/* - * Note that bpf programs need to include either - * vmlinux.h (auto-generated from BTF) or linux/types.h - * in advance since bpf_helper_defs.h uses such types - * as __u64. - */ -#include "bpf_helper_defs.h" - -#define __uint(name, val) int (*name)[val] -#define __type(name, val) typeof(val) *name -#define __array(name, val) typeof(val) *name[] - -/* - * Helper macro to place programs, maps, license in - * different sections in elf_bpf file. Section names - * are interpreted by libbpf depending on the context (BPF programs, BPF maps, - * extern variables, etc). - * To allow use of SEC() with externs (e.g., for extern .maps declarations), - * make sure __attribute__((unused)) doesn't trigger compilation warning. - */ -#define SEC(name) \ - _Pragma("GCC diagnostic push") \ - _Pragma("GCC diagnostic ignored \"-Wignored-attributes\"") \ - __attribute__((section(name), used)) \ - _Pragma("GCC diagnostic pop") \ - -/* Avoid 'linux/stddef.h' definition of '__always_inline'. */ -#undef __always_inline -#define __always_inline inline __attribute__((always_inline)) - -#ifndef __noinline -#define __noinline __attribute__((noinline)) -#endif -#ifndef __weak -#define __weak __attribute__((weak)) -#endif - -/* - * Use __hidden attribute to mark a non-static BPF subprogram effectively - * static for BPF verifier's verification algorithm purposes, allowing more - * extensive and permissive BPF verification process, taking into account - * subprogram's caller context. - */ -#define __hidden __attribute__((visibility("hidden"))) - -/* When utilizing vmlinux.h with BPF CO-RE, user BPF programs can't include - * any system-level headers (such as stddef.h, linux/version.h, etc), and - * commonly-used macros like NULL and KERNEL_VERSION aren't available through - * vmlinux.h. This just adds unnecessary hurdles and forces users to re-define - * them on their own. So as a convenience, provide such definitions here. - */ -#ifndef NULL -#define NULL ((void *)0) -#endif - -#ifndef KERNEL_VERSION -#define KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + ((c) > 255 ? 255 : (c))) -#endif - -/* - * Helper macros to manipulate data structures - */ -#ifndef offsetof -#define offsetof(TYPE, MEMBER) ((unsigned long)&((TYPE *)0)->MEMBER) -#endif -#ifndef container_of -#define container_of(ptr, type, member) \ - ({ \ - void *__mptr = (void *)(ptr); \ - ((type *)(__mptr - offsetof(type, member))); \ - }) -#endif - -/* - * Helper macro to throw a compilation error if __bpf_unreachable() gets - * built into the resulting code. This works given BPF back end does not - * implement __builtin_trap(). This is useful to assert that certain paths - * of the program code are never used and hence eliminated by the compiler. - * - * For example, consider a switch statement that covers known cases used by - * the program. __bpf_unreachable() can then reside in the default case. If - * the program gets extended such that a case is not covered in the switch - * statement, then it will throw a build error due to the default case not - * being compiled out. - */ -#ifndef __bpf_unreachable -# define __bpf_unreachable() __builtin_trap() -#endif - -/* - * Helper function to perform a tail call with a constant/immediate map slot. - */ -#if __clang_major__ >= 8 && defined(__bpf__) -static __always_inline void -bpf_tail_call_static(void *ctx, const void *map, const __u32 slot) -{ - if (!__builtin_constant_p(slot)) - __bpf_unreachable(); - - /* - * Provide a hard guarantee that LLVM won't optimize setting r2 (map - * pointer) and r3 (constant map index) from _different paths_ ending - * up at the _same_ call insn as otherwise we won't be able to use the - * jmpq/nopl retpoline-free patching by the x86-64 JIT in the kernel - * given they mismatch. See also d2e4c1e6c294 ("bpf: Constant map key - * tracking for prog array pokes") for details on verifier tracking. - * - * Note on clobber list: we need to stay in-line with BPF calling - * convention, so even if we don't end up using r0, r4, r5, we need - * to mark them as clobber so that LLVM doesn't end up using them - * before / after the call. - */ - asm volatile("r1 = %[ctx]\n\t" - "r2 = %[map]\n\t" - "r3 = %[slot]\n\t" - "call 12" - :: [ctx]"r"(ctx), [map]"r"(map), [slot]"i"(slot) - : "r0", "r1", "r2", "r3", "r4", "r5"); -} -#endif - -/* - * Helper structure used by eBPF C program - * to describe BPF map attributes to libbpf loader - */ -struct bpf_map_def { - unsigned int type; - unsigned int key_size; - unsigned int value_size; - unsigned int max_entries; - unsigned int map_flags; -}; - -enum libbpf_pin_type { - LIBBPF_PIN_NONE, - /* PIN_BY_NAME: pin maps by name (in /sys/fs/bpf by default) */ - LIBBPF_PIN_BY_NAME, -}; - -enum libbpf_tristate { - TRI_NO = 0, - TRI_YES = 1, - TRI_MODULE = 2, -}; - -#define __kconfig __attribute__((section(".kconfig"))) -#define __ksym __attribute__((section(".ksyms"))) - -#ifndef ___bpf_concat -#define ___bpf_concat(a, b) a ## b -#endif -#ifndef ___bpf_apply -#define ___bpf_apply(fn, n) ___bpf_concat(fn, n) -#endif -#ifndef ___bpf_nth -#define ___bpf_nth(_, _1, _2, _3, _4, _5, _6, _7, _8, _9, _a, _b, _c, N, ...) N -#endif -#ifndef ___bpf_narg -#define ___bpf_narg(...) \ - ___bpf_nth(_, ##__VA_ARGS__, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) -#endif - -#define ___bpf_fill0(arr, p, x) do {} while (0) -#define ___bpf_fill1(arr, p, x) arr[p] = x -#define ___bpf_fill2(arr, p, x, args...) arr[p] = x; ___bpf_fill1(arr, p + 1, args) -#define ___bpf_fill3(arr, p, x, args...) arr[p] = x; ___bpf_fill2(arr, p + 1, args) -#define ___bpf_fill4(arr, p, x, args...) arr[p] = x; ___bpf_fill3(arr, p + 1, args) -#define ___bpf_fill5(arr, p, x, args...) arr[p] = x; ___bpf_fill4(arr, p + 1, args) -#define ___bpf_fill6(arr, p, x, args...) arr[p] = x; ___bpf_fill5(arr, p + 1, args) -#define ___bpf_fill7(arr, p, x, args...) arr[p] = x; ___bpf_fill6(arr, p + 1, args) -#define ___bpf_fill8(arr, p, x, args...) arr[p] = x; ___bpf_fill7(arr, p + 1, args) -#define ___bpf_fill9(arr, p, x, args...) arr[p] = x; ___bpf_fill8(arr, p + 1, args) -#define ___bpf_fill10(arr, p, x, args...) arr[p] = x; ___bpf_fill9(arr, p + 1, args) -#define ___bpf_fill11(arr, p, x, args...) arr[p] = x; ___bpf_fill10(arr, p + 1, args) -#define ___bpf_fill12(arr, p, x, args...) arr[p] = x; ___bpf_fill11(arr, p + 1, args) -#define ___bpf_fill(arr, args...) \ - ___bpf_apply(___bpf_fill, ___bpf_narg(args))(arr, 0, args) - -/* - * BPF_SEQ_PRINTF to wrap bpf_seq_printf to-be-printed values - * in a structure. - */ -#define BPF_SEQ_PRINTF(seq, fmt, args...) \ -({ \ - static const char ___fmt[] = fmt; \ - unsigned long long ___param[___bpf_narg(args)]; \ - \ - _Pragma("GCC diagnostic push") \ - _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ - ___bpf_fill(___param, args); \ - _Pragma("GCC diagnostic pop") \ - \ - bpf_seq_printf(seq, ___fmt, sizeof(___fmt), \ - ___param, sizeof(___param)); \ -}) - -/* - * BPF_SNPRINTF wraps the bpf_snprintf helper with variadic arguments instead of - * an array of u64. - */ -#define BPF_SNPRINTF(out, out_size, fmt, args...) \ -({ \ - static const char ___fmt[] = fmt; \ - unsigned long long ___param[___bpf_narg(args)]; \ - \ - _Pragma("GCC diagnostic push") \ - _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ - ___bpf_fill(___param, args); \ - _Pragma("GCC diagnostic pop") \ - \ - bpf_snprintf(out, out_size, ___fmt, \ - ___param, sizeof(___param)); \ -}) - -#ifdef BPF_NO_GLOBAL_DATA -#define BPF_PRINTK_FMT_MOD -#else -#define BPF_PRINTK_FMT_MOD static const -#endif - -#define __bpf_printk(fmt, ...) \ -({ \ - BPF_PRINTK_FMT_MOD char ____fmt[] = fmt; \ - bpf_trace_printk(____fmt, sizeof(____fmt), \ - ##__VA_ARGS__); \ -}) - -/* - * __bpf_vprintk wraps the bpf_trace_vprintk helper with variadic arguments - * instead of an array of u64. - */ -#define __bpf_vprintk(fmt, args...) \ -({ \ - static const char ___fmt[] = fmt; \ - unsigned long long ___param[___bpf_narg(args)]; \ - \ - _Pragma("GCC diagnostic push") \ - _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \ - ___bpf_fill(___param, args); \ - _Pragma("GCC diagnostic pop") \ - \ - bpf_trace_vprintk(___fmt, sizeof(___fmt), \ - ___param, sizeof(___param)); \ -}) - -/* Use __bpf_printk when bpf_printk call has 3 or fewer fmt args - * Otherwise use __bpf_vprintk - */ -#define ___bpf_pick_printk(...) \ - ___bpf_nth(_, ##__VA_ARGS__, __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, \ - __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, __bpf_vprintk, \ - __bpf_vprintk, __bpf_vprintk, __bpf_printk /*3*/, __bpf_printk /*2*/,\ - __bpf_printk /*1*/, __bpf_printk /*0*/) - -/* Helper macro to print out debug messages */ -#define bpf_printk(fmt, args...) ___bpf_pick_printk(args)(fmt, ##args) - -#endif diff --git a/component/ebpf/bpf/redir.c b/component/ebpf/bpf/redir.c deleted file mode 100644 index 6ef5ee0c..00000000 --- a/component/ebpf/bpf/redir.c +++ /dev/null @@ -1,342 +0,0 @@ -#include -#include -//#include - -#include -#include -//#include -//#include -#include -#include -#include -//#include - -#include - -#include "bpf_endian.h" -#include "bpf_helpers.h" - -#define IP_CSUM_OFF (ETH_HLEN + offsetof(struct iphdr, check)) -#define IP_DST_OFF (ETH_HLEN + offsetof(struct iphdr, daddr)) -#define IP_SRC_OFF (ETH_HLEN + offsetof(struct iphdr, saddr)) -#define IP_PROTO_OFF (ETH_HLEN + offsetof(struct iphdr, protocol)) -#define TCP_CSUM_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct tcphdr, check)) -#define TCP_SRC_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct tcphdr, source)) -#define TCP_DST_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct tcphdr, dest)) -//#define UDP_CSUM_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct udphdr, check)) -//#define UDP_SRC_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct udphdr, source)) -//#define UDP_DST_OFF (ETH_HLEN + sizeof(struct iphdr) + offsetof(struct udphdr, dest)) -#define IS_PSEUDO 0x10 - -struct origin_info { - __be32 ip; - __be16 port; - __u16 pad; -}; - -struct origin_info *origin_info_unused __attribute__((unused)); - -struct redir_info { - __be32 sip; - __be32 dip; - __be16 sport; - __be16 dport; -}; - -struct redir_info *redir_info_unused __attribute__((unused)); - -struct { - __uint(type, BPF_MAP_TYPE_LRU_HASH); - __type(key, struct redir_info); - __type(value, struct origin_info); - __uint(max_entries, 65535); - __uint(pinning, LIBBPF_PIN_BY_NAME); -} pair_original_dst_map SEC(".maps"); - -struct { - __uint(type, BPF_MAP_TYPE_ARRAY); - __type(key, __u32); - __type(value, __u32); - __uint(max_entries, 3); - __uint(pinning, LIBBPF_PIN_BY_NAME); -} redir_params_map SEC(".maps"); - -static __always_inline int rewrite_ip(struct __sk_buff *skb, __be32 new_ip, bool is_dest) { - int ret, off = 0, flags = IS_PSEUDO; - __be32 old_ip; - - if (is_dest) - ret = bpf_skb_load_bytes(skb, IP_DST_OFF, &old_ip, 4); - else - ret = bpf_skb_load_bytes(skb, IP_SRC_OFF, &old_ip, 4); - - if (ret < 0) { - return ret; - } - - off = TCP_CSUM_OFF; -// __u8 proto; -// -// ret = bpf_skb_load_bytes(skb, IP_PROTO_OFF, &proto, 1); -// if (ret < 0) { -// return BPF_DROP; -// } -// -// switch (proto) { -// case IPPROTO_TCP: -// off = TCP_CSUM_OFF; -// break; -// -// case IPPROTO_UDP: -// off = UDP_CSUM_OFF; -// flags |= BPF_F_MARK_MANGLED_0; -// break; -// -// case IPPROTO_ICMPV6: -// off = offsetof(struct icmp6hdr, icmp6_cksum); -// break; -// } -// -// if (off) { - ret = bpf_l4_csum_replace(skb, off, old_ip, new_ip, flags | sizeof(new_ip)); - if (ret < 0) { - return ret; - } -// } - - ret = bpf_l3_csum_replace(skb, IP_CSUM_OFF, old_ip, new_ip, sizeof(new_ip)); - if (ret < 0) { - return ret; - } - - if (is_dest) - ret = bpf_skb_store_bytes(skb, IP_DST_OFF, &new_ip, sizeof(new_ip), 0); - else - ret = bpf_skb_store_bytes(skb, IP_SRC_OFF, &new_ip, sizeof(new_ip), 0); - - if (ret < 0) { - return ret; - } - - return 1; -} - -static __always_inline int rewrite_port(struct __sk_buff *skb, __be16 new_port, bool is_dest) { - int ret, off = 0; - __be16 old_port; - - if (is_dest) - ret = bpf_skb_load_bytes(skb, TCP_DST_OFF, &old_port, 2); - else - ret = bpf_skb_load_bytes(skb, TCP_SRC_OFF, &old_port, 2); - - if (ret < 0) { - return ret; - } - - off = TCP_CSUM_OFF; - - ret = bpf_l4_csum_replace(skb, off, old_port, new_port, sizeof(new_port)); - if (ret < 0) { - return ret; - } - - if (is_dest) - ret = bpf_skb_store_bytes(skb, TCP_DST_OFF, &new_port, sizeof(new_port), 0); - else - ret = bpf_skb_store_bytes(skb, TCP_SRC_OFF, &new_port, sizeof(new_port), 0); - - if (ret < 0) { - return ret; - } - - return 1; -} - -static __always_inline bool is_lan_ip(__be32 addr) { - if (addr == 0xffffffff) - return true; - - __u8 fist = (__u8)(addr & 0xff); - - if (fist == 127 || fist == 10) - return true; - - __u8 second = (__u8)((addr >> 8) & 0xff); - - if (fist == 172 && second >= 16 && second <= 31) - return true; - - if (fist == 192 && second == 168) - return true; - - return false; -} - -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; - struct ethhdr *eth = data; - - if ((void *)(eth + 1) > data_end) - return TC_ACT_OK; - - if (eth->h_proto != bpf_htons(ETH_P_IP)) - return TC_ACT_OK; - - struct iphdr *iph = (struct iphdr *)(eth + 1); - if ((void *)(iph + 1) > data_end) - return TC_ACT_OK; - - __u32 key = 0, *route_index, *redir_ip, *redir_port; - - route_index = bpf_map_lookup_elem(&redir_params_map, &key); - if (!route_index) - return TC_ACT_OK; - - if (iph->protocol == IPPROTO_ICMP && *route_index != 0) - return bpf_redirect(*route_index, 0); - - if (iph->protocol != IPPROTO_TCP) - return TC_ACT_OK; - - struct tcphdr *tcph = (struct tcphdr *)(iph + 1); - if ((void *)(tcph + 1) > data_end) - return TC_ACT_SHOT; - - key = 1; - redir_ip = bpf_map_lookup_elem(&redir_params_map, &key); - if (!redir_ip) - return TC_ACT_OK; - - key = 2; - redir_port = bpf_map_lookup_elem(&redir_params_map, &key); - if (!redir_port) - return TC_ACT_OK; - - __be32 new_ip = bpf_htonl(*redir_ip); - __be16 new_port = bpf_htonl(*redir_port) >> 16; - __be32 old_ip = iph->daddr; - __be16 old_port = tcph->dest; - - if (old_ip == new_ip || is_lan_ip(old_ip) || bpf_ntohs(old_port) == 53) { - return TC_ACT_OK; - } - - struct redir_info p_key = { - .sip = iph->saddr, - .sport = tcph->source, - .dip = new_ip, - .dport = new_port, - }; - - if (tcph->syn && !tcph->ack) { - struct origin_info origin = { - .ip = old_ip, - .port = old_port, - }; - - bpf_map_update_elem(&pair_original_dst_map, &p_key, &origin, BPF_NOEXIST); - - if (rewrite_ip(skb, new_ip, true) < 0) { - return TC_ACT_SHOT; - } - - if (rewrite_port(skb, new_port, true) < 0) { - return TC_ACT_SHOT; - } - } else { - struct origin_info *origin = bpf_map_lookup_elem(&pair_original_dst_map, &p_key); - if (!origin) { - return TC_ACT_OK; - } - - if (rewrite_ip(skb, new_ip, true) < 0) { - return TC_ACT_SHOT; - } - - if (rewrite_port(skb, new_port, true) < 0) { - return TC_ACT_SHOT; - } - } - - return TC_ACT_OK; -} - -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; - struct ethhdr *eth = data; - - if ((void *)(eth + 1) > data_end) - return TC_ACT_OK; - - if (eth->h_proto != bpf_htons(ETH_P_IP)) - return TC_ACT_OK; - - __u32 key = 0, *redir_ip, *redir_port; // *mihomo_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); - if ((void *)(iph + 1) > data_end) - return TC_ACT_OK; - - if (iph->protocol != IPPROTO_TCP) - return TC_ACT_OK; - - struct tcphdr *tcph = (struct tcphdr *)(iph + 1); - if ((void *)(tcph + 1) > data_end) - return TC_ACT_SHOT; - - key = 1; - redir_ip = bpf_map_lookup_elem(&redir_params_map, &key); - if (!redir_ip) - return TC_ACT_OK; - - key = 2; - redir_port = bpf_map_lookup_elem(&redir_params_map, &key); - if (!redir_port) - return TC_ACT_OK; - - __be32 new_ip = bpf_htonl(*redir_ip); - __be16 new_port = bpf_htonl(*redir_port) >> 16; - __be32 old_ip = iph->saddr; - __be16 old_port = tcph->source; - - if (old_ip != new_ip || old_port != new_port) { - return TC_ACT_OK; - } - - struct redir_info p_key = { - .sip = iph->daddr, - .sport = tcph->dest, - .dip = iph->saddr, - .dport = tcph->source, - }; - - struct origin_info *origin = bpf_map_lookup_elem(&pair_original_dst_map, &p_key); - if (!origin) { - return TC_ACT_OK; - } - - if (tcph->fin && tcph->ack) { - bpf_map_delete_elem(&pair_original_dst_map, &p_key); - } - - if (rewrite_ip(skb, origin->ip, false) < 0) { - return TC_ACT_SHOT; - } - - if (rewrite_port(skb, origin->port, false) < 0) { - return TC_ACT_SHOT; - } - - return TC_ACT_OK; -} - -char _license[] SEC("license") = "GPL"; diff --git a/component/ebpf/bpf/tc.c b/component/ebpf/bpf/tc.c deleted file mode 100644 index 3513bf04..00000000 --- a/component/ebpf/bpf/tc.c +++ /dev/null @@ -1,103 +0,0 @@ -#include -#include -#include -#include -#include -//#include -//#include -#include - -#include "bpf_endian.h" -#include "bpf_helpers.h" - -struct { - __uint(type, BPF_MAP_TYPE_ARRAY); - __type(key, __u32); - __type(value, __u32); - __uint(max_entries, 2); - __uint(pinning, LIBBPF_PIN_BY_NAME); -} tc_params_map SEC(".maps"); - -static __always_inline bool is_lan_ip(__be32 addr) { - if (addr == 0xffffffff) - return true; - - __u8 fist = (__u8)(addr & 0xff); - - if (fist == 127 || fist == 10) - return true; - - __u8 second = (__u8)((addr >> 8) & 0xff); - - if (fist == 172 && second >= 16 && second <= 31) - return true; - - if (fist == 192 && second == 168) - return true; - - return false; -} - -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; - struct ethhdr *eth = data; - - if ((void *)(eth + 1) > data_end) - return TC_ACT_OK; - - if (eth->h_proto == bpf_htons(ETH_P_ARP)) - return TC_ACT_OK; - - __u32 key = 0, *mihomo_mark, *tun_ifindex; - - mihomo_mark = bpf_map_lookup_elem(&tc_params_map, &key); - if (!mihomo_mark) - return TC_ACT_OK; - - if (skb->mark == *mihomo_mark) - return TC_ACT_OK; - - if (eth->h_proto == bpf_htons(ETH_P_IP)) { - struct iphdr *iph = (struct iphdr *)(eth + 1); - if ((void *)(iph + 1) > data_end) - return TC_ACT_OK; - - if (iph->protocol == IPPROTO_ICMP) - return TC_ACT_OK; - - __be32 daddr = iph->daddr; - - if (is_lan_ip(daddr)) - return TC_ACT_OK; - -// if (iph->protocol == IPPROTO_TCP) { -// struct tcphdr *tcph = (struct tcphdr *)(iph + 1); -// if ((void *)(tcph + 1) > data_end) -// return TC_ACT_OK; -// -// __u16 source = bpf_ntohs(tcph->source); -// if (source == 22 || source == 80 || source == 443 || source == 8080 || source == 8443 || source == 9090 || (source >= 7890 && source <= 7895)) -// return TC_ACT_OK; -// } else if (iph->protocol == IPPROTO_UDP) { -// struct udphdr *udph = (struct udphdr *)(iph + 1); -// if ((void *)(udph + 1) > data_end) -// return TC_ACT_OK; -// -// __u16 source = bpf_ntohs(udph->source); -// if (source == 53 || (source >= 135 && source <= 139)) -// return TC_ACT_OK; -// } - } - - key = 1; - tun_ifindex = bpf_map_lookup_elem(&tc_params_map, &key); - if (!tun_ifindex) - return TC_ACT_OK; - - //return bpf_redirect(*tun_ifindex, BPF_F_INGRESS); // __bpf_rx_skb - return bpf_redirect(*tun_ifindex, 0); // __bpf_tx_skb / __dev_xmit_skb -} - -char _license[] SEC("license") = "GPL"; diff --git a/component/ebpf/byteorder/byteorder.go b/component/ebpf/byteorder/byteorder.go deleted file mode 100644 index 63e0c611..00000000 --- a/component/ebpf/byteorder/byteorder.go +++ /dev/null @@ -1,13 +0,0 @@ -package byteorder - -import ( - "net" -) - -// NetIPv4ToHost32 converts an net.IP to a uint32 in host byte order. ip -// must be a IPv4 address, otherwise the function will panic. -func NetIPv4ToHost32(ip net.IP) uint32 { - ipv4 := ip.To4() - _ = ipv4[3] // Assert length of ipv4. - return Native.Uint32(ipv4) -} diff --git a/component/ebpf/byteorder/byteorder_bigendian.go b/component/ebpf/byteorder/byteorder_bigendian.go deleted file mode 100644 index 4c5d7105..00000000 --- a/component/ebpf/byteorder/byteorder_bigendian.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 - -package byteorder - -import "encoding/binary" - -var Native binary.ByteOrder = binary.BigEndian - -func HostToNetwork16(u uint16) uint16 { return u } -func HostToNetwork32(u uint32) uint32 { return u } -func NetworkToHost16(u uint16) uint16 { return u } -func NetworkToHost32(u uint32) uint32 { return u } diff --git a/component/ebpf/byteorder/byteorder_littleendian.go b/component/ebpf/byteorder/byteorder_littleendian.go deleted file mode 100644 index d40f3517..00000000 --- a/component/ebpf/byteorder/byteorder_littleendian.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64 || loong64 - -package byteorder - -import ( - "encoding/binary" - "math/bits" -) - -var Native binary.ByteOrder = binary.LittleEndian - -func HostToNetwork16(u uint16) uint16 { return bits.ReverseBytes16(u) } -func HostToNetwork32(u uint32) uint32 { return bits.ReverseBytes32(u) } -func NetworkToHost16(u uint16) uint16 { return bits.ReverseBytes16(u) } -func NetworkToHost32(u uint32) uint32 { return bits.ReverseBytes32(u) } diff --git a/component/ebpf/ebpf.go b/component/ebpf/ebpf.go deleted file mode 100644 index b0f5a65f..00000000 --- a/component/ebpf/ebpf.go +++ /dev/null @@ -1,33 +0,0 @@ -package ebpf - -import ( - "net/netip" - - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/transport/socks5" -) - -type TcEBpfProgram struct { - pros []C.EBpf - rawNICs []string -} - -func (t *TcEBpfProgram) RawNICs() []string { - return t.rawNICs -} - -func (t *TcEBpfProgram) Close() { - for _, p := range t.pros { - p.Close() - } -} - -func (t *TcEBpfProgram) Lookup(srcAddrPort netip.AddrPort) (addr socks5.Addr, err error) { - for _, p := range t.pros { - addr, err = p.Lookup(srcAddrPort) - if err == nil { - return - } - } - return -} diff --git a/component/ebpf/ebpf_linux.go b/component/ebpf/ebpf_linux.go deleted file mode 100644 index 304f32fe..00000000 --- a/component/ebpf/ebpf_linux.go +++ /dev/null @@ -1,137 +0,0 @@ -//go:build !android - -package ebpf - -import ( - "fmt" - "net/netip" - - "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" -) - -func GetAutoDetectInterface() (string, error) { - routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) - if err != nil { - return "", err - } - - for _, route := range routes { - if route.Dst == nil { - lk, err := netlink.LinkByIndex(route.LinkIndex) - if err != nil { - return "", err - } - - if lk.Type() == "tuntap" { - continue - } - - return lk.Attrs().Name, nil - } - } - - return "", fmt.Errorf("interface not found") -} - -// NewTcEBpfProgram new redirect to tun ebpf program -func NewTcEBpfProgram(ifaceNames []string, tunName string) (*TcEBpfProgram, error) { - tunIface, err := netlink.LinkByName(tunName) - if err != nil { - return nil, fmt.Errorf("lookup network iface %q: %w", tunName, err) - } - - tunIndex := uint32(tunIface.Attrs().Index) - - dialer.DefaultRoutingMark.Store(C.MihomoTrafficMark) - - ifMark := uint32(dialer.DefaultRoutingMark.Load()) - - var pros []C.EBpf - for _, ifaceName := range ifaceNames { - iface, err := netlink.LinkByName(ifaceName) - if err != nil { - return nil, fmt.Errorf("lookup network iface %q: %w", ifaceName, err) - } - if iface.Attrs().OperState != netlink.OperUp { - return nil, fmt.Errorf("network iface %q is down", ifaceName) - } - - attrs := iface.Attrs() - index := attrs.Index - - tcPro := tc.NewEBpfTc(ifaceName, index, ifMark, tunIndex) - if err = tcPro.Start(); err != nil { - return nil, err - } - - pros = append(pros, tcPro) - } - - systemSetting(ifaceNames...) - - return &TcEBpfProgram{pros: pros, rawNICs: ifaceNames}, nil -} - -// NewRedirEBpfProgram new auto redirect ebpf program -func NewRedirEBpfProgram(ifaceNames []string, redirPort uint16, defaultRouteInterfaceName string) (*TcEBpfProgram, error) { - defaultRouteInterface, err := netlink.LinkByName(defaultRouteInterfaceName) - if err != nil { - return nil, fmt.Errorf("lookup network iface %q: %w", defaultRouteInterfaceName, err) - } - - defaultRouteIndex := uint32(defaultRouteInterface.Attrs().Index) - - var pros []C.EBpf - for _, ifaceName := range ifaceNames { - iface, err := netlink.LinkByName(ifaceName) - if err != nil { - return nil, fmt.Errorf("lookup network iface %q: %w", ifaceName, err) - } - - attrs := iface.Attrs() - index := attrs.Index - - addrs, err := netlink.AddrList(iface, netlink.FAMILY_V4) - if err != nil { - return nil, fmt.Errorf("lookup network iface %q address: %w", ifaceName, err) - } - - if len(addrs) == 0 { - return nil, fmt.Errorf("network iface %q does not contain any ipv4 addresses", ifaceName) - } - - address, _ := netip.AddrFromSlice(addrs[0].IP) - redirAddrPort := netip.AddrPortFrom(address, redirPort) - - redirPro := redir.NewEBpfRedirect(ifaceName, index, 0, defaultRouteIndex, redirAddrPort) - if err = redirPro.Start(); err != nil { - return nil, err - } - - pros = append(pros, redirPro) - } - - systemSetting(ifaceNames...) - - return &TcEBpfProgram{pros: pros, rawNICs: ifaceNames}, nil -} - -func systemSetting(ifaceNames ...string) { - _, _ = cmd.ExecCmd("sysctl -w net.ipv4.ip_forward=1") - _, _ = cmd.ExecCmd("sysctl -w net.ipv4.conf.all.forwarding=1") - _, _ = cmd.ExecCmd("sysctl -w net.ipv4.conf.all.accept_local=1") - _, _ = cmd.ExecCmd("sysctl -w net.ipv4.conf.all.accept_redirects=1") - _, _ = cmd.ExecCmd("sysctl -w net.ipv4.conf.all.rp_filter=0") - - for _, ifaceName := range ifaceNames { - _, _ = cmd.ExecCmd(fmt.Sprintf("sysctl -w net.ipv4.conf.%s.forwarding=1", ifaceName)) - _, _ = cmd.ExecCmd(fmt.Sprintf("sysctl -w net.ipv4.conf.%s.accept_local=1", ifaceName)) - _, _ = cmd.ExecCmd(fmt.Sprintf("sysctl -w net.ipv4.conf.%s.accept_redirects=1", ifaceName)) - _, _ = cmd.ExecCmd(fmt.Sprintf("sysctl -w net.ipv4.conf.%s.rp_filter=0", ifaceName)) - } -} diff --git a/component/ebpf/ebpf_others.go b/component/ebpf/ebpf_others.go deleted file mode 100644 index 44cf1c3a..00000000 --- a/component/ebpf/ebpf_others.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build !linux || android - -package ebpf - -import ( - "fmt" -) - -// NewTcEBpfProgram new ebpf tc program -func NewTcEBpfProgram(_ []string, _ string) (*TcEBpfProgram, error) { - return nil, fmt.Errorf("system not supported") -} - -// NewRedirEBpfProgram new ebpf redirect program -func NewRedirEBpfProgram(_ []string, _ uint16, _ string) (*TcEBpfProgram, error) { - return nil, fmt.Errorf("system not supported") -} - -func GetAutoDetectInterface() (string, error) { - return "", fmt.Errorf("system not supported") -} diff --git a/component/ebpf/redir/auto_redirect.go b/component/ebpf/redir/auto_redirect.go deleted file mode 100644 index 57c99616..00000000 --- a/component/ebpf/redir/auto_redirect.go +++ /dev/null @@ -1,216 +0,0 @@ -//go:build linux - -package redir - -import ( - "encoding/binary" - "fmt" - "io" - "net" - "net/netip" - "os" - "path/filepath" - - "github.com/cilium/ebpf" - "github.com/cilium/ebpf/rlimit" - "github.com/sagernet/netlink" - "golang.org/x/sys/unix" - - "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 - -const ( - mapKey1 uint32 = 0 - mapKey2 uint32 = 1 - mapKey3 uint32 = 2 -) - -type EBpfRedirect struct { - objs io.Closer - originMap *ebpf.Map - qdisc netlink.Qdisc - filter netlink.Filter - filterEgress netlink.Filter - - ifName string - ifIndex int - ifMark uint32 - rtIndex uint32 - redirIp uint32 - redirPort uint16 - - bpfPath string -} - -func NewEBpfRedirect(ifName string, ifIndex int, ifMark uint32, routeIndex uint32, redirAddrPort netip.AddrPort) *EBpfRedirect { - return &EBpfRedirect{ - ifName: ifName, - ifIndex: ifIndex, - ifMark: ifMark, - rtIndex: routeIndex, - redirIp: binary.BigEndian.Uint32(redirAddrPort.Addr().AsSlice()), - redirPort: redirAddrPort.Port(), - } -} - -func (e *EBpfRedirect) Start() error { - if err := rlimit.RemoveMemlock(); err != nil { - return fmt.Errorf("remove memory lock: %w", err) - } - - e.bpfPath = filepath.Join(C.BpfFSPath, e.ifName) - if err := os.MkdirAll(e.bpfPath, os.ModePerm); err != nil { - return fmt.Errorf("failed to create bpf fs subpath: %w", err) - } - - var objs bpfObjects - if err := loadBpfObjects(&objs, &ebpf.CollectionOptions{ - Maps: ebpf.MapOptions{ - PinPath: e.bpfPath, - }, - }); err != nil { - e.Close() - return fmt.Errorf("loading objects: %w", err) - } - - e.objs = &objs - e.originMap = objs.bpfMaps.PairOriginalDstMap - - if err := objs.bpfMaps.RedirParamsMap.Update(mapKey1, e.rtIndex, ebpf.UpdateAny); err != nil { - e.Close() - return fmt.Errorf("storing objects: %w", err) - } - - if err := objs.bpfMaps.RedirParamsMap.Update(mapKey2, e.redirIp, ebpf.UpdateAny); err != nil { - e.Close() - return fmt.Errorf("storing objects: %w", err) - } - - if err := objs.bpfMaps.RedirParamsMap.Update(mapKey3, uint32(e.redirPort), ebpf.UpdateAny); err != nil { - e.Close() - return fmt.Errorf("storing objects: %w", err) - } - - attrs := netlink.QdiscAttrs{ - LinkIndex: e.ifIndex, - Handle: netlink.MakeHandle(0xffff, 0), - Parent: netlink.HANDLE_CLSACT, - } - - qdisc := &netlink.GenericQdisc{ - QdiscAttrs: attrs, - QdiscType: "clsact", - } - - e.qdisc = qdisc - - if err := netlink.QdiscAdd(qdisc); err != nil { - if os.IsExist(err) { - _ = netlink.QdiscDel(qdisc) - err = netlink.QdiscAdd(qdisc) - } - - if err != nil { - e.Close() - return fmt.Errorf("cannot add clsact qdisc: %w", err) - } - } - - filterAttrs := netlink.FilterAttrs{ - LinkIndex: e.ifIndex, - Parent: netlink.HANDLE_MIN_INGRESS, - Handle: netlink.MakeHandle(0, 1), - Protocol: unix.ETH_P_IP, - Priority: 0, - } - - filter := &netlink.BpfFilter{ - FilterAttrs: filterAttrs, - Fd: objs.bpfPrograms.TcRedirIngressFunc.FD(), - Name: "mihomo-redir-ingress-" + e.ifName, - DirectAction: true, - } - - if err := netlink.FilterAdd(filter); err != nil { - e.Close() - return fmt.Errorf("cannot attach ebpf object to filter ingress: %w", err) - } - - e.filter = filter - - filterAttrsEgress := netlink.FilterAttrs{ - LinkIndex: e.ifIndex, - Parent: netlink.HANDLE_MIN_EGRESS, - Handle: netlink.MakeHandle(0, 1), - Protocol: unix.ETH_P_IP, - Priority: 0, - } - - filterEgress := &netlink.BpfFilter{ - FilterAttrs: filterAttrsEgress, - Fd: objs.bpfPrograms.TcRedirEgressFunc.FD(), - Name: "mihomo-redir-egress-" + e.ifName, - DirectAction: true, - } - - if err := netlink.FilterAdd(filterEgress); err != nil { - e.Close() - return fmt.Errorf("cannot attach ebpf object to filter egress: %w", err) - } - - e.filterEgress = filterEgress - - return nil -} - -func (e *EBpfRedirect) Close() { - if e.filter != nil { - _ = netlink.FilterDel(e.filter) - } - if e.filterEgress != nil { - _ = netlink.FilterDel(e.filterEgress) - } - if e.qdisc != nil { - _ = netlink.QdiscDel(e.qdisc) - } - if e.objs != nil { - _ = e.objs.Close() - } - _ = os.Remove(filepath.Join(e.bpfPath, "redir_params_map")) - _ = os.Remove(filepath.Join(e.bpfPath, "pair_original_dst_map")) -} - -func (e *EBpfRedirect) Lookup(srcAddrPort netip.AddrPort) (socks5.Addr, error) { - rAddr := srcAddrPort.Addr().Unmap() - if rAddr.Is6() { - return nil, fmt.Errorf("remote address is ipv6") - } - - srcIp := binary.BigEndian.Uint32(rAddr.AsSlice()) - scrPort := srcAddrPort.Port() - - key := bpfRedirInfo{ - Sip: byteorder.HostToNetwork32(srcIp), - Sport: byteorder.HostToNetwork16(scrPort), - Dip: byteorder.HostToNetwork32(e.redirIp), - Dport: byteorder.HostToNetwork16(e.redirPort), - } - - origin := bpfOriginInfo{} - - err := e.originMap.Lookup(key, &origin) - if err != nil { - return nil, err - } - - addr := make([]byte, net.IPv4len+3) - addr[0] = socks5.AtypIPv4 - - binary.BigEndian.PutUint32(addr[1:1+net.IPv4len], byteorder.NetworkToHost32(origin.Ip)) // big end - binary.BigEndian.PutUint16(addr[1+net.IPv4len:3+net.IPv4len], byteorder.NetworkToHost16(origin.Port)) // big end - return addr, nil -} diff --git a/component/ebpf/redir/bpf_bpfeb.go b/component/ebpf/redir/bpf_bpfeb.go deleted file mode 100644 index 57a526e8..00000000 --- a/component/ebpf/redir/bpf_bpfeb.go +++ /dev/null @@ -1,139 +0,0 @@ -// Code generated by bpf2go; DO NOT EDIT. -//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 -// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64 - -package redir - -import ( - "bytes" - _ "embed" - "fmt" - "io" - - "github.com/cilium/ebpf" -) - -type bpfOriginInfo struct { - Ip uint32 - Port uint16 - Pad uint16 -} - -type bpfRedirInfo struct { - Sip uint32 - Dip uint32 - Sport uint16 - Dport uint16 -} - -// loadBpf returns the embedded CollectionSpec for bpf. -func loadBpf() (*ebpf.CollectionSpec, error) { - reader := bytes.NewReader(_BpfBytes) - spec, err := ebpf.LoadCollectionSpecFromReader(reader) - if err != nil { - return nil, fmt.Errorf("can't load bpf: %w", err) - } - - return spec, err -} - -// loadBpfObjects loads bpf and converts it into a struct. -// -// The following types are suitable as obj argument: -// -// *bpfObjects -// *bpfPrograms -// *bpfMaps -// -// See ebpf.CollectionSpec.LoadAndAssign documentation for details. -func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { - spec, err := loadBpf() - if err != nil { - return err - } - - return spec.LoadAndAssign(obj, opts) -} - -// bpfSpecs contains maps and programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfSpecs struct { - bpfProgramSpecs - bpfMapSpecs -} - -// bpfSpecs contains programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfProgramSpecs struct { - TcRedirEgressFunc *ebpf.ProgramSpec `ebpf:"tc_redir_egress_func"` - TcRedirIngressFunc *ebpf.ProgramSpec `ebpf:"tc_redir_ingress_func"` -} - -// bpfMapSpecs contains maps before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfMapSpecs struct { - PairOriginalDstMap *ebpf.MapSpec `ebpf:"pair_original_dst_map"` - RedirParamsMap *ebpf.MapSpec `ebpf:"redir_params_map"` -} - -// bpfObjects contains all objects after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfObjects struct { - bpfPrograms - bpfMaps -} - -func (o *bpfObjects) Close() error { - return _BpfClose( - &o.bpfPrograms, - &o.bpfMaps, - ) -} - -// bpfMaps contains all maps after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfMaps struct { - PairOriginalDstMap *ebpf.Map `ebpf:"pair_original_dst_map"` - RedirParamsMap *ebpf.Map `ebpf:"redir_params_map"` -} - -func (m *bpfMaps) Close() error { - return _BpfClose( - m.PairOriginalDstMap, - m.RedirParamsMap, - ) -} - -// bpfPrograms contains all programs after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfPrograms struct { - TcRedirEgressFunc *ebpf.Program `ebpf:"tc_redir_egress_func"` - TcRedirIngressFunc *ebpf.Program `ebpf:"tc_redir_ingress_func"` -} - -func (p *bpfPrograms) Close() error { - return _BpfClose( - p.TcRedirEgressFunc, - p.TcRedirIngressFunc, - ) -} - -func _BpfClose(closers ...io.Closer) error { - for _, closer := range closers { - if err := closer.Close(); err != nil { - return err - } - } - return nil -} - -// Do not access this directly. -// -//go:embed bpf_bpfeb.o -var _BpfBytes []byte diff --git a/component/ebpf/redir/bpf_bpfeb.o b/component/ebpf/redir/bpf_bpfeb.o deleted file mode 100644 index ed511691..00000000 Binary files a/component/ebpf/redir/bpf_bpfeb.o and /dev/null differ diff --git a/component/ebpf/redir/bpf_bpfel.go b/component/ebpf/redir/bpf_bpfel.go deleted file mode 100644 index 1fe3454a..00000000 --- a/component/ebpf/redir/bpf_bpfel.go +++ /dev/null @@ -1,139 +0,0 @@ -// Code generated by bpf2go; DO NOT EDIT. -//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64 || loong64 -// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64 loong64 - -package redir - -import ( - "bytes" - _ "embed" - "fmt" - "io" - - "github.com/cilium/ebpf" -) - -type bpfOriginInfo struct { - Ip uint32 - Port uint16 - Pad uint16 -} - -type bpfRedirInfo struct { - Sip uint32 - Dip uint32 - Sport uint16 - Dport uint16 -} - -// loadBpf returns the embedded CollectionSpec for bpf. -func loadBpf() (*ebpf.CollectionSpec, error) { - reader := bytes.NewReader(_BpfBytes) - spec, err := ebpf.LoadCollectionSpecFromReader(reader) - if err != nil { - return nil, fmt.Errorf("can't load bpf: %w", err) - } - - return spec, err -} - -// loadBpfObjects loads bpf and converts it into a struct. -// -// The following types are suitable as obj argument: -// -// *bpfObjects -// *bpfPrograms -// *bpfMaps -// -// See ebpf.CollectionSpec.LoadAndAssign documentation for details. -func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { - spec, err := loadBpf() - if err != nil { - return err - } - - return spec.LoadAndAssign(obj, opts) -} - -// bpfSpecs contains maps and programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfSpecs struct { - bpfProgramSpecs - bpfMapSpecs -} - -// bpfSpecs contains programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfProgramSpecs struct { - TcRedirEgressFunc *ebpf.ProgramSpec `ebpf:"tc_redir_egress_func"` - TcRedirIngressFunc *ebpf.ProgramSpec `ebpf:"tc_redir_ingress_func"` -} - -// bpfMapSpecs contains maps before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfMapSpecs struct { - PairOriginalDstMap *ebpf.MapSpec `ebpf:"pair_original_dst_map"` - RedirParamsMap *ebpf.MapSpec `ebpf:"redir_params_map"` -} - -// bpfObjects contains all objects after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfObjects struct { - bpfPrograms - bpfMaps -} - -func (o *bpfObjects) Close() error { - return _BpfClose( - &o.bpfPrograms, - &o.bpfMaps, - ) -} - -// bpfMaps contains all maps after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfMaps struct { - PairOriginalDstMap *ebpf.Map `ebpf:"pair_original_dst_map"` - RedirParamsMap *ebpf.Map `ebpf:"redir_params_map"` -} - -func (m *bpfMaps) Close() error { - return _BpfClose( - m.PairOriginalDstMap, - m.RedirParamsMap, - ) -} - -// bpfPrograms contains all programs after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfPrograms struct { - TcRedirEgressFunc *ebpf.Program `ebpf:"tc_redir_egress_func"` - TcRedirIngressFunc *ebpf.Program `ebpf:"tc_redir_ingress_func"` -} - -func (p *bpfPrograms) Close() error { - return _BpfClose( - p.TcRedirEgressFunc, - p.TcRedirIngressFunc, - ) -} - -func _BpfClose(closers ...io.Closer) error { - for _, closer := range closers { - if err := closer.Close(); err != nil { - return err - } - } - return nil -} - -// Do not access this directly. -// -//go:embed bpf_bpfel.o -var _BpfBytes []byte diff --git a/component/ebpf/redir/bpf_bpfel.o b/component/ebpf/redir/bpf_bpfel.o deleted file mode 100644 index 0ac4be06..00000000 Binary files a/component/ebpf/redir/bpf_bpfel.o and /dev/null differ diff --git a/component/ebpf/tc/bpf_bpfeb.go b/component/ebpf/tc/bpf_bpfeb.go deleted file mode 100644 index 623986dc..00000000 --- a/component/ebpf/tc/bpf_bpfeb.go +++ /dev/null @@ -1,120 +0,0 @@ -// Code generated by bpf2go; DO NOT EDIT. -//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 -// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64 - -package tc - -import ( - "bytes" - _ "embed" - "fmt" - "io" - - "github.com/cilium/ebpf" -) - -// loadBpf returns the embedded CollectionSpec for bpf. -func loadBpf() (*ebpf.CollectionSpec, error) { - reader := bytes.NewReader(_BpfBytes) - spec, err := ebpf.LoadCollectionSpecFromReader(reader) - if err != nil { - return nil, fmt.Errorf("can't load bpf: %w", err) - } - - return spec, err -} - -// loadBpfObjects loads bpf and converts it into a struct. -// -// The following types are suitable as obj argument: -// -// *bpfObjects -// *bpfPrograms -// *bpfMaps -// -// See ebpf.CollectionSpec.LoadAndAssign documentation for details. -func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { - spec, err := loadBpf() - if err != nil { - return err - } - - return spec.LoadAndAssign(obj, opts) -} - -// bpfSpecs contains maps and programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfSpecs struct { - bpfProgramSpecs - bpfMapSpecs -} - -// bpfSpecs contains programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfProgramSpecs struct { - TcTunFunc *ebpf.ProgramSpec `ebpf:"tc_tun_func"` -} - -// bpfMapSpecs contains maps before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfMapSpecs struct { - TcParamsMap *ebpf.MapSpec `ebpf:"tc_params_map"` -} - -// bpfObjects contains all objects after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfObjects struct { - bpfPrograms - bpfMaps -} - -func (o *bpfObjects) Close() error { - return _BpfClose( - &o.bpfPrograms, - &o.bpfMaps, - ) -} - -// bpfMaps contains all maps after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfMaps struct { - TcParamsMap *ebpf.Map `ebpf:"tc_params_map"` -} - -func (m *bpfMaps) Close() error { - return _BpfClose( - m.TcParamsMap, - ) -} - -// bpfPrograms contains all programs after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfPrograms struct { - TcTunFunc *ebpf.Program `ebpf:"tc_tun_func"` -} - -func (p *bpfPrograms) Close() error { - return _BpfClose( - p.TcTunFunc, - ) -} - -func _BpfClose(closers ...io.Closer) error { - for _, closer := range closers { - if err := closer.Close(); err != nil { - return err - } - } - return nil -} - -// Do not access this directly. -// -//go:embed bpf_bpfeb.o -var _BpfBytes []byte diff --git a/component/ebpf/tc/bpf_bpfeb.o b/component/ebpf/tc/bpf_bpfeb.o deleted file mode 100644 index f0e7e608..00000000 Binary files a/component/ebpf/tc/bpf_bpfeb.o and /dev/null differ diff --git a/component/ebpf/tc/bpf_bpfel.go b/component/ebpf/tc/bpf_bpfel.go deleted file mode 100644 index 3bfa1655..00000000 --- a/component/ebpf/tc/bpf_bpfel.go +++ /dev/null @@ -1,120 +0,0 @@ -// -//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64 || loong64 -// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64 loong64 - -package tc - -import ( - "bytes" - _ "embed" - "fmt" - "io" - - "github.com/cilium/ebpf" -) - -// loadBpf returns the embedded CollectionSpec for bpf. -func loadBpf() (*ebpf.CollectionSpec, error) { - reader := bytes.NewReader(_BpfBytes) - spec, err := ebpf.LoadCollectionSpecFromReader(reader) - if err != nil { - return nil, fmt.Errorf("can't load bpf: %w", err) - } - - return spec, err -} - -// loadBpfObjects loads bpf and converts it into a struct. -// -// The following types are suitable as obj argument: -// -// *bpfObjects -// *bpfPrograms -// *bpfMaps -// -// See ebpf.CollectionSpec.LoadAndAssign documentation for details. -func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { - spec, err := loadBpf() - if err != nil { - return err - } - - return spec.LoadAndAssign(obj, opts) -} - -// bpfSpecs contains maps and programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfSpecs struct { - bpfProgramSpecs - bpfMapSpecs -} - -// bpfSpecs contains programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfProgramSpecs struct { - TcTunFunc *ebpf.ProgramSpec `ebpf:"tc_tun_func"` -} - -// bpfMapSpecs contains maps before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type bpfMapSpecs struct { - TcParamsMap *ebpf.MapSpec `ebpf:"tc_params_map"` -} - -// bpfObjects contains all objects after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfObjects struct { - bpfPrograms - bpfMaps -} - -func (o *bpfObjects) Close() error { - return _BpfClose( - &o.bpfPrograms, - &o.bpfMaps, - ) -} - -// bpfMaps contains all maps after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfMaps struct { - TcParamsMap *ebpf.Map `ebpf:"tc_params_map"` -} - -func (m *bpfMaps) Close() error { - return _BpfClose( - m.TcParamsMap, - ) -} - -// bpfPrograms contains all programs after they have been loaded into the kernel. -// -// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. -type bpfPrograms struct { - TcTunFunc *ebpf.Program `ebpf:"tc_tun_func"` -} - -func (p *bpfPrograms) Close() error { - return _BpfClose( - p.TcTunFunc, - ) -} - -func _BpfClose(closers ...io.Closer) error { - for _, closer := range closers { - if err := closer.Close(); err != nil { - return err - } - } - return nil -} - -// Do not access this directly. -// -//go:embed bpf_bpfel.o -var _BpfBytes []byte diff --git a/component/ebpf/tc/bpf_bpfel.o b/component/ebpf/tc/bpf_bpfel.o deleted file mode 100644 index 290ae9ca..00000000 Binary files a/component/ebpf/tc/bpf_bpfel.o and /dev/null differ diff --git a/component/ebpf/tc/redirect_to_tun.go b/component/ebpf/tc/redirect_to_tun.go deleted file mode 100644 index d7be64af..00000000 --- a/component/ebpf/tc/redirect_to_tun.go +++ /dev/null @@ -1,147 +0,0 @@ -//go:build linux - -package tc - -import ( - "fmt" - "io" - "net/netip" - "os" - "path/filepath" - - "github.com/cilium/ebpf" - "github.com/cilium/ebpf/rlimit" - "github.com/sagernet/netlink" - "golang.org/x/sys/unix" - - 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 - -const ( - mapKey1 uint32 = 0 - mapKey2 uint32 = 1 -) - -type EBpfTC struct { - objs io.Closer - qdisc netlink.Qdisc - filter netlink.Filter - - ifName string - ifIndex int - ifMark uint32 - tunIfIndex uint32 - - bpfPath string -} - -func NewEBpfTc(ifName string, ifIndex int, ifMark uint32, tunIfIndex uint32) *EBpfTC { - return &EBpfTC{ - ifName: ifName, - ifIndex: ifIndex, - ifMark: ifMark, - tunIfIndex: tunIfIndex, - } -} - -func (e *EBpfTC) Start() error { - if err := rlimit.RemoveMemlock(); err != nil { - return fmt.Errorf("remove memory lock: %w", err) - } - - e.bpfPath = filepath.Join(C.BpfFSPath, e.ifName) - if err := os.MkdirAll(e.bpfPath, os.ModePerm); err != nil { - return fmt.Errorf("failed to create bpf fs subpath: %w", err) - } - - var objs bpfObjects - if err := loadBpfObjects(&objs, &ebpf.CollectionOptions{ - Maps: ebpf.MapOptions{ - PinPath: e.bpfPath, - }, - }); err != nil { - e.Close() - return fmt.Errorf("loading objects: %w", err) - } - - e.objs = &objs - - if err := objs.bpfMaps.TcParamsMap.Update(mapKey1, e.ifMark, ebpf.UpdateAny); err != nil { - e.Close() - return fmt.Errorf("storing objects: %w", err) - } - - if err := objs.bpfMaps.TcParamsMap.Update(mapKey2, e.tunIfIndex, ebpf.UpdateAny); err != nil { - e.Close() - return fmt.Errorf("storing objects: %w", err) - } - - attrs := netlink.QdiscAttrs{ - LinkIndex: e.ifIndex, - Handle: netlink.MakeHandle(0xffff, 0), - Parent: netlink.HANDLE_CLSACT, - } - - qdisc := &netlink.GenericQdisc{ - QdiscAttrs: attrs, - QdiscType: "clsact", - } - - e.qdisc = qdisc - - if err := netlink.QdiscAdd(qdisc); err != nil { - if os.IsExist(err) { - _ = netlink.QdiscDel(qdisc) - err = netlink.QdiscAdd(qdisc) - } - - if err != nil { - e.Close() - return fmt.Errorf("cannot add clsact qdisc: %w", err) - } - } - - filterAttrs := netlink.FilterAttrs{ - LinkIndex: e.ifIndex, - Parent: netlink.HANDLE_MIN_EGRESS, - Handle: netlink.MakeHandle(0, 1), - Protocol: unix.ETH_P_ALL, - Priority: 1, - } - - filter := &netlink.BpfFilter{ - FilterAttrs: filterAttrs, - Fd: objs.bpfPrograms.TcTunFunc.FD(), - Name: "mihomo-tc-" + e.ifName, - DirectAction: true, - } - - if err := netlink.FilterAdd(filter); err != nil { - e.Close() - return fmt.Errorf("cannot attach ebpf object to filter: %w", err) - } - - e.filter = filter - - return nil -} - -func (e *EBpfTC) Close() { - if e.filter != nil { - _ = netlink.FilterDel(e.filter) - } - if e.qdisc != nil { - _ = netlink.QdiscDel(e.qdisc) - } - if e.objs != nil { - _ = e.objs.Close() - } - _ = os.Remove(filepath.Join(e.bpfPath, "tc_params_map")) -} - -func (e *EBpfTC) Lookup(_ netip.AddrPort) (socks5.Addr, error) { - return nil, fmt.Errorf("not supported") -} diff --git a/component/fakeip/cachefile.go b/component/fakeip/cachefile.go index 6f0cc48b..92d09721 100644 --- a/component/fakeip/cachefile.go +++ b/component/fakeip/cachefile.go @@ -7,46 +7,32 @@ import ( ) type cachefileStore struct { - cache *cachefile.CacheFile + cache *cachefile.FakeIpStore } // GetByHost implements store.GetByHost func (c *cachefileStore) GetByHost(host string) (netip.Addr, bool) { - elm := c.cache.GetFakeip([]byte(host)) - if elm == nil { - return netip.Addr{}, false - } - - if len(elm) == 4 { - return netip.AddrFrom4(*(*[4]byte)(elm)), true - } else { - return netip.AddrFrom16(*(*[16]byte)(elm)), true - } + return c.cache.GetByHost(host) } // PutByHost implements store.PutByHost func (c *cachefileStore) PutByHost(host string, ip netip.Addr) { - c.cache.PutFakeip([]byte(host), ip.AsSlice()) + c.cache.PutByHost(host, ip) } // GetByIP implements store.GetByIP func (c *cachefileStore) GetByIP(ip netip.Addr) (string, bool) { - elm := c.cache.GetFakeip(ip.AsSlice()) - if elm == nil { - return "", false - } - return string(elm), true + return c.cache.GetByIP(ip) } // PutByIP implements store.PutByIP func (c *cachefileStore) PutByIP(ip netip.Addr, host string) { - c.cache.PutFakeip(ip.AsSlice(), []byte(host)) + c.cache.PutByIP(ip, host) } // DelByIP implements store.DelByIP func (c *cachefileStore) DelByIP(ip netip.Addr) { - addr := ip.AsSlice() - c.cache.DelFakeipPair(addr, c.cache.GetFakeip(addr)) + c.cache.DelByIP(ip) } // Exist implements store.Exist @@ -63,3 +49,7 @@ func (c *cachefileStore) CloneTo(store store) {} func (c *cachefileStore) FlushFakeIP() error { return c.cache.FlushFakeIP() } + +func newCachefileStore(cache *cachefile.CacheFile) *cachefileStore { + return &cachefileStore{cache.FakeIpStore()} +} diff --git a/component/fakeip/memory.go b/component/fakeip/memory.go index 00eff810..0a8492f8 100644 --- a/component/fakeip/memory.go +++ b/component/fakeip/memory.go @@ -67,8 +67,9 @@ func (m *memoryStore) CloneTo(store store) { // FlushFakeIP implements store.FlushFakeIP func (m *memoryStore) FlushFakeIP() error { - _ = m.cacheIP.Clear() - return m.cacheHost.Clear() + m.cacheIP.Clear() + m.cacheHost.Clear() + return nil } func newMemoryStore(size int) *memoryStore { diff --git a/component/fakeip/pool.go b/component/fakeip/pool.go index 2b06fc0b..41b848b3 100644 --- a/component/fakeip/pool.go +++ b/component/fakeip/pool.go @@ -8,7 +8,7 @@ import ( "github.com/metacubex/mihomo/common/nnip" "github.com/metacubex/mihomo/component/profile/cachefile" - "github.com/metacubex/mihomo/component/trie" + C "github.com/metacubex/mihomo/constant" ) const ( @@ -35,7 +35,8 @@ type Pool struct { offset netip.Addr cycle bool mux sync.Mutex - host *trie.DomainTrie[struct{}] + host []C.DomainMatcher + mode C.FilterMode ipnet netip.Prefix store store } @@ -66,10 +67,20 @@ func (p *Pool) LookBack(ip netip.Addr) (string, bool) { // ShouldSkipped return if domain should be skipped func (p *Pool) ShouldSkipped(domain string) bool { - if p.host == nil { - return false + should := p.shouldSkipped(domain) + if p.mode == C.FilterWhiteList { + return !should } - return p.host.Search(domain) != nil + return should +} + +func (p *Pool) shouldSkipped(domain string) bool { + for _, matcher := range p.host { + if matcher.MatchDomain(domain) { + return true + } + } + return false } // Exist returns if given ip exists in fake-ip pool @@ -154,7 +165,8 @@ func (p *Pool) restoreState() { type Options struct { IPNet netip.Prefix - Host *trie.DomainTrie[struct{}] + Host []C.DomainMatcher + Mode C.FilterMode // Size sets the maximum number of entries in memory // and does not work if Persistence is true @@ -185,12 +197,11 @@ func New(options Options) (*Pool, error) { offset: first.Prev(), cycle: false, host: options.Host, + mode: options.Mode, ipnet: options.IPNet, } if options.Persistence { - pool.store = &cachefileStore{ - cache: cachefile.Cache(), - } + pool.store = newCachefileStore(cachefile.Cache()) } else { pool.store = newMemoryStore(options.Size) } diff --git a/component/fakeip/pool_test.go b/component/fakeip/pool_test.go index cc50fcf7..be78b87c 100644 --- a/component/fakeip/pool_test.go +++ b/component/fakeip/pool_test.go @@ -9,8 +9,9 @@ import ( "github.com/metacubex/mihomo/component/profile/cachefile" "github.com/metacubex/mihomo/component/trie" + C "github.com/metacubex/mihomo/constant" - "github.com/sagernet/bbolt" + "github.com/metacubex/bbolt" "github.com/stretchr/testify/assert" ) @@ -42,9 +43,7 @@ func createCachefileStore(options Options) (*Pool, string, error) { return nil, "", err } - pool.store = &cachefileStore{ - cache: &cachefile.CacheFile{DB: db}, - } + pool.store = newCachefileStore(&cachefile.CacheFile{DB: db}) return pool, f.Name(), nil } @@ -62,13 +61,13 @@ func TestPool_Basic(t *testing.T) { last := pool.Lookup("bar.com") bar, exist := pool.LookBack(last) - assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 4})) - assert.True(t, pool.Lookup("foo.com") == netip.AddrFrom4([4]byte{192, 168, 0, 4})) - assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5})) + assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4})) + assert.Equal(t, pool.Lookup("foo.com"), netip.AddrFrom4([4]byte{192, 168, 0, 4})) + assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5})) assert.True(t, exist) assert.Equal(t, bar, "bar.com") - assert.True(t, pool.Gateway() == netip.AddrFrom4([4]byte{192, 168, 0, 1})) - assert.True(t, pool.Broadcast() == netip.AddrFrom4([4]byte{192, 168, 0, 15})) + assert.Equal(t, pool.Gateway(), netip.AddrFrom4([4]byte{192, 168, 0, 1})) + assert.Equal(t, pool.Broadcast(), netip.AddrFrom4([4]byte{192, 168, 0, 15})) assert.Equal(t, pool.IPNet().String(), ipnet.String()) assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5}))) assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 6}))) @@ -90,13 +89,13 @@ func TestPool_BasicV6(t *testing.T) { last := pool.Lookup("bar.com") bar, exist := pool.LookBack(last) - assert.True(t, first == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) - assert.True(t, pool.Lookup("foo.com") == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) - assert.True(t, last == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")) + assert.Equal(t, first, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) + assert.Equal(t, pool.Lookup("foo.com"), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) + assert.Equal(t, last, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")) assert.True(t, exist) assert.Equal(t, bar, "bar.com") - assert.True(t, pool.Gateway() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801")) - assert.True(t, pool.Broadcast() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff")) + assert.Equal(t, pool.Gateway(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801")) + assert.Equal(t, pool.Broadcast(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff")) assert.Equal(t, pool.IPNet().String(), ipnet.String()) assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805"))) assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8806"))) @@ -142,19 +141,20 @@ func TestPool_CycleUsed(t *testing.T) { } baz := pool.Lookup("baz.com") next := pool.Lookup("foo.com") - assert.True(t, foo == baz) - assert.True(t, next == bar) + assert.Equal(t, foo, baz) + assert.Equal(t, next, bar) } } func TestPool_Skip(t *testing.T) { ipnet := netip.MustParsePrefix("192.168.0.1/29") tree := trie.New[struct{}]() - tree.Insert("example.com", struct{}{}) + assert.NoError(t, tree.Insert("example.com", struct{}{})) + assert.False(t, tree.IsEmpty()) pools, tempfile, err := createPools(Options{ IPNet: ipnet, Size: 10, - Host: tree, + Host: []C.DomainMatcher{tree.NewDomainSet()}, }) assert.Nil(t, err) defer os.Remove(tempfile) @@ -162,6 +162,28 @@ func TestPool_Skip(t *testing.T) { for _, pool := range pools { assert.True(t, pool.ShouldSkipped("example.com")) assert.False(t, pool.ShouldSkipped("foo.com")) + assert.False(t, pool.shouldSkipped("baz.com")) + } +} + +func TestPool_SkipWhiteList(t *testing.T) { + ipnet := netip.MustParsePrefix("192.168.0.1/29") + tree := trie.New[struct{}]() + assert.NoError(t, tree.Insert("example.com", struct{}{})) + assert.False(t, tree.IsEmpty()) + pools, tempfile, err := createPools(Options{ + IPNet: ipnet, + Size: 10, + Host: []C.DomainMatcher{tree.NewDomainSet()}, + Mode: C.FilterWhiteList, + }) + assert.Nil(t, err) + defer os.Remove(tempfile) + + for _, pool := range pools { + assert.False(t, pool.ShouldSkipped("example.com")) + assert.True(t, pool.ShouldSkipped("foo.com")) + assert.True(t, pool.ShouldSkipped("baz.com")) } } @@ -177,7 +199,7 @@ func TestPool_MaxCacheSize(t *testing.T) { pool.Lookup("baz.com") next := pool.Lookup("foo.com") - assert.False(t, first == next) + assert.NotEqual(t, first, next) } func TestPool_DoubleMapping(t *testing.T) { @@ -207,7 +229,7 @@ func TestPool_DoubleMapping(t *testing.T) { assert.False(t, bazExist) assert.True(t, barExist) - assert.False(t, bazIP == newBazIP) + assert.NotEqual(t, bazIP, newBazIP) } func TestPool_Clone(t *testing.T) { @@ -219,8 +241,8 @@ func TestPool_Clone(t *testing.T) { first := pool.Lookup("foo.com") last := pool.Lookup("bar.com") - assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 4})) - assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5})) + assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4})) + assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5})) newPool, _ := New(Options{ IPNet: ipnet, @@ -265,13 +287,13 @@ func TestPool_FlushFileCache(t *testing.T) { baz := pool.Lookup("foo.com") nero := pool.Lookup("foo.com") - assert.True(t, foo == fox) - assert.True(t, foo == next) - assert.False(t, foo == baz) - assert.True(t, bar == bax) - assert.True(t, bar == baz) - assert.False(t, bar == next) - assert.True(t, baz == nero) + assert.Equal(t, foo, fox) + assert.Equal(t, foo, next) + assert.NotEqual(t, foo, baz) + assert.Equal(t, bar, bax) + assert.Equal(t, bar, baz) + assert.NotEqual(t, bar, next) + assert.Equal(t, baz, nero) } } @@ -294,11 +316,11 @@ func TestPool_FlushMemoryCache(t *testing.T) { baz := pool.Lookup("foo.com") nero := pool.Lookup("foo.com") - assert.True(t, foo == fox) - assert.True(t, foo == next) - assert.False(t, foo == baz) - assert.True(t, bar == bax) - assert.True(t, bar == baz) - assert.False(t, bar == next) - assert.True(t, baz == nero) + assert.Equal(t, foo, fox) + assert.Equal(t, foo, next) + assert.NotEqual(t, foo, baz) + assert.Equal(t, bar, bax) + assert.Equal(t, bar, baz) + assert.NotEqual(t, bar, next) + assert.Equal(t, baz, nero) } diff --git a/component/geodata/attr.go b/component/geodata/attr.go index a9742aca..2fd41ad6 100644 --- a/component/geodata/attr.go +++ b/component/geodata/attr.go @@ -7,7 +7,7 @@ import ( ) type AttributeList struct { - matcher []AttributeMatcher + matcher []BooleanMatcher } func (al *AttributeList) Match(domain *router.Domain) bool { @@ -23,6 +23,14 @@ func (al *AttributeList) IsEmpty() bool { return len(al.matcher) == 0 } +func (al *AttributeList) String() string { + matcher := make([]string, len(al.matcher)) + for i, match := range al.matcher { + matcher[i] = string(match) + } + return strings.Join(matcher, ",") +} + func parseAttrs(attrs []string) *AttributeList { al := new(AttributeList) for _, attr := range attrs { diff --git a/component/geodata/init.go b/component/geodata/init.go index 834567a4..08ec1b94 100644 --- a/component/geodata/init.go +++ b/component/geodata/init.go @@ -6,8 +6,10 @@ import ( "io" "net/http" "os" + "sync" "time" + "github.com/metacubex/mihomo/common/atomic" mihomoHttp "github.com/metacubex/mihomo/component/http" "github.com/metacubex/mihomo/component/mmdb" C "github.com/metacubex/mihomo/constant" @@ -18,12 +20,79 @@ var ( initGeoSite bool initGeoIP int initASN bool + + initGeoSiteMutex sync.Mutex + initGeoIPMutex sync.Mutex + initASNMutex sync.Mutex + + geoIpEnable atomic.Bool + geoSiteEnable atomic.Bool + asnEnable atomic.Bool + + geoIpUrl string + mmdbUrl string + geoSiteUrl string + asnUrl string ) +func GeoIpUrl() string { + return geoIpUrl +} + +func SetGeoIpUrl(url string) { + geoIpUrl = url +} + +func MmdbUrl() string { + return mmdbUrl +} + +func SetMmdbUrl(url string) { + mmdbUrl = url +} + +func GeoSiteUrl() string { + return geoSiteUrl +} + +func SetGeoSiteUrl(url string) { + geoSiteUrl = url +} + +func ASNUrl() string { + return asnUrl +} + +func SetASNUrl(url string) { + asnUrl = url +} + +func downloadToPath(url string, path string) (err error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil) + if err != nil { + return + } + defer resp.Body.Close() + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + + return err +} + func InitGeoSite() error { + geoSiteEnable.Store(true) + initGeoSiteMutex.Lock() + defer initGeoSiteMutex.Unlock() if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { log.Infoln("Can't find GeoSite.dat, start download") - if err := downloadGeoSite(C.Path.GeoSite()); err != nil { + if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil { return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) } log.Infoln("Download GeoSite.dat finish") @@ -35,7 +104,7 @@ func InitGeoSite() error { if err := os.Remove(C.Path.GeoSite()); err != nil { return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error()) } - if err := downloadGeoSite(C.Path.GeoSite()); err != nil { + if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil { return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) } } @@ -44,49 +113,14 @@ func InitGeoSite() error { return nil } -func downloadGeoSite(path string) (err error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, C.GeoSiteUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) - if err != nil { - return - } - defer resp.Body.Close() - - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(f, resp.Body) - - return err -} - -func downloadGeoIP(path string) (err error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, C.GeoIpUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) - if err != nil { - return - } - defer resp.Body.Close() - - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(f, resp.Body) - - return err -} - func InitGeoIP() error { - if C.GeodataMode { + geoIpEnable.Store(true) + initGeoIPMutex.Lock() + defer initGeoIPMutex.Unlock() + if GeodataMode() { if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) { log.Infoln("Can't find GeoIP.dat, start download") - if err := downloadGeoIP(C.Path.GeoIP()); err != nil { + if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil { return fmt.Errorf("can't download GeoIP.dat: %s", err.Error()) } log.Infoln("Download GeoIP.dat finish") @@ -99,7 +133,7 @@ func InitGeoIP() error { if err := os.Remove(C.Path.GeoIP()); err != nil { return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error()) } - if err := downloadGeoIP(C.Path.GeoIP()); err != nil { + if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil { return fmt.Errorf("can't download GeoIP.dat: %s", err.Error()) } } @@ -110,7 +144,7 @@ func InitGeoIP() error { if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) { log.Infoln("Can't find MMDB, start download") - if err := mmdb.DownloadMMDB(C.Path.MMDB()); err != nil { + if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil { return fmt.Errorf("can't download MMDB: %s", err.Error()) } } @@ -121,7 +155,7 @@ func InitGeoIP() error { if err := os.Remove(C.Path.MMDB()); err != nil { return fmt.Errorf("can't remove invalid MMDB: %s", err.Error()) } - if err := mmdb.DownloadMMDB(C.Path.MMDB()); err != nil { + if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil { return fmt.Errorf("can't download MMDB: %s", err.Error()) } } @@ -131,9 +165,12 @@ func InitGeoIP() error { } func InitASN() error { + asnEnable.Store(true) + initASNMutex.Lock() + defer initASNMutex.Unlock() if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) { log.Infoln("Can't find ASN.mmdb, start download") - if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { + if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil { return fmt.Errorf("can't download ASN.mmdb: %s", err.Error()) } log.Infoln("Download ASN.mmdb finish") @@ -145,7 +182,7 @@ func InitASN() error { if err := os.Remove(C.Path.ASN()); err != nil { return fmt.Errorf("can't remove invalid ASN: %s", err.Error()) } - if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { + if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil { return fmt.Errorf("can't download ASN: %s", err.Error()) } } @@ -153,3 +190,15 @@ func InitASN() error { } return nil } + +func GeoIpEnable() bool { + return geoIpEnable.Load() +} + +func GeoSiteEnable() bool { + return geoSiteEnable.Load() +} + +func ASNEnable() bool { + return asnEnable.Load() +} diff --git a/component/geodata/router/condition.go b/component/geodata/router/condition.go index 5261d2fe..fb47e4a4 100644 --- a/component/geodata/router/condition.go +++ b/component/geodata/router/condition.go @@ -33,12 +33,13 @@ func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) { type DomainMatcher interface { ApplyDomain(string) bool + Count() int } type succinctDomainMatcher struct { set *trie.DomainSet otherMatchers []strmatcher.Matcher - not bool + count int } func (m *succinctDomainMatcher) ApplyDomain(domain string) bool { @@ -51,16 +52,17 @@ func (m *succinctDomainMatcher) ApplyDomain(domain string) bool { } } } - if m.not { - isMatched = !isMatched - } return isMatched } -func NewSuccinctMatcherGroup(domains []*Domain, not bool) (DomainMatcher, error) { +func (m *succinctDomainMatcher) Count() int { + return m.count +} + +func NewSuccinctMatcherGroup(domains []*Domain) (DomainMatcher, error) { t := trie.New[struct{}]() m := &succinctDomainMatcher{ - not: not, + count: len(domains), } for _, d := range domains { switch d.Type { @@ -90,10 +92,10 @@ func NewSuccinctMatcherGroup(domains []*Domain, not bool) (DomainMatcher, error) type v2rayDomainMatcher struct { matchers strmatcher.IndexMatcher - not bool + count int } -func NewMphMatcherGroup(domains []*Domain, not bool) (DomainMatcher, error) { +func NewMphMatcherGroup(domains []*Domain) (DomainMatcher, error) { g := strmatcher.NewMphMatcherGroup() for _, d := range domains { matcherType, f := matcherTypeMap[d.Type] @@ -108,119 +110,80 @@ func NewMphMatcherGroup(domains []*Domain, not bool) (DomainMatcher, error) { g.Build() return &v2rayDomainMatcher{ matchers: g, - not: not, + count: len(domains), }, nil } func (m *v2rayDomainMatcher) ApplyDomain(domain string) bool { - isMatched := len(m.matchers.Match(strings.ToLower(domain))) > 0 - if m.not { - isMatched = !isMatched - } - return isMatched + return len(m.matchers.Match(strings.ToLower(domain))) > 0 } -type GeoIPMatcher struct { - countryCode string - reverseMatch bool - cidrSet *cidr.IpCidrSet +func (m *v2rayDomainMatcher) Count() int { + return m.count } -func (m *GeoIPMatcher) Init(cidrs []*CIDR) error { - for _, cidr := range cidrs { - addr, ok := netip.AddrFromSlice(cidr.Ip) - if !ok { - return fmt.Errorf("error when loading GeoIP: invalid IP: %s", cidr.Ip) - } - err := m.cidrSet.AddIpCidr(netip.PrefixFrom(addr, int(cidr.Prefix))) - if err != nil { - return fmt.Errorf("error when loading GeoIP: %w", err) - } - } - return m.cidrSet.Merge() +type notDomainMatcher struct { + DomainMatcher } -func (m *GeoIPMatcher) SetReverseMatch(isReverseMatch bool) { - m.reverseMatch = isReverseMatch +func (m notDomainMatcher) ApplyDomain(domain string) bool { + return !m.DomainMatcher.ApplyDomain(domain) +} + +func NewNotDomainMatcherGroup(matcher DomainMatcher) DomainMatcher { + return notDomainMatcher{matcher} +} + +type IPMatcher interface { + Match(ip netip.Addr) bool + Count() int +} + +type geoIPMatcher struct { + cidrSet *cidr.IpCidrSet + count int } // Match returns true if the given ip is included by the GeoIP. -func (m *GeoIPMatcher) Match(ip netip.Addr) bool { - match := m.cidrSet.IsContain(ip) - if m.reverseMatch { - return !match +func (m *geoIPMatcher) Match(ip netip.Addr) bool { + return m.cidrSet.IsContain(ip) +} + +func (m *geoIPMatcher) Count() int { + return m.count +} + +func NewGeoIPMatcher(cidrList []*CIDR) (IPMatcher, error) { + m := &geoIPMatcher{ + cidrSet: cidr.NewIpCidrSet(), + count: len(cidrList), } - return match -} - -// GeoIPMatcherContainer is a container for GeoIPMatchers. It keeps unique copies of GeoIPMatcher by country code. -type GeoIPMatcherContainer struct { - matchers []*GeoIPMatcher -} - -// Add adds a new GeoIP set into the container. -// If the country code of GeoIP is not empty, GeoIPMatcherContainer will try to find an existing one, instead of adding a new one. -func (c *GeoIPMatcherContainer) Add(geoip *GeoIP) (*GeoIPMatcher, error) { - if len(geoip.CountryCode) > 0 { - for _, m := range c.matchers { - if m.countryCode == geoip.CountryCode && m.reverseMatch == geoip.ReverseMatch { - return m, nil - } + for _, cidr := range cidrList { + addr, ok := netip.AddrFromSlice(cidr.Ip) + if !ok { + return nil, fmt.Errorf("error when loading GeoIP: invalid IP: %s", cidr.Ip) + } + err := m.cidrSet.AddIpCidr(netip.PrefixFrom(addr, int(cidr.Prefix))) + if err != nil { + return nil, fmt.Errorf("error when loading GeoIP: %w", err) } } - - m := &GeoIPMatcher{ - countryCode: geoip.CountryCode, - reverseMatch: geoip.ReverseMatch, - cidrSet: cidr.NewIpCidrSet(), - } - if err := m.Init(geoip.Cidr); err != nil { - return nil, err - } - if len(geoip.CountryCode) > 0 { - c.matchers = append(c.matchers, m) - } - return m, nil -} - -var globalGeoIPContainer GeoIPMatcherContainer - -type MultiGeoIPMatcher struct { - matchers []*GeoIPMatcher -} - -func NewGeoIPMatcher(geoip *GeoIP) (*GeoIPMatcher, error) { - matcher, err := globalGeoIPContainer.Add(geoip) + err := m.cidrSet.Merge() if err != nil { return nil, err } - return matcher, nil + return m, nil } -func (m *MultiGeoIPMatcher) ApplyIp(ip netip.Addr) bool { - for _, matcher := range m.matchers { - if matcher.Match(ip) { - return true - } - } - - return false +type notIPMatcher struct { + IPMatcher } -func NewMultiGeoIPMatcher(geoips []*GeoIP) (*MultiGeoIPMatcher, error) { - var matchers []*GeoIPMatcher - for _, geoip := range geoips { - matcher, err := globalGeoIPContainer.Add(geoip) - if err != nil { - return nil, err - } - matchers = append(matchers, matcher) - } - - matcher := &MultiGeoIPMatcher{ - matchers: matchers, - } - - return matcher, nil +func (m notIPMatcher) Match(ip netip.Addr) bool { + return !m.IPMatcher.Match(ip) +} + +func NewNotIpMatcherGroup(matcher IPMatcher) IPMatcher { + return notIPMatcher{matcher} } diff --git a/component/geodata/utils.go b/component/geodata/utils.go index 981d7eba..47966249 100644 --- a/component/geodata/utils.go +++ b/component/geodata/utils.go @@ -5,8 +5,7 @@ import ( "fmt" "strings" - "golang.org/x/sync/singleflight" - + "github.com/metacubex/mihomo/common/singleflight" "github.com/metacubex/mihomo/component/geodata/router" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" @@ -14,8 +13,6 @@ import ( var ( geoMode bool - AutoUpdate bool - UpdateInterval int geoLoaderName = "memconservative" geoSiteMatcher = "succinct" ) @@ -26,14 +23,6 @@ func GeodataMode() bool { return geoMode } -func GeoAutoUpdate() bool { - return AutoUpdate -} - -func GeoUpdateInterval() int { - return UpdateInterval -} - func LoaderName() string { return geoLoaderName } @@ -45,12 +34,6 @@ func SiteMatcherName() string { func SetGeodataMode(newGeodataMode bool) { geoMode = newGeodataMode } -func SetGeoAutoUpdate(newAutoUpdate bool) { - AutoUpdate = newAutoUpdate -} -func SetGeoUpdateInterval(newGeoUpdateInterval int) { - UpdateInterval = newGeoUpdateInterval -} func SetLoader(newLoader string) { if newLoader == "memc" { @@ -71,21 +54,22 @@ func SetSiteMatcher(newMatcher string) { func Verify(name string) error { switch name { case C.GeositeName: - _, _, err := LoadGeoSiteMatcher("CN") + _, err := LoadGeoSiteMatcher("CN") return err case C.GeoipName: - _, _, err := LoadGeoIPMatcher("CN") + _, err := LoadGeoIPMatcher("CN") return err default: return fmt.Errorf("not support name") } } -var loadGeoSiteMatcherSF = singleflight.Group{} +var loadGeoSiteMatcherListSF = singleflight.Group[[]*router.Domain]{StoreResult: true} +var loadGeoSiteMatcherSF = singleflight.Group[router.DomainMatcher]{StoreResult: true} -func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, int, error) { +func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, error) { if countryCode == "" { - return nil, 0, fmt.Errorf("country code could not be empty") + return nil, fmt.Errorf("country code could not be empty") } not := false @@ -97,73 +81,84 @@ func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, int, error) { parts := strings.Split(countryCode, "@") if len(parts) == 0 { - return nil, 0, errors.New("empty rule") + return nil, errors.New("empty rule") } listName := strings.TrimSpace(parts[0]) attrVal := parts[1:] + attrs := parseAttrs(attrVal) if listName == "" { - return nil, 0, fmt.Errorf("empty listname in rule: %s", countryCode) + return nil, fmt.Errorf("empty listname in rule: %s", countryCode) } - v, err, shared := loadGeoSiteMatcherSF.Do(listName, func() (interface{}, error) { - geoLoader, err := GetGeoDataLoader(geoLoaderName) + matcherName := listName + if !attrs.IsEmpty() { + matcherName += "@" + attrs.String() + } + matcher, err, shared := loadGeoSiteMatcherSF.Do(matcherName, func() (router.DomainMatcher, error) { + log.Infoln("Load GeoSite rule: %s", matcherName) + domains, err, shared := loadGeoSiteMatcherListSF.Do(listName, func() ([]*router.Domain, error) { + geoLoader, err := GetGeoDataLoader(geoLoaderName) + if err != nil { + return nil, err + } + return geoLoader.LoadGeoSite(listName) + }) if err != nil { + if !shared { + loadGeoSiteMatcherListSF.Forget(listName) // don't store the error result + } return nil, err } - return geoLoader.LoadGeoSite(listName) + + if attrs.IsEmpty() { + if strings.Contains(countryCode, "@") { + log.Warnln("empty attribute list: %s", countryCode) + } + } else { + filteredDomains := make([]*router.Domain, 0, len(domains)) + hasAttrMatched := false + for _, domain := range domains { + if attrs.Match(domain) { + hasAttrMatched = true + filteredDomains = append(filteredDomains, domain) + } + } + if !hasAttrMatched { + log.Warnln("attribute match no rule: geosite: %s", countryCode) + } + domains = filteredDomains + } + + /** + linear: linear algorithm + matcher, err := router.NewDomainMatcher(domains) + mph:minimal perfect hash algorithm + */ + if geoSiteMatcher == "mph" { + return router.NewMphMatcherGroup(domains) + } else { + return router.NewSuccinctMatcherGroup(domains) + } }) if err != nil { if !shared { - loadGeoSiteMatcherSF.Forget(listName) // don't store the error result + loadGeoSiteMatcherSF.Forget(matcherName) // don't store the error result } - return nil, 0, err + return nil, 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 + if not { + matcher = router.NewNotDomainMatcherGroup(matcher) } - /** - linear: linear algorithm - matcher, err := router.NewDomainMatcher(domains) - mph:minimal perfect hash algorithm - */ - var matcher router.DomainMatcher - if geoSiteMatcher == "mph" { - matcher, err = router.NewMphMatcherGroup(domains, not) - } else { - matcher, err = router.NewSuccinctMatcherGroup(domains, not) - } - if err != nil { - return nil, 0, err - } - - return matcher, len(domains), nil + return matcher, nil } -var loadGeoIPMatcherSF = singleflight.Group{} +var loadGeoIPMatcherSF = singleflight.Group[router.IPMatcher]{StoreResult: true} -func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) { +func LoadGeoIPMatcher(country string) (router.IPMatcher, error) { if len(country) == 0 { - return nil, 0, fmt.Errorf("country code could not be empty") + return nil, fmt.Errorf("country code could not be empty") } not := false @@ -173,35 +168,36 @@ func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) { } country = strings.ToLower(country) - v, err, shared := loadGeoIPMatcherSF.Do(country, func() (interface{}, error) { + matcher, err, shared := loadGeoIPMatcherSF.Do(country, func() (router.IPMatcher, error) { + log.Infoln("Load GeoIP rule: %s", country) geoLoader, err := GetGeoDataLoader(geoLoaderName) if err != nil { return nil, err } - return geoLoader.LoadGeoIP(country) + cidrList, err := geoLoader.LoadGeoIP(country) + if err != nil { + return nil, err + } + return router.NewGeoIPMatcher(cidrList) }) if err != nil { if !shared { loadGeoIPMatcherSF.Forget(country) // don't store the error result + log.Warnln("Load GeoIP rule: %s", country) } - return nil, 0, err + return nil, err } - records := v.([]*router.CIDR) - - geoIP := &router.GeoIP{ - CountryCode: country, - Cidr: records, - ReverseMatch: not, + if not { + matcher = router.NewNotIpMatcherGroup(matcher) } - - matcher, err := router.NewGeoIPMatcher(geoIP) - if err != nil { - return nil, 0, err - } - return matcher, len(records), nil + return matcher, nil } -func ClearCache() { - loadGeoSiteMatcherSF = singleflight.Group{} - loadGeoIPMatcherSF = singleflight.Group{} +func ClearGeoSiteCache() { + loadGeoSiteMatcherListSF.Reset() + loadGeoSiteMatcherSF.Reset() +} + +func ClearGeoIPCache() { + loadGeoIPMatcherSF.Reset() } diff --git a/component/http/http.go b/component/http/http.go index 455db681..63ea5be7 100644 --- a/component/http/http.go +++ b/component/http/http.go @@ -12,12 +12,27 @@ import ( "time" "github.com/metacubex/mihomo/component/ca" - C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/listener/inner" ) +var ( + ua string +) + +func UA() string { + return ua +} + +func SetUA(UA string) { + ua = UA +} + func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) { - UA := C.UA + return HttpRequestWithProxy(ctx, url, method, header, body, "") +} + +func HttpRequestWithProxy(ctx context.Context, url, method string, header map[string][]string, body io.Reader, specialProxy string) (*http.Response, error) { method = strings.ToUpper(method) urlRes, err := URL.Parse(url) if err != nil { @@ -32,7 +47,7 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st } if _, ok := header["User-Agent"]; !ok { - req.Header.Set("User-Agent", UA) + req.Header.Set("User-Agent", UA()) } if err != nil { @@ -54,11 +69,10 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - if conn, err := inner.HandleTcp(address); err == nil { + if conn, err := inner.HandleTcp(address, specialProxy); err == nil { return conn, nil } else { - d := net.Dialer{} - return d.DialContext(ctx, network, address) + return dialer.DialContext(ctx, network, address) } }, TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}), @@ -66,5 +80,4 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st client := http.Client{Transport: transport} return client.Do(req) - } diff --git a/component/iface/iface.go b/component/iface/iface.go index bf186165..a0fa4d5b 100644 --- a/component/iface/iface.go +++ b/component/iface/iface.go @@ -4,7 +4,6 @@ import ( "errors" "net" "net/netip" - "strings" "time" "github.com/metacubex/mihomo/common/singledo" @@ -12,9 +11,11 @@ import ( type Interface struct { Index int + MTU int Name string - Addrs []netip.Prefix + Addresses []netip.Prefix HardwareAddr net.HardwareAddr + Flags net.Flags } var ( @@ -24,7 +25,7 @@ var ( var interfaces = singledo.NewSingle[map[string]*Interface](time.Second * 20) -func ResolveInterface(name string) (*Interface, error) { +func Interfaces() (map[string]*Interface, error) { value, err, _ := interfaces.Do(func() (map[string]*Interface, error) { ifaces, err := net.Interfaces() if err != nil { @@ -38,45 +39,49 @@ 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)) 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 { + var pf netip.Prefix + switch ipNet := addr.(type) { + case *net.IPNet: + ip, _ := netip.AddrFromSlice(ipNet.IP) + ones, bits := ipNet.Mask.Size() + if bits == 32 { + ip = ip.Unmap() + } + pf = netip.PrefixFrom(ip, ones) + case *net.IPAddr: + ip, _ := netip.AddrFromSlice(ipNet.IP) ip = ip.Unmap() + pf = netip.PrefixFrom(ip, ip.BitLen()) + } + if pf.IsValid() { + ipNets = append(ipNets, pf) } - - pf := netip.PrefixFrom(ip, ones) - ipNets = append(ipNets, pf) } r[iface.Name] = &Interface{ Index: iface.Index, + MTU: iface.MTU, Name: iface.Name, - Addrs: ipNets, + Addresses: ipNets, HardwareAddr: iface.HardwareAddr, + Flags: iface.Flags, } } return r, nil }) + return value, err +} + +func ResolveInterface(name string) (*Interface, error) { + ifaces, err := Interfaces() if err != nil { return nil, err } - ifaces := value iface, ok := ifaces[name] if !ok { return nil, ErrIfaceNotFound @@ -85,6 +90,21 @@ func ResolveInterface(name string) (*Interface, error) { return iface, nil } +func IsLocalIp(ip netip.Addr) (bool, error) { + ifaces, err := Interfaces() + if err != nil { + return false, err + } + for _, iface := range ifaces { + for _, addr := range iface.Addresses { + if addr.Contains(ip) { + return true, nil + } + } + } + return false, nil +} + func FlushCache() { interfaces.Reset() } @@ -104,7 +124,7 @@ func (iface *Interface) PickIPv6Addr(destination netip.Addr) (netip.Prefix, erro 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 { + for _, addr := range iface.Addresses { if !accept(addr) { continue } diff --git a/component/keepalive/tcp_keepalive.go b/component/keepalive/tcp_keepalive.go new file mode 100644 index 00000000..14ec862b --- /dev/null +++ b/component/keepalive/tcp_keepalive.go @@ -0,0 +1,65 @@ +package keepalive + +import ( + "net" + "runtime" + "time" + + "github.com/metacubex/mihomo/common/atomic" + "github.com/metacubex/mihomo/common/utils" +) + +var ( + keepAliveIdle = atomic.NewTypedValue[time.Duration](0 * time.Second) + keepAliveInterval = atomic.NewTypedValue[time.Duration](0 * time.Second) + disableKeepAlive = atomic.NewBool(false) + + SetDisableKeepAliveCallback = utils.NewCallback[bool]() +) + +func SetKeepAliveIdle(t time.Duration) { + keepAliveIdle.Store(t) +} + +func SetKeepAliveInterval(t time.Duration) { + keepAliveInterval.Store(t) +} + +func KeepAliveIdle() time.Duration { + return keepAliveIdle.Load() +} + +func KeepAliveInterval() time.Duration { + return keepAliveInterval.Load() +} + +func SetDisableKeepAlive(disable bool) { + if runtime.GOOS == "android" { + setDisableKeepAlive(true) + } else { + setDisableKeepAlive(disable) + } +} + +func setDisableKeepAlive(disable bool) { + disableKeepAlive.Store(disable) + SetDisableKeepAliveCallback.Emit(disable) +} + +func DisableKeepAlive() bool { + return disableKeepAlive.Load() +} + +func SetNetDialer(dialer *net.Dialer) { + setNetDialer(dialer) +} + +func SetNetListenConfig(lc *net.ListenConfig) { + setNetListenConfig(lc) +} + +func TCPKeepAlive(c net.Conn) { + if tcp, ok := c.(*net.TCPConn); ok && tcp != nil { + tcpKeepAlive(tcp) + } +} diff --git a/component/keepalive/tcp_keepalive_go122.go b/component/keepalive/tcp_keepalive_go122.go new file mode 100644 index 00000000..5d88827d --- /dev/null +++ b/component/keepalive/tcp_keepalive_go122.go @@ -0,0 +1,30 @@ +//go:build !go1.23 + +package keepalive + +import "net" + +func tcpKeepAlive(tcp *net.TCPConn) { + if DisableKeepAlive() { + _ = tcp.SetKeepAlive(false) + } else { + _ = tcp.SetKeepAlive(true) + _ = tcp.SetKeepAlivePeriod(KeepAliveInterval()) + } +} + +func setNetDialer(dialer *net.Dialer) { + if DisableKeepAlive() { + dialer.KeepAlive = -1 // If negative, keep-alive probes are disabled. + } else { + dialer.KeepAlive = KeepAliveInterval() + } +} + +func setNetListenConfig(lc *net.ListenConfig) { + if DisableKeepAlive() { + lc.KeepAlive = -1 // If negative, keep-alive probes are disabled. + } else { + lc.KeepAlive = KeepAliveInterval() + } +} diff --git a/component/keepalive/tcp_keepalive_go123.go b/component/keepalive/tcp_keepalive_go123.go new file mode 100644 index 00000000..4c08118b --- /dev/null +++ b/component/keepalive/tcp_keepalive_go123.go @@ -0,0 +1,45 @@ +//go:build go1.23 + +package keepalive + +import "net" + +func keepAliveConfig() net.KeepAliveConfig { + config := net.KeepAliveConfig{ + Enable: true, + Idle: KeepAliveIdle(), + Interval: KeepAliveInterval(), + } + if !SupportTCPKeepAliveCount() { + // it's recommended to set both Idle and Interval to non-negative values in conjunction with a -1 + // for Count on those old Windows if you intend to customize the TCP keep-alive settings. + config.Count = -1 + } + return config +} + +func tcpKeepAlive(tcp *net.TCPConn) { + if DisableKeepAlive() { + _ = tcp.SetKeepAlive(false) + } else { + _ = tcp.SetKeepAliveConfig(keepAliveConfig()) + } +} + +func setNetDialer(dialer *net.Dialer) { + if DisableKeepAlive() { + dialer.KeepAlive = -1 // If negative, keep-alive probes are disabled. + dialer.KeepAliveConfig.Enable = false + } else { + dialer.KeepAliveConfig = keepAliveConfig() + } +} + +func setNetListenConfig(lc *net.ListenConfig) { + if DisableKeepAlive() { + lc.KeepAlive = -1 // If negative, keep-alive probes are disabled. + lc.KeepAliveConfig.Enable = false + } else { + lc.KeepAliveConfig = keepAliveConfig() + } +} diff --git a/component/keepalive/tcp_keepalive_go123_unix.go b/component/keepalive/tcp_keepalive_go123_unix.go new file mode 100644 index 00000000..8033cc6c --- /dev/null +++ b/component/keepalive/tcp_keepalive_go123_unix.go @@ -0,0 +1,15 @@ +//go:build go1.23 && unix + +package keepalive + +func SupportTCPKeepAliveIdle() bool { + return true +} + +func SupportTCPKeepAliveInterval() bool { + return true +} + +func SupportTCPKeepAliveCount() bool { + return true +} diff --git a/component/keepalive/tcp_keepalive_go123_windows.go b/component/keepalive/tcp_keepalive_go123_windows.go new file mode 100644 index 00000000..2462e80c --- /dev/null +++ b/component/keepalive/tcp_keepalive_go123_windows.go @@ -0,0 +1,63 @@ +//go:build go1.23 && windows + +// copy and modify from golang1.23's internal/syscall/windows/version_windows.go + +package keepalive + +import ( + "errors" + "sync" + "syscall" + + "github.com/metacubex/mihomo/constant/features" + + "golang.org/x/sys/windows" +) + +var ( + supportTCPKeepAliveIdle bool + supportTCPKeepAliveInterval bool + supportTCPKeepAliveCount bool +) + +var initTCPKeepAlive = sync.OnceFunc(func() { + s, err := windows.WSASocket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP, nil, 0, windows.WSA_FLAG_NO_HANDLE_INHERIT) + if err != nil { + // Fallback to checking the Windows version. + major, build := features.WindowsMajorVersion, features.WindowsBuildNumber + supportTCPKeepAliveIdle = major >= 10 && build >= 16299 + supportTCPKeepAliveInterval = major >= 10 && build >= 16299 + supportTCPKeepAliveCount = major >= 10 && build >= 15063 + return + } + defer windows.Closesocket(s) + var optSupported = func(opt int) bool { + err := windows.SetsockoptInt(s, syscall.IPPROTO_TCP, opt, 1) + return !errors.Is(err, syscall.WSAENOPROTOOPT) + } + supportTCPKeepAliveIdle = optSupported(windows.TCP_KEEPIDLE) + supportTCPKeepAliveInterval = optSupported(windows.TCP_KEEPINTVL) + supportTCPKeepAliveCount = optSupported(windows.TCP_KEEPCNT) +}) + +// SupportTCPKeepAliveIdle indicates whether TCP_KEEPIDLE is supported. +// The minimal requirement is Windows 10.0.16299. +func SupportTCPKeepAliveIdle() bool { + initTCPKeepAlive() + return supportTCPKeepAliveIdle +} + +// SupportTCPKeepAliveInterval indicates whether TCP_KEEPINTVL is supported. +// The minimal requirement is Windows 10.0.16299. +func SupportTCPKeepAliveInterval() bool { + initTCPKeepAlive() + return supportTCPKeepAliveInterval +} + +// SupportTCPKeepAliveCount indicates whether TCP_KEEPCNT is supported. +// supports TCP_KEEPCNT. +// The minimal requirement is Windows 10.0.15063. +func SupportTCPKeepAliveCount() bool { + initTCPKeepAlive() + return supportTCPKeepAliveCount +} diff --git a/component/loopback/detector.go b/component/loopback/detector.go new file mode 100644 index 00000000..c639ab22 --- /dev/null +++ b/component/loopback/detector.go @@ -0,0 +1,115 @@ +package loopback + +import ( + "errors" + "fmt" + "net/netip" + "os" + "strconv" + + "github.com/metacubex/mihomo/common/callback" + "github.com/metacubex/mihomo/component/iface" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/features" + + "github.com/puzpuzpuz/xsync/v3" +) + +var disableLoopBackDetector, _ = strconv.ParseBool(os.Getenv("DISABLE_LOOPBACK_DETECTOR")) + +func init() { + if features.CMFA { + disableLoopBackDetector = true + } +} + +var ErrReject = errors.New("reject loopback connection") + +type Detector struct { + connMap *xsync.MapOf[netip.AddrPort, struct{}] + packetConnMap *xsync.MapOf[uint16, struct{}] +} + +func NewDetector() *Detector { + if disableLoopBackDetector { + return nil + } + return &Detector{ + connMap: xsync.NewMapOf[netip.AddrPort, struct{}](), + packetConnMap: xsync.NewMapOf[uint16, struct{}](), + } +} + +func (l *Detector) NewConn(conn C.Conn) C.Conn { + if l == nil || l.connMap == nil { + return conn + } + metadata := C.Metadata{} + if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { + return conn + } + connAddr := metadata.AddrPort() + if !connAddr.IsValid() { + return conn + } + l.connMap.Store(connAddr, struct{}{}) + return callback.NewCloseCallbackConn(conn, func() { + l.connMap.Delete(connAddr) + }) +} + +func (l *Detector) NewPacketConn(conn C.PacketConn) C.PacketConn { + if l == nil || l.packetConnMap == nil { + return conn + } + metadata := C.Metadata{} + if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { + return conn + } + connAddr := metadata.AddrPort() + if !connAddr.IsValid() { + return conn + } + port := connAddr.Port() + l.packetConnMap.Store(port, struct{}{}) + return callback.NewCloseCallbackPacketConn(conn, func() { + l.packetConnMap.Delete(port) + }) +} + +func (l *Detector) CheckConn(metadata *C.Metadata) error { + if l == nil || l.connMap == nil { + return nil + } + connAddr := metadata.SourceAddrPort() + if !connAddr.IsValid() { + return nil + } + if _, ok := l.connMap.Load(connAddr); ok { + return fmt.Errorf("%w to: %s", ErrReject, metadata.RemoteAddress()) + } + return nil +} + +func (l *Detector) CheckPacketConn(metadata *C.Metadata) error { + if l == nil || l.packetConnMap == nil { + return nil + } + connAddr := metadata.SourceAddrPort() + if !connAddr.IsValid() { + return nil + } + + isLocalIp, err := iface.IsLocalIp(connAddr.Addr()) + if err != nil { + return err + } + if !isLocalIp && !connAddr.Addr().IsLoopback() { + return nil + } + + if _, ok := l.packetConnMap.Load(connAddr.Port()); ok { + return fmt.Errorf("%w to: %s", ErrReject, metadata.RemoteAddress()) + } + return nil +} diff --git a/component/mmdb/mmdb.go b/component/mmdb/mmdb.go index 81156bc6..81644b00 100644 --- a/component/mmdb/mmdb.go +++ b/component/mmdb/mmdb.go @@ -1,15 +1,9 @@ package mmdb import ( - "context" - "io" - "net/http" - "os" "sync" - "time" mihomoOnce "github.com/metacubex/mihomo/common/once" - mihomoHttp "github.com/metacubex/mihomo/component/http" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" @@ -25,26 +19,26 @@ const ( ) var ( - IPreader IPReader - ASNreader ASNReader - IPonce sync.Once - ASNonce sync.Once + ipReader IPReader + asnReader ASNReader + ipOnce sync.Once + asnOnce sync.Once ) func LoadFromBytes(buffer []byte) { - IPonce.Do(func() { + ipOnce.Do(func() { mmdb, err := maxminddb.FromBytes(buffer) if err != nil { log.Fatalln("Can't load mmdb: %s", err.Error()) } - IPreader = IPReader{Reader: mmdb} + ipReader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": - IPreader.databaseType = typeSing + ipReader.databaseType = typeSing case "Meta-geoip0": - IPreader.databaseType = typeMetaV0 + ipReader.databaseType = typeMetaV0 default: - IPreader.databaseType = typeMaxmind + ipReader.databaseType = typeMaxmind } }) } @@ -58,83 +52,45 @@ func Verify(path string) bool { } func IPInstance() IPReader { - IPonce.Do(func() { + ipOnce.Do(func() { mmdbPath := C.Path.MMDB() log.Infoln("Load MMDB file: %s", mmdbPath) mmdb, err := maxminddb.Open(mmdbPath) if err != nil { log.Fatalln("Can't load MMDB: %s", err.Error()) } - IPreader = IPReader{Reader: mmdb} + ipReader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": - IPreader.databaseType = typeSing + ipReader.databaseType = typeSing case "Meta-geoip0": - IPreader.databaseType = typeMetaV0 + ipReader.databaseType = typeMetaV0 default: - IPreader.databaseType = typeMaxmind + ipReader.databaseType = typeMaxmind } }) - return IPreader -} - -func DownloadMMDB(path string) (err error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, C.MmdbUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) - if err != nil { - return - } - defer resp.Body.Close() - - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(f, resp.Body) - - return err + return ipReader } func ASNInstance() ASNReader { - ASNonce.Do(func() { + asnOnce.Do(func() { ASNPath := C.Path.ASN() log.Infoln("Load ASN file: %s", ASNPath) asn, err := maxminddb.Open(ASNPath) if err != nil { log.Fatalln("Can't load ASN: %s", err.Error()) } - ASNreader = ASNReader{Reader: asn} + asnReader = ASNReader{Reader: asn} }) - return ASNreader -} - -func DownloadASN(path string) (err error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, C.ASNUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) - if err != nil { - return - } - defer resp.Body.Close() - - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(f, resp.Body) - - return err + return asnReader } func ReloadIP() { - mihomoOnce.Reset(&IPonce) + mihomoOnce.Reset(&ipOnce) } func ReloadASN() { - mihomoOnce.Reset(&ASNonce) + mihomoOnce.Reset(&asnOnce) } diff --git a/component/mmdb/patch_android.go b/component/mmdb/patch_android.go deleted file mode 100644 index 147a3324..00000000 --- a/component/mmdb/patch_android.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build android && cmfa - -package mmdb - -import "github.com/oschwald/maxminddb-golang" - -func InstallOverride(override *maxminddb.Reader) { - newReader := IPReader{Reader: override} - switch override.Metadata.DatabaseType { - case "sing-geoip": - IPreader.databaseType = typeSing - case "Meta-geoip0": - IPreader.databaseType = typeMetaV0 - default: - IPreader.databaseType = typeMaxmind - } - IPreader = newReader -} diff --git a/component/mmdb/reader.go b/component/mmdb/reader.go index e76e9939..42e500c9 100644 --- a/component/mmdb/reader.go +++ b/component/mmdb/reader.go @@ -5,6 +5,7 @@ import ( "net" "strings" + "github.com/metacubex/mihomo/log" "github.com/oschwald/maxminddb-golang" ) @@ -23,11 +24,16 @@ type ASNReader struct { *maxminddb.Reader } -type ASNResult struct { +type GeoLite2 struct { AutonomousSystemNumber uint32 `maxminddb:"autonomous_system_number"` AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` } +type IPInfo struct { + ASN string `maxminddb:"asn"` + Name string `maxminddb:"name"` +} + func (r IPReader) LookupCode(ipAddress net.IP) []string { switch r.databaseType { case typeMaxmind: @@ -66,8 +72,18 @@ func (r IPReader) LookupCode(ipAddress net.IP) []string { } } -func (r ASNReader) LookupASN(ip net.IP) ASNResult { - var result ASNResult - r.Lookup(ip, &result) - return result +func (r ASNReader) LookupASN(ip net.IP) (string, string) { + switch r.Metadata.DatabaseType { + case "GeoLite2-ASN", "DBIP-ASN-Lite (compat=GeoLite2-ASN)": + var result GeoLite2 + _ = r.Lookup(ip, &result) + return fmt.Sprint(result.AutonomousSystemNumber), result.AutonomousSystemOrganization + case "ipinfo generic_asn_free.mmdb": + var result IPInfo + _ = r.Lookup(ip, &result) + return result.ASN[2:], result.Name + default: + log.Warnln("Unsupported ASN type: %s", r.Metadata.DatabaseType) + } + return "", "" } diff --git a/component/nat/table.go b/component/nat/table.go index bb5ab755..66241fb4 100644 --- a/component/nat/table.go +++ b/component/nat/table.go @@ -10,47 +10,30 @@ import ( ) type Table struct { - mapping *xsync.MapOf[string, *Entry] - lockMap *xsync.MapOf[string, *sync.Cond] + mapping *xsync.MapOf[string, *entry] } -type Entry struct { - PacketConn C.PacketConn - WriteBackProxy C.WriteBackProxy +type entry struct { + PacketSender C.PacketSender LocalUDPConnMap *xsync.MapOf[string, *net.UDPConn] LocalLockMap *xsync.MapOf[string, *sync.Cond] } -func (t *Table) Set(key string, e C.PacketConn, w C.WriteBackProxy) { - t.mapping.Store(key, &Entry{ - PacketConn: e, - WriteBackProxy: w, - LocalUDPConnMap: xsync.NewMapOf[string, *net.UDPConn](), - LocalLockMap: xsync.NewMapOf[string, *sync.Cond](), +func (t *Table) GetOrCreate(key string, maker func() C.PacketSender) (C.PacketSender, bool) { + item, loaded := t.mapping.LoadOrCompute(key, func() *entry { + return &entry{ + PacketSender: maker(), + LocalUDPConnMap: xsync.NewMapOf[string, *net.UDPConn](), + LocalLockMap: xsync.NewMapOf[string, *sync.Cond](), + } }) -} - -func (t *Table) Get(key string) (C.PacketConn, C.WriteBackProxy) { - entry, exist := t.getEntry(key) - if !exist { - return nil, nil - } - return entry.PacketConn, entry.WriteBackProxy -} - -func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) { - item, loaded := t.lockMap.LoadOrCompute(key, makeLock) - return item, loaded + return item.PacketSender, loaded } func (t *Table) Delete(key string) { t.mapping.Delete(key) } -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 { @@ -105,7 +88,7 @@ func (t *Table) DeleteLockForLocalConn(lAddr, key string) { entry.LocalLockMap.Delete(key) } -func (t *Table) getEntry(key string) (*Entry, bool) { +func (t *Table) getEntry(key string) (*entry, bool) { return t.mapping.Load(key) } @@ -116,7 +99,6 @@ func makeLock() *sync.Cond { // New return *Cache func New() *Table { return &Table{ - mapping: xsync.NewMapOf[string, *Entry](), - lockMap: xsync.NewMapOf[string, *sync.Cond](), + mapping: xsync.NewMapOf[string, *entry](), } } diff --git a/component/power/event_windows.go b/component/power/event_windows.go index 12655695..a9489df4 100644 --- a/component/power/event_windows.go +++ b/component/power/event_windows.go @@ -55,6 +55,11 @@ func NewEventListener(cb func(Type)) (func(), error) { } handle := uintptr(0) + // DWORD PowerRegisterSuspendResumeNotification( + // [in] DWORD Flags, + // [in] HANDLE Recipient, + // [out] PHPOWERNOTIFY RegistrationHandle + //); _, _, err := powerRegisterSuspendResumeNotification.Call( _DEVICE_NOTIFY_CALLBACK, uintptr(unsafe.Pointer(¶ms)), @@ -65,8 +70,11 @@ func NewEventListener(cb func(Type)) (func(), error) { } return func() { + // DWORD PowerUnregisterSuspendResumeNotification( + // [in, out] HPOWERNOTIFY RegistrationHandle + //); _, _, _ = powerUnregisterSuspendResumeNotification.Call( - uintptr(unsafe.Pointer(&handle)), + handle, ) runtime.KeepAlive(params) runtime.KeepAlive(handle) diff --git a/component/process/process.go b/component/process/process.go index 76ec2c45..464f5a79 100644 --- a/component/process/process.go +++ b/component/process/process.go @@ -3,6 +3,8 @@ package process import ( "errors" "net/netip" + + C "github.com/metacubex/mihomo/constant" ) var ( @@ -19,3 +21,18 @@ const ( func FindProcessName(network string, srcIP netip.Addr, srcPort int) (uint32, string, error) { return findProcessName(network, srcIP, srcPort) } + +// PackageNameResolver +// never change type traits because it's used in CMFA +type PackageNameResolver func(metadata *C.Metadata) (string, error) + +// DefaultPackageNameResolver +// never change type traits because it's used in CMFA +var DefaultPackageNameResolver PackageNameResolver + +func FindPackageName(metadata *C.Metadata) (string, error) { + if resolver := DefaultPackageNameResolver; resolver != nil { + return resolver(metadata) + } + return "", ErrPlatformNotSupport +} diff --git a/component/process/process_android.go b/component/process/process_android.go deleted file mode 100644 index fd5d3b6c..00000000 --- a/component/process/process_android.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build android && cmfa - -package process - -import "github.com/metacubex/mihomo/constant" - -type PackageNameResolver func(metadata *constant.Metadata) (string, error) - -var DefaultPackageNameResolver PackageNameResolver - -func FindPackageName(metadata *constant.Metadata) (string, error) { - if resolver := DefaultPackageNameResolver; resolver != nil { - return resolver(metadata) - } - return "", ErrPlatformNotSupport -} diff --git a/component/process/process_common.go b/component/process/process_common.go deleted file mode 100644 index fa7eeb9f..00000000 --- a/component/process/process_common.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !(android && cmfa) - -package process - -import "github.com/metacubex/mihomo/constant" - -func FindPackageName(metadata *constant.Metadata) (string, error) { - return "", nil -} diff --git a/component/process/process_darwin.go b/component/process/process_darwin.go index 67d2e833..c02771ed 100644 --- a/component/process/process_darwin.go +++ b/component/process/process_darwin.go @@ -46,12 +46,12 @@ func findProcessName(network string, ip netip.Addr, port int) (uint32, string, e isIPv4 := ip.Is4() - value, err := syscall.Sysctl(spath) + value, err := unix.SysctlRaw(spath) if err != nil { return 0, "", err } - buf := []byte(value) + buf := value itemSize := structSize if network == TCP { // rup8(sizeof(xtcpcb_n)) diff --git a/component/process/process_linux.go b/component/process/process_linux.go index f8174495..3ce45ae8 100644 --- a/component/process/process_linux.go +++ b/component/process/process_linux.go @@ -64,7 +64,6 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string if err != nil { return 0, "", err } - pp, err := resolveProcessNameByProcSearch(inode, uid) return uid, pp, err } @@ -160,6 +159,7 @@ func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) { if err != nil { continue } + if runtime.GOOS == "android" { if bytes.Equal(buffer[:n], socket) { cmdline, err := os.ReadFile(path.Join(processPath, "cmdline")) @@ -174,7 +174,6 @@ func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) { return os.Readlink(filepath.Join(processPath, "exe")) } } - } } @@ -185,7 +184,7 @@ func splitCmdline(cmdline []byte) string { cmdline = bytes.Trim(cmdline, " ") idx := bytes.IndexFunc(cmdline, func(r rune) bool { - return unicode.IsControl(r) || unicode.IsSpace(r) || r == ':' + return unicode.IsControl(r) || unicode.IsSpace(r) }) if idx == -1 { diff --git a/component/process/process_windows.go b/component/process/process_windows.go index f8cd00d8..73ac0255 100644 --- a/component/process/process_windows.go +++ b/component/process/process_windows.go @@ -180,7 +180,7 @@ func newSearcher(isV4, isTCP bool) *searcher { func getTransportTable(fn uintptr, family int, class int) ([]byte, error) { for size, buf := uint32(8), make([]byte, 8); ; { ptr := unsafe.Pointer(&buf[0]) - err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0) + err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0) switch err { case 0: @@ -215,13 +215,13 @@ func getExecPathFromPID(pid uint32) (string, error) { buf := make([]uint16, syscall.MAX_LONG_PATH) size := uint32(len(buf)) - r1, _, err := syscall.SyscallN( - queryProcName, + r1, _, err := syscall.Syscall6( + queryProcName, 4, uintptr(h), uintptr(0), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)), - ) + 0, 0) if r1 == 0 { return "", err } diff --git a/component/profile/cachefile/cache.go b/component/profile/cachefile/cache.go index 11068647..7b4cdfc2 100644 --- a/component/profile/cachefile/cache.go +++ b/component/profile/cachefile/cache.go @@ -9,7 +9,7 @@ import ( C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" - "github.com/sagernet/bbolt" + "github.com/metacubex/bbolt" ) var ( @@ -17,8 +17,10 @@ var ( fileMode os.FileMode = 0o666 defaultCache *CacheFile - bucketSelected = []byte("selected") - bucketFakeip = []byte("fakeip") + bucketSelected = []byte("selected") + bucketFakeip = []byte("fakeip") + bucketETag = []byte("etag") + bucketSubscriptionInfo = []byte("subscriptioninfo") ) // CacheFile store and update the cache file @@ -69,80 +71,6 @@ func (c *CacheFile) SelectedMap() map[string]string { return mapping } -func (c *CacheFile) PutFakeip(key, value []byte) error { - if c.DB == nil { - return nil - } - - err := c.DB.Batch(func(t *bbolt.Tx) error { - bucket, err := t.CreateBucketIfNotExists(bucketFakeip) - if err != nil { - return err - } - return bucket.Put(key, value) - }) - if err != nil { - log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) - } - - return err -} - -func (c *CacheFile) DelFakeipPair(ip, host []byte) error { - if c.DB == nil { - return nil - } - - err := c.DB.Batch(func(t *bbolt.Tx) error { - bucket, err := t.CreateBucketIfNotExists(bucketFakeip) - if err != nil { - return err - } - err = bucket.Delete(ip) - if len(host) > 0 { - if err := bucket.Delete(host); err != nil { - return err - } - } - return err - }) - if err != nil { - log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) - } - - return err -} - -func (c *CacheFile) GetFakeip(key []byte) []byte { - if c.DB == nil { - return nil - } - - tx, err := c.DB.Begin(false) - if err != nil { - return nil - } - defer tx.Rollback() - - bucket := tx.Bucket(bucketFakeip) - if bucket == nil { - return nil - } - - return bucket.Get(key) -} - -func (c *CacheFile) FlushFakeIP() error { - err := c.DB.Batch(func(t *bbolt.Tx) error { - bucket := t.Bucket(bucketFakeip) - if bucket == nil { - return nil - } - return t.DeleteBucket(bucketFakeip) - }) - return err -} - func (c *CacheFile) Close() error { return c.DB.Close() } diff --git a/component/profile/cachefile/etag.go b/component/profile/cachefile/etag.go new file mode 100644 index 00000000..028fe504 --- /dev/null +++ b/component/profile/cachefile/etag.go @@ -0,0 +1,58 @@ +package cachefile + +import ( + "time" + + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/bbolt" + "github.com/vmihailenco/msgpack/v5" +) + +type EtagWithHash struct { + Hash utils.HashType + ETag string + Time time.Time +} + +func (c *CacheFile) SetETagWithHash(url string, etagWithHash EtagWithHash) { + if c.DB == nil { + return + } + + data, err := msgpack.Marshal(etagWithHash) + if err != nil { + return // maybe panic is better + } + + err = c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketETag) + if err != nil { + return err + } + + return bucket.Put([]byte(url), data) + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + return + } +} +func (c *CacheFile) GetETagWithHash(key string) (etagWithHash EtagWithHash) { + if c.DB == nil { + return + } + c.DB.View(func(t *bbolt.Tx) error { + if bucket := t.Bucket(bucketETag); bucket != nil { + if v := bucket.Get([]byte(key)); v != nil { + if err := msgpack.Unmarshal(v, &etagWithHash); err != nil { + return err + } + } + } + return nil + }) + + return +} diff --git a/component/profile/cachefile/fakeip.go b/component/profile/cachefile/fakeip.go new file mode 100644 index 00000000..20a09f9c --- /dev/null +++ b/component/profile/cachefile/fakeip.go @@ -0,0 +1,115 @@ +package cachefile + +import ( + "net/netip" + + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/bbolt" +) + +type FakeIpStore struct { + *CacheFile +} + +func (c *CacheFile) FakeIpStore() *FakeIpStore { + return &FakeIpStore{c} +} + +func (c *FakeIpStore) GetByHost(host string) (ip netip.Addr, exist bool) { + if c.DB == nil { + return + } + c.DB.View(func(t *bbolt.Tx) error { + if bucket := t.Bucket(bucketFakeip); bucket != nil { + if v := bucket.Get([]byte(host)); v != nil { + ip, exist = netip.AddrFromSlice(v) + } + } + return nil + }) + return +} + +func (c *FakeIpStore) PutByHost(host string, ip netip.Addr) { + if c.DB == nil { + return + } + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketFakeip) + if err != nil { + return err + } + return bucket.Put([]byte(host), ip.AsSlice()) + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + } +} + +func (c *FakeIpStore) GetByIP(ip netip.Addr) (host string, exist bool) { + if c.DB == nil { + return + } + c.DB.View(func(t *bbolt.Tx) error { + if bucket := t.Bucket(bucketFakeip); bucket != nil { + if v := bucket.Get(ip.AsSlice()); v != nil { + host, exist = string(v), true + } + } + return nil + }) + return +} + +func (c *FakeIpStore) PutByIP(ip netip.Addr, host string) { + if c.DB == nil { + return + } + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketFakeip) + if err != nil { + return err + } + return bucket.Put(ip.AsSlice(), []byte(host)) + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + } +} + +func (c *FakeIpStore) DelByIP(ip netip.Addr) { + if c.DB == nil { + return + } + + addr := ip.AsSlice() + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketFakeip) + if err != nil { + return err + } + host := bucket.Get(addr) + err = bucket.Delete(addr) + if len(host) > 0 { + if err = bucket.Delete(host); err != nil { + return err + } + } + return err + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + } +} + +func (c *FakeIpStore) FlushFakeIP() error { + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket := t.Bucket(bucketFakeip) + if bucket == nil { + return nil + } + return t.DeleteBucket(bucketFakeip) + }) + return err +} diff --git a/component/profile/cachefile/subscriptioninfo.go b/component/profile/cachefile/subscriptioninfo.go new file mode 100644 index 00000000..c68f92eb --- /dev/null +++ b/component/profile/cachefile/subscriptioninfo.go @@ -0,0 +1,41 @@ +package cachefile + +import ( + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/bbolt" +) + +func (c *CacheFile) SetSubscriptionInfo(name string, userInfo string) { + if c.DB == nil { + return + } + + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketSubscriptionInfo) + if err != nil { + return err + } + + return bucket.Put([]byte(name), []byte(userInfo)) + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + return + } +} +func (c *CacheFile) GetSubscriptionInfo(name string) (userInfo string) { + if c.DB == nil { + return + } + c.DB.View(func(t *bbolt.Tx) error { + if bucket := t.Bucket(bucketSubscriptionInfo); bucket != nil { + if v := bucket.Get([]byte(name)); v != nil { + userInfo = string(v) + } + } + return nil + }) + + return +} diff --git a/component/resolver/defaults.go b/component/resolver/defaults.go deleted file mode 100644 index 8a04bd17..00000000 --- a/component/resolver/defaults.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris - -package resolver - -import _ "unsafe" - -//go:linkname defaultNS net.defaultNS -var defaultNS []string - -func init() { - defaultNS = []string{"114.114.114.114:53", "8.8.8.8:53"} -} diff --git a/component/resolver/host.go b/component/resolver/host.go index 69c29a3c..34da8e9f 100644 --- a/component/resolver/host.go +++ b/component/resolver/host.go @@ -3,12 +3,20 @@ package resolver import ( "errors" "net/netip" + "os" + "strconv" "strings" _ "unsafe" "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/resolver/hosts" "github.com/metacubex/mihomo/component/trie" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" +) + +var ( + DisableSystemHosts, _ = strconv.ParseBool(os.Getenv("DISABLE_SYSTEM_HOSTS")) + UseSystemHosts bool ) type Hosts struct { @@ -21,11 +29,6 @@ func NewHosts(hosts *trie.DomainTrie[HostValue]) 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 { @@ -47,8 +50,9 @@ func (h *Hosts) Search(domain string, isDomain bool) (*HostValue, bool) { return &hostValue, false } - if !isDomain { - addr, _ := lookupStaticHost(domain) + + if !isDomain && !DisableSystemHosts && UseSystemHosts { + addr, _ := hosts.LookupStaticHost(domain) if hostValue, err := NewHostValue(addr); err == nil { return &hostValue, true } @@ -121,5 +125,5 @@ 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 + return hv.IPs[randv2.IntN(len(hv.IPs))], nil } diff --git a/component/resolver/host_windows.go b/component/resolver/host_windows.go deleted file mode 100644 index 669f9547..00000000 --- a/component/resolver/host_windows.go +++ /dev/null @@ -1,19 +0,0 @@ -//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/hosts/hosts.go b/component/resolver/hosts/hosts.go new file mode 100644 index 00000000..780ebb24 --- /dev/null +++ b/component/resolver/hosts/hosts.go @@ -0,0 +1,309 @@ +package hosts + +// this file copy and modify from golang's std net/hosts.go + +import ( + "errors" + "io" + "io/fs" + "net/netip" + "os" + "strings" + "sync" + "time" +) + +var hostsFilePath = "/etc/hosts" + +const cacheMaxAge = 5 * time.Second + +func parseLiteralIP(addr string) string { + ip, err := netip.ParseAddr(addr) + if err != nil { + return "" + } + return ip.String() +} + +type byName struct { + addrs []string + canonicalName string +} + +// hosts contains known host entries. +var hosts struct { + sync.Mutex + + // Key for the list of literal IP addresses must be a host + // name. It would be part of DNS labels, a FQDN or an absolute + // FQDN. + // For now the key is converted to lower case for convenience. + byName map[string]byName + + // Key for the list of host names must be a literal IP address + // including IPv6 address with zone identifier. + // We don't support old-classful IP address notation. + byAddr map[string][]string + + expire time.Time + path string + mtime time.Time + size int64 +} + +func readHosts() { + now := time.Now() + hp := hostsFilePath + + if now.Before(hosts.expire) && hosts.path == hp && len(hosts.byName) > 0 { + return + } + mtime, size, err := stat(hp) + if err == nil && hosts.path == hp && hosts.mtime.Equal(mtime) && hosts.size == size { + hosts.expire = now.Add(cacheMaxAge) + return + } + + hs := make(map[string]byName) + is := make(map[string][]string) + + file, err := open(hp) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrPermission) { + return + } + } + + if file != nil { + defer file.close() + for line, ok := file.readLine(); ok; line, ok = file.readLine() { + if i := strings.IndexByte(line, '#'); i >= 0 { + // Discard comments. + line = line[0:i] + } + f := getFields(line) + if len(f) < 2 { + continue + } + addr := parseLiteralIP(f[0]) + if addr == "" { + continue + } + + var canonical string + for i := 1; i < len(f); i++ { + name := absDomainName(f[i]) + h := []byte(f[i]) + lowerASCIIBytes(h) + key := absDomainName(string(h)) + + if i == 1 { + canonical = key + } + + is[addr] = append(is[addr], name) + + if v, ok := hs[key]; ok { + hs[key] = byName{ + addrs: append(v.addrs, addr), + canonicalName: v.canonicalName, + } + continue + } + + hs[key] = byName{ + addrs: []string{addr}, + canonicalName: canonical, + } + } + } + } + // Update the data cache. + hosts.expire = now.Add(cacheMaxAge) + hosts.path = hp + hosts.byName = hs + hosts.byAddr = is + hosts.mtime = mtime + hosts.size = size +} + +// LookupStaticHost looks up the addresses and the canonical name for the given host from /etc/hosts. +func LookupStaticHost(host string) ([]string, string) { + hosts.Lock() + defer hosts.Unlock() + readHosts() + if len(hosts.byName) != 0 { + if hasUpperCase(host) { + lowerHost := []byte(host) + lowerASCIIBytes(lowerHost) + host = string(lowerHost) + } + if byName, ok := hosts.byName[absDomainName(host)]; ok { + ipsCp := make([]string, len(byName.addrs)) + copy(ipsCp, byName.addrs) + return ipsCp, byName.canonicalName + } + } + return nil, "" +} + +// LookupStaticAddr looks up the hosts for the given address from /etc/hosts. +func LookupStaticAddr(addr string) []string { + hosts.Lock() + defer hosts.Unlock() + readHosts() + addr = parseLiteralIP(addr) + if addr == "" { + return nil + } + if len(hosts.byAddr) != 0 { + if hosts, ok := hosts.byAddr[addr]; ok { + hostsCp := make([]string, len(hosts)) + copy(hostsCp, hosts) + return hostsCp + } + } + return nil +} + +func stat(name string) (mtime time.Time, size int64, err error) { + st, err := os.Stat(name) + if err != nil { + return time.Time{}, 0, err + } + return st.ModTime(), st.Size(), nil +} + +type file struct { + file *os.File + data []byte + atEOF bool +} + +func (f *file) close() { f.file.Close() } + +func (f *file) getLineFromData() (s string, ok bool) { + data := f.data + i := 0 + for i = 0; i < len(data); i++ { + if data[i] == '\n' { + s = string(data[0:i]) + ok = true + // move data + i++ + n := len(data) - i + copy(data[0:], data[i:]) + f.data = data[0:n] + return + } + } + if f.atEOF && len(f.data) > 0 { + // EOF, return all we have + s = string(data) + f.data = f.data[0:0] + ok = true + } + return +} + +func (f *file) readLine() (s string, ok bool) { + if s, ok = f.getLineFromData(); ok { + return + } + if len(f.data) < cap(f.data) { + ln := len(f.data) + n, err := io.ReadFull(f.file, f.data[ln:cap(f.data)]) + if n >= 0 { + f.data = f.data[0 : ln+n] + } + if err == io.EOF || err == io.ErrUnexpectedEOF { + f.atEOF = true + } + } + s, ok = f.getLineFromData() + return +} + +func (f *file) stat() (mtime time.Time, size int64, err error) { + st, err := f.file.Stat() + if err != nil { + return time.Time{}, 0, err + } + return st.ModTime(), st.Size(), nil +} + +func open(name string) (*file, error) { + fd, err := os.Open(name) + if err != nil { + return nil, err + } + return &file{fd, make([]byte, 0, 64*1024), false}, nil +} + +func getFields(s string) []string { return splitAtBytes(s, " \r\t\n") } + +// Count occurrences in s of any bytes in t. +func countAnyByte(s string, t string) int { + n := 0 + for i := 0; i < len(s); i++ { + if strings.IndexByte(t, s[i]) >= 0 { + n++ + } + } + return n +} + +// Split s at any bytes in t. +func splitAtBytes(s string, t string) []string { + a := make([]string, 1+countAnyByte(s, t)) + n := 0 + last := 0 + for i := 0; i < len(s); i++ { + if strings.IndexByte(t, s[i]) >= 0 { + if last < i { + a[n] = s[last:i] + n++ + } + last = i + 1 + } + } + if last < len(s) { + a[n] = s[last:] + n++ + } + return a[0:n] +} + +// lowerASCIIBytes makes x ASCII lowercase in-place. +func lowerASCIIBytes(x []byte) { + for i, b := range x { + if 'A' <= b && b <= 'Z' { + x[i] += 'a' - 'A' + } + } +} + +// hasUpperCase tells whether the given string contains at least one upper-case. +func hasUpperCase(s string) bool { + for i := range s { + if 'A' <= s[i] && s[i] <= 'Z' { + return true + } + } + return false +} + +// absDomainName returns an absolute domain name which ends with a +// trailing dot to match pure Go reverse resolver and all other lookup +// routines. +// See golang.org/issue/12189. +// But we don't want to add dots for local names from /etc/hosts. +// It's hard to tell so we settle on the heuristic that names without dots +// (like "localhost" or "myhost") do not get trailing dots, but any other +// names do. +func absDomainName(s string) string { + if strings.IndexByte(s, '.') != -1 && s[len(s)-1] != '.' { + s += "." + } + return s +} diff --git a/component/resolver/hosts/hosts_windows.go b/component/resolver/hosts/hosts_windows.go new file mode 100644 index 00000000..8ad76e0a --- /dev/null +++ b/component/resolver/hosts/hosts_windows.go @@ -0,0 +1,13 @@ +package hosts + +// this file copy and modify from golang's std net/hook_windows.go + +import ( + "golang.org/x/sys/windows" +) + +func init() { + if dir, err := windows.GetSystemDirectory(); err == nil { + hostsFilePath = dir + "/Drivers/etc/hosts" + } +} diff --git a/component/resolver/relay.go b/component/resolver/relay.go index 27b25af1..818b4152 100644 --- a/component/resolver/relay.go +++ b/component/resolver/relay.go @@ -46,7 +46,7 @@ func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration) ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) defer cancel() inData := buff[:n] - msg, err := RelayDnsPacket(ctx, inData, buff) + msg, err := relayDnsPacket(ctx, inData, buff, 0) if err != nil { return err } @@ -69,7 +69,7 @@ func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration) return nil } -func RelayDnsPacket(ctx context.Context, payload []byte, target []byte) ([]byte, error) { +func relayDnsPacket(ctx context.Context, payload []byte, target []byte, maxSize int) ([]byte, error) { msg := &D.Msg{} if err := msg.Unpack(payload); err != nil { return nil, err @@ -83,6 +83,14 @@ func RelayDnsPacket(ctx context.Context, payload []byte, target []byte) ([]byte, } r.SetRcode(msg, r.Rcode) + if maxSize > 0 { + r.Truncate(maxSize) + } r.Compress = true return r.PackBuffer(target) } + +// RelayDnsPacket will truncate udp message up to SafeDnsPacketSize +func RelayDnsPacket(ctx context.Context, payload []byte, target []byte) ([]byte, error) { + return relayDnsPacket(ctx, payload, target, SafeDnsPacketSize) +} diff --git a/component/resolver/resolver.go b/component/resolver/resolver.go index 8cbc62fa..1eb3d642 100644 --- a/component/resolver/resolver.go +++ b/component/resolver/resolver.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "net" "net/netip" "strings" "time" @@ -12,17 +11,23 @@ import ( "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/trie" + "github.com/metacubex/randv2" "github.com/miekg/dns" - "github.com/zhangyunhao116/fastrand" ) var ( // DefaultResolver aim to resolve ip DefaultResolver Resolver - // ProxyServerHostResolver resolve ip to proxies server host + // ProxyServerHostResolver resolve ip for proxies server host, only nil when DefaultResolver is nil ProxyServerHostResolver Resolver + // DirectHostResolver resolve ip for direct outbound host, only nil when DefaultResolver is nil + DirectHostResolver Resolver + + // SystemResolver always using system dns, and was init in dns module + SystemResolver Resolver + // DisableIPv6 means don't resolve ipv6 host // default value is true DisableIPv6 = true @@ -46,6 +51,8 @@ type Resolver interface { LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error) ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error) Invalid() bool + ClearCache() + ResetConnection() } // LookupIPv4WithResolver same as LookupIPv4, but with a resolver @@ -70,14 +77,7 @@ func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]net return r.LookupIPv4(ctx, host) } - ipAddrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", host) - if err != nil { - return nil, err - } else if len(ipAddrs) == 0 { - return nil, ErrIPNotFound - } - - return ipAddrs, nil + return SystemResolver.LookupIPv4(ctx, host) } // LookupIPv4 with a host, return ipv4 list @@ -93,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[fastrand.Intn(len(ips))], nil + return ips[randv2.IntN(len(ips))], nil } // ResolveIPv4 with a host, return ipv4 @@ -126,14 +126,7 @@ func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]net return r.LookupIPv6(ctx, host) } - ipAddrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip6", host) - if err != nil { - return nil, err - } else if len(ipAddrs) == 0 { - return nil, ErrIPNotFound - } - - return ipAddrs, nil + return SystemResolver.LookupIPv6(ctx, host) } // LookupIPv6 with a host, return ipv6 list @@ -149,7 +142,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[fastrand.Intn(len(ips))], nil + return ips[randv2.IntN(len(ips))], nil } func ResolveIPv6(ctx context.Context, host string) (netip.Addr, error) { @@ -175,14 +168,7 @@ func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip return []netip.Addr{ip}, nil } - ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host) - if err != nil { - return nil, err - } else if len(ips) == 0 { - return nil, ErrIPNotFound - } - - return ips, nil + return SystemResolver.LookupIP(ctx, host) } // LookupIP with a host, return ip @@ -200,9 +186,9 @@ func ResolveIPWithResolver(ctx context.Context, host string, r Resolver) (netip. } ipv4s, ipv6s := SortationAddr(ips) if len(ipv4s) > 0 { - return ipv4s[fastrand.Intn(len(ipv4s))], nil + return ipv4s[randv2.IntN(len(ipv4s))], nil } - return ipv6s[fastrand.Intn(len(ipv6s))], nil + return ipv6s[randv2.IntN(len(ipv6s))], nil } // ResolveIP with a host, return ip and priority return TypeA @@ -210,61 +196,10 @@ func ResolveIP(ctx context.Context, host string) (netip.Addr, error) { return ResolveIPWithResolver(ctx, host, DefaultResolver) } -// ResolveIPv4ProxyServerHost proxies server host only -func ResolveIPv4ProxyServerHost(ctx context.Context, host string) (netip.Addr, error) { - if ProxyServerHostResolver != nil { - if ip, err := ResolveIPv4WithResolver(ctx, host, ProxyServerHostResolver); err != nil { - return ResolveIPv4(ctx, host) - } else { - return ip, nil - } +func ResetConnection() { + if DefaultResolver != nil { + go DefaultResolver.ResetConnection() } - return ResolveIPv4(ctx, host) -} - -// ResolveIPv6ProxyServerHost proxies server host only -func ResolveIPv6ProxyServerHost(ctx context.Context, host string) (netip.Addr, error) { - if ProxyServerHostResolver != nil { - if ip, err := ResolveIPv6WithResolver(ctx, host, ProxyServerHostResolver); err != nil { - return ResolveIPv6(ctx, host) - } else { - return ip, nil - } - } - return ResolveIPv6(ctx, host) -} - -// ResolveProxyServerHost proxies server host only -func ResolveProxyServerHost(ctx context.Context, host string) (netip.Addr, error) { - if ProxyServerHostResolver != nil { - if ip, err := ResolveIPWithResolver(ctx, host, ProxyServerHostResolver); err != nil { - return ResolveIP(ctx, host) - } else { - return ip, err - } - } - return ResolveIP(ctx, host) -} - -func LookupIPv6ProxyServerHost(ctx context.Context, host string) ([]netip.Addr, error) { - if ProxyServerHostResolver != nil { - return LookupIPv6WithResolver(ctx, host, ProxyServerHostResolver) - } - return LookupIPv6(ctx, host) -} - -func LookupIPv4ProxyServerHost(ctx context.Context, host string) ([]netip.Addr, error) { - if ProxyServerHostResolver != nil { - return LookupIPv4WithResolver(ctx, host, ProxyServerHostResolver) - } - return LookupIPv4(ctx, host) -} - -func LookupIPProxyServerHost(ctx context.Context, host string) ([]netip.Addr, error) { - if ProxyServerHostResolver != nil { - return LookupIPWithResolver(ctx, host, ProxyServerHostResolver) - } - return LookupIP(ctx, host) } func SortationAddr(ips []netip.Addr) (ipv4s, ipv6s []netip.Addr) { diff --git a/component/resolver/system.go b/component/resolver/system.go new file mode 100644 index 00000000..a134bb05 --- /dev/null +++ b/component/resolver/system.go @@ -0,0 +1,39 @@ +package resolver + +import "sync" + +var blacklist struct { + Map map[string]struct{} + Mutex sync.Mutex +} + +func init() { + blacklist.Map = make(map[string]struct{}) +} + +func AddSystemDnsBlacklist(names ...string) { + blacklist.Mutex.Lock() + defer blacklist.Mutex.Unlock() + for _, name := range names { + blacklist.Map[name] = struct{}{} + } +} + +func RemoveSystemDnsBlacklist(names ...string) { + blacklist.Mutex.Lock() + defer blacklist.Mutex.Unlock() + for _, name := range names { + delete(blacklist.Map, name) + } +} + +func IsSystemDnsBlacklisted(names ...string) bool { + blacklist.Mutex.Lock() + defer blacklist.Mutex.Unlock() + for _, name := range names { + if _, ok := blacklist.Map[name]; ok { + return true + } + } + return false +} diff --git a/component/resource/fetcher.go b/component/resource/fetcher.go index e6291293..39beee85 100644 --- a/component/resource/fetcher.go +++ b/component/resource/fetcher.go @@ -1,35 +1,32 @@ package resource import ( - "bytes" - "crypto/md5" + "context" "os" - "path/filepath" "time" + "github.com/metacubex/mihomo/common/utils" types "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" + "github.com/sagernet/fswatch" "github.com/samber/lo" ) -var ( - fileMode os.FileMode = 0o666 - dirMode os.FileMode = 0o755 -) - type Parser[V any] func([]byte) (V, error) type Fetcher[V any] struct { + ctx context.Context + ctxCancel context.CancelFunc resourceType string name string vehicle types.Vehicle - UpdatedAt time.Time - done chan struct{} - hash [16]byte + updatedAt time.Time + hash utils.HashType parser Parser[V] interval time.Duration - OnUpdate func(V) + onUpdate func(V) + watcher *fswatch.Watcher } func (f *Fetcher[V]) Name() string { @@ -44,93 +41,69 @@ func (f *Fetcher[V]) VehicleType() types.VehicleType { return f.vehicle.Type() } +func (f *Fetcher[V]) UpdatedAt() time.Time { + return f.updatedAt +} + func (f *Fetcher[V]) Initial() (V, error) { var ( - buf []byte - err error - isLocal bool - forceUpdate bool + buf []byte + contents V + err error ) if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil { + // local file exists, use it first buf, err = os.ReadFile(f.vehicle.Path()) modTime := stat.ModTime() - f.UpdatedAt = modTime - isLocal = true - if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) { - log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name()) - forceUpdate = true + contents, _, err = f.loadBuf(buf, utils.MakeHash(buf), false) + f.updatedAt = modTime // reset updatedAt to file's modTime + + if err == nil { + err = f.startPullLoop(time.Since(modTime) > f.interval) + if err != nil { + return lo.Empty[V](), err + } + return contents, nil } - } else { - buf, err = f.vehicle.Read() - f.UpdatedAt = time.Now() } + // parse local file error, fallback to remote + contents, _, err = f.Update() + if err != nil { return lo.Empty[V](), err } - - var contents V - if forceUpdate { - var forceBuf []byte - if forceBuf, err = f.vehicle.Read(); err == nil { - if contents, err = f.parser(forceBuf); err == nil { - isLocal = false - buf = forceBuf - } - } - } - - if err != nil || !forceUpdate { - contents, err = f.parser(buf) - } - + err = f.startPullLoop(false) if err != nil { - if !isLocal { - return lo.Empty[V](), err - } - - // parse local file error, fallback to remote - buf, err = f.vehicle.Read() - if err != nil { - return lo.Empty[V](), err - } - - contents, err = f.parser(buf) - if err != nil { - return lo.Empty[V](), err - } - - isLocal = false + return lo.Empty[V](), err } - - if f.vehicle.Type() != types.File && !isLocal { - if err := safeWrite(f.vehicle.Path(), buf); err != nil { - return lo.Empty[V](), err - } - } - - f.hash = md5.Sum(buf) - - // pull contents automatically - if f.interval > 0 { - go f.pullLoop() - } - return contents, nil } func (f *Fetcher[V]) Update() (V, bool, error) { - buf, err := f.vehicle.Read() + buf, hash, err := f.vehicle.Read(f.ctx, f.hash) if err != nil { return lo.Empty[V](), false, err } + return f.loadBuf(buf, hash, f.vehicle.Type() != types.File) +} +func (f *Fetcher[V]) SideUpdate(buf []byte) (V, bool, error) { + return f.loadBuf(buf, utils.MakeHash(buf), true) +} + +func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) (V, bool, error) { now := time.Now() - hash := md5.Sum(buf) - if bytes.Equal(f.hash[:], hash[:]) { - f.UpdatedAt = now - _ = os.Chtimes(f.vehicle.Path(), now, now) + if f.hash.Equal(hash) { + if updateFile { + _ = os.Chtimes(f.vehicle.Path(), now, now) + } + f.updatedAt = now + return lo.Empty[V](), true, nil + } + + if buf == nil { // f.hash has been changed between f.vehicle.Read but should not happen (cause by concurrent) return lo.Empty[V](), true, nil } @@ -139,78 +112,103 @@ func (f *Fetcher[V]) Update() (V, bool, error) { return lo.Empty[V](), false, err } - if f.vehicle.Type() != types.File { - if err := safeWrite(f.vehicle.Path(), buf); err != nil { + if updateFile { + if err = f.vehicle.Write(buf); err != nil { return lo.Empty[V](), false, err } } - - f.UpdatedAt = now + f.updatedAt = now f.hash = hash + if f.onUpdate != nil { + f.onUpdate(contents) + } + return contents, false, nil } -func (f *Fetcher[V]) Destroy() error { - if f.interval > 0 { - f.done <- struct{}{} +func (f *Fetcher[V]) Close() error { + f.ctxCancel() + if f.watcher != nil { + _ = f.watcher.Close() } return nil } -func (f *Fetcher[V]) pullLoop() { - initialInterval := f.interval - time.Since(f.UpdatedAt) +func (f *Fetcher[V]) pullLoop(forceUpdate bool) { + initialInterval := f.interval - time.Since(f.updatedAt) if initialInterval > f.interval { initialInterval = f.interval } + if forceUpdate { + log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name()) + f.updateWithLog() + } + timer := time.NewTimer(initialInterval) defer timer.Stop() for { select { case <-timer.C: timer.Reset(f.interval) - elm, same, err := f.Update() - if err != nil { - log.Errorln("[Provider] %s pull error: %s", f.Name(), err.Error()) - continue - } - - if same { - log.Debugln("[Provider] %s's content doesn't change", f.Name()) - continue - } - - log.Infoln("[Provider] %s's content update", f.Name()) - if f.OnUpdate != nil { - f.OnUpdate(elm) - } - case <-f.done: + f.updateWithLog() + case <-f.ctx.Done(): return } } } -func safeWrite(path string, buf []byte) error { - dir := filepath.Dir(path) - - if _, err := os.Stat(dir); os.IsNotExist(err) { - if err := os.MkdirAll(dir, dirMode); err != nil { +func (f *Fetcher[V]) startPullLoop(forceUpdate bool) (err error) { + // pull contents automatically + if f.vehicle.Type() == types.File { + f.watcher, err = fswatch.NewWatcher(fswatch.Options{ + Path: []string{f.vehicle.Path()}, + Direct: true, + Callback: f.updateCallback, + }) + if err != nil { return err } + err = f.watcher.Start() + if err != nil { + return err + } + } else if f.interval > 0 { + go f.pullLoop(forceUpdate) + } + return +} + +func (f *Fetcher[V]) updateCallback(path string) { + f.updateWithLog() +} + +func (f *Fetcher[V]) updateWithLog() { + _, same, err := f.Update() + if err != nil { + log.Errorln("[Provider] %s pull error: %s", f.Name(), err.Error()) + return } - return os.WriteFile(path, buf, fileMode) + if same { + log.Debugln("[Provider] %s's content doesn't change", f.Name()) + return + } + + log.Infoln("[Provider] %s's content update", f.Name()) + return } func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] { - + ctx, cancel := context.WithCancel(context.Background()) return &Fetcher[V]{ - name: name, - vehicle: vehicle, - parser: parser, - done: make(chan struct{}, 8), - OnUpdate: onUpdate, - interval: interval, + ctx: ctx, + ctxCancel: cancel, + name: name, + vehicle: vehicle, + parser: parser, + onUpdate: onUpdate, + interval: interval, } } diff --git a/component/resource/vehicle.go b/component/resource/vehicle.go index b2e29418..00b3170b 100644 --- a/component/resource/vehicle.go +++ b/component/resource/vehicle.go @@ -6,12 +6,46 @@ import ( "io" "net/http" "os" + "path/filepath" "time" + "github.com/metacubex/mihomo/common/utils" mihomoHttp "github.com/metacubex/mihomo/component/http" + "github.com/metacubex/mihomo/component/profile/cachefile" types "github.com/metacubex/mihomo/constant/provider" ) +const ( + DefaultHttpTimeout = time.Second * 20 + + fileMode os.FileMode = 0o666 + dirMode os.FileMode = 0o755 +) + +var ( + etag = false +) + +func ETag() bool { + return etag +} + +func SetETag(b bool) { + etag = b +} + +func safeWrite(path string, buf []byte) error { + dir := filepath.Dir(path) + + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, dirMode); err != nil { + return err + } + } + + return os.WriteFile(path, buf, fileMode) +} + type FileVehicle struct { path string } @@ -24,8 +58,25 @@ func (f *FileVehicle) Path() string { return f.path } -func (f *FileVehicle) Read() ([]byte, error) { - return os.ReadFile(f.path) +func (f *FileVehicle) Url() string { + return "file://" + f.path +} + +func (f *FileVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) { + buf, err = os.ReadFile(f.path) + if err != nil { + return + } + hash = utils.MakeHash(buf) + return +} + +func (f *FileVehicle) Proxy() string { + return "" +} + +func (f *FileVehicle) Write(buf []byte) error { + return safeWrite(f.path, buf) } func NewFileVehicle(path string) *FileVehicle { @@ -33,8 +84,14 @@ func NewFileVehicle(path string) *FileVehicle { } type HTTPVehicle struct { - url string - path string + url string + path string + proxy string + header http.Header + timeout time.Duration + sizeLimit int64 + inRead func(response *http.Response) + provider types.ProxyProvider } func (h *HTTPVehicle) Url() string { @@ -49,24 +106,78 @@ func (h *HTTPVehicle) Path() string { return h.path } -func (h *HTTPVehicle) Read() ([]byte, error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) - defer cancel() - 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 - } - return buf, nil +func (h *HTTPVehicle) Proxy() string { + return h.proxy } -func NewHTTPVehicle(url string, path string) *HTTPVehicle { - return &HTTPVehicle{url, path} +func (h *HTTPVehicle) Write(buf []byte) error { + return safeWrite(h.path, buf) +} + +func (h *HTTPVehicle) SetInRead(fn func(response *http.Response)) { + h.inRead = fn +} + +func (h *HTTPVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) { + ctx, cancel := context.WithTimeout(ctx, h.timeout) + defer cancel() + header := h.header + setIfNoneMatch := false + if etag && oldHash.IsValid() { + etagWithHash := cachefile.Cache().GetETagWithHash(h.url) + if oldHash.Equal(etagWithHash.Hash) && etagWithHash.ETag != "" { + if header == nil { + header = http.Header{} + } else { + header = header.Clone() + } + header.Set("If-None-Match", etagWithHash.ETag) + setIfNoneMatch = true + } + } + resp, err := mihomoHttp.HttpRequestWithProxy(ctx, h.url, http.MethodGet, header, nil, h.proxy) + if err != nil { + return + } + defer resp.Body.Close() + + if h.inRead != nil { + h.inRead(resp) + } + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + if setIfNoneMatch && resp.StatusCode == http.StatusNotModified { + return nil, oldHash, nil + } + err = errors.New(resp.Status) + return + } + var reader io.Reader = resp.Body + if h.sizeLimit > 0 { + reader = io.LimitReader(reader, h.sizeLimit) + } + buf, err = io.ReadAll(reader) + if err != nil { + return + } + hash = utils.MakeHash(buf) + if etag { + cachefile.Cache().SetETagWithHash(h.url, cachefile.EtagWithHash{ + Hash: hash, + ETag: resp.Header.Get("ETag"), + Time: time.Now(), + }) + } + return +} + +func NewHTTPVehicle(url string, path string, proxy string, header http.Header, timeout time.Duration, sizeLimit int64) *HTTPVehicle { + return &HTTPVehicle{ + url: url, + path: path, + proxy: proxy, + header: header, + timeout: timeout, + sizeLimit: sizeLimit, + } } diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go index 96e9272c..ada43176 100644 --- a/component/sniffer/dispatcher.go +++ b/component/sniffer/dispatcher.go @@ -2,15 +2,12 @@ package sniffer import ( "errors" - "fmt" "net" "net/netip" - "sync" "time" "github.com/metacubex/mihomo/common/lru" 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" @@ -22,29 +19,46 @@ var ( ErrNoClue = errors.New("not enough information for making a decision") ) -var Dispatcher *SnifferDispatcher - -type SnifferDispatcher struct { +type Dispatcher struct { enable bool sniffers map[sniffer.Sniffer]SnifferConfig - forceDomain *trie.DomainSet - skipSNI *trie.DomainSet - skipList *lru.LruCache[string, uint8] - rwMux sync.RWMutex + forceDomain []C.DomainMatcher + skipSrcAddress []C.IpMatcher + skipDstAddress []C.IpMatcher + skipDomain []C.DomainMatcher + skipList *lru.LruCache[netip.AddrPort, uint8] forceDnsMapping bool parsePureIp bool } -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 *Dispatcher) shouldOverride(metadata *C.Metadata) bool { + for _, matcher := range sd.skipDstAddress { + if matcher.MatchIp(metadata.DstIP) { + return false + } + } + for _, matcher := range sd.skipSrcAddress { + if matcher.MatchIp(metadata.SrcIP) { + return false + } + } + if metadata.Host == "" && sd.parsePureIp { + return true + } + if metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping { + return true + } + for _, matcher := range sd.forceDomain { + if matcher.MatchDomain(metadata.Host) { + return true + } + } + return false } -func (sd *SnifferDispatcher) UDPSniff(packet C.PacketAdapter) bool { +func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter) bool { metadata := packet.Metadata() - - if sd.shouldOverride(packet.Metadata()) { + if sd.shouldOverride(metadata) { for sniffer, config := range sd.sniffers { if sniffer.SupportNetwork() == C.UDP || sniffer.SupportNetwork() == C.ALLNet { inWhitelist := sniffer.SupportPort(metadata.DstPort) @@ -67,7 +81,7 @@ func (sd *SnifferDispatcher) UDPSniff(packet C.PacketAdapter) bool { } // TCPSniff returns true if the connection is sniffed to have a domain -func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool { +func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool { if sd.shouldOverride(metadata) { inWhitelist := false overrideDest := false @@ -85,57 +99,58 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata return false } - sd.rwMux.RLock() - dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) + dst := metadata.AddrPort() 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 false } - sd.rwMux.RUnlock() - if host, err := sd.sniffDomain(conn, metadata); err != nil { + host, err := sd.sniffDomain(conn, metadata) + if err != nil { sd.cacheSniffFailed(metadata) log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) return false - } else { - if sd.skipSNI.Has(host) { + } + + for _, matcher := range sd.skipDomain { + if matcher.MatchDomain(host) { log.Debugln("[Sniffer] Skip sni[%s]", host) return false } - - sd.rwMux.RLock() - sd.skipList.Delete(dst) - sd.rwMux.RUnlock() - - sd.replaceDomain(metadata, host, overrideDest) - return true } + + sd.skipList.Delete(dst) + + 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) +func (sd *Dispatcher) replaceDomain(metadata *C.Metadata, host string, overrideDest bool) { metadata.SniffHost = host if overrideDest { + log.Debugln("[Sniffer] Sniff %s [%s]-->[%s] success, replace domain [%s]-->[%s]", + metadata.NetWork, + metadata.SourceDetail(), + metadata.RemoteAddress(), + metadata.Host, host) metadata.Host = host } metadata.DNSMode = C.DNSNormal } -func (sd *SnifferDispatcher) Enable() bool { - return sd.enable +func (sd *Dispatcher) Enable() bool { + return sd != nil && sd.enable } -func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metadata) (string, error) { +func (sd *Dispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metadata) (string, error) { + //defer func(start time.Time) { + // log.Debugln("[Sniffer] [%s] Sniffing took %s", metadata.DstIP, time.Since(start)) + //}(time.Now()) + for s := range sd.sniffers { - if s.SupportNetwork() == C.TCP { + if s.SupportNetwork() == C.TCP && s.SupportPort(metadata.DstPort) { _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) _, err := conn.Peek(1) _ = conn.SetReadDeadline(time.Time{}) @@ -143,7 +158,7 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad _, ok := err.(*net.OpError) if ok { sd.cacheSniffFailed(metadata) - log.Errorln("[Sniffer] [%s] may not have any sent data, Consider adding skip", metadata.DstIP.String()) + log.Errorln("[Sniffer] [%s] [%s] may not have any sent data, Consider adding skip", metadata.DstIP, s.Protocol()) _ = conn.Close() } @@ -153,22 +168,36 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad bufferedLen := conn.Buffered() bytes, err := conn.Peek(bufferedLen) if err != nil { - log.Debugln("[Sniffer] the data length not enough") + log.Debugln("[Sniffer] [%s] [%s] the data length not enough, error: %v", metadata.DstIP, s.Protocol(), err) continue } host, err := s.SniffData(bytes) + var e *errNeedAtLeastData + if errors.As(err, &e) { + //log.Debugln("[Sniffer] [%s] [%s] %v, got length: %d", metadata.DstIP, s.Protocol(), e, len(bytes)) + _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + bytes, err = conn.Peek(e.length) + _ = conn.SetReadDeadline(time.Time{}) + //log.Debugln("[Sniffer] [%s] [%s] try again, got length: %d", metadata.DstIP, s.Protocol(), len(bytes)) + if err != nil { + log.Debugln("[Sniffer] [%s] [%s] the data length not enough, error: %v", metadata.DstIP, s.Protocol(), err) + continue + } + host, err = s.SniffData(bytes) + } if err != nil { - //log.Debugln("[Sniffer] [%s] Sniff data failed %s", s.Protocol(), metadata.DstIP) + //log.Debugln("[Sniffer] [%s] [%s] Sniff data failed, error: %v", metadata.DstIP, s.Protocol(), err) continue } _, err = netip.ParseAddr(host) if err == nil { - //log.Debugln("[Sniffer] [%s] Sniff data failed %s", s.Protocol(), metadata.DstIP) + //log.Debugln("[Sniffer] [%s] [%s] Sniff data failed, got host [%s]", metadata.DstIP, s.Protocol(), host) continue } + //log.Debugln("[Sniffer] [%s] [%s] Sniffed [%s]", metadata.DstIP, s.Protocol(), host) return host, nil } } @@ -176,43 +205,45 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad return "", ErrorSniffFailed } -func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) { - sd.rwMux.Lock() - dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) - count, _ := sd.skipList.Get(dst) - if count <= 5 { - count++ - } - sd.skipList.Set(dst, count) - sd.rwMux.Unlock() +func (sd *Dispatcher) cacheSniffFailed(metadata *C.Metadata) { + dst := metadata.AddrPort() + sd.skipList.Compute(dst, func(oldValue uint8, loaded bool) (newValue uint8, delete bool) { + if oldValue <= 5 { + oldValue++ + } + return oldValue, false + }) } -func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) { - dispatcher := SnifferDispatcher{ - enable: false, - } - - return &dispatcher, nil +type Config struct { + Enable bool + Sniffers map[sniffer.Type]SnifferConfig + ForceDomain []C.DomainMatcher + SkipSrcAddress []C.IpMatcher + SkipDstAddress []C.IpMatcher + SkipDomain []C.DomainMatcher + ForceDnsMapping bool + ParsePureIp bool } -func NewSnifferDispatcher(snifferConfig map[sniffer.Type]SnifferConfig, - forceDomain *trie.DomainSet, skipSNI *trie.DomainSet, - forceDnsMapping bool, parsePureIp bool) (*SnifferDispatcher, error) { - dispatcher := SnifferDispatcher{ - enable: true, - forceDomain: forceDomain, - skipSNI: skipSNI, - skipList: lru.New(lru.WithSize[string, uint8](128), lru.WithAge[string, uint8](600)), - forceDnsMapping: forceDnsMapping, - parsePureIp: parsePureIp, - sniffers: make(map[sniffer.Sniffer]SnifferConfig, 0), +func NewDispatcher(snifferConfig *Config) (*Dispatcher, error) { + dispatcher := Dispatcher{ + enable: snifferConfig.Enable, + forceDomain: snifferConfig.ForceDomain, + skipSrcAddress: snifferConfig.SkipSrcAddress, + skipDstAddress: snifferConfig.SkipDstAddress, + skipDomain: snifferConfig.SkipDomain, + skipList: lru.New(lru.WithSize[netip.AddrPort, uint8](128), lru.WithAge[netip.AddrPort, uint8](600)), + forceDnsMapping: snifferConfig.ForceDnsMapping, + parsePureIp: snifferConfig.ParsePureIp, + sniffers: make(map[sniffer.Sniffer]SnifferConfig, len(snifferConfig.Sniffers)), } - for snifferName, config := range snifferConfig { + for snifferName, config := range snifferConfig.Sniffers { s, err := NewSniffer(snifferName, config) if err != nil { log.Errorln("Sniffer name[%s] is error", snifferName) - return &SnifferDispatcher{enable: false}, err + return &Dispatcher{enable: false}, err } dispatcher.sniffers[s] = config } diff --git a/component/sniffer/tls_sniffer.go b/component/sniffer/tls_sniffer.go index 974df79a..b57f36ec 100644 --- a/component/sniffer/tls_sniffer.go +++ b/component/sniffer/tls_sniffer.go @@ -3,6 +3,7 @@ package sniffer import ( "encoding/binary" "errors" + "fmt" "strings" "github.com/metacubex/mihomo/common/utils" @@ -15,6 +16,19 @@ var ( errNotClientHello = errors.New("not client hello") ) +type errNeedAtLeastData struct { + length int + err error +} + +func (e *errNeedAtLeastData) Error() string { + return fmt.Sprintf("%v, need at least length: %d", e.err, e.length) +} + +func (e *errNeedAtLeastData) Unwrap() error { + return e.err +} + var _ sniffer.Sniffer = (*TLSSniffer)(nil) type TLSSniffer struct { @@ -160,7 +174,10 @@ func SniffTLS(b []byte) (*string, error) { } headerLen := int(binary.BigEndian.Uint16(b[3:5])) if 5+headerLen > len(b) { - return nil, ErrNoClue + return nil, &errNeedAtLeastData{ + length: 5 + headerLen, + err: ErrNoClue, + } } domain, err := ReadClientHello(b[5 : 5+headerLen]) diff --git a/component/tls/reality.go b/component/tls/reality.go index 687ef1ef..ff028257 100644 --- a/component/tls/reality.go +++ b/component/tls/reality.go @@ -16,17 +16,14 @@ import ( "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" + "github.com/metacubex/randv2" + utls "github.com/metacubex/utls" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" "golang.org/x/net/http2" @@ -39,9 +36,6 @@ type RealityConfig struct { 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++ { @@ -102,7 +96,7 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string return nil, err } var aeadCipher cipher.AEAD - if aesgcmPreferred(hello.CipherSuites) { + if utls.AesgcmPreferred(hello.CipherSuites) { aesBlock, _ := aes.NewCipher(authKey) aeadCipher, _ = cipher.NewGCM(aesBlock) } else { @@ -139,15 +133,18 @@ func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.C }, }, } - request, _ := http.NewRequest("GET", "https://"+serverName, nil) + request, err := http.NewRequest("GET", "https://"+serverName, nil) + if err != nil { + return + } request.Header.Set("User-Agent", fingerprint.Client) - request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", fastrand.Intn(32)+30)}) + request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", randv2.IntN(32)+30)}) response, err := client.Do(request) if err != nil { return } //_, _ = io.Copy(io.Discard, response.Body) - time.Sleep(time.Duration(5+fastrand.Int63n(10)) * time.Second) + time.Sleep(time.Duration(5+randv2.IntN(10)) * time.Second) response.Body.Close() client.CloseIdleConnections() } @@ -159,11 +156,12 @@ type realityVerifier struct { verified bool } -var pOffset = utils.MustOK(reflect.TypeOf((*utls.Conn)(nil)).Elem().FieldByName("peerCertificates")).Offset +//var pOffset = utils.MustOK(reflect.TypeOf((*utls.Conn)(nil)).Elem().FieldByName("peerCertificates")).Offset func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { //p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") - certs := *(*[]*x509.Certificate)(unsafe.Add(unsafe.Pointer(c.Conn), pOffset)) + //certs := *(*[]*x509.Certificate)(unsafe.Add(unsafe.Pointer(c.Conn), pOffset)) + certs := c.Conn.PeerCertificates() if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok { h := hmac.New(sha512.New, c.authKey) h.Write(pub) diff --git a/component/tls/utls.go b/component/tls/utls.go index 3063fc55..31733e50 100644 --- a/component/tls/utls.go +++ b/component/tls/utls.go @@ -6,8 +6,8 @@ import ( "github.com/metacubex/mihomo/log" + utls "github.com/metacubex/utls" "github.com/mroth/weightedrand/v2" - utls "github.com/sagernet/utls" ) type UConn struct { diff --git a/component/trie/domain.go b/component/trie/domain.go index 3decbb02..574a59ca 100644 --- a/component/trie/domain.go +++ b/component/trie/domain.go @@ -3,6 +3,8 @@ package trie import ( "errors" "strings" + "unicode" + "unicode/utf8" ) const ( @@ -25,6 +27,14 @@ func ValidAndSplitDomain(domain string) ([]string, bool) { if domain != "" && domain[len(domain)-1] == '.' { return nil, false } + if domain != "" { + if r, _ := utf8.DecodeRuneInString(domain); unicode.IsSpace(r) { + return nil, false + } + if r, _ := utf8.DecodeLastRuneInString(domain); unicode.IsSpace(r) { + return nil, false + } + } domain = strings.ToLower(domain) parts := strings.Split(domain, domainStep) if len(parts) == 1 { @@ -123,27 +133,41 @@ func (t *DomainTrie[T]) Optimize() { t.root.optimize() } -func (t *DomainTrie[T]) Foreach(print func(domain string, data T)) { +func (t *DomainTrie[T]) Foreach(fn func(domain string, data T) bool) { for key, data := range t.root.getChildren() { - recursion([]string{key}, data, print) - if data != nil && data.inited { - print(joinDomain([]string{key}), data.data) + recursion([]string{key}, data, fn) + if !data.isEmpty() { + if !fn(joinDomain([]string{key}), data.data) { + return + } } } } -func recursion[T any](items []string, node *Node[T], fn func(domain string, data T)) { +func (t *DomainTrie[T]) IsEmpty() bool { + if t == nil || t.root == nil { + return true + } + return len(t.root.getChildren()) == 0 +} + +func recursion[T any](items []string, node *Node[T], fn func(domain string, data T) bool) bool { for key, data := range node.getChildren() { newItems := append([]string{key}, items...) - if data != nil && data.inited { + if !data.isEmpty() { domain := joinDomain(newItems) if domain[0] == domainStepByte { domain = complexWildcard + domain } - fn(domain, data.Data()) + if !fn(domain, data.Data()) { + return false + } + } + if !recursion(newItems, data, fn) { + return false } - recursion(newItems, data, fn) } + return true } func joinDomain(items []string) string { diff --git a/component/trie/domain_set.go b/component/trie/domain_set.go index 860d1235..3fd8041f 100644 --- a/component/trie/domain_set.go +++ b/component/trie/domain_set.go @@ -28,8 +28,9 @@ 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) { + t.Foreach(func(domain string, data T) bool { reserveDomains = append(reserveDomains, utils.Reverse(domain)) + return true }) // ensure that the same prefix is continuous // and according to the ascending sequence of length @@ -136,6 +137,46 @@ func (ss *DomainSet) Has(key string) bool { } +func (ss *DomainSet) keys(f func(key string) bool) { + var currentKey []byte + var traverse func(int, int) bool + traverse = func(nodeId, bmIdx int) bool { + if getBit(ss.leaves, nodeId) != 0 { + if !f(string(currentKey)) { + return false + } + } + + for ; ; bmIdx++ { + if getBit(ss.labelBitmap, bmIdx) != 0 { + return true + } + nextLabel := ss.labels[bmIdx-nodeId] + currentKey = append(currentKey, nextLabel) + nextNodeId := countZeros(ss.labelBitmap, ss.ranks, bmIdx+1) + nextBmIdx := selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nextNodeId-1) + 1 + if !traverse(nextNodeId, nextBmIdx) { + return false + } + currentKey = currentKey[:len(currentKey)-1] + } + } + + traverse(0, 0) + return +} + +func (ss *DomainSet) Foreach(f func(key string) bool) { + ss.keys(func(key string) bool { + return f(utils.Reverse(key)) + }) +} + +// MatchDomain implements C.DomainMatcher +func (ss *DomainSet) MatchDomain(domain string) bool { + return ss.Has(domain) +} + func setBit(bm *[]uint64, i int, v int) { for i>>6 >= len(*bm) { *bm = append(*bm, 0) diff --git a/component/trie/domain_set_bin.go b/component/trie/domain_set_bin.go new file mode 100644 index 00000000..27d15802 --- /dev/null +++ b/component/trie/domain_set_bin.go @@ -0,0 +1,115 @@ +package trie + +import ( + "encoding/binary" + "errors" + "io" +) + +func (ss *DomainSet) WriteBin(w io.Writer) (err error) { + // version + _, err = w.Write([]byte{1}) + if err != nil { + return err + } + + // leaves + err = binary.Write(w, binary.BigEndian, int64(len(ss.leaves))) + if err != nil { + return err + } + for _, d := range ss.leaves { + err = binary.Write(w, binary.BigEndian, d) + if err != nil { + return err + } + } + + // labelBitmap + err = binary.Write(w, binary.BigEndian, int64(len(ss.labelBitmap))) + if err != nil { + return err + } + for _, d := range ss.labelBitmap { + err = binary.Write(w, binary.BigEndian, d) + if err != nil { + return err + } + } + + // labels + err = binary.Write(w, binary.BigEndian, int64(len(ss.labels))) + if err != nil { + return err + } + _, err = w.Write(ss.labels) + if err != nil { + return err + } + + return nil +} + +func ReadDomainSetBin(r io.Reader) (ds *DomainSet, err error) { + // version + version := make([]byte, 1) + _, err = io.ReadFull(r, version) + if err != nil { + return nil, err + } + if version[0] != 1 { + return nil, errors.New("version is invalid") + } + + ds = &DomainSet{} + var length int64 + + // leaves + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 1 { + return nil, errors.New("length is invalid") + } + ds.leaves = make([]uint64, length) + for i := int64(0); i < length; i++ { + err = binary.Read(r, binary.BigEndian, &ds.leaves[i]) + if err != nil { + return nil, err + } + } + + // labelBitmap + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 1 { + return nil, errors.New("length is invalid") + } + ds.labelBitmap = make([]uint64, length) + for i := int64(0); i < length; i++ { + err = binary.Read(r, binary.BigEndian, &ds.labelBitmap[i]) + if err != nil { + return nil, err + } + } + + // labels + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 1 { + return nil, errors.New("length is invalid") + } + ds.labels = make([]byte, length) + _, err = io.ReadFull(r, ds.labels) + if err != nil { + return nil, err + } + + ds.init() + return ds, nil +} diff --git a/component/trie/domain_set_test.go b/component/trie/domain_set_test.go index 77106d5f..38ba1622 100644 --- a/component/trie/domain_set_test.go +++ b/component/trie/domain_set_test.go @@ -1,12 +1,29 @@ package trie_test import ( + "golang.org/x/exp/slices" "testing" "github.com/metacubex/mihomo/component/trie" "github.com/stretchr/testify/assert" ) +func testDump(t *testing.T, tree *trie.DomainTrie[struct{}], set *trie.DomainSet) { + var dataSrc []string + tree.Foreach(func(domain string, data struct{}) bool { + dataSrc = append(dataSrc, domain) + return true + }) + slices.Sort(dataSrc) + var dataSet []string + set.Foreach(func(key string) bool { + dataSet = append(dataSet, key) + return true + }) + slices.Sort(dataSet) + assert.Equal(t, dataSrc, dataSet) +} + func TestDomainSet(t *testing.T) { tree := trie.New[struct{}]() domainSet := []string{ @@ -23,6 +40,7 @@ func TestDomainSet(t *testing.T) { for _, domain := range domainSet { assert.NoError(t, tree.Insert(domain, struct{}{})) } + assert.False(t, tree.IsEmpty()) set := tree.NewDomainSet() assert.NotNil(t, set) assert.True(t, set.Has("test.cn")) @@ -33,6 +51,7 @@ func TestDomainSet(t *testing.T) { assert.True(t, set.Has("google.com")) assert.False(t, set.Has("qq.com")) assert.False(t, set.Has("www.baidu.com")) + testDump(t, tree, set) } func TestDomainSetComplexWildcard(t *testing.T) { @@ -50,11 +69,13 @@ func TestDomainSetComplexWildcard(t *testing.T) { for _, domain := range domainSet { assert.NoError(t, tree.Insert(domain, struct{}{})) } + assert.False(t, tree.IsEmpty()) set := tree.NewDomainSet() assert.NotNil(t, set) assert.False(t, set.Has("google.com")) assert.True(t, set.Has("www.baidu.com")) assert.True(t, set.Has("test.test.baidu.com")) + testDump(t, tree, set) } func TestDomainSetWildcard(t *testing.T) { @@ -71,6 +92,7 @@ func TestDomainSetWildcard(t *testing.T) { for _, domain := range domainSet { assert.NoError(t, tree.Insert(domain, struct{}{})) } + assert.False(t, tree.IsEmpty()) set := tree.NewDomainSet() assert.NotNil(t, set) assert.True(t, set.Has("www.baidu.com")) @@ -82,4 +104,5 @@ func TestDomainSetWildcard(t *testing.T) { assert.False(t, set.Has("a.www.google.com")) assert.False(t, set.Has("test.qq.com")) assert.False(t, set.Has("test.test.test.qq.com")) + testDump(t, tree, set) } diff --git a/component/trie/domain_test.go b/component/trie/domain_test.go index 4c5d8002..6aab72d3 100644 --- a/component/trie/domain_test.go +++ b/component/trie/domain_test.go @@ -121,8 +121,20 @@ func TestTrie_Foreach(t *testing.T) { assert.NoError(t, tree.Insert(domain, localIP)) } count := 0 - tree.Foreach(func(domain string, data netip.Addr) { + tree.Foreach(func(domain string, data netip.Addr) bool { count++ + return true }) assert.Equal(t, 7, count) } + +func TestTrie_Space(t *testing.T) { + validDomain := func(domain string) bool { + _, ok := trie.ValidAndSplitDomain(domain) + return ok + } + assert.True(t, validDomain("google.com")) + assert.False(t, validDomain(" google.com")) + assert.False(t, validDomain(" google.com ")) + assert.True(t, validDomain("Mijia Cloud")) +} diff --git a/hub/updater/updater.go b/component/updater/update_core.go similarity index 96% rename from hub/updater/updater.go rename to component/updater/update_core.go index 02ff07ba..2aab7833 100644 --- a/hub/updater/updater.go +++ b/component/updater/update_core.go @@ -16,7 +16,6 @@ import ( "time" mihomoHttp "github.com/metacubex/mihomo/component/http" - "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" @@ -52,6 +51,10 @@ func init() { if runtime.GOARCH == "amd64" && cpuid.CPU.X64Level() < 3 { amd64Compatible = "-compatible" } + if !strings.HasPrefix(C.Version, "alpha") { + baseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/mihomo" + versionURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt" + } } type updateError struct { @@ -64,7 +67,7 @@ func (e *updateError) Error() string { // 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) { +func UpdateCore(execPath string) (err error) { mu.Lock() defer mu.Unlock() @@ -73,9 +76,9 @@ func Update(execPath string) (err error) { return err } - log.Infoln("current version %s, latest version %s", constant.Version, latestVersion) + log.Infoln("current version %s, latest version %s", C.Version, latestVersion) - if latestVersion == constant.Version { + if latestVersion == C.Version { err := &updateError{Message: "already using latest version"} return err } @@ -227,14 +230,14 @@ func clean() { // 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. +// approximately 32 MiB. const MaxPackageFileSize = 32 * 1024 * 1024 // Download package file and save it to disk func downloadPackageFile() (err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) + resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil) if err != nil { return fmt.Errorf("http request failed: %w", err) } @@ -415,7 +418,7 @@ func copyFile(src, dst string) error { 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": {C.UA}}, nil) + resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil) if err != nil { return "", fmt.Errorf("get Latest Version fail: %w", err) } diff --git a/component/updater/update_geo.go b/component/updater/update_geo.go new file mode 100644 index 00000000..719a5215 --- /dev/null +++ b/component/updater/update_geo.go @@ -0,0 +1,282 @@ +package updater + +import ( + "context" + "errors" + "fmt" + "os" + "runtime" + "time" + + "github.com/metacubex/mihomo/common/atomic" + "github.com/metacubex/mihomo/common/batch" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/geodata" + _ "github.com/metacubex/mihomo/component/geodata/standard" + "github.com/metacubex/mihomo/component/mmdb" + "github.com/metacubex/mihomo/component/resource" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + + "github.com/oschwald/maxminddb-golang" +) + +var ( + autoUpdate bool + updateInterval int + + updatingGeo atomic.Bool +) + +func GeoAutoUpdate() bool { + return autoUpdate +} + +func GeoUpdateInterval() int { + return updateInterval +} + +func SetGeoAutoUpdate(newAutoUpdate bool) { + autoUpdate = newAutoUpdate +} + +func SetGeoUpdateInterval(newGeoUpdateInterval int) { + updateInterval = newGeoUpdateInterval +} + +func UpdateMMDB() (err error) { + vehicle := resource.NewHTTPVehicle(geodata.MmdbUrl(), C.Path.MMDB(), "", nil, defaultHttpTimeout, 0) + var oldHash utils.HashType + if buf, err := os.ReadFile(vehicle.Path()); err == nil { + oldHash = utils.MakeHash(buf) + } + data, hash, err := vehicle.Read(context.Background(), oldHash) + if err != nil { + return fmt.Errorf("can't download MMDB database file: %w", err) + } + if oldHash.Equal(hash) { // same hash, ignored + return nil + } + if len(data) == 0 { + return fmt.Errorf("can't download MMDB database file: no data") + } + + instance, err := maxminddb.FromBytes(data) + if err != nil { + return fmt.Errorf("invalid MMDB database file: %s", err) + } + _ = instance.Close() + + defer mmdb.ReloadIP() + mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file + if err = vehicle.Write(data); err != nil { + return fmt.Errorf("can't save MMDB database file: %w", err) + } + return nil +} + +func UpdateASN() (err error) { + vehicle := resource.NewHTTPVehicle(geodata.ASNUrl(), C.Path.ASN(), "", nil, defaultHttpTimeout, 0) + var oldHash utils.HashType + if buf, err := os.ReadFile(vehicle.Path()); err == nil { + oldHash = utils.MakeHash(buf) + } + data, hash, err := vehicle.Read(context.Background(), oldHash) + if err != nil { + return fmt.Errorf("can't download ASN database file: %w", err) + } + if oldHash.Equal(hash) { // same hash, ignored + return nil + } + if len(data) == 0 { + return fmt.Errorf("can't download ASN database file: no data") + } + + instance, err := maxminddb.FromBytes(data) + if err != nil { + return fmt.Errorf("invalid ASN database file: %s", err) + } + _ = instance.Close() + + defer mmdb.ReloadASN() + mmdb.ASNInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file + if err = vehicle.Write(data); err != nil { + return fmt.Errorf("can't save ASN database file: %w", err) + } + return nil +} + +func UpdateGeoIp() (err error) { + geoLoader, err := geodata.GetGeoDataLoader("standard") + + vehicle := resource.NewHTTPVehicle(geodata.GeoIpUrl(), C.Path.GeoIP(), "", nil, defaultHttpTimeout, 0) + var oldHash utils.HashType + if buf, err := os.ReadFile(vehicle.Path()); err == nil { + oldHash = utils.MakeHash(buf) + } + data, hash, err := vehicle.Read(context.Background(), oldHash) + if err != nil { + return fmt.Errorf("can't download GeoIP database file: %w", err) + } + if oldHash.Equal(hash) { // same hash, ignored + return nil + } + if len(data) == 0 { + return fmt.Errorf("can't download GeoIP database file: no data") + } + + if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil { + return fmt.Errorf("invalid GeoIP database file: %s", err) + } + + defer geodata.ClearGeoIPCache() + if err = vehicle.Write(data); err != nil { + return fmt.Errorf("can't save GeoIP database file: %w", err) + } + return nil +} + +func UpdateGeoSite() (err error) { + geoLoader, err := geodata.GetGeoDataLoader("standard") + + vehicle := resource.NewHTTPVehicle(geodata.GeoSiteUrl(), C.Path.GeoSite(), "", nil, defaultHttpTimeout, 0) + var oldHash utils.HashType + if buf, err := os.ReadFile(vehicle.Path()); err == nil { + oldHash = utils.MakeHash(buf) + } + data, hash, err := vehicle.Read(context.Background(), oldHash) + if err != nil { + return fmt.Errorf("can't download GeoSite database file: %w", err) + } + if oldHash.Equal(hash) { // same hash, ignored + return nil + } + if len(data) == 0 { + return fmt.Errorf("can't download GeoSite database file: no data") + } + + if _, err = geoLoader.LoadSiteByBytes(data, "cn"); err != nil { + return fmt.Errorf("invalid GeoSite database file: %s", err) + } + + defer geodata.ClearGeoSiteCache() + if err = vehicle.Write(data); err != nil { + return fmt.Errorf("can't save GeoSite database file: %w", err) + } + return nil +} + +func updateGeoDatabases() error { + defer runtime.GC() + + b, _ := batch.New[interface{}](context.Background()) + + if geodata.GeoIpEnable() { + if geodata.GeodataMode() { + b.Go("UpdateGeoIp", func() (_ interface{}, err error) { + err = UpdateGeoIp() + return + }) + } else { + b.Go("UpdateMMDB", func() (_ interface{}, err error) { + err = UpdateMMDB() + return + }) + } + } + + if geodata.ASNEnable() { + b.Go("UpdateASN", func() (_ interface{}, err error) { + err = UpdateASN() + return + }) + } + + if geodata.GeoSiteEnable() { + b.Go("UpdateGeoSite", func() (_ interface{}, err error) { + err = UpdateGeoSite() + return + }) + } + + if e := b.Wait(); e != nil { + return e.Err + } + + return nil +} + +var ErrGetDatabaseUpdateSkip = errors.New("GEO database is updating, skip") + +func UpdateGeoDatabases() error { + log.Infoln("[GEO] Start updating GEO database") + + if updatingGeo.Load() { + return ErrGetDatabaseUpdateSkip + } + + updatingGeo.Store(true) + defer updatingGeo.Store(false) + + log.Infoln("[GEO] Updating GEO database") + + if err := updateGeoDatabases(); err != nil { + log.Errorln("[GEO] update GEO database error: %s", err.Error()) + return err + } + + return nil +} + +func getUpdateTime() (err error, time time.Time) { + filesToCheck := []string{ + C.Path.GeoIP(), + C.Path.MMDB(), + C.Path.ASN(), + C.Path.GeoSite(), + } + + for _, file := range filesToCheck { + var fileInfo os.FileInfo + fileInfo, err = os.Stat(file) + if err == nil { + return nil, fileInfo.ModTime() + } + } + + return +} + +func RegisterGeoUpdater() { + if updateInterval <= 0 { + log.Errorln("[GEO] Invalid update interval: %d", updateInterval) + return + } + + go func() { + ticker := time.NewTicker(time.Duration(updateInterval) * time.Hour) + defer ticker.Stop() + + err, lastUpdate := getUpdateTime() + if err != nil { + log.Errorln("[GEO] Get GEO database update time error: %s", err.Error()) + return + } + + log.Infoln("[GEO] last update time %s", lastUpdate) + if lastUpdate.Add(time.Duration(updateInterval) * time.Hour).Before(time.Now()) { + log.Infoln("[GEO] Database has not been updated for %v, update now", time.Duration(updateInterval)*time.Hour) + if err := UpdateGeoDatabases(); err != nil { + log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) + return + } + } + + for range ticker.C { + log.Infoln("[GEO] updating database every %d hours", updateInterval) + if err := UpdateGeoDatabases(); err != nil { + log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) + } + } + }() +} diff --git a/config/update_ui.go b/component/updater/update_ui.go similarity index 52% rename from config/update_ui.go rename to component/updater/update_ui.go index cff1d6d7..bd2a5881 100644 --- a/config/update_ui.go +++ b/component/updater/update_ui.go @@ -1,8 +1,7 @@ -package config +package updater import ( "archive/zip" - "errors" "fmt" "io" "os" @@ -12,29 +11,72 @@ import ( "sync" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" ) -var ( - ExternalUIURL string - ExternalUIPath string - ExternalUIFolder string - ExternalUIName string -) -var ( - ErrIncompleteConf = errors.New("ExternalUI configure incomplete") -) -var xdMutex sync.Mutex +type UIUpdater struct { + externalUIURL string + externalUIPath string + autoDownloadUI bool -func UpdateUI() error { - xdMutex.Lock() - defer xdMutex.Unlock() + mutex sync.Mutex +} - err := prepare() - if err != nil { - return err +var DefaultUiUpdater = &UIUpdater{} + +func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater { + updater := &UIUpdater{} + // checkout externalUI exist + if externalUI != "" { + updater.autoDownloadUI = true + updater.externalUIPath = C.Path.Resolve(externalUI) + } else { + // default externalUI path + updater.externalUIPath = path.Join(C.Path.HomeDir(), "ui") } - data, err := downloadForBytes(ExternalUIURL) + // checkout UIpath/name exist + if externalUIName != "" { + updater.autoDownloadUI = true + updater.externalUIPath = path.Join(updater.externalUIPath, externalUIName) + } + + if externalUIURL != "" { + updater.externalUIURL = externalUIURL + } + return updater +} + +func (u *UIUpdater) AutoDownloadUI() { + u.mutex.Lock() + defer u.mutex.Unlock() + if u.autoDownloadUI { + dirEntries, _ := os.ReadDir(u.externalUIPath) + if len(dirEntries) > 0 { + log.Infoln("UI already exists, skip downloading") + } else { + log.Infoln("External UI downloading ...") + err := u.downloadUI() + if err != nil { + log.Errorln("Error downloading UI: %s", err) + } + } + } +} + +func (u *UIUpdater) DownloadUI() error { + u.mutex.Lock() + defer u.mutex.Unlock() + return u.downloadUI() +} + +func (u *UIUpdater) downloadUI() error { + err := u.prepareUIPath() + if err != nil { + return fmt.Errorf("prepare UI path failed: %w", err) + } + + data, err := downloadForBytes(u.externalUIURL) if err != nil { return fmt.Errorf("can't download file: %w", err) } @@ -45,7 +87,7 @@ func UpdateUI() error { } defer os.Remove(saved) - err = cleanup(ExternalUIFolder) + err = cleanup(u.externalUIPath) if err != nil { if !os.IsNotExist(err) { return fmt.Errorf("cleanup exist file error: %w", err) @@ -57,29 +99,20 @@ func UpdateUI() error { return fmt.Errorf("can't extract zip file: %w", err) } - err = os.Rename(unzipFolder, ExternalUIFolder) + err = os.Rename(unzipFolder, u.externalUIPath) if err != nil { - return fmt.Errorf("can't rename folder: %w", err) + return fmt.Errorf("rename UI folder failed: %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 - } +func (u *UIUpdater) prepareUIPath() error { + if _, err := os.Stat(u.externalUIPath); os.IsNotExist(err) { + log.Infoln("dir %s does not exist, creating", u.externalUIPath) + if err := os.MkdirAll(u.externalUIPath, os.ModePerm); err != nil { + log.Warnln("create dir %s error: %s", u.externalUIPath, err) } - } else { - ExternalUIFolder = ExternalUIPath } - return nil } diff --git a/hub/updater/limitedreader.go b/component/updater/utils.go similarity index 71% rename from hub/updater/limitedreader.go rename to component/updater/utils.go index c31db601..b5c694ff 100644 --- a/hub/updater/limitedreader.go +++ b/component/updater/utils.go @@ -1,12 +1,36 @@ package updater import ( + "context" "fmt" "io" + "net/http" + "os" + "time" + + mihomoHttp "github.com/metacubex/mihomo/component/http" "golang.org/x/exp/constraints" ) +const defaultHttpTimeout = time.Second * 90 + +func downloadForBytes(url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), defaultHttpTimeout) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) +} + +func saveFile(bytes []byte, path string) error { + return os.WriteFile(path, bytes, 0o644) +} + // LimitReachedError records the limit and the operation that caused it. type LimitReachedError struct { Limit int64 diff --git a/config/config.go b/config/config.go index ca866491..ba6097bc 100644 --- a/config/config.go +++ b/config/config.go @@ -7,29 +7,24 @@ import ( "net" "net/netip" "net/url" - "os" - "path" - "regexp" "strings" "time" + _ "unsafe" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/provider" - N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/auth" + "github.com/metacubex/mihomo/component/cidr" "github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/geodata" - "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/sniffer" "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" providerTypes "github.com/metacubex/mihomo/constant/provider" snifferTypes "github.com/metacubex/mihomo/constant/sniffer" "github.com/metacubex/mihomo/dns" @@ -37,23 +32,24 @@ import ( LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/log" R "github.com/metacubex/mihomo/rules" + RC "github.com/metacubex/mihomo/rules/common" RP "github.com/metacubex/mihomo/rules/provider" T "github.com/metacubex/mihomo/tunnel" orderedmap "github.com/wk8/go-ordered-map/v2" + "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) // General config type General struct { Inbound - Controller - Mode T.TunnelMode `json:"mode"` - UnifiedDelay bool + Mode T.TunnelMode `json:"mode"` + UnifiedDelay bool `json:"unified-delay"` LogLevel log.LogLevel `json:"log-level"` IPv6 bool `json:"ipv6"` Interface string `json:"interface-name"` - RoutingMark int `json:"-"` + RoutingMark int `json:"routing-mark"` GeoXUrl GeoXUrl `json:"geox-url"` GeoAutoUpdate bool `json:"geo-auto-update"` GeoUpdateInterval int `json:"geo-update-interval"` @@ -63,9 +59,12 @@ type General struct { TCPConcurrent bool `json:"tcp-concurrent"` FindProcessMode P.FindProcessMode `json:"find-process-mode"` Sniffing bool `json:"sniffing"` - EBpf EBpf `json:"-"` GlobalClientFingerprint string `json:"global-client-fingerprint"` GlobalUA string `json:"global-ua"` + ETagSupport bool `json:"etag-support"` + KeepAliveIdle int `json:"keep-alive-idle"` + KeepAliveInterval int `json:"keep-alive-interval"` + DisableKeepAlive bool `json:"disable-keep-alive"` } // Inbound config @@ -89,96 +88,103 @@ type Inbound struct { InboundMPTCP bool `json:"inbound-mptcp"` } +// GeoXUrl config +type GeoXUrl struct { + GeoIp string `json:"geo-ip"` + Mmdb string `json:"mmdb"` + ASN string `json:"asn"` + GeoSite string `json:"geo-site"` +} + // Controller config type Controller struct { - ExternalController string `json:"-"` - ExternalControllerTLS string `json:"-"` - ExternalUI string `json:"-"` - Secret string `json:"-"` + ExternalController string + ExternalControllerTLS string + ExternalControllerUnix string + ExternalControllerPipe string + ExternalUI string + ExternalUIURL string + ExternalUIName string + ExternalDohServer string + Secret string + Cors Cors } -// 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"` - Listen string `yaml:"listen"` - EnhancedMode C.DNSMode `yaml:"enhanced-mode"` - DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` - CacheAlgorithm string `yaml:"cache-algorithm"` - FakeIPRange *fakeip.Pool - Hosts *trie.DomainTrie[resolver.HostValue] - NameServerPolicy *orderedmap.OrderedMap[string, []dns.NameServer] - ProxyServerNameserver []dns.NameServer -} - -// FallbackFilter config -type FallbackFilter struct { - GeoIP bool `yaml:"geoip"` - GeoIPCode string `yaml:"geoip-code"` - IPCIDR []netip.Prefix `yaml:"ipcidr"` - Domain []string `yaml:"domain"` - GeoSite []router.DomainMatcher `yaml:"geosite"` -} - -// Profile config -type Profile struct { - StoreSelected bool `yaml:"store-selected"` - StoreFakeIP bool `yaml:"store-fake-ip"` -} - -type TLS struct { - Certificate string `yaml:"certificate"` - PrivateKey string `yaml:"private-key"` - CustomTrustCert []string `yaml:"custom-certifactes"` -} - -// IPTables config -type IPTables struct { - Enable bool `yaml:"enable" json:"enable"` - InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"` - Bypass []string `yaml:"bypass" json:"bypass"` - DnsRedirect bool `yaml:"dns-redirect" json:"dns-redirect"` -} - -type Sniffer struct { - Enable bool - Sniffers map[snifferTypes.Type]SNIFF.SnifferConfig - ForceDomain *trie.DomainSet - SkipDomain *trie.DomainSet - ForceDnsMapping bool - ParsePureIp bool +type Cors struct { + AllowOrigins []string + AllowPrivateNetwork bool } // Experimental config type Experimental struct { - Fingerprints []string `yaml:"fingerprints"` - QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"` - QUICGoDisableECN bool `yaml:"quic-go-disable-ecn"` - IP4PEnable bool `yaml:"dialer-ip4p-convert"` + Fingerprints []string + QUICGoDisableGSO bool + QUICGoDisableECN bool + IP4PEnable bool +} + +// IPTables config +type IPTables struct { + Enable bool + InboundInterface string + Bypass []string + DnsRedirect bool +} + +// NTP config +type NTP struct { + Enable bool + Server string + Port int + Interval int + DialerProxy string + WriteToSystem bool +} + +// DNS config +type DNS struct { + Enable bool + PreferH3 bool + IPv6 bool + IPv6Timeout uint + UseSystemHosts bool + NameServer []dns.NameServer + Fallback []dns.NameServer + FallbackIPFilter []C.IpMatcher + FallbackDomainFilter []C.DomainMatcher + Listen string + EnhancedMode C.DNSMode + DefaultNameserver []dns.NameServer + CacheAlgorithm string + FakeIPRange *fakeip.Pool + Hosts *trie.DomainTrie[resolver.HostValue] + NameServerPolicy []dns.Policy + ProxyServerNameserver []dns.NameServer + DirectNameServer []dns.NameServer + DirectFollowPolicy bool +} + +// Profile config +type Profile struct { + StoreSelected bool + StoreFakeIP bool +} + +// TLS config +type TLS struct { + Certificate string + PrivateKey string + CustomTrustCert []string } // Config is mihomo config manager type Config struct { General *General + Controller *Controller + Experimental *Experimental IPTables *IPTables NTP *NTP DNS *DNS - Experimental *Experimental Hosts *trie.DomainTrie[resolver.HostValue] Profile *Profile Rules []C.Rule @@ -189,36 +195,37 @@ type Config struct { Providers map[string]providerTypes.ProxyProvider RuleProviders map[string]providerTypes.RuleProvider Tunnels []LC.Tunnel - Sniffer *Sniffer + Sniffer *sniffer.Config 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 RawCors struct { + AllowOrigins []string `yaml:"allow-origins" json:"allow-origins"` + AllowPrivateNetwork bool `yaml:"allow-private-network" json:"allow-private-network"` } type RawDNS struct { - Enable bool `yaml:"enable" json:"enable"` - PreferH3 bool `yaml:"prefer-h3" json:"prefer-h3"` - IPv6 bool `yaml:"ipv6" json:"ipv6"` - IPv6Timeout uint `yaml:"ipv6-timeout" json:"ipv6-timeout"` - UseHosts bool `yaml:"use-hosts" json:"use-hosts"` - NameServer []string `yaml:"nameserver" json:"nameserver"` - Fallback []string `yaml:"fallback" json:"fallback"` - FallbackFilter RawFallbackFilter `yaml:"fallback-filter" json:"fallback-filter"` - Listen string `yaml:"listen" json:"listen"` - EnhancedMode C.DNSMode `yaml:"enhanced-mode" json:"enhanced-mode"` - FakeIPRange string `yaml:"fake-ip-range" json:"fake-ip-range"` - FakeIPFilter []string `yaml:"fake-ip-filter" json:"fake-ip-filter"` - DefaultNameserver []string `yaml:"default-nameserver" json:"default-nameserver"` - CacheAlgorithm string `yaml:"cache-algorithm" json:"cache-algorithm"` - NameServerPolicy *orderedmap.OrderedMap[string, any] `yaml:"nameserver-policy" json:"nameserver-policy"` - ProxyServerNameserver []string `yaml:"proxy-server-nameserver" json:"proxy-server-nameserver"` + Enable bool `yaml:"enable" json:"enable"` + PreferH3 bool `yaml:"prefer-h3" json:"prefer-h3"` + IPv6 bool `yaml:"ipv6" json:"ipv6"` + IPv6Timeout uint `yaml:"ipv6-timeout" json:"ipv6-timeout"` + UseHosts bool `yaml:"use-hosts" json:"use-hosts"` + UseSystemHosts bool `yaml:"use-system-hosts" json:"use-system-hosts"` + RespectRules bool `yaml:"respect-rules" json:"respect-rules"` + NameServer []string `yaml:"nameserver" json:"nameserver"` + Fallback []string `yaml:"fallback" json:"fallback"` + FallbackFilter RawFallbackFilter `yaml:"fallback-filter" json:"fallback-filter"` + Listen string `yaml:"listen" json:"listen"` + EnhancedMode C.DNSMode `yaml:"enhanced-mode" json:"enhanced-mode"` + FakeIPRange string `yaml:"fake-ip-range" json:"fake-ip-range"` + FakeIPFilter []string `yaml:"fake-ip-filter" json:"fake-ip-filter"` + FakeIPFilterMode C.FilterMode `yaml:"fake-ip-filter-mode" json:"fake-ip-filter-mode"` + DefaultNameserver []string `yaml:"default-nameserver" json:"default-nameserver"` + CacheAlgorithm string `yaml:"cache-algorithm" json:"cache-algorithm"` + NameServerPolicy *orderedmap.OrderedMap[string, any] `yaml:"nameserver-policy" json:"nameserver-policy"` + ProxyServerNameserver []string `yaml:"proxy-server-nameserver" json:"proxy-server-nameserver"` + DirectNameServer []string `yaml:"direct-nameserver" json:"direct-nameserver"` + DirectNameServerFollowPolicy bool `yaml:"direct-nameserver-follow-policy" json:"direct-nameserver-follow-policy"` } type RawFallbackFilter struct { @@ -234,6 +241,15 @@ type RawClashForAndroid struct { UiSubtitlePattern string `yaml:"ui-subtitle-pattern" json:"ui-subtitle-pattern"` } +type RawNTP struct { + Enable bool `yaml:"enable" json:"enable"` + Server string `yaml:"server" json:"server"` + Port int `yaml:"port" json:"port"` + Interval int `yaml:"interval" json:"interval"` + DialerProxy string `yaml:"dialer-proxy" json:"dialer-proxy"` + WriteToSystem bool `yaml:"write-to-system" json:"write-to-system"` +} + type RawTun struct { Enable bool `yaml:"enable" json:"enable"` Device string `yaml:"device" json:"device"` @@ -241,30 +257,39 @@ type RawTun struct { DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"` AutoRoute bool `yaml:"auto-route" json:"auto-route"` AutoDetectInterface bool `yaml:"auto-detect-interface"` - RedirectToTun []string `yaml:"-" json:"-"` MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` GSO bool `yaml:"gso" json:"gso,omitempty"` GSOMaxSize uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` - //Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4_address,omitempty"` - Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6_address,omitempty"` - 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"` - IncludeInterface []string `yaml:"include-interface" json:"include-interface,omitempty"` - ExcludeInterface []string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` - IncludeUID []uint32 `yaml:"include-uid" json:"include_uid,omitempty"` - IncludeUIDRange []string `yaml:"include-uid-range" json:"include_uid_range,omitempty"` - ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude_uid,omitempty"` - ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude_uid_range,omitempty"` - IncludeAndroidUser []int `yaml:"include-android-user" json:"include_android_user,omitempty"` - IncludePackage []string `yaml:"include-package" json:"include_package,omitempty"` - ExcludePackage []string `yaml:"exclude-package" json:"exclude_package,omitempty"` - EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"` - UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"` - FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` + //Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` + Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` + IPRoute2TableIndex int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` + IPRoute2RuleIndex int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` + AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` + AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` + AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` + StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` + RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` + RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"` + RouteExcludeAddress []netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` + RouteExcludeAddressSet []string `yaml:"route-exclude-address-set" json:"route-exclude-address-set,omitempty"` + IncludeInterface []string `yaml:"include-interface" json:"include-interface,omitempty"` + ExcludeInterface []string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` + IncludeUID []uint32 `yaml:"include-uid" json:"include-uid,omitempty"` + IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` + ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` + ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` + IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` + IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` + ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` + EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` + UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` + FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` + + Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` + Inet6RouteAddress []netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` + Inet4RouteExcludeAddress []netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"` + Inet6RouteExcludeAddress []netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"` } type RawTuicServer struct { @@ -282,70 +307,26 @@ type RawTuicServer struct { CWND int `yaml:"cwnd" json:"cwnd,omitempty"` } -type RawConfig struct { - Port int `yaml:"port" json:"port"` - SocksPort int `yaml:"socks-port" json:"socks-port"` - RedirPort int `yaml:"redir-port" json:"redir-port"` - TProxyPort int `yaml:"tproxy-port" json:"tproxy-port"` - MixedPort int `yaml:"mixed-port" json:"mixed-port"` - ShadowSocksConfig string `yaml:"ss-config"` - VmessConfig string `yaml:"vmess-config"` - InboundTfo bool `yaml:"inbound-tfo"` - InboundMPTCP bool `yaml:"inbound-mptcp"` - Authentication []string `yaml:"authentication" json:"authentication"` - SkipAuthPrefixes []netip.Prefix `yaml:"skip-auth-prefixes"` - LanAllowedIPs []netip.Prefix `yaml:"lan-allowed-ips"` - LanDisAllowedIPs []netip.Prefix `yaml:"lan-disallowed-ips"` - AllowLan bool `yaml:"allow-lan" json:"allow-lan"` - BindAddress string `yaml:"bind-address" json:"bind-address"` - Mode T.TunnelMode `yaml:"mode" json:"mode"` - UnifiedDelay bool `yaml:"unified-delay" json:"unified-delay"` - LogLevel log.LogLevel `yaml:"log-level" json:"log-level"` - IPv6 bool `yaml:"ipv6" json:"ipv6"` - ExternalController string `yaml:"external-controller"` - 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"` - Tunnels []LC.Tunnel `yaml:"tunnels"` - GeoAutoUpdate bool `yaml:"geo-auto-update" json:"geo-auto-update"` - GeoUpdateInterval int `yaml:"geo-update-interval" json:"geo-update-interval"` - GeodataMode bool `yaml:"geodata-mode" json:"geodata-mode"` - GeodataLoader string `yaml:"geodata-loader" json:"geodata-loader"` - GeositeMatcher string `yaml:"geosite-matcher" json:"geosite-matcher"` - TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` - FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"` - GlobalClientFingerprint string `yaml:"global-client-fingerprint"` - GlobalUA string `yaml:"global-ua"` - KeepAliveInterval int `yaml:"keep-alive-interval"` - - Sniffer RawSniffer `yaml:"sniffer" json:"sniffer"` - ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` - RuleProvider map[string]map[string]any `yaml:"rule-providers"` - Hosts map[string]any `yaml:"hosts" json:"hosts"` - NTP RawNTP `yaml:"ntp" json:"ntp"` - DNS RawDNS `yaml:"dns" json:"dns"` - Tun RawTun `yaml:"tun"` - TuicServer RawTuicServer `yaml:"tuic-server"` - EBpf EBpf `yaml:"ebpf"` - IPTables IPTables `yaml:"iptables"` - Experimental Experimental `yaml:"experimental"` - Profile Profile `yaml:"profile"` - GeoXUrl GeoXUrl `yaml:"geox-url"` - Proxy []map[string]any `yaml:"proxies"` - ProxyGroup []map[string]any `yaml:"proxy-groups"` - Rule []string `yaml:"rules"` - SubRules map[string][]string `yaml:"sub-rules"` - RawTLS TLS `yaml:"tls"` - Listeners []map[string]any `yaml:"listeners"` - - ClashForAndroid RawClashForAndroid `yaml:"clash-for-android" json:"clash-for-android"` +type RawIPTables struct { + Enable bool `yaml:"enable" json:"enable"` + InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"` + Bypass []string `yaml:"bypass" json:"bypass"` + DnsRedirect bool `yaml:"dns-redirect" json:"dns-redirect"` } -type GeoXUrl struct { +type RawExperimental struct { + Fingerprints []string `yaml:"fingerprints"` + QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"` + QUICGoDisableECN bool `yaml:"quic-go-disable-ecn"` + IP4PEnable bool `yaml:"dialer-ip4p-convert"` +} + +type RawProfile struct { + StoreSelected bool `yaml:"store-selected" json:"store-selected"` + StoreFakeIP bool `yaml:"store-fake-ip" json:"store-fake-ip"` +} + +type RawGeoXUrl struct { GeoIp string `yaml:"geoip" json:"geoip"` Mmdb string `yaml:"mmdb" json:"mmdb"` ASN string `yaml:"asn" json:"asn"` @@ -353,15 +334,18 @@ type GeoXUrl struct { } type RawSniffer struct { - Enable bool `yaml:"enable" json:"enable"` - OverrideDest bool `yaml:"override-destination" json:"override-destination"` - Sniffing []string `yaml:"sniffing" json:"sniffing"` - ForceDomain []string `yaml:"force-domain" json:"force-domain"` - SkipDomain []string `yaml:"skip-domain" json:"skip-domain"` - Ports []string `yaml:"port-whitelist" json:"port-whitelist"` - ForceDnsMapping bool `yaml:"force-dns-mapping" json:"force-dns-mapping"` - ParsePureIp bool `yaml:"parse-pure-ip" json:"parse-pure-ip"` - Sniff map[string]RawSniffingConfig `yaml:"sniff" json:"sniff"` + Enable bool `yaml:"enable" json:"enable"` + OverrideDest bool `yaml:"override-destination" json:"override-destination"` + Sniffing []string `yaml:"sniffing" json:"sniffing"` + ForceDomain []string `yaml:"force-domain" json:"force-domain"` + SkipSrcAddress []string `yaml:"skip-src-address" json:"skip-src-address"` + SkipDstAddress []string `yaml:"skip-dst-address" json:"skip-dst-address"` + SkipDomain []string `yaml:"skip-domain" json:"skip-domain"` + Ports []string `yaml:"port-whitelist" json:"port-whitelist"` + ForceDnsMapping bool `yaml:"force-dns-mapping" json:"force-dns-mapping"` + ParsePureIp bool `yaml:"parse-pure-ip" json:"parse-pure-ip"` + + Sniff map[string]RawSniffingConfig `yaml:"sniff" json:"sniff"` } type RawSniffingConfig struct { @@ -369,10 +353,79 @@ type RawSniffingConfig struct { OverrideDest *bool `yaml:"override-destination" json:"override-destination"` } -// EBpf config -type EBpf struct { - RedirectToTun []string `yaml:"redirect-to-tun" json:"redirect-to-tun"` - AutoRedir []string `yaml:"auto-redir" json:"auto-redir"` +type RawTLS struct { + Certificate string `yaml:"certificate" json:"certificate"` + PrivateKey string `yaml:"private-key" json:"private-key"` + CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"` +} + +type RawConfig struct { + Port int `yaml:"port" json:"port"` + SocksPort int `yaml:"socks-port" json:"socks-port"` + RedirPort int `yaml:"redir-port" json:"redir-port"` + TProxyPort int `yaml:"tproxy-port" json:"tproxy-port"` + MixedPort int `yaml:"mixed-port" json:"mixed-port"` + ShadowSocksConfig string `yaml:"ss-config" json:"ss-config"` + VmessConfig string `yaml:"vmess-config" json:"vmess-config"` + InboundTfo bool `yaml:"inbound-tfo" json:"inbound-tfo"` + InboundMPTCP bool `yaml:"inbound-mptcp" json:"inbound-mptcp"` + Authentication []string `yaml:"authentication" json:"authentication"` + SkipAuthPrefixes []netip.Prefix `yaml:"skip-auth-prefixes" json:"skip-auth-prefixes"` + LanAllowedIPs []netip.Prefix `yaml:"lan-allowed-ips" json:"lan-allowed-ips"` + LanDisAllowedIPs []netip.Prefix `yaml:"lan-disallowed-ips" json:"lan-disallowed-ips"` + AllowLan bool `yaml:"allow-lan" json:"allow-lan"` + BindAddress string `yaml:"bind-address" json:"bind-address"` + Mode T.TunnelMode `yaml:"mode" json:"mode"` + UnifiedDelay bool `yaml:"unified-delay" json:"unified-delay"` + LogLevel log.LogLevel `yaml:"log-level" json:"log-level"` + IPv6 bool `yaml:"ipv6" json:"ipv6"` + ExternalController string `yaml:"external-controller" json:"external-controller"` + ExternalControllerPipe string `yaml:"external-controller-pipe" json:"external-controller-pipe"` + ExternalControllerUnix string `yaml:"external-controller-unix" json:"external-controller-unix"` + ExternalControllerTLS string `yaml:"external-controller-tls" json:"external-controller-tls"` + ExternalControllerCors RawCors `yaml:"external-controller-cors" json:"external-controller-cors"` + ExternalUI string `yaml:"external-ui" json:"external-ui"` + ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"` + ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"` + ExternalDohServer string `yaml:"external-doh-server" json:"external-doh-server"` + Secret string `yaml:"secret" json:"secret"` + Interface string `yaml:"interface-name" json:"interface-name"` + RoutingMark int `yaml:"routing-mark" json:"routing-mark"` + Tunnels []LC.Tunnel `yaml:"tunnels" json:"tunnels"` + GeoAutoUpdate bool `yaml:"geo-auto-update" json:"geo-auto-update"` + GeoUpdateInterval int `yaml:"geo-update-interval" json:"geo-update-interval"` + GeodataMode bool `yaml:"geodata-mode" json:"geodata-mode"` + GeodataLoader string `yaml:"geodata-loader" json:"geodata-loader"` + GeositeMatcher string `yaml:"geosite-matcher" json:"geosite-matcher"` + TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"` + FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"` + GlobalClientFingerprint string `yaml:"global-client-fingerprint" json:"global-client-fingerprint"` + GlobalUA string `yaml:"global-ua" json:"global-ua"` + ETagSupport bool `yaml:"etag-support" json:"etag-support"` + KeepAliveIdle int `yaml:"keep-alive-idle" json:"keep-alive-idle"` + KeepAliveInterval int `yaml:"keep-alive-interval" json:"keep-alive-interval"` + DisableKeepAlive bool `yaml:"disable-keep-alive" json:"disable-keep-alive"` + + ProxyProvider map[string]map[string]any `yaml:"proxy-providers" json:"proxy-providers"` + RuleProvider map[string]map[string]any `yaml:"rule-providers" json:"rule-providers"` + Proxy []map[string]any `yaml:"proxies" json:"proxies"` + ProxyGroup []map[string]any `yaml:"proxy-groups" json:"proxy-groups"` + Rule []string `yaml:"rules" json:"rule"` + SubRules map[string][]string `yaml:"sub-rules" json:"sub-rules"` + Listeners []map[string]any `yaml:"listeners" json:"listeners"` + Hosts map[string]any `yaml:"hosts" json:"hosts"` + DNS RawDNS `yaml:"dns" json:"dns"` + NTP RawNTP `yaml:"ntp" json:"ntp"` + Tun RawTun `yaml:"tun" json:"tun"` + TuicServer RawTuicServer `yaml:"tuic-server" json:"tuic-server"` + IPTables RawIPTables `yaml:"iptables" json:"iptables"` + Experimental RawExperimental `yaml:"experimental" json:"experimental"` + Profile RawProfile `yaml:"profile" json:"profile"` + GeoXUrl RawGeoXUrl `yaml:"geox-url" json:"geox-url"` + Sniffer RawSniffer `yaml:"sniffer" json:"sniffer"` + TLS RawTLS `yaml:"tls" json:"tls"` + + ClashForAndroid RawClashForAndroid `yaml:"clash-for-android" json:"clash-for-android"` } var ( @@ -391,9 +444,8 @@ func Parse(buf []byte) (*Config, error) { return ParseRawConfig(rawCfg) } -func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { - // config with default value - rawCfg := &RawConfig{ +func DefaultRawConfig() *RawConfig { + return &RawConfig{ AllowLan: false, BindAddress: "*", LanAllowedIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")}, @@ -401,7 +453,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { Mode: T.Rule, GeoAutoUpdate: false, GeoUpdateInterval: 24, - GeodataMode: C.GeodataMode, + GeodataMode: geodata.GeodataMode(), GeodataLoader: "memconservative", UnifiedDelay: false, Authentication: []string{}, @@ -412,7 +464,46 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { ProxyGroup: []map[string]any{}, TCPConcurrent: false, FindProcessMode: P.FindProcessStrict, - GlobalUA: "clash.meta", + GlobalUA: "clash.meta/" + C.Version, + ETagSupport: true, + DNS: RawDNS{ + Enable: false, + IPv6: false, + UseHosts: true, + UseSystemHosts: true, + IPv6Timeout: 100, + EnhancedMode: C.DNSMapping, + FakeIPRange: "198.18.0.1/16", + FallbackFilter: RawFallbackFilter{ + GeoIP: true, + GeoIPCode: "CN", + IPCIDR: []string{}, + GeoSite: []string{}, + }, + DefaultNameserver: []string{ + "114.114.114.114", + "223.5.5.5", + "8.8.8.8", + "1.0.0.1", + }, + NameServer: []string{ + "https://doh.pub/dns-query", + "tls://223.5.5.5:853", + }, + FakeIPFilter: []string{ + "dns.msftnsci.com", + "www.msftnsci.com", + "www.msftconnecttest.com", + }, + FakeIPFilterMode: C.FilterBlackList, + }, + NTP: RawNTP{ + Enable: false, + WriteToSystem: false, + Server: "time.apple.com", + Port: 123, + Interval: 30, + }, Tun: RawTun{ Enable: false, Device: "", @@ -435,55 +526,29 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { ALPN: []string{"h3"}, MaxUdpRelayPacketSize: 1500, }, - EBpf: EBpf{ - RedirectToTun: []string{}, - AutoRedir: []string{}, - }, - IPTables: IPTables{ + IPTables: RawIPTables{ Enable: false, InboundInterface: "lo", Bypass: []string{}, DnsRedirect: true, }, - NTP: RawNTP{ - Enable: false, - WriteToSystem: false, - Server: "time.apple.com", - ServerPort: 123, - Interval: 30, + Experimental: RawExperimental{ + // https://github.com/quic-go/quic-go/issues/4178 + // Quic-go currently cannot automatically fall back on platforms that do not support ecn, so this feature is turned off by default. + QUICGoDisableECN: true, }, - DNS: RawDNS{ - Enable: false, - IPv6: false, - UseHosts: true, - IPv6Timeout: 100, - EnhancedMode: C.DNSMapping, - FakeIPRange: "198.18.0.1/16", - FallbackFilter: RawFallbackFilter{ - GeoIP: true, - GeoIPCode: "CN", - IPCIDR: []string{}, - GeoSite: []string{}, - }, - DefaultNameserver: []string{ - "114.114.114.114", - "223.5.5.5", - "8.8.8.8", - "1.0.0.1", - }, - NameServer: []string{ - "https://doh.pub/dns-query", - "tls://223.5.5.5:853", - }, - FakeIPFilter: []string{ - "dns.msftnsci.com", - "www.msftnsci.com", - "www.msftconnecttest.com", - }, + Profile: RawProfile{ + StoreSelected: true, + }, + GeoXUrl: RawGeoXUrl{ + Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", + ASN: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb", + GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", + GeoSite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat", }, Sniffer: RawSniffer{ Enable: false, - Sniffing: []string{}, + Sniff: map[string]RawSniffingConfig{}, ForceDomain: []string{}, SkipDomain: []string{}, Ports: []string{}, @@ -491,17 +556,17 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { ParsePureIp: true, OverrideDest: true, }, - Profile: Profile{ - StoreSelected: true, - }, - GeoXUrl: GeoXUrl{ - Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", - ASN: "https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb", - GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", - GeoSite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat", - }, ExternalUIURL: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip", + ExternalControllerCors: RawCors{ + AllowOrigins: []string{"*"}, + AllowPrivateNetwork: true, + }, } +} + +func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { + // config with default value + rawCfg := DefaultRawConfig() if err := yaml.Unmarshal(buf, rawCfg); err != nil { return nil, err @@ -514,10 +579,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config := &Config{} log.Infoln("Start initial configuration in progress") //Segment finished in xxm startTime := time.Now() - config.Experimental = &rawCfg.Experimental - config.Profile = &rawCfg.Profile - config.IPTables = &rawCfg.IPTables - config.TLS = &rawCfg.RawTLS general, err := parseGeneral(rawCfg) if err != nil { @@ -525,10 +586,47 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.General = general - if len(config.General.GlobalClientFingerprint) != 0 { - log.Debugln("GlobalClientFingerprint: %s", config.General.GlobalClientFingerprint) - tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint) + // We need to temporarily apply some configuration in general and roll back after parsing the complete configuration. + // The loading and downloading of geodata in the parseRules and parseRuleProviders rely on these. + // This implementation is very disgusting, but there is currently no better solution + rollback := temporaryUpdateGeneral(config.General) + defer rollback() + + controller, err := parseController(rawCfg) + if err != nil { + return nil, err } + config.Controller = controller + + experimental, err := parseExperimental(rawCfg) + if err != nil { + return nil, err + } + config.Experimental = experimental + + iptables, err := parseIPTables(rawCfg) + if err != nil { + return nil, err + } + config.IPTables = iptables + + ntpCfg, err := parseNTP(rawCfg) + if err != nil { + return nil, err + } + config.NTP = ntpCfg + + profile, err := parseProfile(rawCfg) + if err != nil { + return nil, err + } + config.Profile = profile + + tlsCfg, err := parseTLS(rawCfg) + if err != nil { + return nil, err + } + config.TLS = tlsCfg proxies, providers, err := parseProxies(rawCfg) if err != nil { @@ -551,13 +649,13 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.RuleProviders = ruleProviders - subRules, err := parseSubRules(rawCfg, proxies) + subRules, err := parseSubRules(rawCfg, proxies, ruleProviders) if err != nil { return nil, err } config.SubRules = subRules - rules, err := parseRules(rawCfg.Rule, proxies, subRules, "rules") + rules, err := parseRules(rawCfg.Rule, proxies, ruleProviders, subRules, "rules") if err != nil { return nil, err } @@ -569,17 +667,14 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.Hosts = hosts - ntpCfg := paresNTP(rawCfg) - config.NTP = ntpCfg - - dnsCfg, err := parseDNS(rawCfg, hosts, rules, ruleProviders) + dnsCfg, err := parseDNS(rawCfg, hosts, ruleProviders) if err != nil { return nil, err } config.DNS = dnsCfg err = parseTun(rawCfg.Tun, config.General) - if !features.CMFA && err != nil { + if err != nil { return nil, err } @@ -600,7 +695,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } } - config.Sniffer, err = parseSniffer(rawCfg.Sniffer) + config.Sniffer, err = parseSniffer(rawCfg.Sniffer, ruleProviders) if err != nil { return nil, err } @@ -611,49 +706,10 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { return config, nil } +//go:linkname temporaryUpdateGeneral +func temporaryUpdateGeneral(general *General) func() + func parseGeneral(cfg *RawConfig) (*General, error) { - geodata.SetGeodataMode(cfg.GeodataMode) - geodata.SetGeoAutoUpdate(cfg.GeoAutoUpdate) - geodata.SetGeoUpdateInterval(cfg.GeoUpdateInterval) - geodata.SetLoader(cfg.GeodataLoader) - geodata.SetSiteMatcher(cfg.GeositeMatcher) - C.GeoAutoUpdate = cfg.GeoAutoUpdate - C.GeoUpdateInterval = cfg.GeoUpdateInterval - C.GeoIpUrl = cfg.GeoXUrl.GeoIp - C.GeoSiteUrl = cfg.GeoXUrl.GeoSite - C.MmdbUrl = cfg.GeoXUrl.Mmdb - C.ASNUrl = cfg.GeoXUrl.ASN - 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 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{ Port: cfg.Port, @@ -671,28 +727,93 @@ func parseGeneral(cfg *RawConfig) (*General, error) { InboundTfo: cfg.InboundTfo, InboundMPTCP: cfg.InboundMPTCP, }, - Controller: Controller{ - ExternalController: cfg.ExternalController, - ExternalUI: cfg.ExternalUI, - Secret: cfg.Secret, - ExternalControllerTLS: cfg.ExternalControllerTLS, + UnifiedDelay: cfg.UnifiedDelay, + Mode: cfg.Mode, + LogLevel: cfg.LogLevel, + IPv6: cfg.IPv6, + Interface: cfg.Interface, + RoutingMark: cfg.RoutingMark, + GeoXUrl: GeoXUrl{ + GeoIp: cfg.GeoXUrl.GeoIp, + Mmdb: cfg.GeoXUrl.Mmdb, + ASN: cfg.GeoXUrl.ASN, + GeoSite: cfg.GeoXUrl.GeoSite, }, - UnifiedDelay: cfg.UnifiedDelay, - Mode: cfg.Mode, - LogLevel: cfg.LogLevel, - IPv6: cfg.IPv6, - Interface: cfg.Interface, - RoutingMark: cfg.RoutingMark, - GeoXUrl: cfg.GeoXUrl, GeoAutoUpdate: cfg.GeoAutoUpdate, GeoUpdateInterval: cfg.GeoUpdateInterval, GeodataMode: cfg.GeodataMode, GeodataLoader: cfg.GeodataLoader, + GeositeMatcher: cfg.GeositeMatcher, TCPConcurrent: cfg.TCPConcurrent, FindProcessMode: cfg.FindProcessMode, - EBpf: cfg.EBpf, GlobalClientFingerprint: cfg.GlobalClientFingerprint, GlobalUA: cfg.GlobalUA, + ETagSupport: cfg.ETagSupport, + KeepAliveIdle: cfg.KeepAliveIdle, + KeepAliveInterval: cfg.KeepAliveInterval, + DisableKeepAlive: cfg.DisableKeepAlive, + }, nil +} + +func parseController(cfg *RawConfig) (*Controller, error) { + return &Controller{ + ExternalController: cfg.ExternalController, + ExternalUI: cfg.ExternalUI, + ExternalUIURL: cfg.ExternalUIURL, + ExternalUIName: cfg.ExternalUIName, + Secret: cfg.Secret, + ExternalControllerPipe: cfg.ExternalControllerPipe, + ExternalControllerUnix: cfg.ExternalControllerUnix, + ExternalControllerTLS: cfg.ExternalControllerTLS, + ExternalDohServer: cfg.ExternalDohServer, + Cors: Cors{ + AllowOrigins: cfg.ExternalControllerCors.AllowOrigins, + AllowPrivateNetwork: cfg.ExternalControllerCors.AllowPrivateNetwork, + }, + }, nil +} + +func parseExperimental(cfg *RawConfig) (*Experimental, error) { + return &Experimental{ + Fingerprints: cfg.Experimental.Fingerprints, + QUICGoDisableGSO: cfg.Experimental.QUICGoDisableGSO, + QUICGoDisableECN: cfg.Experimental.QUICGoDisableECN, + IP4PEnable: cfg.Experimental.IP4PEnable, + }, nil +} + +func parseIPTables(cfg *RawConfig) (*IPTables, error) { + return &IPTables{ + Enable: cfg.IPTables.Enable, + InboundInterface: cfg.IPTables.InboundInterface, + Bypass: cfg.IPTables.Bypass, + DnsRedirect: cfg.IPTables.DnsRedirect, + }, nil +} + +func parseNTP(cfg *RawConfig) (*NTP, error) { + return &NTP{ + Enable: cfg.NTP.Enable, + Server: cfg.NTP.Server, + Port: cfg.NTP.Port, + Interval: cfg.NTP.Interval, + DialerProxy: cfg.NTP.DialerProxy, + WriteToSystem: cfg.NTP.WriteToSystem, + }, nil +} + +func parseProfile(cfg *RawConfig) (*Profile, error) { + return &Profile{ + StoreSelected: cfg.Profile.StoreSelected, + StoreFakeIP: cfg.Profile.StoreFakeIP, + }, nil +} + +func parseTLS(cfg *RawConfig) (*TLS, error) { + return &TLS{ + Certificate: cfg.TLS.Certificate, + PrivateKey: cfg.TLS.PrivateKey, + CustomTrustCert: cfg.TLS.CustomTrustCert, }, nil } @@ -703,8 +824,11 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ groupsConfig := cfg.ProxyGroup providersConfig := cfg.ProxyProvider - var proxyList []string - var AllProxies []string + var ( + proxyList []string + AllProxies []string + hasGlobal bool + ) proxiesList := list.New() groupsList := list.New() @@ -737,6 +861,9 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ if !existName { return nil, nil, fmt.Errorf("proxy group %d: missing name", idx) } + if groupName == "GLOBAL" { + hasGlobal = true + } proxyList = append(proxyList, groupName) groupsList.PushBack(mapping) } @@ -762,6 +889,9 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ AllProviders = append(AllProviders, name) } + slices.Sort(AllProxies) + slices.Sort(AllProviders) + // parse proxy group for idx, mapping := range groupsConfig { group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap, AllProxies, AllProviders) @@ -788,13 +918,15 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) providersMap[provider.ReservedName] = pd - global := outboundgroup.NewSelector( - &outboundgroup.GroupCommonOption{ - Name: "GLOBAL", - }, - []providerTypes.ProxyProvider{pd}, - ) - proxies["GLOBAL"] = adapter.NewProxy(global) + if !hasGlobal { + global := outboundgroup.NewSelector( + &outboundgroup.GroupCommonOption{ + Name: "GLOBAL", + }, + []providerTypes.ProxyProvider{pd}, + ) + proxies["GLOBAL"] = adapter.NewProxy(global) + } ProxiesList = proxiesList GroupsList = groupsList if ParsingProxiesCallback != nil { @@ -823,6 +955,7 @@ func parseListeners(cfg *RawConfig) (listeners map[string]C.InboundListener, err } func parseRuleProviders(cfg *RawConfig) (ruleProviders map[string]providerTypes.RuleProvider, err error) { + RP.SetTunnel(T.Tunnel) ruleProviders = map[string]providerTypes.RuleProvider{} // parse rule provider for name, mapping := range cfg.RuleProvider { @@ -832,12 +965,11 @@ func parseRuleProviders(cfg *RawConfig) (ruleProviders map[string]providerTypes. } ruleProviders[name] = rp - RP.SetRuleProvider(rp) } return } -func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (subRules map[string][]C.Rule, err error) { +func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy, ruleProviders map[string]providerTypes.RuleProvider) (subRules map[string][]C.Rule, err error) { subRules = map[string][]C.Rule{} for name := range cfg.SubRules { subRules[name] = make([]C.Rule, 0) @@ -847,7 +979,7 @@ func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (subRules map[str return nil, fmt.Errorf("sub-rule name is empty") } var rules []C.Rule - rules, err = parseRules(rawRules, proxies, subRules, fmt.Sprintf("sub-rules[%s]", name)) + rules, err = parseRules(rawRules, proxies, ruleProviders, subRules, fmt.Sprintf("sub-rules[%s]", name)) if err != nil { return nil, err } @@ -900,7 +1032,7 @@ func verifySubRuleCircularReferences(n string, subRules map[string][]C.Rule, arr return nil } -func parseRules(rulesConfig []string, proxies map[string]C.Proxy, subRules map[string][]C.Rule, format string) ([]C.Rule, error) { +func parseRules(rulesConfig []string, proxies map[string]C.Proxy, ruleProviders map[string]providerTypes.RuleProvider, subRules map[string][]C.Rule, format string) ([]C.Rule, error) { var rules []C.Rule // parse rules @@ -915,7 +1047,7 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, subRules map[s l := len(rule) - if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" || ruleName == "SUB-RULE" { + if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" || ruleName == "SUB-RULE" || ruleName == "DOMAIN-REGEX" || ruleName == "PROCESS-NAME-REGEX" || ruleName == "PROCESS-PATH-REGEX" { target = rule[l-1] payload = strings.Join(rule[1:l-1], ",") } else { @@ -949,6 +1081,12 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, subRules map[s return nil, fmt.Errorf("%s[%d] [%s] error: %s", format, idx, line, parseErr.Error()) } + for _, name := range parsed.ProviderNames() { + if _, ok := ruleProviders[name]; !ok { + return nil, fmt.Errorf("%s[%d] [%s] error: rule set [%s] not found", format, idx, line, name) + } + } + rules = append(rules, parsed) } @@ -1018,7 +1156,7 @@ func hostWithDefaultPort(host string, defPort string) (string, error) { return net.JoinHostPort(hostname, port), nil } -func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) { +func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.NameServer, error) { var nameservers []dns.NameServer for idx, server := range servers { @@ -1042,13 +1180,16 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) case "tls": addr, err = hostWithDefaultPort(u.Host, "853") dnsNetType = "tcp-tls" // DNS over TLS - case "https": + case "http", "https": addr, err = hostWithDefaultPort(u.Host, "443") + dnsNetType = "https" // DNS over HTTPS + if u.Scheme == "http" { + addr, err = hostWithDefaultPort(u.Host, "80") + } if err == nil { proxyName = "" - clearURL := url.URL{Scheme: "https", Host: addr, Path: u.Path, User: u.User} + clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User} addr = clearURL.String() - dnsNetType = "https" // DNS over HTTPS if len(u.Fragment) != 0 { for _, s := range strings.Split(u.Fragment, "&") { arr := strings.Split(s, "=") @@ -1064,14 +1205,18 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) } } } - case "dhcp": - addr = u.Host - dnsNetType = "dhcp" // UDP from DHCP case "quic": addr, err = hostWithDefaultPort(u.Host, "853") dnsNetType = "quic" // DNS over QUIC case "system": dnsNetType = "system" // System DNS + case "dhcp": + addr = server[len("dhcp://"):] // some special notation cannot be parsed by url + dnsNetType = "dhcp" // UDP from DHCP + if addr == "system" { // Compatible with old writing "dhcp://system" + dnsNetType = "system" + addr = "" + } case "rcode": dnsNetType = "rcode" addr = u.Host @@ -1093,23 +1238,29 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) } - nameservers = append( - nameservers, - dns.NameServer{ - Net: dnsNetType, - Addr: addr, - ProxyName: proxyName, - Params: params, - PreferH3: preferH3, - }, - ) + if respectRules && len(proxyName) == 0 { + proxyName = dns.RespectRules + } + + nameserver := dns.NameServer{ + Net: dnsNetType, + Addr: addr, + ProxyName: proxyName, + Params: params, + PreferH3: preferH3, + } + if slices.ContainsFunc(nameservers, nameserver.Equal) { + continue // skip duplicates nameserver + } + + nameservers = append(nameservers, nameserver) } return nameservers, nil } func init() { dns.ParseNameServer = func(servers []string) ([]dns.NameServer, error) { // using by wireguard - return parseNameServer(servers, false) + return parseNameServer(servers, false, false) } } @@ -1135,179 +1286,127 @@ func parsePureDNSServer(server string) string { } } } -func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], ruleProviders map[string]providerTypes.RuleProvider, preferH3 bool) (*orderedmap.OrderedMap[string, []dns.NameServer], error) { - policy := orderedmap.New[string, []dns.NameServer]() - updatedPolicy := orderedmap.New[string, any]() - re := regexp.MustCompile(`[a-zA-Z0-9\-]+\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?`) + +func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], ruleProviders map[string]providerTypes.RuleProvider, respectRules bool, preferH3 bool) ([]dns.Policy, error) { + var policy []dns.Policy for pair := nsPolicy.Oldest(); pair != nil; pair = pair.Next() { k, v := pair.Key, pair.Value - if strings.Contains(strings.ToLower(k), ",") { - if strings.Contains(k, "geosite:") { + servers, err := utils.ToStringSlice(v) + if err != nil { + return nil, err + } + nameservers, err := parseNameServer(servers, respectRules, preferH3) + if err != nil { + return nil, err + } + kLower := strings.ToLower(k) + if strings.Contains(kLower, ",") { + if strings.Contains(kLower, "geosite:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") for _, subkey := range subkeys { newKey := "geosite:" + subkey - updatedPolicy.Store(newKey, v) + policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) } - } else if strings.Contains(strings.ToLower(k), "rule-set:") { + } else if strings.Contains(kLower, "rule-set:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") for _, subkey := range subkeys { newKey := "rule-set:" + subkey - updatedPolicy.Store(newKey, v) + policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) } - } else if re.MatchString(k) { + } else { subkeys := strings.Split(k, ",") for _, subkey := range subkeys { - updatedPolicy.Store(subkey, v) + policy = append(policy, dns.Policy{Domain: subkey, NameServers: nameservers}) } } } else { - if strings.Contains(strings.ToLower(k), "geosite:") { - updatedPolicy.Store("geosite:"+k[8:], v) - } else if strings.Contains(strings.ToLower(k), "rule-set:") { - updatedPolicy.Store("rule-set:"+k[9:], v) + if strings.Contains(kLower, "geosite:") { + policy = append(policy, dns.Policy{Domain: "geosite:" + k[8:], NameServers: nameservers}) + } else if strings.Contains(kLower, "rule-set:") { + policy = append(policy, dns.Policy{Domain: "rule-set:" + k[9:], NameServers: nameservers}) + } else { + policy = append(policy, dns.Policy{Domain: k, NameServers: nameservers}) } - updatedPolicy.Store(k, v) } } - for pair := updatedPolicy.Oldest(); pair != nil; pair = pair.Next() { - domain, server := pair.Key, pair.Value - 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) - } + for idx, p := range policy { + domain, nameservers := p.Domain, p.NameServers + 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()) - } + matcher, err := parseDomainRuleSet(domainSetName, "dns.nameserver-policy", ruleProviders) + if err != nil { + return nil, err + } + policy[idx] = dns.Policy{Matcher: matcher, NameServers: nameservers} + } else if strings.HasPrefix(domain, "geosite:") { + country := domain[8:] + matcher, err := RC.NewGEOSITE(country, "dns.nameserver-policy") + if err != nil { + return nil, err + } + policy[idx] = dns.Policy{Matcher: matcher, NameServers: nameservers} + } else { + if _, valid := trie.ValidAndSplitDomain(domain); !valid { + return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain) } } - policy.Store(domain, nameservers) } return policy, nil } -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) - } - - return ipNets, nil -} - -func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]router.DomainMatcher, error) { - var sites []router.DomainMatcher - if len(countries) > 0 { - if err := geodata.InitGeoSite(); err != nil { - return nil, fmt.Errorf("can't initial GeoSite: %s", err) - } - log.Warnln("replace fallback-filter.geosite with nameserver-policy, it will be removed in the future") - } - - for _, country := range countries { - found := false - for _, rule := range rules { - if rule.RuleType() == C.GEOSITE { - if strings.EqualFold(country, rule.Payload()) { - found = true - sites = append(sites, rule.(C.RuleGeoSite).GetDomainMatcher()) - log.Infoln("Start initial GeoSite dns fallback filter from rule `%s`", country) - } - } - } - - if !found { - matcher, recordsCount, err := geodata.LoadGeoSiteMatcher(country) - if err != nil { - return nil, err - } - - sites = append(sites, matcher) - - log.Infoln("Start initial GeoSite dns fallback filter `%s`, records: %d", country, recordsCount) - } - } - return sites, nil -} - -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) { +func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) { cfg := rawCfg.DNS if cfg.Enable && len(cfg.NameServer) == 0 { return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") } + if cfg.RespectRules && len(cfg.ProxyServerNameserver) == 0 { + return nil, fmt.Errorf("if “respect-rules” is turned on, “proxy-server-nameserver” cannot be empty") + } + dnsCfg := &DNS{ - Enable: cfg.Enable, - Listen: cfg.Listen, - PreferH3: cfg.PreferH3, - IPv6Timeout: cfg.IPv6Timeout, - IPv6: cfg.IPv6, - EnhancedMode: cfg.EnhancedMode, - FallbackFilter: FallbackFilter{ - IPCIDR: []netip.Prefix{}, - GeoSite: []router.DomainMatcher{}, - }, + Enable: cfg.Enable, + Listen: cfg.Listen, + PreferH3: cfg.PreferH3, + IPv6Timeout: cfg.IPv6Timeout, + IPv6: cfg.IPv6, + UseSystemHosts: cfg.UseSystemHosts, + EnhancedMode: cfg.EnhancedMode, } var err error - if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer, cfg.PreferH3); err != nil { + if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer, cfg.RespectRules, cfg.PreferH3); err != nil { return nil, err } - if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback, cfg.PreferH3); err != nil { + if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback, cfg.RespectRules, cfg.PreferH3); err != nil { return nil, err } - if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy, ruleProviders, cfg.PreferH3); err != nil { + if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy, ruleProviders, cfg.RespectRules, cfg.PreferH3); err != nil { return nil, err } - if dnsCfg.ProxyServerNameserver, err = parseNameServer(cfg.ProxyServerNameserver, cfg.PreferH3); err != nil { + if dnsCfg.ProxyServerNameserver, err = parseNameServer(cfg.ProxyServerNameserver, false, cfg.PreferH3); err != nil { return nil, err } + if dnsCfg.DirectNameServer, err = parseNameServer(cfg.DirectNameServer, false, cfg.PreferH3); err != nil { + return nil, err + } + dnsCfg.DirectFollowPolicy = cfg.DirectNameServerFollowPolicy + if len(cfg.DefaultNameserver) == 0 { return nil, errors.New("default nameserver should have at least one nameserver") } - if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver, cfg.PreferH3); err != nil { + if dnsCfg.DefaultNameserver, err = parseNameServer(cfg.DefaultNameserver, false, cfg.PreferH3); err != nil { return nil, err } // check default nameserver is pure ip addr @@ -1333,33 +1432,28 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul return nil, err } - var host *trie.DomainTrie[struct{}] - // fake ip skip host filter - if len(cfg.FakeIPFilter) != 0 { - host = trie.New[struct{}]() - for _, domain := range cfg.FakeIPFilter { - _ = host.Insert(domain, struct{}{}) - } - host.Optimize() - } - + var fakeIPTrie *trie.DomainTrie[struct{}] if len(dnsCfg.Fallback) != 0 { - if host == nil { - host = trie.New[struct{}]() - } + fakeIPTrie = trie.New[struct{}]() for _, fb := range dnsCfg.Fallback { if net.ParseIP(fb.Addr) != nil { continue } - _ = host.Insert(fb.Addr, struct{}{}) + _ = fakeIPTrie.Insert(fb.Addr, struct{}{}) } - host.Optimize() + } + + // fake ip skip host filter + host, err := parseDomain(cfg.FakeIPFilter, fakeIPTrie, "dns.fake-ip-filter", ruleProviders) + if err != nil { + return nil, err } pool, err := fakeip.New(fakeip.Options{ IPNet: fakeIPRange, Size: 1000, Host: host, + Mode: cfg.FakeIPFilterMode, Persistence: rawCfg.Profile.StoreFakeIP, }) if err != nil { @@ -1370,17 +1464,49 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul } if len(cfg.Fallback) != 0 { - dnsCfg.FallbackFilter.GeoIP = cfg.FallbackFilter.GeoIP - dnsCfg.FallbackFilter.GeoIPCode = cfg.FallbackFilter.GeoIPCode - if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil { - dnsCfg.FallbackFilter.IPCIDR = fallbackip + if cfg.FallbackFilter.GeoIP { + matcher, err := RC.NewGEOIP(cfg.FallbackFilter.GeoIPCode, "dns.fallback-filter.geoip", false, true) + if err != nil { + return nil, fmt.Errorf("load GeoIP dns fallback filter error, %w", err) + } + dnsCfg.FallbackIPFilter = append(dnsCfg.FallbackIPFilter, matcher.DnsFallbackFilter()) } - dnsCfg.FallbackFilter.Domain = cfg.FallbackFilter.Domain - fallbackGeoSite, err := parseFallbackGeoSite(cfg.FallbackFilter.GeoSite, rules) - if err != nil { - return nil, fmt.Errorf("load GeoSite dns fallback filter error, %w", err) + if len(cfg.FallbackFilter.IPCIDR) > 0 { + cidrSet := cidr.NewIpCidrSet() + for idx, ipcidr := range cfg.FallbackFilter.IPCIDR { + err = cidrSet.AddIpCidrForString(ipcidr) + if err != nil { + return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %w", idx, err) + } + } + err = cidrSet.Merge() + if err != nil { + return nil, err + } + matcher := cidrSet // dns.fallback-filter.ipcidr + dnsCfg.FallbackIPFilter = append(dnsCfg.FallbackIPFilter, matcher) + } + if len(cfg.FallbackFilter.Domain) > 0 { + domainTrie := trie.New[struct{}]() + for idx, domain := range cfg.FallbackFilter.Domain { + err = domainTrie.Insert(domain, struct{}{}) + if err != nil { + return nil, fmt.Errorf("DNS FallbackDomain[%d] format error: %w", idx, err) + } + } + matcher := domainTrie.NewDomainSet() // dns.fallback-filter.domain + dnsCfg.FallbackDomainFilter = append(dnsCfg.FallbackDomainFilter, matcher) + } + if len(cfg.FallbackFilter.GeoSite) > 0 { + log.Warnln("replace fallback-filter.geosite with nameserver-policy, it will be removed in the future") + for idx, geoSite := range cfg.FallbackFilter.GeoSite { + matcher, err := RC.NewGEOSITE(geoSite, "dns.fallback-filter.geosite") + if err != nil { + return nil, fmt.Errorf("DNS FallbackGeosite[%d] format error: %w", idx, err) + } + dnsCfg.FallbackDomainFilter = append(dnsCfg.FallbackDomainFilter, matcher) + } } - dnsCfg.FallbackFilter.GeoSite = fallbackGeoSite } if cfg.UseHosts { @@ -1424,30 +1550,39 @@ func parseTun(rawTun RawTun, general *General) error { DNSHijack: rawTun.DNSHijack, AutoRoute: rawTun.AutoRoute, AutoDetectInterface: rawTun.AutoDetectInterface, - RedirectToTun: rawTun.RedirectToTun, - MTU: rawTun.MTU, - GSO: rawTun.GSO, - GSOMaxSize: rawTun.GSOMaxSize, - Inet4Address: []netip.Prefix{tunAddressPrefix}, - Inet6Address: rawTun.Inet6Address, - StrictRoute: rawTun.StrictRoute, + MTU: rawTun.MTU, + GSO: rawTun.GSO, + GSOMaxSize: rawTun.GSOMaxSize, + Inet4Address: []netip.Prefix{tunAddressPrefix}, + Inet6Address: rawTun.Inet6Address, + IPRoute2TableIndex: rawTun.IPRoute2TableIndex, + IPRoute2RuleIndex: rawTun.IPRoute2RuleIndex, + AutoRedirect: rawTun.AutoRedirect, + AutoRedirectInputMark: rawTun.AutoRedirectInputMark, + AutoRedirectOutputMark: rawTun.AutoRedirectOutputMark, + StrictRoute: rawTun.StrictRoute, + RouteAddress: rawTun.RouteAddress, + RouteAddressSet: rawTun.RouteAddressSet, + RouteExcludeAddress: rawTun.RouteExcludeAddress, + RouteExcludeAddressSet: rawTun.RouteExcludeAddressSet, + IncludeInterface: rawTun.IncludeInterface, + ExcludeInterface: rawTun.ExcludeInterface, + IncludeUID: rawTun.IncludeUID, + IncludeUIDRange: rawTun.IncludeUIDRange, + ExcludeUID: rawTun.ExcludeUID, + ExcludeUIDRange: rawTun.ExcludeUIDRange, + IncludeAndroidUser: rawTun.IncludeAndroidUser, + IncludePackage: rawTun.IncludePackage, + ExcludePackage: rawTun.ExcludePackage, + EndpointIndependentNat: rawTun.EndpointIndependentNat, + UDPTimeout: rawTun.UDPTimeout, + FileDescriptor: rawTun.FileDescriptor, + Inet4RouteAddress: rawTun.Inet4RouteAddress, Inet6RouteAddress: rawTun.Inet6RouteAddress, Inet4RouteExcludeAddress: rawTun.Inet4RouteExcludeAddress, Inet6RouteExcludeAddress: rawTun.Inet6RouteExcludeAddress, - IncludeInterface: rawTun.IncludeInterface, - ExcludeInterface: rawTun.ExcludeInterface, - 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 @@ -1471,13 +1606,13 @@ func parseTuicServer(rawTuic RawTuicServer, general *General) error { return nil } -func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { - sniffer := &Sniffer{ +func parseSniffer(snifferRaw RawSniffer, ruleProviders map[string]providerTypes.RuleProvider) (*sniffer.Config, error) { + snifferConfig := &sniffer.Config{ Enable: snifferRaw.Enable, ForceDnsMapping: snifferRaw.ForceDnsMapping, ParsePureIp: snifferRaw.ParsePureIp, } - loadSniffer := make(map[snifferTypes.Type]SNIFF.SnifferConfig) + loadSniffer := make(map[snifferTypes.Type]sniffer.SnifferConfig) if len(snifferRaw.Sniff) != 0 { for sniffType, sniffConfig := range snifferRaw.Sniff { @@ -1493,7 +1628,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { for _, snifferType := range snifferTypes.List { if snifferType.String() == strings.ToUpper(sniffType) { find = true - loadSniffer[snifferType] = SNIFF.SnifferConfig{ + loadSniffer[snifferType] = sniffer.SnifferConfig{ Ports: ports, OverrideDest: overrideDest, } @@ -1505,7 +1640,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { } } } else { - if sniffer.Enable { + if snifferConfig.Enable && len(snifferRaw.Sniffing) != 0 { // Deprecated: Use Sniff instead log.Warnln("Deprecated: Use Sniff instead") } @@ -1519,7 +1654,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { for _, snifferType := range snifferTypes.List { if snifferType.String() == strings.ToUpper(snifferName) { find = true - loadSniffer[snifferType] = SNIFF.SnifferConfig{ + loadSniffer[snifferType] = sniffer.SnifferConfig{ Ports: globalPorts, OverrideDest: snifferRaw.OverrideDest, } @@ -1532,25 +1667,151 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { } } - sniffer.Sniffers = loadSniffer + snifferConfig.Sniffers = loadSniffer - 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) - } + forceDomain, err := parseDomain(snifferRaw.ForceDomain, nil, "sniffer.force-domain", ruleProviders) + if err != nil { + return nil, fmt.Errorf("error in force-domain, error:%w", err) } - sniffer.ForceDomain = forceDomainTrie.NewDomainSet() + snifferConfig.ForceDomain = forceDomain - 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) - } + skipSrcAddress, err := parseIPCIDR(snifferRaw.SkipSrcAddress, nil, "sniffer.skip-src-address", ruleProviders) + if err != nil { + return nil, fmt.Errorf("error in skip-src-address, error:%w", err) } - sniffer.SkipDomain = skipDomainTrie.NewDomainSet() + snifferConfig.SkipSrcAddress = skipSrcAddress - return sniffer, nil + skipDstAddress, err := parseIPCIDR(snifferRaw.SkipDstAddress, nil, "sniffer.skip-src-address", ruleProviders) + if err != nil { + return nil, fmt.Errorf("error in skip-dst-address, error:%w", err) + } + snifferConfig.SkipDstAddress = skipDstAddress + + skipDomain, err := parseDomain(snifferRaw.SkipDomain, nil, "sniffer.skip-domain", ruleProviders) + if err != nil { + return nil, fmt.Errorf("error in skip-domain, error:%w", err) + } + snifferConfig.SkipDomain = skipDomain + + return snifferConfig, nil +} + +func parseIPCIDR(addresses []string, cidrSet *cidr.IpCidrSet, adapterName string, ruleProviders map[string]providerTypes.RuleProvider) (matchers []C.IpMatcher, err error) { + var matcher C.IpMatcher + for _, ipcidr := range addresses { + ipcidrLower := strings.ToLower(ipcidr) + if strings.Contains(ipcidrLower, "geoip:") { + subkeys := strings.Split(ipcidr, ":") + subkeys = subkeys[1:] + subkeys = strings.Split(subkeys[0], ",") + for _, country := range subkeys { + matcher, err = RC.NewGEOIP(country, adapterName, false, false) + if err != nil { + return nil, err + } + matchers = append(matchers, matcher) + } + } else if strings.Contains(ipcidrLower, "rule-set:") { + subkeys := strings.Split(ipcidr, ":") + subkeys = subkeys[1:] + subkeys = strings.Split(subkeys[0], ",") + for _, domainSetName := range subkeys { + matcher, err = parseIPRuleSet(domainSetName, adapterName, ruleProviders) + if err != nil { + return nil, err + } + matchers = append(matchers, matcher) + } + } else { + if cidrSet == nil { + cidrSet = cidr.NewIpCidrSet() + } + err = cidrSet.AddIpCidrForString(ipcidr) + if err != nil { + return nil, err + } + } + } + if !cidrSet.IsEmpty() { + err = cidrSet.Merge() + if err != nil { + return nil, err + } + matcher = cidrSet + matchers = append(matchers, matcher) + } + return +} + +func parseDomain(domains []string, domainTrie *trie.DomainTrie[struct{}], adapterName string, ruleProviders map[string]providerTypes.RuleProvider) (matchers []C.DomainMatcher, err error) { + var matcher C.DomainMatcher + for _, domain := range domains { + domainLower := strings.ToLower(domain) + if strings.Contains(domainLower, "geosite:") { + subkeys := strings.Split(domain, ":") + subkeys = subkeys[1:] + subkeys = strings.Split(subkeys[0], ",") + for _, country := range subkeys { + matcher, err = RC.NewGEOSITE(country, adapterName) + if err != nil { + return nil, err + } + matchers = append(matchers, matcher) + } + } else if strings.Contains(domainLower, "rule-set:") { + subkeys := strings.Split(domain, ":") + subkeys = subkeys[1:] + subkeys = strings.Split(subkeys[0], ",") + for _, domainSetName := range subkeys { + matcher, err = parseDomainRuleSet(domainSetName, adapterName, ruleProviders) + if err != nil { + return nil, err + } + matchers = append(matchers, matcher) + } + } else { + if domainTrie == nil { + domainTrie = trie.New[struct{}]() + } + err = domainTrie.Insert(domain, struct{}{}) + if err != nil { + return nil, err + } + } + } + if !domainTrie.IsEmpty() { + matcher = domainTrie.NewDomainSet() + matchers = append(matchers, matcher) + } + return +} + +func parseIPRuleSet(domainSetName string, adapterName string, ruleProviders map[string]providerTypes.RuleProvider) (C.IpMatcher, error) { + if rp, ok := ruleProviders[domainSetName]; !ok { + return nil, fmt.Errorf("not found rule-set: %s", domainSetName) + } else { + switch rp.Behavior() { + case providerTypes.Domain: + return nil, fmt.Errorf("rule provider type error, except ipcidr,actual %s", rp.Behavior()) + case providerTypes.Classical: + log.Warnln("%s provider is %s, only matching it contain ip rule", rp.Name(), rp.Behavior()) + default: + } + } + return RP.NewRuleSet(domainSetName, adapterName, false, true) +} + +func parseDomainRuleSet(domainSetName string, adapterName string, ruleProviders map[string]providerTypes.RuleProvider) (C.DomainMatcher, error) { + if rp, ok := ruleProviders[domainSetName]; !ok { + return nil, fmt.Errorf("not found rule-set: %s", domainSetName) + } else { + switch rp.Behavior() { + case providerTypes.IPCIDR: + return nil, fmt.Errorf("rule provider type error, except domain,actual %s", rp.Behavior()) + case providerTypes.Classical: + log.Warnln("%s provider is %s, only matching it contain domain rule", rp.Name(), rp.Behavior()) + default: + } + } + return RP.NewRuleSet(domainSetName, adapterName, false, true) } diff --git a/config/update_geo.go b/config/update_geo.go deleted file mode 100644 index 43cac25c..00000000 --- a/config/update_geo.go +++ /dev/null @@ -1,90 +0,0 @@ -package config - -import ( - "fmt" - "runtime" - - "github.com/metacubex/mihomo/component/geodata" - _ "github.com/metacubex/mihomo/component/geodata/standard" - "github.com/metacubex/mihomo/component/mmdb" - C "github.com/metacubex/mihomo/constant" - - "github.com/oschwald/maxminddb-golang" -) - -func UpdateGeoDatabases() error { - defer runtime.GC() - geoLoader, err := geodata.GetGeoDataLoader("standard") - if err != nil { - return err - } - - if C.GeodataMode { - data, err := downloadForBytes(C.GeoIpUrl) - if err != nil { - return fmt.Errorf("can't download GeoIP database file: %w", err) - } - - if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil { - return fmt.Errorf("invalid GeoIP database file: %s", err) - } - - if err = saveFile(data, C.Path.GeoIP()); err != nil { - return fmt.Errorf("can't save GeoIP database file: %w", err) - } - - } else { - defer mmdb.ReloadIP() - data, err := downloadForBytes(C.MmdbUrl) - if err != nil { - return fmt.Errorf("can't download MMDB database file: %w", err) - } - - instance, err := maxminddb.FromBytes(data) - if err != nil { - return fmt.Errorf("invalid MMDB database file: %s", err) - } - _ = instance.Close() - - mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file - if err = saveFile(data, C.Path.MMDB()); err != nil { - return fmt.Errorf("can't save MMDB database file: %w", err) - } - } - - if C.ASNEnable { - defer mmdb.ReloadASN() - data, err := downloadForBytes(C.ASNUrl) - if err != nil { - return fmt.Errorf("can't download ASN database file: %w", err) - } - - instance, err := maxminddb.FromBytes(data) - if err != nil { - return fmt.Errorf("invalid ASN database file: %s", err) - } - _ = instance.Close() - - mmdb.ASNInstance().Reader.Close() - if err = saveFile(data, C.Path.ASN()); err != nil { - return fmt.Errorf("can't save ASN database file: %w", err) - } - } - - data, err := downloadForBytes(C.GeoSiteUrl) - if err != nil { - return fmt.Errorf("can't download GeoSite database file: %w", err) - } - - if _, err = geoLoader.LoadSiteByBytes(data, "cn"); err != nil { - return fmt.Errorf("invalid GeoSite database file: %s", err) - } - - if err = saveFile(data, C.Path.GeoSite()); err != nil { - return fmt.Errorf("can't save GeoSite database file: %w", err) - } - - geodata.ClearCache() - - return nil -} diff --git a/config/utils.go b/config/utils.go index 66bf3441..f87fb341 100644 --- a/config/utils.go +++ b/config/utils.go @@ -1,38 +1,15 @@ package config import ( - "context" "fmt" - "io" "net" - "net/http" "net/netip" - "os" "strings" - "time" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/structure" - mihomoHttp "github.com/metacubex/mihomo/component/http" - C "github.com/metacubex/mihomo/constant" ) -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": {C.UA}}, 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, " ")) diff --git a/constant/adapters.go b/constant/adapters.go index cb213b3c..420a797f 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -42,6 +42,7 @@ const ( WireGuard Tuic Ssh + Mieru ) const ( @@ -99,13 +100,24 @@ type Dialer interface { ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) } +type ProxyInfo struct { + XUDP bool + TFO bool + MPTCP bool + SMUX bool + Interface string + RoutingMark int + DialerProxy string +} + type ProxyAdapter interface { Name() string Type() AdapterType Addr() string SupportUDP() bool - SupportXUDP() bool - SupportTFO() bool + + // ProxyInfo contains some extra information maybe useful for MarshalJSON + ProxyInfo() ProxyInfo MarshalJSON() ([]byte, error) // Deprecated: use DialContextWithDialer and ListenPacketWithDialer instead. @@ -158,6 +170,7 @@ type DelayHistoryStoreType int type Proxy interface { ProxyAdapter + Adapter() ProxyAdapter AliveForTestUrl(url string) bool DelayHistory() []DelayHistory ExtraDelayHistories() map[string]ProxyState @@ -212,7 +225,10 @@ func (at AdapterType) String() string { return "WireGuard" case Tuic: return "Tuic" - + case Ssh: + return "Ssh" + case Mieru: + return "Mieru" case Relay: return "Relay" case Selector: @@ -223,8 +239,6 @@ func (at AdapterType) String() string { return "URLTest" case LoadBalance: return "LoadBalance" - case Ssh: - return "Ssh" default: return "Unknown" } @@ -255,12 +269,16 @@ type UDPPacketInAddr interface { // PacketAdapter is a UDP Packet adapter for socks/redir/tun type PacketAdapter interface { UDPPacket + // Metadata returns destination metadata Metadata() *Metadata + // Key is a SNAT key + Key() string } type packetAdapter struct { UDPPacket metadata *Metadata + key string } // Metadata returns destination metadata @@ -268,10 +286,16 @@ func (s *packetAdapter) Metadata() *Metadata { return s.metadata } +// Key is a SNAT key +func (s *packetAdapter) Key() string { + return s.key +} + func NewPacketAdapter(packet UDPPacket, metadata *Metadata) PacketAdapter { return &packetAdapter{ packet, metadata, + packet.LocalAddr().String(), } } @@ -284,17 +308,23 @@ type WriteBackProxy interface { UpdateWriteBack(wb WriteBack) } +type PacketSender interface { + // Send will send PacketAdapter nonblocking + // the implement must call UDPPacket.Drop() inside Send + Send(PacketAdapter) + // Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy + Process(PacketConn, WriteBackProxy) + // ResolveUDP do a local resolve UDP dns blocking if metadata is not resolved + ResolveUDP(*Metadata) error + // Close stop the Process loop + Close() +} + type NatTable interface { - Set(key string, e PacketConn, w WriteBackProxy) - - Get(key string) (PacketConn, WriteBackProxy) - - GetOrCreateLock(key string) (*sync.Cond, bool) + GetOrCreate(key string, maker func() PacketSender) (PacketSender, bool) Delete(key string) - DeleteLock(key string) - GetForLocalConn(lAddr, rAddr string) *net.UDPConn AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool diff --git a/constant/dns.go b/constant/dns.go index 3d97d97b..b0874eef 100644 --- a/constant/dns.go +++ b/constant/dns.go @@ -3,6 +3,7 @@ package constant import ( "encoding/json" "errors" + "strings" ) // DNSModeMapping is a mapping for EnhancedMode enum @@ -27,7 +28,7 @@ func (e *DNSMode) UnmarshalYAML(unmarshal func(any) error) error { if err := unmarshal(&tp); err != nil { return err } - mode, exist := DNSModeMapping[tp] + mode, exist := DNSModeMapping[strings.ToLower(tp)] if !exist { return errors.New("invalid mode") } @@ -43,8 +44,10 @@ func (e DNSMode) MarshalYAML() (any, error) { // UnmarshalJSON unserialize EnhancedMode with json func (e *DNSMode) UnmarshalJSON(data []byte) error { var tp string - json.Unmarshal(data, &tp) - mode, exist := DNSModeMapping[tp] + if err := json.Unmarshal(data, &tp); err != nil { + return err + } + mode, exist := DNSModeMapping[strings.ToLower(tp)] if !exist { return errors.New("invalid mode") } @@ -57,6 +60,21 @@ func (e DNSMode) MarshalJSON() ([]byte, error) { return json.Marshal(e.String()) } +// UnmarshalText unserialize EnhancedMode +func (e *DNSMode) UnmarshalText(data []byte) error { + mode, exist := DNSModeMapping[strings.ToLower(string(data))] + if !exist { + return errors.New("invalid mode") + } + *e = mode + return nil +} + +// MarshalText serialize EnhancedMode +func (e DNSMode) MarshalText() ([]byte, error) { + return []byte(e.String()), nil +} + func (e DNSMode) String() string { switch e { case DNSNormal: @@ -115,6 +133,77 @@ func NewDNSPrefer(prefer string) DNSPrefer { } } +// FilterModeMapping is a mapping for FilterMode enum +var FilterModeMapping = map[string]FilterMode{ + FilterBlackList.String(): FilterBlackList, + FilterWhiteList.String(): FilterWhiteList, +} + +type FilterMode int + +const ( + FilterBlackList FilterMode = iota + FilterWhiteList +) + +func (e FilterMode) String() string { + switch e { + case FilterBlackList: + return "blacklist" + case FilterWhiteList: + return "whitelist" + default: + return "unknown" + } +} + +func (e FilterMode) MarshalYAML() (interface{}, error) { + return e.String(), nil +} + +func (e *FilterMode) UnmarshalYAML(unmarshal func(interface{}) error) error { + var tp string + if err := unmarshal(&tp); err != nil { + return err + } + mode, exist := FilterModeMapping[strings.ToLower(tp)] + if !exist { + return errors.New("invalid mode") + } + *e = mode + return nil +} + +func (e FilterMode) MarshalJSON() ([]byte, error) { + return json.Marshal(e.String()) +} + +func (e *FilterMode) UnmarshalJSON(data []byte) error { + var tp string + if err := json.Unmarshal(data, &tp); err != nil { + return err + } + mode, exist := FilterModeMapping[strings.ToLower(tp)] + if !exist { + return errors.New("invalid mode") + } + *e = mode + return nil +} + +func (e FilterMode) MarshalText() ([]byte, error) { + return []byte(e.String()), nil +} + +func (e *FilterMode) UnmarshalText(data []byte) error { + mode, exist := FilterModeMapping[strings.ToLower(string(data))] + if !exist { + return errors.New("invalid mode") + } + *e = mode + return nil +} + type HTTPVersion string const ( diff --git a/constant/ebpf.go b/constant/ebpf.go deleted file mode 100644 index e3bb62fe..00000000 --- a/constant/ebpf.go +++ /dev/null @@ -1,20 +0,0 @@ -package constant - -import ( - "net/netip" - - "github.com/metacubex/mihomo/transport/socks5" -) - -const ( - BpfFSPath = "/sys/fs/bpf/mihomo" - - TcpAutoRedirPort = 't'<<8 | 'r'<<0 - MihomoTrafficMark = 'c'<<24 | 'l'<<16 | 't'<<8 | 'm'<<0 -) - -type EBpf interface { - Start() error - Close() - Lookup(srcAddrPort netip.AddrPort) (socks5.Addr, error) -} diff --git a/constant/features/version.go b/constant/features/version.go new file mode 100644 index 00000000..1710e218 --- /dev/null +++ b/constant/features/version.go @@ -0,0 +1,5 @@ +package features + +var WindowsMajorVersion uint32 +var WindowsMinorVersion uint32 +var WindowsBuildNumber uint32 diff --git a/constant/features/version_windows.go b/constant/features/version_windows.go new file mode 100644 index 00000000..2f756188 --- /dev/null +++ b/constant/features/version_windows.go @@ -0,0 +1,10 @@ +package features + +import "golang.org/x/sys/windows" + +func init() { + version := windows.RtlGetVersion() + WindowsMajorVersion = version.MajorVersion + WindowsMinorVersion = version.MinorVersion + WindowsBuildNumber = version.BuildNumber +} diff --git a/constant/geodata.go b/constant/geodata.go deleted file mode 100644 index cd3f74e3..00000000 --- a/constant/geodata.go +++ /dev/null @@ -1,12 +0,0 @@ -package constant - -var ( - ASNEnable bool - GeodataMode bool - GeoAutoUpdate bool - GeoUpdateInterval int - GeoIpUrl string - MmdbUrl string - GeoSiteUrl string - ASNUrl string -) diff --git a/constant/http.go b/constant/http.go deleted file mode 100644 index 8e321f6b..00000000 --- a/constant/http.go +++ /dev/null @@ -1,5 +0,0 @@ -package constant - -var ( - UA string -) diff --git a/constant/matcher.go b/constant/matcher.go new file mode 100644 index 00000000..107f843d --- /dev/null +++ b/constant/matcher.go @@ -0,0 +1,11 @@ +package constant + +import "net/netip" + +type DomainMatcher interface { + MatchDomain(domain string) bool +} + +type IpMatcher interface { + MatchIp(ip netip.Addr) bool +} diff --git a/constant/metadata.go b/constant/metadata.go index 381e2dd4..54362989 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -133,7 +133,9 @@ type Metadata struct { Type Type `json:"type"` SrcIP netip.Addr `json:"sourceIP"` DstIP netip.Addr `json:"destinationIP"` + SrcGeoIP []string `json:"sourceGeoIP"` // can be nil if never queried, empty slice if got no result DstGeoIP []string `json:"destinationGeoIP"` // can be nil if never queried, empty slice if got no result + SrcIPASN string `json:"sourceIPASN"` DstIPASN string `json:"destinationIPASN"` SrcPort uint16 `json:"sourcePort,string"` // `,string` is used to compatible with old version json output DstPort uint16 `json:"destinationPort,string"` // `,string` is used to compatible with old version json output @@ -300,3 +302,10 @@ func (m *Metadata) SetRemoteAddress(rawAddress string) error { return nil } + +func (m *Metadata) SwapSrcDst() { + m.SrcIP, m.DstIP = m.DstIP, m.SrcIP + m.SrcPort, m.DstPort = m.DstPort, m.SrcPort + m.SrcIPASN, m.DstIPASN = m.DstIPASN, m.SrcIPASN + m.SrcGeoIP, m.DstGeoIP = m.DstGeoIP, m.SrcGeoIP +} diff --git a/constant/path.go b/constant/path.go index 77f7d0ef..1594441c 100644 --- a/constant/path.go +++ b/constant/path.go @@ -1,13 +1,14 @@ package constant import ( - "crypto/md5" - "encoding/hex" "os" P "path" "path/filepath" "strconv" "strings" + + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/constant/features" ) const Name = "mihomo" @@ -73,7 +74,7 @@ func (p *path) Resolve(path string) string { // IsSafePath return true if path is a subpath of homedir func (p *path) IsSafePath(path string) bool { - if p.allowUnsafePath { + if p.allowUnsafePath || features.CMFA { return true } homedir := p.HomeDir() @@ -87,8 +88,8 @@ func (p *path) IsSafePath(path string) bool { } func (p *path) GetPathByHash(prefix, name string) string { - hash := md5.Sum([]byte(name)) - filename := hex.EncodeToString(hash[:]) + hash := utils.MakeHash([]byte(name)) + filename := hash.String() return filepath.Join(p.HomeDir(), prefix, filename) } diff --git a/constant/provider/interface.go b/constant/provider/interface.go index f2b6939e..8c6ee6f8 100644 --- a/constant/provider/interface.go +++ b/constant/provider/interface.go @@ -1,6 +1,9 @@ package provider import ( + "context" + "fmt" + "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/constant" ) @@ -10,6 +13,7 @@ const ( File VehicleType = iota HTTP Compatible + Inline ) // VehicleType defined @@ -23,14 +27,19 @@ func (v VehicleType) String() string { return "HTTP" case Compatible: return "Compatible" + case Inline: + return "Inline" default: return "Unknown" } } type Vehicle interface { - Read() ([]byte, error) + Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) + Write(buf []byte) error Path() string + Url() string + Proxy() string Type() VehicleType } @@ -67,6 +76,7 @@ type Provider interface { type ProxyProvider interface { Provider Proxies() []constant.Proxy + Count() int // Touch is used to inform the provider that the proxy is actually being used while getting the list of proxies. // Commonly used in DialContext and DialPacketConn Touch() @@ -80,10 +90,11 @@ type ProxyProvider interface { type RuleProvider interface { Provider Behavior() RuleBehavior + Count() int Match(*constant.Metadata) bool ShouldResolveIP() bool ShouldFindProcess() bool - AsRule(adaptor string) constant.Rule + Strategy() any } // Rule Behavior @@ -109,9 +120,37 @@ func (rt RuleBehavior) String() string { } } +func (rt RuleBehavior) Byte() byte { + switch rt { + case Domain: + return 0 + case IPCIDR: + return 1 + case Classical: + return 2 + default: + return 255 + } +} + +func ParseBehavior(s string) (behavior RuleBehavior, err error) { + switch s { + case "domain": + behavior = Domain + case "ipcidr": + behavior = IPCIDR + case "classical": + behavior = Classical + default: + err = fmt.Errorf("unsupported behavior type: %s", s) + } + return +} + const ( YamlRule RuleFormat = iota TextRule + MrsRule ) type RuleFormat int @@ -122,7 +161,29 @@ func (rf RuleFormat) String() string { return "YamlRule" case TextRule: return "TextRule" + case MrsRule: + return "MrsRule" default: return "Unknown" } } + +func ParseRuleFormat(s string) (format RuleFormat, err error) { + switch s { + case "", "yaml": + format = YamlRule + case "text": + format = TextRule + case "mrs": + format = MrsRule + default: + err = fmt.Errorf("unsupported format type: %s", s) + } + return +} + +type Tunnel interface { + Providers() map[string]ProxyProvider + RuleProviders() map[string]RuleProvider + RuleUpdateCallback() *utils.Callback[RuleProvider] +} diff --git a/constant/rule.go b/constant/rule.go index fcefaba6..31702ddc 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -8,8 +8,10 @@ const ( DomainRegex GEOSITE GEOIP - IPCIDR + SrcGEOIP IPASN + SrcIPASN + IPCIDR SrcIPCIDR IPSuffix SrcIPSuffix @@ -20,8 +22,10 @@ const ( InUser InName InType - Process + ProcessName ProcessPath + ProcessNameRegex + ProcessPathRegex RuleSet Network Uid @@ -48,10 +52,14 @@ func (rt RuleType) String() string { return "GeoSite" case GEOIP: return "GeoIP" - case IPCIDR: - return "IPCIDR" + case SrcGEOIP: + return "SrcGeoIP" case IPASN: return "IPASN" + case SrcIPASN: + return "SrcIPASN" + case IPCIDR: + return "IPCIDR" case SrcIPCIDR: return "SrcIPCIDR" case IPSuffix: @@ -70,10 +78,14 @@ func (rt RuleType) String() string { return "InName" case InType: return "InType" - case Process: - return "Process" + case ProcessName: + return "ProcessName" case ProcessPath: return "ProcessPath" + case ProcessNameRegex: + return "ProcessNameRegex" + case ProcessPathRegex: + return "ProcessPathRegex" case MATCH: return "Match" case RuleSet: @@ -104,4 +116,10 @@ type Rule interface { Payload() string ShouldResolveIP() bool ShouldFindProcess() bool + ProviderNames() []string +} + +type RuleGroup interface { + Rule + GetRecodeSize() int } diff --git a/constant/rule_extra.go b/constant/rule_extra.go deleted file mode 100644 index b4ba65d9..00000000 --- a/constant/rule_extra.go +++ /dev/null @@ -1,17 +0,0 @@ -package constant - -import ( - "github.com/metacubex/mihomo/component/geodata/router" -) - -type RuleGeoSite interface { - GetDomainMatcher() router.DomainMatcher -} - -type RuleGeoIP interface { - GetIPMatcher() *router.GeoIPMatcher -} - -type RuleGroup interface { - GetRecodeSize() int -} diff --git a/constant/tun.go b/constant/tun.go index f6c0e011..669f7a2e 100644 --- a/constant/tun.go +++ b/constant/tun.go @@ -56,6 +56,21 @@ func (e TUNStack) MarshalJSON() ([]byte, error) { return json.Marshal(e.String()) } +// UnmarshalText unserialize TUNStack +func (e *TUNStack) UnmarshalText(data []byte) error { + mode, exist := StackTypeMapping[strings.ToLower(string(data))] + if !exist { + return errors.New("invalid tun stack") + } + *e = mode + return nil +} + +// MarshalText serialize TUNStack with json +func (e TUNStack) MarshalText() ([]byte, error) { + return []byte(e.String()), nil +} + func (e TUNStack) String() string { switch e { case TunGvisor: diff --git a/dns/client.go b/dns/client.go index 95f0f29b..62fc12f9 100644 --- a/dns/client.go +++ b/dns/client.go @@ -5,27 +5,20 @@ import ( "crypto/tls" "fmt" "net" - "net/netip" "strings" "github.com/metacubex/mihomo/component/ca" - "github.com/metacubex/mihomo/component/dialer" - "github.com/metacubex/mihomo/component/resolver" - C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" - "github.com/zhangyunhao116/fastrand" ) type client struct { *D.Client - r *Resolver - port string - host string - iface string - proxyAdapter C.ProxyAdapter - proxyName string - addr string + port string + host string + dialer *dnsDialer + addr string } var _ dnsClient = (*client)(nil) @@ -48,36 +41,13 @@ func (c *client) Address() string { } func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { - var ( - ip netip.Addr - err error - ) - if c.r == nil { - // a default ip dns - if ip, err = netip.ParseAddr(c.host); err != nil { - return nil, fmt.Errorf("dns %s not a valid ip", c.host) - } - } else { - ips, err := resolver.LookupIPWithResolver(ctx, c.host, c.r) - if err != nil { - return nil, fmt.Errorf("use default dns resolve failed: %w", err) - } else if len(ips) == 0 { - return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, c.host) - } - ip = ips[fastrand.Intn(len(ips))] - } - network := "udp" if strings.HasPrefix(c.Client.Net, "tcp") { network = "tcp" } - var options []dialer.Option - if c.iface != "" { - options = append(options, dialer.WithInterface(c.iface)) - } - - conn, err := getDialHandler(c.r, c.proxyAdapter, c.proxyName, options...)(ctx, network, net.JoinHostPort(ip.String(), c.port)) + addr := net.JoinHostPort(c.host, c.port) + conn, err := c.dialer.DialContext(ctx, network, addr) if err != nil { return nil, err } @@ -97,12 +67,31 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) conn = tls.Client(conn, ca.GetGlobalTLSConfig(c.Client.TLSConfig)) } - msg, _, err := c.Client.ExchangeWithConn(m, &D.Conn{ + dConn := &D.Conn{ Conn: conn, UDPSize: c.Client.UDPSize, TsigSecret: c.Client.TsigSecret, TsigProvider: c.Client.TsigProvider, - }) + } + + msg, _, err := c.Client.ExchangeWithConn(m, dConn) + + // Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)! + if msg != nil && msg.Truncated && c.Client.Net == "" { + tcpClient := *c.Client // copy a client + tcpClient.Net = "tcp" + network = "tcp" + log.Debugln("[DNS] Truncated reply from %s:%s for %s over UDP, retrying over TCP", c.host, c.port, m.Question[0].String()) + dConn.Conn, err = c.dialer.DialContext(ctx, network, addr) + if err != nil { + ch <- result{msg, err} + return + } + defer func() { + _ = conn.Close() + }() + msg, _, err = tcpClient.ExchangeWithConn(m, dConn) + } ch <- result{msg, err} }() @@ -114,3 +103,5 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) return ret.msg, ret.err } } + +func (c *client) ResetConnection() {} diff --git a/dns/dhcp.go b/dns/dhcp.go index d4944a96..e3829b7c 100644 --- a/dns/dhcp.go +++ b/dns/dhcp.go @@ -1,5 +1,3 @@ -//go:build !(android && cmfa) - package dns import ( @@ -55,6 +53,12 @@ func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, return } +func (d *dhcpClient) ResetConnection() { + for _, client := range d.clients { + client.ResetConnection() + } +} + func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) { d.lock.Lock() diff --git a/dns/dialer.go b/dns/dialer.go new file mode 100644 index 00000000..f4d9e128 --- /dev/null +++ b/dns/dialer.go @@ -0,0 +1,11 @@ +package dns + +// export functions from tunnel module + +import "github.com/metacubex/mihomo/tunnel" + +const RespectRules = tunnel.DnsRespectRules + +type dnsDialer = tunnel.DNSDialer + +var newDNSDialer = tunnel.NewDNSDialer diff --git a/dns/doh.go b/dns/doh.go index ef4b653f..027afd58 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -9,6 +9,7 @@ import ( "io" "net" "net/http" + "net/netip" "net/url" "runtime" "strconv" @@ -61,12 +62,14 @@ type dnsOverHTTPS struct { // for this upstream. quicConfig *quic.Config quicConfigGuard sync.Mutex - url *url.URL - r *Resolver - httpVersions []C.HTTPVersion - proxyAdapter C.ProxyAdapter - proxyName string - addr string + + url *url.URL + httpVersions []C.HTTPVersion + dialer *dnsDialer + addr string + skipCertVerify bool + ecsPrefix netip.Prefix + ecsOverride bool } // type check @@ -85,11 +88,9 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin } doh := &dnsOverHTTPS{ - url: u, - addr: u.String(), - r: r, - proxyAdapter: proxyAdapter, - proxyName: proxyName, + url: u, + addr: u.String(), + dialer: newDNSDialer(r, proxyAdapter, proxyName), quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, TokenStore: newQUICTokenStore(), @@ -97,6 +98,32 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin httpVersions: httpVersions, } + if params["skip-cert-verify"] == "true" { + doh.skipCertVerify = true + } + + if ecs := params["ecs"]; ecs != "" { + prefix, err := netip.ParsePrefix(ecs) + if err != nil { + addr, err := netip.ParseAddr(ecs) + if err != nil { + log.Warnln("DOH [%s] config with invalid ecs: %s", doh.addr, ecs) + } else { + doh.ecsPrefix = netip.PrefixFrom(addr, addr.BitLen()) + } + } else { + doh.ecsPrefix = prefix + } + } + + if doh.ecsPrefix.IsValid() { + log.Debugln("DOH [%s] config with ecs: %s", doh.addr, doh.ecsPrefix) + } + + if params["ecs-override"] == "true" { + doh.ecsOverride = true + } + runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close) return doh @@ -106,6 +133,7 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin func (doh *dnsOverHTTPS) Address() string { return doh.addr } + func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { // Quote from https://www.rfc-editor.org/rfc/rfc8484.html: // In order to maximize HTTP cache friendliness, DoH clients using media @@ -123,6 +151,10 @@ func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D. } }() + if doh.ecsPrefix.IsValid() { + setEdns0Subnet(m, doh.ecsPrefix, doh.ecsOverride) + } + // Check if there was already an active client before sending the request. // We'll only attempt to re-connect if there was one. client, isCached, err := doh.getClient(ctx) @@ -171,30 +203,32 @@ func (doh *dnsOverHTTPS) Close() (err error) { return doh.closeClient(doh.client) } -// closeClient cleans up resources used by client if necessary. Note, that at -// this point it should only be done for HTTP/3 as it may leak due to keep-alive -// connections. +func (doh *dnsOverHTTPS) ResetConnection() { + doh.clientMu.Lock() + defer doh.clientMu.Unlock() + + if doh.client == nil { + return + } + + _ = doh.closeClient(doh.client) + doh.client = nil +} + +// closeClient cleans up resources used by client if necessary. func (doh *dnsOverHTTPS) closeClient(client *http.Client) (err error) { - if isHTTP3(client) { + client.CloseIdleConnections() + + if isHTTP3(client) { // HTTP/3 may leak due to keep-alive connections. return client.Transport.(io.Closer).Close() } return nil } -// exchangeHTTPS logs the request and its result and calls exchangeHTTPSClient. -func (doh *dnsOverHTTPS) exchangeHTTPS(ctx context.Context, client *http.Client, req *D.Msg) (resp *D.Msg, err error) { - resp, err = doh.exchangeHTTPSClient(ctx, client, req) - return resp, err -} - -// exchangeHTTPSClient sends the DNS query to a DoH resolver using the specified +// exchangeHTTPS sends the DNS query to a DoH resolver using the specified // http.Client instance. -func (doh *dnsOverHTTPS) exchangeHTTPSClient( - ctx context.Context, - client *http.Client, - req *D.Msg, -) (resp *D.Msg, err error) { +func (doh *dnsOverHTTPS) exchangeHTTPS(ctx context.Context, client *http.Client, req *D.Msg) (resp *D.Msg, err error) { buf, err := req.Pack() if err != nil { return nil, fmt.Errorf("packing message: %w", err) @@ -208,24 +242,24 @@ func (doh *dnsOverHTTPS) exchangeHTTPSClient( method = http3.MethodGet0RTT } - url := doh.url - url.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf)) - httpReq, err := http.NewRequestWithContext(ctx, method, url.String(), nil) + requestUrl := *doh.url // don't modify origin url + requestUrl.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf)) + httpReq, err := http.NewRequestWithContext(ctx, method, requestUrl.String(), nil) if err != nil { - return nil, fmt.Errorf("creating http request to %s: %w", url, err) + return nil, fmt.Errorf("creating http request to %s: %w", doh.url, err) } httpReq.Header.Set("Accept", "application/dns-message") httpReq.Header.Set("User-Agent", "") httpResp, err := client.Do(httpReq) if err != nil { - return nil, fmt.Errorf("requesting %s: %w", url, err) + return nil, fmt.Errorf("requesting %s: %w", doh.url, err) } defer httpResp.Body.Close() body, err := io.ReadAll(httpResp.Body) if err != nil { - return nil, fmt.Errorf("reading %s: %w", url, err) + return nil, fmt.Errorf("reading %s: %w", doh.url, err) } if httpResp.StatusCode != http.StatusOK { @@ -234,7 +268,7 @@ func (doh *dnsOverHTTPS) exchangeHTTPSClient( "expected status %d, got %d from %s", http.StatusOK, httpResp.StatusCode, - url, + doh.url, ) } @@ -243,7 +277,7 @@ func (doh *dnsOverHTTPS) exchangeHTTPSClient( if err != nil { return nil, fmt.Errorf( "unpacking response from %s: body is %s: %w", - url, + doh.url, body, err, ) @@ -377,9 +411,21 @@ 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) { + transport := &http.Transport{ + DisableCompression: true, + DialContext: doh.dialer.DialContext, + IdleConnTimeout: transportDefaultIdleConnTimeout, + MaxConnsPerHost: dohMaxConnsPerHost, + MaxIdleConns: dohMaxIdleConns, + } + + if doh.url.Scheme == "http" { + return transport, nil + } + tlsConfig := ca.GetGlobalTLSConfig( &tls.Config{ - InsecureSkipVerify: false, + InsecureSkipVerify: doh.skipCertVerify, MinVersion: tls.VersionTLS12, SessionTicketsDisabled: false, }) @@ -388,13 +434,13 @@ func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripp nextProtos = append(nextProtos, string(v)) } tlsConfig.NextProtos = nextProtos - dialContext := getDialHandler(doh.r, doh.proxyAdapter, doh.proxyName) + transport.TLSClientConfig = tlsConfig if slices.Contains(doh.httpVersions, C.HTTPVersion3) { // First, we attempt to create an HTTP3 transport. If the probe QUIC // connection is established successfully, we'll be using HTTP3 for this // upstream. - transportH3, err := doh.createTransportH3(ctx, tlsConfig, dialContext) + transportH3, err := doh.createTransportH3(ctx, tlsConfig) if err == nil { log.Debugln("[%s] using HTTP/3 for this upstream: QUIC was faster", doh.url.String()) return transportH3, nil @@ -407,18 +453,10 @@ func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripp return nil, errors.New("HTTP1/1 and HTTP2 are not supported by this upstream") } - transport := &http.Transport{ - TLSClientConfig: tlsConfig, - DisableCompression: true, - DialContext: dialContext, - IdleConnTimeout: transportDefaultIdleConnTimeout, - MaxConnsPerHost: dohMaxConnsPerHost, - MaxIdleConns: dohMaxIdleConns, - // Since we have a custom DialContext, we need to use this field to - // make golang http.Client attempt to use HTTP/2. Otherwise, it would - // only be used when negotiated on the TLS level. - ForceAttemptHTTP2: true, - } + // Since we have a custom DialContext, we need to use this field to + // make golang http.Client attempt to use HTTP/2. Otherwise, it would + // only be used when negotiated on the TLS level. + transport.ForceAttemptHTTP2 = true // Explicitly configure transport to use HTTP/2. // @@ -482,6 +520,13 @@ func (h *http3Transport) Close() (err error) { return h.baseTransport.Close() } +func (h *http3Transport) CloseIdleConnections() { + h.mu.RLock() + defer h.mu.RUnlock() + + h.baseTransport.CloseIdleConnections() +} + // createTransportH3 tries to create an HTTP/3 transport for this upstream. // We should be able to fall back to H1/H2 in case if HTTP/3 is unavailable or // if it is too slow. In order to do that, this method will run two probes @@ -490,13 +535,12 @@ func (h *http3Transport) Close() (err error) { func (doh *dnsOverHTTPS) createTransportH3( ctx context.Context, tlsConfig *tls.Config, - dialContext dialHandler, ) (roundTripper http.RoundTripper, err error) { if !doh.supportsH3() { return nil, errors.New("HTTP3 support is not enabled") } - addr, err := doh.probeH3(ctx, tlsConfig, dialContext) + addr, err := doh.probeH3(ctx, tlsConfig) if err != nil { return nil, err } @@ -515,7 +559,7 @@ func (doh *dnsOverHTTPS) createTransportH3( }, DisableCompression: true, TLSClientConfig: tlsConfig, - QuicConfig: doh.getQUICConfig(), + QUICConfig: doh.getQUICConfig(), } return &http3Transport{baseTransport: rt}, nil @@ -534,7 +578,7 @@ func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls. IP: net.ParseIP(ip), Port: portInt, } - conn, err := listenPacket(ctx, doh.proxyAdapter, doh.proxyName, "udp", addr, doh.r) + conn, err := doh.dialer.ListenPacket(ctx, "udp", addr) if err != nil { return nil, err } @@ -557,12 +601,11 @@ func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls. func (doh *dnsOverHTTPS) probeH3( ctx context.Context, tlsConfig *tls.Config, - dialContext dialHandler, ) (addr string, err error) { // We're using bootstrapped address instead of what's passed to the function // it does not create an actual connection, but it helps us determine // what IP is actually reachable (when there are v4/v6 addresses). - rawConn, err := dialContext(ctx, "udp", doh.url.Host) + rawConn, err := doh.dialer.DialContext(ctx, "udp", doh.url.Host) if err != nil { return "", fmt.Errorf("failed to dial: %w", err) } @@ -592,7 +635,7 @@ func (doh *dnsOverHTTPS) probeH3( chQuic := make(chan error, 1) chTLS := make(chan error, 1) go doh.probeQUIC(ctx, addr, probeTLSCfg, chQuic) - go doh.probeTLS(ctx, dialContext, probeTLSCfg, chTLS) + go doh.probeTLS(ctx, probeTLSCfg, chTLS) select { case quicErr := <-chQuic: @@ -635,10 +678,10 @@ func (doh *dnsOverHTTPS) probeQUIC(ctx context.Context, addr string, tlsConfig * // probeTLS attempts to establish a TLS connection to the specified address. We // run probeQUIC and probeTLS in parallel and see which one is faster. -func (doh *dnsOverHTTPS) probeTLS(ctx context.Context, dialContext dialHandler, tlsConfig *tls.Config, ch chan error) { +func (doh *dnsOverHTTPS) probeTLS(ctx context.Context, tlsConfig *tls.Config, ch chan error) { startTime := time.Now() - conn, err := doh.tlsDial(ctx, dialContext, "tcp", tlsConfig) + conn, err := doh.tlsDial(ctx, "tcp", tlsConfig) if err != nil { ch <- fmt.Errorf("opening TLS connection: %w", err) return @@ -694,10 +737,10 @@ func isHTTP3(client *http.Client) (ok bool) { // tlsDial is basically the same as tls.DialWithDialer, but we will call our own // dialContext function to get connection. -func (doh *dnsOverHTTPS) tlsDial(ctx context.Context, dialContext dialHandler, network string, config *tls.Config) (*tls.Conn, error) { +func (doh *dnsOverHTTPS) tlsDial(ctx context.Context, network string, config *tls.Config) (*tls.Conn, error) { // We're using bootstrapped address instead of what's passed // to the function. - rawConn, err := dialContext(ctx, network, doh.url.Host) + rawConn, err := doh.dialer.DialContext(ctx, network, doh.url.Host) if err != nil { return nil, err } diff --git a/dns/doq.go b/dns/doq.go index 70b67c2a..29fdd006 100644 --- a/dns/doq.go +++ b/dns/doq.go @@ -60,10 +60,8 @@ type dnsOverQUIC struct { bytesPool *sync.Pool bytesPoolGuard sync.Mutex - addr string - proxyAdapter C.ProxyAdapter - proxyName string - r *Resolver + addr string + dialer *dnsDialer } // type check @@ -72,10 +70,8 @@ var _ dnsClient = (*dnsOverQUIC)(nil) // newDoQ returns the DNS-over-QUIC Upstream. func newDoQ(resolver *Resolver, addr string, proxyAdapter C.ProxyAdapter, proxyName string) (dnsClient, error) { doq := &dnsOverQUIC{ - addr: addr, - proxyAdapter: proxyAdapter, - proxyName: proxyName, - r: resolver, + addr: addr, + dialer: newDNSDialer(resolver, proxyAdapter, proxyName), quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, TokenStore: newQUICTokenStore(), @@ -148,6 +144,10 @@ func (doq *dnsOverQUIC) Close() (err error) { return err } +func (doq *dnsOverQUIC) ResetConnection() { + doq.closeConnWithError(nil) +} + // exchangeQUIC attempts to open a QUIC connection, send the DNS message // through it and return the response it got from the server. func (doq *dnsOverQUIC) exchangeQUIC(ctx context.Context, msg *D.Msg) (resp *D.Msg, err error) { @@ -300,7 +300,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connectio // 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, doq.proxyName)(ctx, "udp", doq.addr) + rawConn, err := doq.dialer.DialContext(ctx, "udp", doq.addr) if err != nil { return nil, fmt.Errorf("failed to open a QUIC connection: %w", err) } @@ -315,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, doq.proxyName, "udp", addr, doq.r) + udp, err := doq.dialer.ListenPacket(ctx, "udp", addr) if err != nil { return nil, err } diff --git a/dns/edns0_subnet.go b/dns/edns0_subnet.go new file mode 100644 index 00000000..2ed4f140 --- /dev/null +++ b/dns/edns0_subnet.go @@ -0,0 +1,51 @@ +package dns + +import ( + "net/netip" + + "github.com/miekg/dns" +) + +func setEdns0Subnet(message *dns.Msg, clientSubnet netip.Prefix, override bool) bool { + var ( + optRecord *dns.OPT + subnetOption *dns.EDNS0_SUBNET + ) +findExists: + for _, record := range message.Extra { + var isOPTRecord bool + if optRecord, isOPTRecord = record.(*dns.OPT); isOPTRecord { + for _, option := range optRecord.Option { + var isEDNS0Subnet bool + if subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET); isEDNS0Subnet { + if !override { + return false + } + break findExists + } + } + } + } + if optRecord == nil { + optRecord = &dns.OPT{ + Hdr: dns.RR_Header{ + Name: ".", + Rrtype: dns.TypeOPT, + }, + } + message.Extra = append(message.Extra, optRecord) + } + if subnetOption == nil { + subnetOption = new(dns.EDNS0_SUBNET) + optRecord.Option = append(optRecord.Option, subnetOption) + } + subnetOption.Code = dns.EDNS0SUBNET + if clientSubnet.Addr().Is4() { + subnetOption.Family = 1 + } else { + subnetOption.Family = 2 + } + subnetOption.SourceNetmask = uint8(clientSubnet.Bits()) + subnetOption.Address = clientSubnet.Addr().AsSlice() + return true +} diff --git a/dns/filters.go b/dns/filters.go deleted file mode 100644 index 138f3429..00000000 --- a/dns/filters.go +++ /dev/null @@ -1,102 +0,0 @@ -package dns - -import ( - "net/netip" - "strings" - - "github.com/metacubex/mihomo/component/geodata" - "github.com/metacubex/mihomo/component/geodata/router" - "github.com/metacubex/mihomo/component/mmdb" - "github.com/metacubex/mihomo/component/trie" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" -) - -type fallbackIPFilter interface { - Match(netip.Addr) bool -} - -type geoipFilter struct { - code string -} - -var geoIPMatcher *router.GeoIPMatcher - -func (gf *geoipFilter) Match(ip netip.Addr) bool { - if !C.GeodataMode { - codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) - for _, code := range codes { - if !strings.EqualFold(code, gf.code) && !ip.IsPrivate() { - return true - } - } - return false - } - - if geoIPMatcher == nil { - var err error - geoIPMatcher, _, err = geodata.LoadGeoIPMatcher("CN") - if err != nil { - log.Errorln("[GeoIPFilter] LoadGeoIPMatcher error: %s", err.Error()) - return false - } - } - return !geoIPMatcher.Match(ip) -} - -type ipnetFilter struct { - ipnet netip.Prefix -} - -func (inf *ipnetFilter) Match(ip netip.Addr) bool { - return inf.ipnet.Contains(ip) -} - -type fallbackDomainFilter interface { - Match(domain string) bool -} - -type domainFilter struct { - tree *trie.DomainTrie[struct{}] -} - -func NewDomainFilter(domains []string) *domainFilter { - df := domainFilter{tree: trie.New[struct{}]()} - for _, domain := range domains { - _ = df.tree.Insert(domain, struct{}{}) - } - df.tree.Optimize() - return &df -} - -func (df *domainFilter) Match(domain string) bool { - return df.tree.Search(domain) != nil -} - -type geoSiteFilter struct { - matchers []router.DomainMatcher -} - -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 - } - filter := &geoSiteFilter{ - matchers: []router.DomainMatcher{matcher}, - } - return filter, nil -} - -func (gsf *geoSiteFilter) Match(domain string) bool { - for _, matcher := range gsf.matchers { - if matcher.ApplyDomain(domain) { - return true - } - } - return false -} diff --git a/dns/patch_android.go b/dns/patch_android.go index e62aabcd..8e744fcd 100644 --- a/dns/patch_android.go +++ b/dns/patch_android.go @@ -3,54 +3,19 @@ package dns import ( - "context" - - D "github.com/miekg/dns" - - "github.com/metacubex/mihomo/common/lru" - "github.com/metacubex/mihomo/component/dhcp" "github.com/metacubex/mihomo/component/resolver" ) -const SystemDNSPlaceholder = "system" - -var systemResolver *Resolver -var isolateHandler handler - -var _ dnsClient = (*dhcpClient)(nil) - -type dhcpClient struct { - enable bool -} - -func (d *dhcpClient) Address() string { - return SystemDNSPlaceholder -} - -func (d *dhcpClient) Exchange(m *D.Msg) (msg *D.Msg, err error) { - return d.ExchangeContext(context.Background(), m) -} - -func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { - if s := systemResolver; s != nil { - return s.ExchangeContext(ctx, m) - } - - return nil, dhcp.ErrNotFound -} - -func ServeDNSWithDefaultServer(msg *D.Msg) (*D.Msg, error) { - if h := isolateHandler; h != nil { - return handlerWithContext(context.Background(), h, msg) - } - - return nil, D.ErrTime -} +var systemResolver []dnsClient func FlushCacheWithDefaultResolver() { if r := resolver.DefaultResolver; r != nil { - r.(*Resolver).cache = lru.New(lru.WithSize[string, *D.Msg](4096), lru.WithStale[string, *D.Msg](true)) + r.ClearCache() } + if r := resolver.SystemResolver; r != nil { + r.ClearCache() + } + resolver.ResetConnection() } func UpdateSystemDNS(addr []string) { @@ -63,19 +28,15 @@ func UpdateSystemDNS(addr []string) { ns = append(ns, NameServer{Addr: d}) } - systemResolver = NewResolver(Config{Main: ns}) + systemResolver = transform(ns, nil) } -func UpdateIsolateHandler(resolver *Resolver, mapper *ResolverEnhancer) { - if resolver == nil { - isolateHandler = nil +func (c *systemClient) getDnsClients() ([]dnsClient, error) { + return systemResolver, nil +} - return +func (c *systemClient) ResetConnection() { + for _, r := range systemResolver { + r.ResetConnection() } - - isolateHandler = NewHandler(resolver, mapper) -} - -func newDHCPClient(ifaceName string) *dhcpClient { - return &dhcpClient{enable: ifaceName == SystemDNSPlaceholder} } diff --git a/dns/patch_common.go b/dns/patch_common.go deleted file mode 100644 index fae1e126..00000000 --- a/dns/patch_common.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build !(android && cmfa) - -package dns - -func UpdateIsolateHandler(resolver *Resolver, mapper *ResolverEnhancer) { -} diff --git a/dns/policy.go b/dns/policy.go index a58123e3..50dc1719 100644 --- a/dns/policy.go +++ b/dns/policy.go @@ -3,7 +3,6 @@ package dns import ( "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/provider" ) type dnsPolicy interface { @@ -22,28 +21,13 @@ func (p domainTriePolicy) Match(domain string) []dnsClient { return nil } -type geositePolicy struct { - matcher fallbackDomainFilter - inverse bool +type domainMatcherPolicy struct { + matcher C.DomainMatcher dnsClients []dnsClient } -func (p geositePolicy) Match(domain string) []dnsClient { - matched := p.matcher.Match(domain) - if matched != p.inverse { - return p.dnsClients - } - return nil -} - -type domainSetPolicy struct { - domainSetProvider provider.RuleProvider - dnsClients []dnsClient -} - -func (p domainSetPolicy) Match(domain string) []dnsClient { - metadata := &C.Metadata{Host: domain} - if ok := p.domainSetProvider.Match(metadata); ok { +func (p domainMatcherPolicy) Match(domain string) []dnsClient { + if p.matcher.MatchDomain(domain) { return p.dnsClients } return nil diff --git a/dns/rcode.go b/dns/rcode.go index 9777d2e7..901d1019 100644 --- a/dns/rcode.go +++ b/dns/rcode.go @@ -48,3 +48,5 @@ func (r rcodeClient) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, err func (r rcodeClient) Address() string { return r.addr } + +func (r rcodeClient) ResetConnection() {} diff --git a/dns/resolver.go b/dns/resolver.go index 8ea68ed7..9f7e28f3 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -4,34 +4,32 @@ import ( "context" "errors" "net/netip" - "strings" "time" "github.com/metacubex/mihomo/common/arc" "github.com/metacubex/mihomo/common/lru" + "github.com/metacubex/mihomo/common/singleflight" "github.com/metacubex/mihomo/component/fakeip" - "github.com/metacubex/mihomo/component/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" - orderedmap "github.com/wk8/go-ordered-map/v2" "golang.org/x/exp/maps" - "golang.org/x/sync/singleflight" ) type dnsClient interface { ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) Address() string + ResetConnection() } type dnsCache interface { GetWithExpire(key string) (*D.Msg, time.Time, bool) SetWithExpire(key string, value *D.Msg, expire time.Time) + Clear() } type result struct { @@ -45,12 +43,12 @@ type Resolver struct { hosts *trie.DomainTrie[resolver.HostValue] main []dnsClient fallback []dnsClient - fallbackDomainFilters []fallbackDomainFilter - fallbackIPFilters []fallbackIPFilter - group singleflight.Group + fallbackDomainFilters []C.DomainMatcher + fallbackIPFilters []C.IpMatcher + group singleflight.Group[*D.Msg] cache dnsCache policy []dnsPolicy - proxyServer []dnsClient + defaultResolver *Resolver } func (r *Resolver) LookupIPPrimaryIPv4(ctx context.Context, host string) (ips []netip.Addr, err error) { @@ -122,7 +120,7 @@ func (r *Resolver) LookupIPv6(ctx context.Context, host string) ([]netip.Addr, e func (r *Resolver) shouldIPFallback(ip netip.Addr) bool { for _, filter := range r.fallbackIPFilters { - if filter.Match(ip) { + if filter.MatchIp(ip) { return true } } @@ -146,9 +144,12 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e }() q := m.Question[0] + domain := msgToDomain(m) + _, qTypeStr := msgToQtype(m) cacheM, expireTime, hit := r.cache.GetWithExpire(q.String()) if hit { - log.Debugln("[DNS] cache hit for %s, expire at %s", q.Name, expireTime.Format("2006-01-02 15:04:05")) + ips := msgToIP(cacheM) + log.Debugln("[DNS] cache hit %s --> %s %s, expire at %s", domain, ips, qTypeStr, expireTime.Format("2006-01-02 15:04:05")) now := time.Now() msg = cacheM.Copy() if expireTime.Before(now) { @@ -169,19 +170,20 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M retryNum := 0 retryMax := 3 - fn := func() (result any, err error) { + fn := func() (result *D.Msg, err error) { ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) // reset timeout in singleflight defer cancel() cache := false defer func() { if err != nil { - result = retryNum + result = &D.Msg{} + result.Opcode = retryNum retryNum++ return } - msg := result.(*D.Msg) + msg := result if cache { // OPT RRs MUST NOT be cached, forwarded, or stored in or loaded from master files. @@ -208,7 +210,7 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M ch := r.group.DoChan(q.String(), fn) - var result singleflight.Result + var result singleflight.Result[*D.Msg] select { case result = <-ch: @@ -221,7 +223,7 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M go func() { // start a retrying monitor in background result := <-ch ret, err, shared := result.Val, result.Err, result.Shared - if err != nil && !shared && ret.(int) < retryMax { // retry + if err != nil && !shared && ret.Opcode < retryMax { // retry r.group.DoChan(q.String(), fn) } }() @@ -230,12 +232,12 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M } ret, err, shared := result.Val, result.Err, result.Shared - if err != nil && !shared && ret.(int) < retryMax { // retry + if err != nil && !shared && ret.Opcode < retryMax { // retry r.group.DoChan(q.String(), fn) } if err == nil { - msg = ret.(*D.Msg) + msg = ret if shared { msg = msg.Copy() } @@ -274,7 +276,7 @@ func (r *Resolver) shouldOnlyQueryFallback(m *D.Msg) bool { } for _, df := range r.fallbackDomainFilters { - if df.Match(domain) { + if df.MatchDomain(domain) { return true } } @@ -324,7 +326,7 @@ func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err er func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (ips []netip.Addr, err error) { ip, err := netip.ParseAddr(host) if err == nil { - isIPv4 := ip.Is4() + isIPv4 := ip.Is4() || ip.Is4In6() if dnsType == D.TypeAAAA && !isIPv4 { return []netip.Addr{ip}, nil } else if dnsType == D.TypeA && isIPv4 { @@ -368,6 +370,26 @@ func (r *Resolver) Invalid() bool { return len(r.main) > 0 } +func (r *Resolver) ClearCache() { + if r != nil && r.cache != nil { + r.cache.Clear() + } +} + +func (r *Resolver) ResetConnection() { + if r != nil { + for _, c := range r.main { + c.ResetConnection() + } + for _, c := range r.fallback { + c.ResetConnection() + } + if dr := r.defaultResolver; dr != nil { + dr.ResetConnection() + } + } +} + type NameServer struct { Net string Addr string @@ -395,39 +417,59 @@ func (ns NameServer) Equal(ns2 NameServer) bool { return false } -type FallbackFilter struct { - GeoIP bool - GeoIPCode string - IPCIDR []netip.Prefix - Domain []string - GeoSite []router.DomainMatcher +type Policy struct { + Domain string + Matcher C.DomainMatcher + NameServers []NameServer } type Config struct { - 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 *orderedmap.OrderedMap[string, []NameServer] - RuleProviders map[string]provider.RuleProvider - CacheAlgorithm string + Main, Fallback []NameServer + Default []NameServer + ProxyServer []NameServer + DirectServer []NameServer + DirectFollowPolicy bool + IPv6 bool + IPv6Timeout uint + EnhancedMode C.DNSMode + FallbackIPFilter []C.IpMatcher + FallbackDomainFilter []C.DomainMatcher + Pool *fakeip.Pool + Hosts *trie.DomainTrie[resolver.HostValue] + Policy []Policy + CacheAlgorithm string } -func NewResolver(config Config) *Resolver { - var cache dnsCache - if config.CacheAlgorithm == "lru" { - cache = lru.New(lru.WithSize[string, *D.Msg](4096), lru.WithStale[string, *D.Msg](true)) +func (config Config) newCache() dnsCache { + if config.CacheAlgorithm == "" || config.CacheAlgorithm == "lru" { + return lru.New(lru.WithSize[string, *D.Msg](4096), lru.WithStale[string, *D.Msg](true)) } else { - cache = arc.New(arc.WithSize[string, *D.Msg](4096)) + return arc.New(arc.WithSize[string, *D.Msg](4096)) } +} + +type Resolvers struct { + *Resolver + ProxyResolver *Resolver + DirectResolver *Resolver +} + +func (rs Resolvers) ClearCache() { + rs.Resolver.ClearCache() + rs.ProxyResolver.ClearCache() + rs.DirectResolver.ClearCache() +} + +func (rs Resolvers) ResetConnection() { + rs.Resolver.ResetConnection() + rs.ProxyResolver.ResetConnection() + rs.DirectResolver.ResetConnection() +} + +func NewResolver(config Config) (rs Resolvers) { defaultResolver := &Resolver{ main: transform(config.Default, nil), - cache: cache, + cache: config.newCache(), ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } @@ -458,28 +500,43 @@ func NewResolver(config Config) *Resolver { return } - if config.CacheAlgorithm == "" || config.CacheAlgorithm == "lru" { - cache = lru.New(lru.WithSize[string, *D.Msg](4096), lru.WithStale[string, *D.Msg](true)) - } else { - cache = arc.New(arc.WithSize[string, *D.Msg](4096)) - } r := &Resolver{ ipv6: config.IPv6, main: cacheTransform(config.Main), - cache: cache, + cache: config.newCache(), hosts: config.Hosts, ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } + r.defaultResolver = defaultResolver + rs.Resolver = r + + if len(config.ProxyServer) != 0 { + rs.ProxyResolver = &Resolver{ + ipv6: config.IPv6, + main: cacheTransform(config.ProxyServer), + cache: config.newCache(), + hosts: config.Hosts, + ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, + } + } + + if len(config.DirectServer) != 0 { + rs.DirectResolver = &Resolver{ + ipv6: config.IPv6, + main: cacheTransform(config.DirectServer), + cache: config.newCache(), + hosts: config.Hosts, + ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, + } + } if len(config.Fallback) != 0 { r.fallback = cacheTransform(config.Fallback) + r.fallbackIPFilters = config.FallbackIPFilter + r.fallbackDomainFilters = config.FallbackDomainFilter } - if len(config.ProxyServer) != 0 { - r.proxyServer = cacheTransform(config.ProxyServer) - } - - if config.Policy.Len() != 0 { + if len(config.Policy) != 0 { r.policy = make([]dnsPolicy, 0) var triePolicy *trie.DomainTrie[[]dnsClient] @@ -494,87 +551,24 @@ func NewResolver(config Config) *Resolver { } } - for pair := config.Policy.Oldest(); pair != nil; pair = pair.Next() { - domain, nameserver := pair.Key, pair.Value - - if temp := strings.Split(domain, ":"); len(temp) == 2 { - prefix := temp[0] - key := temp[1] - switch prefix { - case "rule-set": - if p, ok := config.RuleProviders[key]; ok { - log.Debugln("Adding rule-set policy: %s ", key) - insertPolicy(domainSetPolicy{ - domainSetProvider: p, - dnsClients: cacheTransform(nameserver), - }) - continue - } else { - log.Warnln("Can't found ruleset policy: %s", key) - } - case "geosite": - inverse := false - if strings.HasPrefix(key, "!") { - inverse = true - key = key[1:] - } - log.Debugln("Adding geosite policy: %s inversed %t", key, inverse) - matcher, err := NewGeoSite(key) - if err != nil { - log.Warnln("adding geosite policy %s error: %s", key, err) - continue - } - insertPolicy(geositePolicy{ - matcher: matcher, - inverse: inverse, - dnsClients: cacheTransform(nameserver), - }) - continue // skip triePolicy new + for _, policy := range config.Policy { + if policy.Matcher != nil { + insertPolicy(domainMatcherPolicy{matcher: policy.Matcher, dnsClients: cacheTransform(policy.NameServers)}) + } else { + if triePolicy == nil { + triePolicy = trie.New[[]dnsClient]() } + _ = triePolicy.Insert(policy.Domain, cacheTransform(policy.NameServers)) } - if triePolicy == nil { - triePolicy = trie.New[[]dnsClient]() - } - _ = triePolicy.Insert(domain, cacheTransform(nameserver)) } insertPolicy(nil) + + if rs.DirectResolver != nil && config.DirectFollowPolicy { + rs.DirectResolver.policy = r.policy + } } - fallbackIPFilters := []fallbackIPFilter{} - if config.FallbackFilter.GeoIP { - fallbackIPFilters = append(fallbackIPFilters, &geoipFilter{ - code: config.FallbackFilter.GeoIPCode, - }) - } - for _, ipnet := range config.FallbackFilter.IPCIDR { - fallbackIPFilters = append(fallbackIPFilters, &ipnetFilter{ipnet: ipnet}) - } - r.fallbackIPFilters = fallbackIPFilters - - fallbackDomainFilters := []fallbackDomainFilter{} - if len(config.FallbackFilter.Domain) != 0 { - fallbackDomainFilters = append(fallbackDomainFilters, NewDomainFilter(config.FallbackFilter.Domain)) - } - - if len(config.FallbackFilter.GeoSite) != 0 { - fallbackDomainFilters = append(fallbackDomainFilters, &geoSiteFilter{ - matchers: config.FallbackFilter.GeoSite, - }) - } - r.fallbackDomainFilters = fallbackDomainFilters - - return r -} - -func NewProxyServerHostResolver(old *Resolver) *Resolver { - r := &Resolver{ - ipv6: old.ipv6, - main: old.proxyServer, - cache: old.cache, - hosts: old.hosts, - ipv6Timeout: old.ipv6Timeout, - } - return r + return } var ParseNameServer func(servers []string) ([]NameServer, error) // define in config/config.go diff --git a/dns/server.go b/dns/server.go index d45fb5eb..1cf58d4d 100644 --- a/dns/server.go +++ b/dns/server.go @@ -6,7 +6,6 @@ import ( "net" "github.com/metacubex/mihomo/common/sockopt" - "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/context" "github.com/metacubex/mihomo/log" @@ -50,10 +49,6 @@ func (s *Server) SetHandler(handler handler) { } func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { - if features.CMFA { - UpdateIsolateHandler(resolver, mapper) - } - if addr == address && resolver != nil { handler := NewHandler(resolver, mapper) server.SetHandler(handler) diff --git a/dns/system.go b/dns/system.go index 37607a60..ab6c0100 100644 --- a/dns/system.go +++ b/dns/system.go @@ -3,15 +3,13 @@ package dns import ( "context" "fmt" - "net" "strings" "sync" "time" - "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/component/resolver" D "github.com/miekg/dns" - "golang.org/x/exp/slices" ) const ( @@ -28,65 +26,15 @@ 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 + defaultNS []dnsClient } func (c *systemClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { dnsClients, err := c.getDnsClients() + if len(dnsClients) == 0 && len(c.defaultNS) > 0 { + dnsClients = c.defaultNS + err = nil + } if err != nil { return } @@ -97,11 +45,16 @@ func (c *systemClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Ms // Address implements dnsClient func (c *systemClient) Address() string { dnsClients, _ := c.getDnsClients() + isDefault := "" + if len(dnsClients) == 0 && len(c.defaultNS) > 0 { + dnsClients = c.defaultNS + isDefault = "[defaultNS]" + } addrs := make([]string, 0, len(dnsClients)) for _, c := range dnsClients { addrs = append(addrs, c.Address()) } - return fmt.Sprintf("system(%s)", strings.Join(addrs, ",")) + return fmt.Sprintf("system%s(%s)", isDefault, strings.Join(addrs, ",")) } var _ dnsClient = (*systemClient)(nil) @@ -111,3 +64,11 @@ func newSystemClient() *systemClient { dnsClients: map[string]*systemDnsClient{}, } } + +func init() { + r := NewResolver(Config{}) + c := newSystemClient() + c.defaultNS = transform([]NameServer{{Addr: "114.114.114.114:53"}, {Addr: "8.8.8.8:53"}}, nil) + r.main = []dnsClient{c} + resolver.SystemResolver = r +} diff --git a/dns/system_common.go b/dns/system_common.go new file mode 100644 index 00000000..e6dabdcf --- /dev/null +++ b/dns/system_common.go @@ -0,0 +1,73 @@ +//go:build !(android && cmfa) + +package dns + +import ( + "net" + "time" + + "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/log" + + "golang.org/x/exp/slices" +) + +func (c *systemClient) getDnsClients() ([]dnsClient, error) { + c.mu.Lock() + defer c.mu.Unlock() + var err error + if time.Since(c.lastFlush) > SystemDnsFlushTime { + var nameservers []string + if nameservers, err = dnsReadConfig(); err == nil { + log.Debugln("[DNS] system dns update to %s", nameservers) + for _, addr := range nameservers { + if resolver.IsSystemDnsBlacklisted(addr) { + continue + } + if _, ok := c.dnsClients[addr]; !ok { + clients := transform( + []NameServer{{ + Addr: net.JoinHostPort(addr, "53"), + Net: "udp", + }}, + nil, + ) + if len(clients) > 0 { + c.dnsClients[addr] = &systemDnsClient{ + disableTimes: 0, + dnsClient: clients[0], + } + } + } + } + available := 0 + for nameserver, sdc := range c.dnsClients { + if slices.Contains(nameservers, nameserver) { + sdc.disableTimes = 0 // enable + available++ + } else { + if sdc.disableTimes > SystemDnsDeleteTimes { + delete(c.dnsClients, nameserver) // drop too old dnsClient + } else { + sdc.disableTimes++ + } + } + } + if available > 0 { + c.lastFlush = time.Now() + } + } + } + dnsClients := make([]dnsClient, 0, len(c.dnsClients)) + for _, sdc := range c.dnsClients { + if sdc.disableTimes == 0 { + dnsClients = append(dnsClients, sdc.dnsClient) + } + } + if len(dnsClients) > 0 { + return dnsClients, nil + } + return nil, err +} + +func (c *systemClient) ResetConnection() {} diff --git a/dns/system_windows.go b/dns/system_windows.go index 47c1ebaa..2f4d1b63 100644 --- a/dns/system_windows.go +++ b/dns/system_windows.go @@ -8,6 +8,7 @@ import ( "syscall" "unsafe" + "golang.org/x/exp/slices" "golang.org/x/sys/windows" ) @@ -40,6 +41,9 @@ func dnsReadConfig() (servers []string, err error) { // Unexpected type. continue } + if slices.Contains(servers, ip.String()) { + continue + } servers = append(servers, ip.String()) } } diff --git a/dns/util.go b/dns/util.go index 516c63fb..50459cc1 100644 --- a/dns/util.go +++ b/dns/util.go @@ -7,18 +7,14 @@ import ( "fmt" "net" "net/netip" - "strconv" "strings" "time" - 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" @@ -120,6 +116,11 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { continue } + var options []dialer.Option + if s.Interface != "" { + options = append(options, dialer.WithInterface(s.Interface)) + } + host, port, _ := net.SplitHostPort(s.Addr) ret = append(ret, &client{ Client: &D.Client{ @@ -130,12 +131,9 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { UDPSize: 4096, Timeout: 5 * time.Second, }, - port: port, - host: host, - iface: s.Interface, - r: resolver, - proxyAdapter: s.ProxyAdapter, - proxyName: s.ProxyName, + port: port, + host: host, + dialer: newDNSDialer(resolver, s.ProxyAdapter, s.ProxyName, options...), }) } return ret @@ -175,118 +173,12 @@ func msgToDomain(msg *D.Msg) string { return "" } -type dialHandler func(ctx context.Context, network, addr string) (net.Conn, error) - -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(proxyName) == 0 && proxyAdapter == nil { - opts = append(opts, dialer.WithResolver(r)) - return dialer.DialContext(ctx, network, addr, opts...) - } else { - host, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - 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: uint16(uintPort), - } - 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...) - } else { - // udp must resolve host first - dstIP, err := resolver.ResolveIPWithResolver(ctx, host, r) - if err != nil { - return nil, err - } - metadata := &C.Metadata{ - NetWork: C.UDP, - Host: "", - DstIP: dstIP, - DstPort: uint16(uintPort), - } - if proxyAdapter == nil { - return dialer.DialContext(ctx, network, addr, opts...) - } - - if !proxyAdapter.SupportUDP() { - return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) - } - - packetConn, err := proxyAdapter.ListenPacketContext(ctx, metadata, opts...) - if err != nil { - return nil, err - } - - return N.NewBindPacketConn(packetConn, metadata.UDPAddr()), nil - } - } +func msgToQtype(msg *D.Msg) (uint16, string) { + if len(msg.Question) > 0 { + qType := msg.Question[0].Qtype + return qType, D.Type(qType).String() } -} - -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 - } - 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 - dstIP, err := resolver.ResolveIPWithResolver(ctx, host, r) - if err != nil { - return nil, err - } - metadata := &C.Metadata{ - NetWork: C.UDP, - Host: "", - DstIP: dstIP, - DstPort: uint16(uintPort), - } - if proxyAdapter == nil { - return dialer.NewDialer(opts...).ListenPacket(ctx, network, "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort)) - } - - if !proxyAdapter.SupportUDP() { - return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) - } - - return proxyAdapter.ListenPacketContext(ctx, metadata, opts...) + return 0, "" } func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, cache bool, err error) { @@ -294,6 +186,7 @@ func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.M fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout) defer fast.Close() domain := msgToDomain(m) + qType, qTypeStr := msgToQtype(m) var noIpMsg *D.Msg for _, client := range clients { if _, isRCodeClient := client.(rcodeClient); isRCodeClient { @@ -302,7 +195,7 @@ func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.M } 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, client.Address()) + log.Debugln("[DNS] resolve %s %s from %s", domain, qTypeStr, client.Address()) m, err := client.ExchangeContext(ctx, m) if err != nil { return nil, err @@ -311,20 +204,18 @@ func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.M // 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, resolver.ErrIPNotFound - } - case D.TypeA: - if len(ips) == 0 { - noIpMsg = m - return nil, resolver.ErrIPNotFound - } + ips := msgToIP(m) + log.Debugln("[DNS] %s --> %s %s from %s", domain, ips, qTypeStr, client.Address()) + switch qType { + case D.TypeAAAA: + if len(ips) == 0 { + noIpMsg = m + return nil, resolver.ErrIPNotFound + } + case D.TypeA: + if len(ips) == 0 { + noIpMsg = m + return nil, resolver.ErrIPNotFound } } return m, nil diff --git a/docs/config.yaml b/docs/config.yaml index d912eb65..6dfeb586 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -8,15 +8,15 @@ mixed-port: 10801 # HTTP(S) 和 SOCKS 代理混合端口 allow-lan: true # 允许局域网连接 bind-address: "*" # 绑定 IP 地址,仅作用于 allow-lan 为 true,'*'表示所有地址 -authentication: # http,socks入口的验证用户名,密码 +authentication: # http,socks 入口的验证用户名,密码 - "username:password" -skip-auth-prefixes: # 设置跳过验证的IP段 +skip-auth-prefixes: # 设置跳过验证的 IP 段 - 127.0.0.1/8 - ::1/128 -lan-allowed-ips: # 允许连接的 IP 地址段,仅作用于 allow-lan 为 true, 默认值为0.0.0.0/0和::/0 +lan-allowed-ips: # 允许连接的 IP 地址段,仅作用于 allow-lan 为 true, 默认值为 0.0.0.0/0 和::/0 - 0.0.0.0/0 - ::/0 -lan-disallowed-ips: # 禁止连接的 IP 地址段, 黑名单优先级高于白名单, 默认值为空 +lan-disallowed-ips: # 禁止连接的 IP 地址段,黑名单优先级高于白名单,默认值为空 - 192.168.0.3/32 # find-process-mode has 3 values:always, strict, off @@ -58,6 +58,21 @@ external-controller: 0.0.0.0:9093 # RESTful API 监听地址 external-controller-tls: 0.0.0.0:9443 # RESTful API HTTPS 监听地址,需要配置 tls 部分配置文件 # secret: "123456" # `Authorization:Bearer ${secret}` +# RESTful API CORS标头配置 +external-controller-cors: + allow-origins: + - * + allow-private-network: true + +# RESTful API Unix socket 监听地址( windows版本大于17063也可以使用,即大于等于1803/RS4版本即可使用 ) +# !!!注意: 从Unix socket访问api接口不会验证secret, 如果开启请自行保证安全问题 !!! +# 测试方法: curl -v --unix-socket "mihomo.sock" http://localhost/ +external-controller-unix: mihomo.sock + +# RESTful API Windows namedpipe 监听地址 +# !!!注意: 从Windows namedpipe访问api接口不会验证secret, 如果开启请自行保证安全问题 !!! +external-controller-pipe: \\.\pipe\mihomo + # tcp-concurrent: true # TCP 并发连接所有 IP, 将使用最快握手的 TCP # 配置 WEB UI 目录,使用 http://{{external-controller}}/ui 访问 @@ -65,6 +80,10 @@ external-ui: /path/to/ui/folder/ external-ui-name: xd external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip" +# 在RESTful API端口上开启DOH服务器 +# !!!该URL不会验证secret, 如果开启请自行保证安全问题 !!! +external-doh-server: /dns-query + # interface-name: en0 # 设置出口网卡 # 全局 TLS 指纹,优先低于 proxy 内的 client-fingerprint @@ -73,7 +92,9 @@ external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh- global-client-fingerprint: chrome # TCP keep alive interval -keep-alive-interval: 15 +# disable-keep-alive: false #目前在android端强制为true +# keep-alive-idle: 15 +# keep-alive-interval: 15 # routing-mark:6666 # 配置 fwmark 仅用于 Linux experimental: @@ -109,19 +130,31 @@ tun: # auto-detect-interface: true # 自动识别出口网卡 # auto-route: true # 配置路由表 # mtu: 9000 # 最大传输单元 - # gso: false # 启用通用分段卸载, 仅支持 Linux + # gso: false # 启用通用分段卸载,仅支持 Linux # gso-max-size: 65536 # 通用分段卸载包的最大大小 - # strict-route: true # 将所有连接路由到tun来防止泄漏,但你的设备将无法其他设备被访问 - inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 + auto-redirect: false # 自动配置 iptables 以重定向 TCP 连接。仅支持 Linux。带有 auto-redirect 的 auto-route 现在可以在路由器上按预期工作,无需干预。 + # strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问 + route-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 不匹配的流量将绕过路由, 仅支持 Linux,且需要 nftables,`auto-route` 和 `auto-redirect` 已启用。 + - ruleset-1 + - ruleset-2 + route-exclude-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 匹配的流量将绕过路由, 仅支持 Linux,且需要 nftables,`auto-route` 和 `auto-redirect` 已启用。 + - ruleset-3 + - ruleset-4 + route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 - 0.0.0.0/1 - 128.0.0.0/1 - 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-interface: # 限制被路由的接口。默认不限制, 与 `exclude-interface` 冲突 + # include-interface: # 限制被路由的接口。默认不限制,与 `exclude-interface` 冲突 # - "lan0" - # exclude-interface: # 排除路由的接口, 与 `include-interface` 冲突 + # exclude-interface: # 排除路由的接口,与 `include-interface` 冲突 # - "lan1" # include-uid: # UID 规则仅在 Linux 下被支持,并且需要 auto-route # - 0 @@ -143,13 +176,6 @@ tun: # exclude-package: # 排除被路由的 Android 应用包名 # - com.android.captiveportallogin -#ebpf配置 -ebpf: - auto-redir: # redirect 模式,仅支持 TCP - - eth0 - redirect-to-tun: # UDP+TCP 使用该功能请勿启用 auto-route - - eth0 - # 嗅探域名 可选配置 sniffer: enable: false @@ -174,6 +200,10 @@ sniffer: override-destination: true force-domain: - +.v2ex.com + # skip-src-address: # 对于来源ip跳过嗅探 + # - 192.168.0.3/32 + # skip-dst-address: # 对于目标ip跳过嗅探 + # - 192.168.0.3/32 ## 对嗅探结果进行跳过 # skip-domain: # - Mijia Cloud @@ -200,15 +230,15 @@ tunnels: # one line config target: target.com proxy: proxy -# DNS配置 +# DNS 配置 dns: cache-algorithm: arc enable: false # 关闭将使用系统 DNS - prefer-h3: true # 开启 DoH 支持 HTTP/3,将并发尝试 + prefer-h3: false # 是否开启 DoH 支持 HTTP/3,将并发尝试 listen: 0.0.0.0:53 # 开启 DNS 服务器监听 # ipv6: false # false 将返回 AAAA 的空结果 # ipv6-timeout: 300 # 单位:ms,内部双栈并发时,向上游查询 AAAA 时,等待 AAAA 的时间,默认 100ms - # 用于解析 nameserver,fallback 以及其他DNS服务器配置的,DNS 服务域名 + # 用于解析 nameserver,fallback 以及其他 DNS 服务器配置的,DNS 服务域名 # 只能使用纯 IP 地址,可使用加密 DNS default-nameserver: - 114.114.114.114 @@ -220,14 +250,29 @@ dns: fake-ip-range: 198.18.0.1/16 # fake-ip 池设置 + # 配置不使用 fake-ip 的域名 + fake-ip-filter: + - '*.lan' + - localhost.ptlogin2.qq.com + # fakeip-filter 为 rule-providers 中的名为 fakeip-filter 规则订阅, + # 且 behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则 + - rule-set:fakeip-filter + # fakeip-filter 为 geosite 中名为 fakeip-filter 的分类(需要自行保证该分类存在) + - geosite:fakeip-filter + # 配置fake-ip-filter的匹配模式,默认为blacklist,即如果匹配成功不返回fake-ip + # 可设置为whitelist,即只有匹配成功才返回fake-ip + fake-ip-filter-mode: blacklist + # use-hosts: true # 查询 hosts - # 配置不使用fake-ip的域名 - # fake-ip-filter: - # - '*.lan' - # - localhost.ptlogin2.qq.com + # 配置后面的nameserver、fallback和nameserver-policy向dns服务器的连接过程是否遵守遵守rules规则 + # 如果为false(默认值)则这三部分的dns服务器在未特别指定的情况下会直连 + # 如果为true,将会按照rules的规则匹配链接方式(走代理或直连),如果有特别指定则任然以指定值为准 + # 仅当proxy-server-nameserver非空时可以开启此选项, 强烈不建议和prefer-h3一起使用 + # 此外,这三者配置中的dns服务器如果出现域名会采用default-nameserver配置项解析,也请确保正确配置default-nameserver + respect-rules: false - # DNS主要域名配置 + # DNS 主要域名配置 # 支持 UDP,TCP,DoT,DoH,DoQ # 这部分为主要 DNS 配置,影响所有直连,确保使用对大陆解析精准的 DNS nameserver: @@ -239,7 +284,8 @@ dns: - https://mozilla.cloudflare-dns.com/dns-query#DNS&h3=true # 指定策略组和使用 HTTP/3 - dhcp://en0 # dns from dhcp - quic://dns.adguard.com:784 # DNS over QUIC - # - '8.8.8.8#en0' # 兼容指定DNS出口网卡 + # - '8.8.8.8#RULES' # 效果同respect-rules,但仅对该服务器生效 + # - '8.8.8.8#en0' # 兼容指定 DNS 出口网卡 # 当配置 fallback 时,会查询 nameserver 中返回的 IP 是否为 CN,非必要配置 # 当不是 CN,则使用 fallback 中的 DNS 查询结果 @@ -248,11 +294,15 @@ dns: # - tcp://1.1.1.1 # - 'tcp://1.1.1.1#ProxyGroupName' # 指定 DNS 过代理查询,ProxyGroupName 为策略组名或节点名,过代理配置优先于配置出口网卡,当找不到策略组或节点名则设置为出口网卡 - # 专用于节点域名解析的 DNS 服务器,非必要配置项 - # 配置服务器若查询失败将使用 nameserver,非并发查询 + # 专用于节点域名解析的 DNS 服务器,非必要配置项,如果不填则遵循nameserver-policy、nameserver和fallback的配置 # proxy-server-nameserver: - # - https://dns.google/dns-query - # - tls://one.one.one.one + # - https://dns.google/dns-query + # - tls://one.one.one.one + + # 专用于direct出口域名解析的 DNS 服务器,非必要配置项,如果不填则遵循nameserver-policy、nameserver和fallback的配置 + # direct-nameserver: + # - system:// + # direct-nameserver-follow-policy: false # 是否遵循nameserver-policy,默认为不遵守,仅当direct-nameserver不为空时生效 # 配置 fallback 使用条件 # fallback-filter: @@ -338,7 +388,7 @@ proxies: # socks5 # 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 链接, + # ipv4-prefer:优先使用 IPv4 对于 TCP 会进行双栈解析,并发链接但是优先使用 IPv4 链接, # UDP 则为双栈解析,获取结果中的第一个 IPv4 # ipv6-prefer 同 ipv4-prefer # 现有协议都支持此参数,TCP 效果仅在开启 tcp-concurrent 生效 @@ -350,7 +400,7 @@ proxies: # socks5 # 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连接会直接走底层协议 + # only-tcp: false # 如果设置为 true, smux 的设置将不会对 udp 生效,udp 连接会直接走底层协议 - name: "ss2" type: ss @@ -383,6 +433,7 @@ proxies: # socks5 # headers: # custom: value # v2ray-http-upgrade: false + # v2ray-http-upgrade-fast-open: false - name: "ss4-shadow-tls" type: ss @@ -405,18 +456,18 @@ proxies: # socks5 password: [YOUR_SS_PASSWORD] client-fingerprint: chrome # One of: chrome, ios, firefox or safari - # 可以是chrome, ios, firefox, safari中的一个 + # 可以是 chrome, ios, firefox, safari 中的一个 plugin: restls plugin-opts: host: "www.microsoft.com" # Must be a TLS 1.3 server - # 应当是一个TLS 1.3 服务器 + # 应当是一个 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"等特征 + # 用 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" @@ -428,18 +479,18 @@ proxies: # socks5 password: [YOUR_SS_PASSWORD] client-fingerprint: chrome # One of: chrome, ios, firefox or safari - # 可以是chrome, ios, firefox, safari中的一个 + # 可以是 chrome, ios, firefox, safari 中的一个 plugin: restls plugin-opts: host: "vscode.dev" # Must be a TLS 1.2 server - # 应当是一个TLS 1.2 服务器 + # 应当是一个 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 + # cipher 支持 auto/aes-128-gcm/chacha20-poly1305/none - name: "vmess" type: vmess server: server @@ -461,6 +512,7 @@ proxies: # socks5 # max-early-data: 2048 # early-data-header-name: Sec-WebSocket-Protocol # v2ray-http-upgrade: false + # v2ray-http-upgrade-fast-open: false - name: "vmess-h2" type: vmess @@ -589,6 +641,7 @@ proxies: # socks5 headers: Host: example.com # v2ray-http-upgrade: false + # v2ray-http-upgrade-fast-open: false # Trojan - name: "trojan" @@ -604,6 +657,10 @@ proxies: # socks5 # - h2 # - http/1.1 # skip-cert-verify: true + # ss-opts: # like trojan-go's `shadowsocks` config + # enabled: false + # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 + # password: "example" - name: trojan-grpc server: server @@ -633,6 +690,7 @@ proxies: # socks5 # headers: # Host: example.com # v2ray-http-upgrade: false + # v2ray-http-upgrade-fast-open: false - name: "trojan-xtls" type: trojan @@ -676,11 +734,11 @@ proxies: # socks5 port: 443 # ports: 1000,2000-3000,5000 # port 不可省略 # hop-interval: 15 - # up和down均不写或为0则使用BBR流控 + # up 和 down 均不写或为 0 则使用 BBR 流控 # up: "30 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps password: yourpassword - # obfs: salamander # 默认为空,如果填写则开启obfs,目前仅支持salamander + # obfs: salamander # 默认为空,如果填写则开启 obfs,目前仅支持 salamander # obfs-password: yourpassword # sni: server.com # skip-cert-verify: false @@ -706,27 +764,37 @@ proxies: # socks5 # 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会被保留且只能在顶层指定 + # remote-dns-resolve: true # 强制 dns 远程解析,默认值为 false + # dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效 + # refresh-server-ip-interval: 60 # 重新解析server ip的间隔,单位为秒,默认值为0即仅第一次链接时解析server域名,仅应在server域名对应的IP会发生变化时启用该选项(如家宽ddns) + # 如果 peers 不为空,该段落中的 allowed-ips 不可为空;前面段落的 server,port,public-key,pre-shared-key 均会被忽略,但 private-key 会被保留且只能在顶层指定 # peers: # - server: 162.159.192.1 # port: 2480 - # 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] + # 如果存在则开启AmneziaWG功能 + # amnezia-wg-option: + # jc: 5 + # jmin: 500 + # jmax: 501 + # s1: 30 + # s2: 40 + # h1: 123456 + # h2: 67543 + # h4: 32345 + # h3: 123123 # tuic - name: tuic server: www.example.com port: 10443 type: tuic - # tuicV4必须填写token (不可同时填写uuid和password) + # tuicV4 必须填写 token(不可同时填写 uuid 和 password) token: TOKEN - # tuicV5必须填写uuid和password(不可同时填写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' @@ -744,8 +812,8 @@ proxies: # socks5 # 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不兼容!!! + # meta 和 sing-box 私有扩展,将 ss-uot 用于 udp 中继,开启此选项后 udp-relay-mode 将失效 + # 警告,与原版 tuic 不兼容!!! # udp-over-stream: false # udp-over-stream-version: 1 @@ -778,12 +846,24 @@ proxies: # socks5 password: password privateKey: path -# dns出站会将请求劫持到内部dns模块,所有请求均在内部处理 + # mieru + - name: mieru + type: mieru + server: 1.2.3.4 + port: 2999 + # port-range: 2090-2099 #(不可同时填写 port 和 port-range) + transport: TCP # 只支持 TCP + username: user + password: password + # 可以使用的值包括 MULTIPLEXING_OFF, MULTIPLEXING_LOW, MULTIPLEXING_MIDDLE, MULTIPLEXING_HIGH。其中 MULTIPLEXING_OFF 会关闭多路复用功能。默认值为 MULTIPLEXING_LOW。 + # multiplexing: MULTIPLEXING_LOW + +# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理 - name: "dns-out" type: dns proxy-groups: - # 代理链,目前relay可以支持udp的只有vmess/vless/trojan/ss/ssr/tuic - # wireguard目前不支持在relay中使用,请使用proxy中的dialer-proxy配置项 + # 代理链,目前 relay 可以支持 udp 的只有 vmess/vless/trojan/ss/ssr/tuic + # wireguard 目前不支持在 relay 中使用,请使用 proxy 中的 dialer-proxy 配置项 # Traffic: mihomo <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet - name: "relay" type: relay @@ -857,10 +937,20 @@ proxy-groups: # Mihomo 格式的节点或支持 *ray 的分享格式 proxy-providers: provider1: - type: http # http 的 path 可空置,默认储存路径为 homedir的proxies文件夹,文件名为url的md5 + type: http # http 的 path 可空置,默认储存路径为 homedir 的 proxies 文件夹,文件名为 url 的 md5 url: "url" interval: 3600 path: ./provider1.yaml # 默认只允许存储在 mihomo 的 Home Dir,如果想存储到任意位置,添加环境变量 SKIP_SAFE_PATH_CHECK=1 + proxy: DIRECT + # size-limit: 10240 # 限制下载文件最大为10kb,默认为0即不限制文件大小 + header: + User-Agent: + - "Clash/v1.18.0" + - "mihomo/1.18.3" + # Accept: + # - 'application/vnd.github.v3.raw' + # Authorization: + # - 'token 1231231' health-check: enable: true interval: 600 @@ -878,6 +968,24 @@ proxy-providers: # ip-version: ipv4-prefer # additional-prefix: "[provider1]" # additional-suffix: "test" + # # 名字替换,支持正则表达式 + # proxy-name: + # - pattern: "test" + # target: "TEST" + # - pattern: "IPLC-(.*?)倍" + # target: "iplc x $1" + + provider2: + type: inline + dialer-proxy: proxy + payload: + - name: "ss1" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + test: type: file path: /test.yaml @@ -890,13 +998,41 @@ rule-providers: behavior: classical # domain ipcidr interval: 259200 path: /path/to/save/file.yaml # 默认只允许存储在 mihomo 的 Home Dir,如果想存储到任意位置,添加环境变量 SKIP_SAFE_PATH_CHECK=1 - type: http # http 的 path 可空置,默认储存路径为 homedir的rules文件夹,文件名为url的md5 + type: http # http 的 path 可空置,默认储存路径为 homedir 的 rules 文件夹,文件名为 url 的 md5 url: "url" + proxy: DIRECT + # size-limit: 10240 # 限制下载文件最大为10kb,默认为0即不限制文件大小 rule2: behavior: classical interval: 259200 path: /path/to/save/file.yaml type: file + rule3: + # mrs类型ruleset,目前仅支持domain和ipcidr(即不支持classical), + # + # 对于behavior=domain: + # - format=yaml 可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换到mrs格式 + # - format=text 可以通过“mihomo convert-ruleset domain text XXX.text XXX.mrs”转换到mrs格式 + # - XXX.mrs 可以通过"mihomo convert-ruleset domain mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回yaml格式) + # + # 对于behavior=ipcidr: + # - format=yaml 可以通过“mihomo convert-ruleset ipcidr yaml XXX.yaml XXX.mrs”转换到mrs格式 + # - format=text 可以通过“mihomo convert-ruleset ipcidr text XXX.text XXX.mrs”转换到mrs格式 + # - XXX.mrs 可以通过"mihomo convert-ruleset ipcidr mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回yaml格式) + # + type: http + url: "url" + format: mrs + behavior: domain + path: /path/to/save/file.mrs + rule4: + type: inline + behavior: domain # classical / ipcidr + payload: + - '.blogger.com' + - '*.*.microsoft.com' + - 'books.itunes.apple.com' + rules: - RULE-SET,rule1,REJECT - IP-ASN,1,PROXY @@ -940,37 +1076,46 @@ listeners: port: 10808 #listen: 0.0.0.0 # 默认监听 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理 + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 # udp: false # 默认 true + # users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: [] + # - username: aaa + # password: aaa - name: http-in-1 type: http port: 10809 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) + # users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: [] + # - username: aaa + # password: aaa - name: mixed-in-1 type: mixed # HTTP(S) 和 SOCKS 代理混合 port: 10810 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # udp: false # 默认 true + # users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: [] + # - username: aaa + # password: aaa - name: reidr-in-1 type: redir port: 10811 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - name: tproxy-in-1 type: tproxy port: 10812 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # udp: false # 默认 true - name: shadowsocks-in-1 @@ -978,7 +1123,7 @@ listeners: port: 10813 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg= cipher: 2022-blake3-aes-256-gcm @@ -987,13 +1132,13 @@ listeners: port: 10814 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) users: - username: 1 uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 alterId: 1 - # ws-path: "/" # 如果不为空则开启websocket传输层 - # 下面两项如果填写则开启tls(需要同时填写) + # ws-path: "/" # 如果不为空则开启 websocket 传输层 + # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key @@ -1002,10 +1147,10 @@ listeners: port: 10815 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) - # token: # tuicV4填写(可以同时填写users) + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) + # token: # tuicV4 填写(可以同时填写 users) # - TOKEN - # users: # tuicV5填写(可以同时填写token) + # users: # tuicV5 填写(可以同时填写 token) # 00000000-0000-0000-0000-000000000000: PASSWORD_0 # 00000000-0000-0000-0000-000000000001: PASSWORD_1 # certificate: ./server.crt @@ -1022,25 +1167,25 @@ listeners: port: 10816 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) network: [tcp, udp] target: target.com - name: tun-in-1 type: tun # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules - # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) stack: system # gvisor / mixed dns-hijack: - 0.0.0.0:53 # 需要劫持的 DNS # auto-detect-interface: false # 自动识别出口网卡 # auto-route: false # 配置路由表 # mtu: 9000 # 最大传输单元 - inet4-address: # 必须手动设置ipv4地址段 + inet4-address: # 必须手动设置 ipv4 地址段 - 198.19.0.1/30 - inet6-address: # 必须手动设置ipv6地址段 + inet6-address: # 必须手动设置 ipv6 地址段 - "fdfe:dcba:9877::1/126" - # strict-route: true # 将所有连接路由到tun来防止泄漏,但你的设备将无法其他设备被访问 + # strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问 # inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 # - 0.0.0.0/1 # - 128.0.0.0/1 @@ -1068,17 +1213,17 @@ listeners: # exclude-package: # 排除被路由的 Android 应用包名 # - com.android.captiveportallogin # 入口配置与 Listener 等价,传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理 -# shadowsocks,vmess 入口配置(传入流量将和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 服务器入口(传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理) # tuic-server: # enable: true # listen: 127.0.0.1:10443 -# token: # tuicV4填写(可以同时填写users) +# token: # tuicV4 填写(可以同时填写 users) # - TOKEN -# users: # tuicV5填写(可以同时填写token) +# users: # tuicV5 填写(可以同时填写 token) # 00000000-0000-0000-0000-000000000000: PASSWORD_0 # 00000000-0000-0000-0000-000000000001: PASSWORD_1 # certificate: ./server.crt @@ -1088,4 +1233,4 @@ listeners: # authentication-timeout: 1000 # alpn: # - h3 -# max-udp-relay-packet-size: 1500 \ No newline at end of file +# max-udp-relay-packet-size: 1500 diff --git a/go.mod b/go.mod index 2d1ac543..1db2d3b5 100644 --- a/go.mod +++ b/go.mod @@ -4,57 +4,61 @@ go 1.20 require ( github.com/3andne/restls-client-go v0.1.6 - github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da github.com/bahlo/generic-list-go v0.2.0 - github.com/cilium/ebpf v0.12.3 - github.com/coreos/go-iptables v0.7.0 - github.com/dlclark/regexp2 v1.11.0 - github.com/go-chi/chi/v5 v5.0.12 - github.com/go-chi/cors v1.2.1 + github.com/coreos/go-iptables v0.8.0 + github.com/dlclark/regexp2 v1.11.4 + github.com/enfein/mieru/v3 v3.10.0 + github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/render v1.0.3 - github.com/gobwas/ws v1.3.2 - github.com/gofrs/uuid/v5 v5.0.0 - github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8 - github.com/klauspost/cpuid/v2 v2.2.7 + github.com/gobwas/ws v1.4.0 + github.com/gofrs/uuid/v5 v5.3.0 + github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d + github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20 + github.com/klauspost/cpuid/v2 v2.2.9 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/mdlayher/netlink v1.7.2 + github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab + github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 + github.com/metacubex/chacha v0.1.0 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 - github.com/metacubex/quic-go v0.41.1-0.20240307164142-46c6f7cdf2d1 - github.com/metacubex/sing-quic v0.0.0-20240310154810-47bca850fc01 - github.com/metacubex/sing-shadowsocks v0.2.6 - github.com/metacubex/sing-shadowsocks2 v0.2.0 - github.com/metacubex/sing-tun v0.2.1-0.20240214100323-23e40bfb9067 - github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f - github.com/metacubex/sing-wireguard v0.0.0-20231209125515-0594297f7232 - github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 - github.com/miekg/dns v1.1.58 + github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da + github.com/metacubex/randv2 v0.2.0 + github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 + github.com/metacubex/sing-shadowsocks v0.2.8 + github.com/metacubex/sing-shadowsocks2 v0.2.2 + github.com/metacubex/sing-tun v0.4.5 + github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 + github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 + github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 + github.com/metacubex/utls v1.6.6 + github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 + github.com/miekg/dns v1.1.62 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/v3 v3.1.0 - github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a - github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 - github.com/sagernet/sing v0.3.6 - github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 - github.com/sagernet/sing-shadowtls v0.1.4 - github.com/sagernet/utls v1.5.4 - github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e - github.com/samber/lo v1.39.0 - github.com/shirou/gopsutil/v3 v3.24.2 + github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20 + github.com/puzpuzpuz/xsync/v3 v3.4.0 + github.com/sagernet/cors v1.2.1 + github.com/sagernet/fswatch v0.1.1 + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a + github.com/sagernet/sing v0.5.1 + github.com/sagernet/sing-mux v0.2.1 + github.com/sagernet/sing-shadowtls v0.1.5 + github.com/samber/lo v1.47.0 + github.com/shirou/gopsutil/v4 v4.24.11 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 + github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/wk8/go-ordered-map/v2 v2.1.8 - github.com/zhangyunhao116/fastrand v0.3.0 - go.uber.org/automaxprocs v1.5.3 + gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 + go.uber.org/automaxprocs v1.6.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.21.0 - golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 - golang.org/x/net v0.22.0 - golang.org/x/sync v0.6.0 - golang.org/x/sys v0.18.0 - google.golang.org/protobuf v1.33.0 + golang.org/x/crypto v0.31.0 + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // lastest version compatible with golang1.20 + golang.org/x/net v0.33.0 + golang.org/x/sys v0.28.0 + google.golang.org/protobuf v1.34.2 // lastest version compatible with golang1.20 gopkg.in/yaml.v3 v3.0.1 - lukechampine.com/blake3 v1.2.1 + lukechampine.com/blake3 v1.3.0 ) require ( @@ -63,8 +67,9 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/cloudflare/circl v1.3.6 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/ebitengine/purego v0.8.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 @@ -75,16 +80,16 @@ require ( 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/btree v1.1.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mdlayher/socket v0.4.1 // indirect - github.com/metacubex/gvisor v0.0.0-20240214095142-666a73bcf165 // indirect + github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // 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 @@ -92,23 +97,24 @@ require ( 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-20 v0.4.1 // indirect + github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // 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 + github.com/vishvananda/netns v0.0.4 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect - go.uber.org/mock v0.3.0 // indirect - golang.org/x/mod v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.18.0 // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.24.0 // indirect ) -replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240313005020-c77f32e55220 +replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20241121030428-33b6ebc52000 diff --git a/go.sum b/go.sum index 62e8fb69..3e105593 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4 github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= 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.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= @@ -19,17 +17,20 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2 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.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= -github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= -github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= -github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -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/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= +github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= +github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ= +github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o= 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= @@ -39,15 +40,12 @@ github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBE 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.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/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/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= +github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 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= @@ -60,36 +58,36 @@ 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.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= -github.com/gobwas/ws v1.3.2/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/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= +github.com/gofrs/uuid/v5 v5.3.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.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/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8 h1:V3plQrMHRWOB5zMm3yNqvBxDQVW1+/wHBSok5uPdmVs= -github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8/go.mod h1:izxuNQZeFrbx2nK2fAyN5iNUB34Fe9j0nK4PwLzAkKw= +github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk= +github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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= @@ -100,30 +98,42 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4= +github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= +github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= +github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= +github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc= +github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= 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-20240214095142-666a73bcf165 h1:QIQI4gEm+gTwVNdiAyF4EIz5cHm7kSlfDGFpYlAa5dg= -github.com/metacubex/gvisor v0.0.0-20240214095142-666a73bcf165/go.mod h1:SKY70wiF1UTSoyuDZyKPMsUC6MsMxh8Y3ZNkIa6J3fU= -github.com/metacubex/quic-go v0.41.1-0.20240307164142-46c6f7cdf2d1 h1:63zKmEWU4MB5MjUSCmeDhm3OzilF7ypXWPq0gAA2GE8= -github.com/metacubex/quic-go v0.41.1-0.20240307164142-46c6f7cdf2d1/go.mod h1:F/t8VnA47xoia8ABlNA4InkZjssvFJ5p6E6jKdbkgAs= -github.com/metacubex/sing v0.0.0-20240313005020-c77f32e55220 h1:lZLFR28Jf3gjoHh560uC8AKnWcQ2+VQoQY/rSj6kSrc= -github.com/metacubex/sing v0.0.0-20240313005020-c77f32e55220/go.mod h1:+60H3Cm91RnL9dpVGWDPHt0zTQImO9Vfqt9a4rSambI= -github.com/metacubex/sing-quic v0.0.0-20240310154810-47bca850fc01 h1:5INHs85Gp1JZsdF7fQp1pXUjfJOX2dhwZjuUQWJVSt8= -github.com/metacubex/sing-quic v0.0.0-20240310154810-47bca850fc01/go.mod h1:WyY0zYxv+o+18R/Ece+QFontlgXoobKbNqbtYn2zjz8= -github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ= -github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg= -github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A= -github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8= -github.com/metacubex/sing-tun v0.2.1-0.20240214100323-23e40bfb9067 h1:sB9Hiq/Fgq94WD1mAFDUTDaQAJ6y3WZ5nZMEavcK0/o= -github.com/metacubex/sing-tun v0.2.1-0.20240214100323-23e40bfb9067/go.mod h1:HWyO52kAVvuSUN2nms4ZlRfiAwgXO9wGQBJFjymqvOQ= -github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ= -github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= -github.com/metacubex/sing-wireguard v0.0.0-20231209125515-0594297f7232 h1:loWjR+k9dxqBSgruGyT5hE8UCRMmCEjxqZbryfY9no4= -github.com/metacubex/sing-wireguard v0.0.0-20231209125515-0594297f7232/go.mod h1:NGCrBZ+fUmp81yaA1kVskcNWBnwl5z4UHxz47A01zm8= -github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c= -github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts= -github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= -github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg= +github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic= +github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30= +github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA= +github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= +github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= +github.com/metacubex/sing v0.0.0-20241121030428-33b6ebc52000 h1:gUbMXcQXhXGj0vCpCVFTUyIH7TMpD1dpTcNv/MCS+ok= +github.com/metacubex/sing v0.0.0-20241121030428-33b6ebc52000/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM= +github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= +github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= +github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= +github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo= +github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= +github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0= +github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= +github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I= +github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= +github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= +github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc= +github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= +github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= +github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8= +github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo= +github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= +github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 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= @@ -146,37 +156,30 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= -github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= -github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= -github.com/sagernet/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-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14= -github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= -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/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= +github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= +github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= +github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo= +github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE= +github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo= +github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= -github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co= -github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s= -github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo= -github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE= -github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= -github.com/samber/lo v1.39.0/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.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y= -github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk= -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/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= +github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= 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= @@ -195,8 +198,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -206,39 +210,43 @@ github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho 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/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -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/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= +gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= -go.uber.org/automaxprocs v1.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= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -250,30 +258,29 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-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= -lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= -lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= +lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index c23eb9f3..3fed360c 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -9,6 +9,7 @@ import ( "strconv" "sync" "time" + _ "unsafe" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/inbound" @@ -16,16 +17,20 @@ import ( "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/geodata" + mihomoHttp "github.com/metacubex/mihomo/component/http" "github.com/metacubex/mihomo/component/iface" + "github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/mihomo/component/profile" "github.com/metacubex/mihomo/component/profile/cachefile" "github.com/metacubex/mihomo/component/resolver" - SNI "github.com/metacubex/mihomo/component/sniffer" + "github.com/metacubex/mihomo/component/resource" + "github.com/metacubex/mihomo/component/sniffer" + tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/mihomo/component/trie" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/listener" @@ -76,10 +81,11 @@ func ParseWithBytes(buf []byte) (*config.Config, error) { return config.Parse(buf) } -// ApplyConfig dispatch configure to all parts +// ApplyConfig dispatch configure to all parts without ExternalController func ApplyConfig(cfg *config.Config, force bool) { mux.Lock() defer mux.Unlock() + log.SetLevel(cfg.General.LogLevel) tunnel.OnSuspend() @@ -90,18 +96,18 @@ func ApplyConfig(cfg *config.Config, force bool) { } } + updateExperimental(cfg.Experimental) updateUsers(cfg.Users) updateProxies(cfg.Proxies, cfg.Providers) updateRules(cfg.Rules, cfg.SubRules, cfg.RuleProviders) updateSniffer(cfg.Sniffer) updateHosts(cfg.Hosts) - updateGeneral(cfg.General) + updateGeneral(cfg.General, true) updateNTP(cfg.NTP) - updateDNS(cfg.DNS, cfg.RuleProviders, cfg.General.IPv6) + updateDNS(cfg.DNS, cfg.General.IPv6) updateListeners(cfg.General, cfg.Listeners, force) + updateTun(cfg.General) // tun should not care "force" updateIPTables(cfg) - updateTun(cfg.General) - updateExperimental(cfg) updateTunnels(cfg.Tunnels) tunnel.OnInnerLoading() @@ -113,8 +119,9 @@ func ApplyConfig(cfg *config.Config, force bool) { runtime.GC() tunnel.OnRunning() hcCompatibleProvider(cfg.Providers) + updateUpdater(cfg) - log.SetLevel(cfg.General.LogLevel) + resolver.ResetConnection() } func initInnerTcp() { @@ -124,7 +131,7 @@ func initInnerTcp() { func GetGeneral() *config.General { ports := listener.GetPorts() var authenticator []string - if auth := authStore.Authenticator(); auth != nil { + if auth := authStore.Default.Authenticator(); auth != nil { authenticator = auth.Users() } @@ -145,19 +152,35 @@ func GetGeneral() *config.General { LanDisAllowedIPs: inbound.DisAllowedIPs(), AllowLan: listener.AllowLan(), BindAddress: listener.BindAddress(), + InboundTfo: inbound.Tfo(), + InboundMPTCP: inbound.MPTCP(), }, - Controller: config.Controller{}, - Mode: tunnel.Mode(), - LogLevel: log.Level(), - IPv6: !resolver.DisableIPv6, - GeodataMode: G.GeodataMode(), - GeoAutoUpdate: G.GeoAutoUpdate(), - GeoUpdateInterval: G.GeoUpdateInterval(), - GeodataLoader: G.LoaderName(), - GeositeMatcher: G.SiteMatcherName(), - Interface: dialer.DefaultInterface.Load(), - Sniffing: tunnel.IsSniffing(), - TCPConcurrent: dialer.GetTcpConcurrent(), + Mode: tunnel.Mode(), + UnifiedDelay: adapter.UnifiedDelay.Load(), + LogLevel: log.Level(), + IPv6: !resolver.DisableIPv6, + Interface: dialer.DefaultInterface.Load(), + RoutingMark: int(dialer.DefaultRoutingMark.Load()), + GeoXUrl: config.GeoXUrl{ + GeoIp: geodata.GeoIpUrl(), + Mmdb: geodata.MmdbUrl(), + ASN: geodata.ASNUrl(), + GeoSite: geodata.GeoSiteUrl(), + }, + GeoAutoUpdate: updater.GeoAutoUpdate(), + GeoUpdateInterval: updater.GeoUpdateInterval(), + GeodataMode: geodata.GeodataMode(), + GeodataLoader: geodata.LoaderName(), + GeositeMatcher: geodata.SiteMatcherName(), + TCPConcurrent: dialer.GetTcpConcurrent(), + FindProcessMode: tunnel.FindProcessMode(), + Sniffing: tunnel.IsSniffing(), + GlobalClientFingerprint: tlsC.GetGlobalFingerprint(), + GlobalUA: mihomoHttp.UA(), + ETagSupport: resource.ETag(), + KeepAliveInterval: int(keepalive.KeepAliveInterval() / time.Second), + KeepAliveIdle: int(keepalive.KeepAliveIdle() / time.Second), + DisableKeepAlive: keepalive.DisableKeepAlive(), } return general @@ -180,9 +203,6 @@ func updateListeners(general *config.General, listeners map[string]C.InboundList listener.ReCreateHTTP(general.Port, tunnel.Tunnel) listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel) listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel) - if !features.CMFA { - 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) @@ -190,14 +210,18 @@ func updateListeners(general *config.General, listeners map[string]C.InboundList listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel) } -func updateExperimental(c *config.Config) { - if c.Experimental.QUICGoDisableGSO { +func updateTun(general *config.General) { + listener.ReCreateTun(general.Tun, tunnel.Tunnel) +} + +func updateExperimental(c *config.Experimental) { + if c.QUICGoDisableGSO { _ = os.Setenv("QUIC_GO_DISABLE_GSO", strconv.FormatBool(true)) } - if c.Experimental.QUICGoDisableECN { + if c.QUICGoDisableECN { _ = os.Setenv("QUIC_GO_DISABLE_ECN", strconv.FormatBool(true)) } - dialer.GetIP4PEnable(c.Experimental.IP4PEnable) + dialer.GetIP4PEnable(c.IP4PEnable) } func updateNTP(c *config.NTP) { @@ -211,38 +235,35 @@ func updateNTP(c *config.NTP) { } } -func updateDNS(c *config.DNS, ruleProvider map[string]provider.RuleProvider, generalIPv6 bool) { +func updateDNS(c *config.DNS, generalIPv6 bool) { if !c.Enable { resolver.DefaultResolver = nil resolver.DefaultHostMapper = nil resolver.DefaultLocalServer = nil + resolver.ProxyServerHostResolver = nil + resolver.DirectHostResolver = nil dns.ReCreateServer("", nil, nil) return } cfg := dns.Config{ - Main: c.NameServer, - Fallback: c.Fallback, - IPv6: c.IPv6 && generalIPv6, - IPv6Timeout: c.IPv6Timeout, - EnhancedMode: c.EnhancedMode, - Pool: c.FakeIPRange, - Hosts: c.Hosts, - FallbackFilter: dns.FallbackFilter{ - GeoIP: c.FallbackFilter.GeoIP, - GeoIPCode: c.FallbackFilter.GeoIPCode, - IPCIDR: c.FallbackFilter.IPCIDR, - Domain: c.FallbackFilter.Domain, - GeoSite: c.FallbackFilter.GeoSite, - }, - Default: c.DefaultNameserver, - Policy: c.NameServerPolicy, - ProxyServer: c.ProxyServerNameserver, - RuleProviders: ruleProvider, - CacheAlgorithm: c.CacheAlgorithm, + Main: c.NameServer, + Fallback: c.Fallback, + IPv6: c.IPv6 && generalIPv6, + IPv6Timeout: c.IPv6Timeout, + EnhancedMode: c.EnhancedMode, + Pool: c.FakeIPRange, + Hosts: c.Hosts, + FallbackIPFilter: c.FallbackIPFilter, + FallbackDomainFilter: c.FallbackDomainFilter, + Default: c.DefaultNameserver, + Policy: c.NameServerPolicy, + ProxyServer: c.ProxyServerNameserver, + DirectServer: c.DirectNameServer, + DirectFollowPolicy: c.DirectFollowPolicy, + CacheAlgorithm: c.CacheAlgorithm, } r := dns.NewResolver(cfg) - pr := dns.NewProxyServerHostResolver(r) m := dns.NewEnhancer(cfg) // reuse cache of old host mapper @@ -252,13 +273,22 @@ func updateDNS(c *config.DNS, ruleProvider map[string]provider.RuleProvider, gen resolver.DefaultResolver = r resolver.DefaultHostMapper = m - resolver.DefaultLocalServer = dns.NewLocalServer(r, m) + resolver.DefaultLocalServer = dns.NewLocalServer(r.Resolver, m) + resolver.UseSystemHosts = c.UseSystemHosts - if pr.Invalid() { - resolver.ProxyServerHostResolver = pr + if r.ProxyResolver.Invalid() { + resolver.ProxyServerHostResolver = r.ProxyResolver + } else { + resolver.ProxyServerHostResolver = r.Resolver } - dns.ReCreateServer(c.Listen, r, m) + if r.DirectResolver.Invalid() { + resolver.DirectHostResolver = r.DirectResolver + } else { + resolver.DirectHostResolver = r.Resolver + } + + dns.ReCreateServer(c.Listen, r.Resolver, m) } func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) { @@ -349,33 +379,18 @@ func hcCompatibleProvider(proxyProviders map[string]provider.ProxyProvider) { } } -func updateTun(general *config.General) { - if general == nil { - return + +func updateSniffer(snifferConfig *sniffer.Config) { + dispatcher, err := sniffer.NewDispatcher(snifferConfig) + if err != nil { + log.Warnln("initial sniffer failed, err:%v", err) } - listener.ReCreateTun(general.Tun, tunnel.Tunnel) - listener.ReCreateRedirToTun(general.Tun.RedirectToTun) -} -func updateSniffer(sniffer *config.Sniffer) { - if sniffer.Enable { - dispatcher, err := SNI.NewSnifferDispatcher( - sniffer.Sniffers, sniffer.ForceDomain, sniffer.SkipDomain, - sniffer.ForceDnsMapping, sniffer.ParsePureIp, - ) - if err != nil { - log.Warnln("initial sniffer failed, err:%v", err) - } + tunnel.UpdateSniffer(dispatcher) - tunnel.UpdateSniffer(dispatcher) + if snifferConfig.Enable { log.Infoln("Sniffer is loaded and working") } else { - dispatcher, err := SNI.NewCloseSnifferDispatcher() - if err != nil { - log.Warnln("initial sniffer failed, err:%v", err) - } - - tunnel.UpdateSniffer(dispatcher) log.Infoln("Sniffer is closed") } } @@ -384,35 +399,68 @@ func updateTunnels(tunnels []LC.Tunnel) { listener.PatchTunnel(tunnels, tunnel.Tunnel) } -func updateGeneral(general *config.General) { +func updateUpdater(cfg *config.Config) { + general := cfg.General + updater.SetGeoAutoUpdate(general.GeoAutoUpdate) + updater.SetGeoUpdateInterval(general.GeoUpdateInterval) + + controller := cfg.Controller + updater.DefaultUiUpdater = updater.NewUiUpdater(controller.ExternalUI, controller.ExternalUIURL, controller.ExternalUIName) + updater.DefaultUiUpdater.AutoDownloadUI() +} + +//go:linkname temporaryUpdateGeneral github.com/metacubex/mihomo/config.temporaryUpdateGeneral +func temporaryUpdateGeneral(general *config.General) func() { + oldGeneral := GetGeneral() + updateGeneral(general, false) + return func() { + updateGeneral(oldGeneral, false) + } +} + +func updateGeneral(general *config.General, logging bool) { tunnel.SetMode(general.Mode) tunnel.SetFindProcessMode(general.FindProcessMode) resolver.DisableIPv6 = !general.IPv6 - if general.TCPConcurrent { - dialer.SetTcpConcurrent(general.TCPConcurrent) + dialer.SetTcpConcurrent(general.TCPConcurrent) + if logging && general.TCPConcurrent { log.Infoln("Use tcp concurrent") } inbound.SetTfo(general.InboundTfo) inbound.SetMPTCP(general.InboundMPTCP) + keepalive.SetKeepAliveIdle(time.Duration(general.KeepAliveIdle) * time.Second) + keepalive.SetKeepAliveInterval(time.Duration(general.KeepAliveInterval) * time.Second) + keepalive.SetDisableKeepAlive(general.DisableKeepAlive) + adapter.UnifiedDelay.Store(general.UnifiedDelay) dialer.DefaultInterface.Store(general.Interface) dialer.DefaultRoutingMark.Store(int32(general.RoutingMark)) - if general.RoutingMark > 0 { + if logging && general.RoutingMark > 0 { log.Infoln("Use routing mark: %#x", general.RoutingMark) } iface.FlushCache() - G.SetLoader(general.GeodataLoader) - G.SetSiteMatcher(general.GeositeMatcher) + + geodata.SetGeodataMode(general.GeodataMode) + geodata.SetLoader(general.GeodataLoader) + geodata.SetSiteMatcher(general.GeositeMatcher) + geodata.SetGeoIpUrl(general.GeoXUrl.GeoIp) + geodata.SetGeoSiteUrl(general.GeoXUrl.GeoSite) + geodata.SetMmdbUrl(general.GeoXUrl.Mmdb) + geodata.SetASNUrl(general.GeoXUrl.ASN) + mihomoHttp.SetUA(general.GlobalUA) + resource.SetETag(general.ETagSupport) + + tlsC.SetGlobalUtlsClient(general.GlobalClientFingerprint) } func updateUsers(users []auth.AuthUser) { authenticator := auth.NewAuthenticator(users) - authStore.SetAuthenticator(authenticator) + authStore.Default.SetAuthenticator(authenticator) if authenticator != nil { log.Infoln("Authentication of local server updated") } @@ -434,12 +482,12 @@ func patchSelectGroup(proxies map[string]C.Proxy) { } for name, proxy := range proxies { - outbound, ok := proxy.(*adapter.Proxy) + outbound, ok := proxy.(C.Proxy) if !ok { continue } - selector, ok := outbound.ProxyAdapter.(outboundgroup.SelectAble) + selector, ok := outbound.Adapter().(outboundgroup.SelectAble) if !ok { continue } @@ -506,9 +554,7 @@ func updateIPTables(cfg *config.Config) { inboundInterface = iptables.InboundInterface } - if dialer.DefaultRoutingMark.Load() == 0 { - dialer.DefaultRoutingMark.Store(2158) - } + dialer.DefaultRoutingMark.CompareAndSwap(0, 2158) err = tproxy.SetTProxyIPTables(inboundInterface, bypass, uint16(tProxyPort), DnsRedirect, dnsPort.Port()) if err != nil { diff --git a/hub/hub.go b/hub/hub.go index 323f8749..69f627ff 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -11,25 +11,72 @@ type Option func(*config.Config) func WithExternalUI(externalUI string) Option { return func(cfg *config.Config) { - cfg.General.ExternalUI = externalUI + cfg.Controller.ExternalUI = externalUI } } func WithExternalController(externalController string) Option { return func(cfg *config.Config) { - cfg.General.ExternalController = externalController + cfg.Controller.ExternalController = externalController + } +} + +func WithExternalControllerUnix(externalControllerUnix string) Option { + return func(cfg *config.Config) { + cfg.Controller.ExternalControllerUnix = externalControllerUnix + } +} + +func WithExternalControllerPipe(externalControllerPipe string) Option { + return func(cfg *config.Config) { + cfg.Controller.ExternalControllerPipe = externalControllerPipe } } func WithSecret(secret string) Option { return func(cfg *config.Config) { - cfg.General.Secret = secret + cfg.Controller.Secret = secret } } +// ApplyConfig dispatch configure to all parts include ExternalController +func ApplyConfig(cfg *config.Config) { + applyRoute(cfg) + executor.ApplyConfig(cfg, true) +} + +func applyRoute(cfg *config.Config) { + if cfg.Controller.ExternalUI != "" { + route.SetUIPath(cfg.Controller.ExternalUI) + } + route.ReCreateServer(&route.Config{ + Addr: cfg.Controller.ExternalController, + TLSAddr: cfg.Controller.ExternalControllerTLS, + UnixAddr: cfg.Controller.ExternalControllerUnix, + PipeAddr: cfg.Controller.ExternalControllerPipe, + Secret: cfg.Controller.Secret, + Certificate: cfg.TLS.Certificate, + PrivateKey: cfg.TLS.PrivateKey, + DohServer: cfg.Controller.ExternalDohServer, + IsDebug: cfg.General.LogLevel == log.DEBUG, + Cors: route.Cors{ + AllowOrigins: cfg.Controller.Cors.AllowOrigins, + AllowPrivateNetwork: cfg.Controller.Cors.AllowPrivateNetwork, + }, + }) +} + // Parse call at the beginning of mihomo -func Parse(options ...Option) error { - cfg, err := executor.Parse() +func Parse(configBytes []byte, options ...Option) error { + var cfg *config.Config + var err error + + if len(configBytes) != 0 { + cfg, err = executor.ParseWithBytes(configBytes) + } else { + cfg, err = executor.Parse() + } + if err != nil { return err } @@ -38,15 +85,6 @@ func Parse(options ...Option) error { option(cfg) } - if cfg.General.ExternalUI != "" { - route.SetUIPath(cfg.General.ExternalUI) - } - - if cfg.General.ExternalController != "" { - go route.Start(cfg.General.ExternalController, cfg.General.ExternalControllerTLS, - cfg.General.Secret, cfg.TLS.Certificate, cfg.TLS.PrivateKey, cfg.General.LogLevel == log.DEBUG) - } - - executor.ApplyConfig(cfg, true) + ApplyConfig(cfg) return nil } diff --git a/hub/route/configs.go b/hub/route/configs.go index ec0b464c..b23a35a5 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -4,11 +4,11 @@ import ( "net/http" "net/netip" "path/filepath" - "sync" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/hub/executor" @@ -21,17 +21,14 @@ import ( "github.com/go-chi/render" ) -var ( - updateGeoMux sync.Mutex - updatingGeo = false -) - func configRouter() http.Handler { r := chi.NewRouter() r.Get("/", getConfigs) - r.Put("/", updateConfigs) - r.Post("/geo", updateGeoDatabases) - r.Patch("/", patchConfigs) + if !embedMode { // disallow update/patch configs in embed mode + r.Put("/", updateConfigs) + r.Post("/geo", updateGeoDatabases) + r.Patch("/", patchConfigs) + } return r } @@ -67,30 +64,39 @@ type tunSchema struct { DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"` AutoRoute *bool `yaml:"auto-route" json:"auto-route"` AutoDetectInterface *bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` - //RedirectToTun []string `yaml:"-" json:"-"` MTU *uint32 `yaml:"mtu" json:"mtu,omitempty"` GSO *bool `yaml:"gso" json:"gso,omitempty"` GSOMaxSize *uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` //Inet4Address *[]netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` - Inet6Address *[]netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` - StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` + Inet6Address *[]netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` + IPRoute2TableIndex *int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` + IPRoute2RuleIndex *int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` + AutoRedirect *bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` + AutoRedirectInputMark *uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` + AutoRedirectOutputMark *uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` + StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` + RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` + RouteAddressSet *[]string `yaml:"route-address-set" json:"route-address-set,omitempty"` + RouteExcludeAddress *[]netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` + RouteExcludeAddressSet *[]string `yaml:"route-exclude-address-set" json:"route-exclude-address-set,omitempty"` + IncludeInterface *[]string `yaml:"include-interface" json:"include-interface,omitempty"` + ExcludeInterface *[]string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` + IncludeUID *[]uint32 `yaml:"include-uid" json:"include-uid,omitempty"` + IncludeUIDRange *[]string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` + ExcludeUID *[]uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` + ExcludeUIDRange *[]string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` + IncludeAndroidUser *[]int `yaml:"include-android-user" json:"include-android-user,omitempty"` + IncludePackage *[]string `yaml:"include-package" json:"include-package,omitempty"` + ExcludePackage *[]string `yaml:"exclude-package" json:"exclude-package,omitempty"` + EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` + UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` + FileDescriptor *int `yaml:"file-descriptor" json:"file-descriptor"` + Inet4RouteAddress *[]netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` Inet6RouteAddress *[]netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` Inet4RouteExcludeAddress *[]netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"` Inet6RouteExcludeAddress *[]netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"` - IncludeInterface *[]string `yaml:"include-interface" json:"include-interface,omitempty"` - ExcludeInterface *[]string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` - IncludeUID *[]uint32 `yaml:"include-uid" json:"include-uid,omitempty"` - IncludeUIDRange *[]string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` - ExcludeUID *[]uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` - ExcludeUIDRange *[]string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` - IncludeAndroidUser *[]int `yaml:"include-android-user" json:"include-android-user,omitempty"` - IncludePackage *[]string `yaml:"include-package" json:"include-package,omitempty"` - ExcludePackage *[]string `yaml:"exclude-package" json:"exclude-package,omitempty"` - EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` - UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` - FileDescriptor *int `yaml:"file-descriptor" json:"file-descriptor"` } type tuicServerSchema struct { @@ -113,21 +119,13 @@ func getConfigs(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, general) } -func pointerOrDefault(p *int, def int) int { +func pointerOrDefault[T any](p *T, def T) T { if p != nil { return *p } return def } -func pointerOrDefaultString(p *string, def string) string { - if p != nil { - return *p - } - - return def -} - func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { if p != nil { def.Enable = p.Enable @@ -161,6 +159,36 @@ func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { if p.Inet6Address != nil { def.Inet6Address = *p.Inet6Address } + if p.IPRoute2TableIndex != nil { + def.IPRoute2TableIndex = *p.IPRoute2TableIndex + } + if p.IPRoute2RuleIndex != nil { + def.IPRoute2RuleIndex = *p.IPRoute2RuleIndex + } + if p.AutoRedirect != nil { + def.AutoRedirect = *p.AutoRedirect + } + if p.AutoRedirectInputMark != nil { + def.AutoRedirectInputMark = *p.AutoRedirectInputMark + } + if p.AutoRedirectOutputMark != nil { + def.AutoRedirectOutputMark = *p.AutoRedirectOutputMark + } + if p.StrictRoute != nil { + def.StrictRoute = *p.StrictRoute + } + if p.RouteAddress != nil { + def.RouteAddress = *p.RouteAddress + } + if p.RouteAddressSet != nil { + def.RouteAddressSet = *p.RouteAddressSet + } + if p.RouteExcludeAddress != nil { + def.RouteExcludeAddress = *p.RouteExcludeAddress + } + if p.RouteExcludeAddressSet != nil { + def.RouteExcludeAddressSet = *p.RouteExcludeAddressSet + } if p.Inet4RouteAddress != nil { def.Inet4RouteAddress = *p.Inet4RouteAddress } @@ -301,8 +329,8 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) { 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.ReCreateShadowSocks(pointerOrDefault(general.ShadowSocksConfig, ports.ShadowSocksConfig), tunnel.Tunnel) + P.ReCreateVmess(pointerOrDefault(general.VmessConfig, ports.VmessConfig), tunnel.Tunnel) P.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, P.LastTuicConf), tunnel.Tunnel) if general.Mode != nil { @@ -365,40 +393,13 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) { } func updateGeoDatabases(w http.ResponseWriter, r *http.Request) { - updateGeoMux.Lock() - - if updatingGeo { - updateGeoMux.Unlock() - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, newError("updating...")) + err := updater.UpdateGeoDatabases() + if err != nil { + log.Errorln("[GEO] update GEO databases failed: %v", err) + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(err.Error())) return } - updatingGeo = true - updateGeoMux.Unlock() - - go func() { - defer func() { - updatingGeo = false - }() - - log.Warnln("[REST-API] updating GEO databases...") - - if err := config.UpdateGeoDatabases(); err != nil { - log.Errorln("[REST-API] update GEO databases failed: %v", err) - return - } - - cfg, err := executor.ParseWithPath(C.Path.Config()) - if err != nil { - log.Errorln("[REST-API] update GEO databases failed: %v", err) - return - } - - log.Warnln("[REST-API] update GEO databases successful, apply config...") - - executor.ApplyConfig(cfg, false) - }() - render.NoContent(w, r) } diff --git a/hub/route/doh.go b/hub/route/doh.go new file mode 100644 index 00000000..bb5416c4 --- /dev/null +++ b/hub/route/doh.go @@ -0,0 +1,63 @@ +package route + +import ( + "context" + "encoding/base64" + "io" + "net/http" + + "github.com/metacubex/mihomo/component/resolver" + + "github.com/go-chi/render" +) + +func dohRouter() http.Handler { + return http.HandlerFunc(dohHandler) +} + +func dohHandler(w http.ResponseWriter, r *http.Request) { + if resolver.DefaultResolver == nil { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, "DNS section is disabled") + return + } + + var dnsData []byte + var err error + switch r.Method { + case "GET": + dnsData, err = base64.RawURLEncoding.DecodeString(r.URL.Query().Get("dns")) + case "POST": + if r.Header.Get("Content-Type") != "application/dns-message" { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, "invalid content-type") + return + } + reader := io.LimitReader(r.Body, 65535) // according to rfc8484, the maximum size of the DNS message is 65535 bytes + dnsData, err = io.ReadAll(reader) + _ = r.Body.Close() + default: + render.Status(r, http.StatusMethodNotAllowed) + render.PlainText(w, r, "method not allowed") + return + } + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, err.Error()) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) + defer cancel() + + dnsData, err = resolver.RelayDnsPacket(ctx, dnsData, dnsData) + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, err.Error()) + return + } + + w.Header().Set("Content-Type", "application/dns-message") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(dnsData) +} diff --git a/hub/route/groups.go b/hub/route/groups.go index 30dec5f0..873a94df 100644 --- a/hub/route/groups.go +++ b/hub/route/groups.go @@ -9,7 +9,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/render" - "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/profile/cachefile" @@ -17,7 +16,7 @@ import ( "github.com/metacubex/mihomo/tunnel" ) -func GroupRouter() http.Handler { +func groupRouter() http.Handler { r := chi.NewRouter() r.Get("/", getGroups) @@ -32,7 +31,7 @@ func GroupRouter() http.Handler { func getGroups(w http.ResponseWriter, r *http.Request) { var gs []C.Proxy for _, p := range tunnel.Proxies() { - if _, ok := p.(*adapter.Proxy).ProxyAdapter.(C.Group); ok { + if _, ok := p.Adapter().(C.Group); ok { gs = append(gs, p) } } @@ -43,7 +42,7 @@ func getGroups(w http.ResponseWriter, r *http.Request) { func getGroup(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) - if _, ok := proxy.(*adapter.Proxy).ProxyAdapter.(C.Group); ok { + if _, ok := proxy.Adapter().(C.Group); ok { render.JSON(w, r, proxy) return } @@ -53,19 +52,15 @@ func getGroup(w http.ResponseWriter, r *http.Request) { func getGroupDelay(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) - group, ok := proxy.(*adapter.Proxy).ProxyAdapter.(C.Group) + group, ok := proxy.Adapter().(C.Group) if !ok { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) return } - if proxy.(*adapter.Proxy).Type() == C.URLTest { - URLTestGroup := proxy.(*adapter.Proxy).ProxyAdapter.(*outboundgroup.URLTest) - URLTestGroup.ForceSet("") - } - - if proxy.(*adapter.Proxy).Type() != C.Selector { + if selectAble, ok := proxy.Adapter().(outboundgroup.SelectAble); ok && proxy.Type() != C.Selector { + selectAble.ForceSet("") cachefile.Cache().SetSelected(proxy.Name(), "") } diff --git a/hub/route/patch_android.go b/hub/route/patch_android.go new file mode 100644 index 00000000..5eba2796 --- /dev/null +++ b/hub/route/patch_android.go @@ -0,0 +1,7 @@ +//go:build android && cmfa + +package route + +func init() { + SetEmbedMode(true) // set embed mode default +} diff --git a/hub/route/proxies.go b/hub/route/proxies.go index 69c8e446..ba4e03f9 100644 --- a/hub/route/proxies.go +++ b/hub/route/proxies.go @@ -7,7 +7,6 @@ import ( "strconv" "time" - "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/profile/cachefile" @@ -31,6 +30,7 @@ func proxyRouter() http.Handler { r.Get("/", getProxy) r.Get("/delay", getProxyDelay) r.Put("/", updateProxy) + r.Delete("/", unfixedProxy) }) return r } @@ -81,8 +81,8 @@ func updateProxy(w http.ResponseWriter, r *http.Request) { return } - proxy := r.Context().Value(CtxKeyProxy).(*adapter.Proxy) - selector, ok := proxy.ProxyAdapter.(outboundgroup.SelectAble) + proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) + selector, ok := proxy.Adapter().(outboundgroup.SelectAble) if !ok { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("Must be a Selector")) @@ -146,3 +146,15 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) { "delay": delay, }) } + +func unfixedProxy(w http.ResponseWriter, r *http.Request) { + proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) + if selectAble, ok := proxy.Adapter().(outboundgroup.SelectAble); ok && proxy.Type() != C.Selector { + selectAble.ForceSet("") + cachefile.Cache().SetSelected(proxy.Name(), "") + render.NoContent(w, r) + return + } + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) +} diff --git a/hub/route/server.go b/hub/route/server.go index 8e7f225f..3c3e5ca4 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -7,8 +7,11 @@ import ( "encoding/json" "net" "net/http" + "os" + "path/filepath" "runtime/debug" "strings" + "syscall" "time" "github.com/metacubex/mihomo/adapter/inbound" @@ -20,19 +23,27 @@ import ( "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/gobwas/ws" "github.com/gobwas/ws/wsutil" + "github.com/sagernet/cors" ) var ( - serverSecret = "" - serverAddr = "" - uiPath = "" + + httpServer *http.Server + tlsServer *http.Server + unixServer *http.Server + pipeServer *http.Server + + embedMode = false ) +func SetEmbedMode(embed bool) { + embedMode = embed +} + type Traffic struct { Up int64 `json:"up"` Down int64 `json:"down"` @@ -43,28 +54,50 @@ type Memory struct { OSLimit uint64 `json:"oslimit"` // maybe we need it in the future } +type Config struct { + Addr string + TLSAddr string + UnixAddr string + PipeAddr string + Secret string + Certificate string + PrivateKey string + DohServer string + IsDebug bool + Cors Cors +} + +type Cors struct { + AllowOrigins []string + AllowPrivateNetwork bool +} + +func (c Cors) Apply(r chi.Router) { + r.Use(cors.New(cors.Options{ + AllowedOrigins: c.AllowOrigins, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + AllowPrivateNetwork: c.AllowPrivateNetwork, + MaxAge: 300, + }).Handler) +} + +func ReCreateServer(cfg *Config) { + go start(cfg) + go startTLS(cfg) + go startUnix(cfg) + if inbound.SupportNamedPipe { + go startPipe(cfg) + } +} + func SetUIPath(path string) { uiPath = C.Path.Resolve(path) } -func Start(addr string, tlsAddr string, secret string, - certificat, privateKey string, isDebug bool) { - if serverAddr != "" { - return - } - - serverAddr = addr - serverSecret = secret - +func router(isDebug bool, secret string, dohServer string, cors Cors) *chi.Mux { r := chi.NewRouter() - corsM := cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, - AllowedHeaders: []string{"Content-Type", "Authorization"}, - MaxAge: 300, - }) - r.Use(setPrivateNetworkAccess) - r.Use(corsM.Handler) + cors.Apply(r) if isDebug { r.Mount("/debug", func() http.Handler { r := chi.NewRouter() @@ -77,7 +110,9 @@ func Start(addr string, tlsAddr string, secret string, }()) } r.Group(func(r chi.Router) { - r.Use(authentication) + if secret != "" { + r.Use(authentication(secret)) + } r.Get("/", hello) r.Get("/logs", getLogs) r.Get("/traffic", traffic) @@ -85,14 +120,16 @@ func Start(addr string, tlsAddr string, secret string, r.Get("/version", version) r.Mount("/configs", configRouter()) r.Mount("/proxies", proxyRouter()) - r.Mount("/group", GroupRouter()) + r.Mount("/group", groupRouter()) r.Mount("/rules", ruleRouter()) r.Mount("/connections", connectionRouter()) r.Mount("/providers/proxies", proxyProviderRouter()) r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/cache", cacheRouter()) r.Mount("/dns", dnsRouter()) - r.Mount("/restart", restartRouter()) + if !embedMode { // disallow restart in embed mode + r.Mount("/restart", restartRouter()) + } r.Mount("/upgrade", upgradeRouter()) addExternalRouters(r) @@ -107,56 +144,149 @@ func Start(addr string, tlsAddr string, secret string, }) }) } - - if len(tlsAddr) > 0 { - go func() { - c, err := CN.ParseCert(certificat, privateKey, C.Path) - if err != nil { - log.Errorln("External controller tls listen error: %s", err) - return - } - - l, err := inbound.Listen("tcp", tlsAddr) - if err != nil { - log.Errorln("External controller tls listen error: %s", err) - return - } - - serverAddr = l.Addr().String() - log.Infoln("RESTful API tls listening at: %s", serverAddr) - tlsServe := &http.Server{ - Handler: r, - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{c}, - }, - } - if err = tlsServe.ServeTLS(l, "", ""); err != nil { - log.Errorln("External controller tls serve error: %s", err) - } - }() - } - - l, err := inbound.Listen("tcp", addr) - if err != nil { - log.Errorln("External controller listen error: %s", err) - return - } - serverAddr = l.Addr().String() - log.Infoln("RESTful API listening at: %s", serverAddr) - - if err = http.Serve(l, r); err != nil { - log.Errorln("External controller serve error: %s", err) + if len(dohServer) > 0 && dohServer[0] == '/' { + r.Mount(dohServer, dohRouter()) } + return r } -func setPrivateNetworkAccess(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { - w.Header().Add("Access-Control-Allow-Private-Network", "true") +func start(cfg *Config) { + // first stop existing server + if httpServer != nil { + _ = httpServer.Close() + httpServer = nil + } + + // handle addr + if len(cfg.Addr) > 0 { + l, err := inbound.Listen("tcp", cfg.Addr) + if err != nil { + log.Errorln("External controller listen error: %s", err) + return } - next.ServeHTTP(w, r) - }) + log.Infoln("RESTful API listening at: %s", l.Addr().String()) + + server := &http.Server{ + Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), + } + httpServer = server + if err = server.Serve(l); err != nil { + log.Errorln("External controller serve error: %s", err) + } + } +} + +func startTLS(cfg *Config) { + // first stop existing server + if tlsServer != nil { + _ = tlsServer.Close() + tlsServer = nil + } + + // handle tlsAddr + if len(cfg.TLSAddr) > 0 { + c, err := CN.ParseCert(cfg.Certificate, cfg.PrivateKey, C.Path) + if err != nil { + log.Errorln("External controller tls listen error: %s", err) + return + } + + l, err := inbound.Listen("tcp", cfg.TLSAddr) + if err != nil { + log.Errorln("External controller tls listen error: %s", err) + return + } + + log.Infoln("RESTful API tls listening at: %s", l.Addr().String()) + server := &http.Server{ + Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{c}, + }, + } + tlsServer = server + if err = server.ServeTLS(l, "", ""); err != nil { + log.Errorln("External controller tls serve error: %s", err) + } + } +} + +func startUnix(cfg *Config) { + // first stop existing server + if unixServer != nil { + _ = unixServer.Close() + unixServer = nil + } + + // handle addr + if len(cfg.UnixAddr) > 0 { + addr := C.Path.Resolve(cfg.UnixAddr) + + dir := filepath.Dir(addr) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0o755); err != nil { + log.Errorln("External controller unix listen error: %s", err) + return + } + } + + // https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ + // + // Note: As mentioned above in the ‘security’ section, when a socket binds a socket to a valid pathname address, + // a socket file is created within the filesystem. On Linux, the application is expected to unlink + // (see the notes section in the man page for AF_UNIX) before any other socket can be bound to the same address. + // The same applies to Windows unix sockets, except that, DeleteFile (or any other file delete API) + // should be used to delete the socket file prior to calling bind with the same path. + _ = syscall.Unlink(addr) + + l, err := inbound.Listen("unix", addr) + if err != nil { + log.Errorln("External controller unix listen error: %s", err) + return + } + _ = os.Chmod(addr, 0o666) + log.Infoln("RESTful API unix listening at: %s", l.Addr().String()) + + server := &http.Server{ + Handler: router(cfg.IsDebug, "", cfg.DohServer, cfg.Cors), + } + unixServer = server + if err = server.Serve(l); err != nil { + log.Errorln("External controller unix serve error: %s", err) + } + } +} + +func startPipe(cfg *Config) { + // first stop existing server + if pipeServer != nil { + _ = pipeServer.Close() + pipeServer = nil + } + + // handle addr + if len(cfg.PipeAddr) > 0 { + if !strings.HasPrefix(cfg.PipeAddr, "\\\\.\\pipe\\") { // windows namedpipe must start with "\\.\pipe\" + log.Errorln("External controller pipe listen error: windows namedpipe must start with \"\\\\.\\pipe\\\"") + return + } + + l, err := inbound.ListenNamedPipe(cfg.PipeAddr) + if err != nil { + log.Errorln("External controller pipe listen error: %s", err) + return + } + log.Infoln("RESTful API pipe listening at: %s", l.Addr().String()) + + server := &http.Server{ + Handler: router(cfg.IsDebug, "", cfg.DohServer, cfg.Cors), + } + pipeServer = server + if err = server.Serve(l); err != nil { + log.Errorln("External controller pipe serve error: %s", err) + } + } } func safeEuqal(a, b string) bool { @@ -165,38 +295,35 @@ func safeEuqal(a, b string) bool { return subtle.ConstantTimeCompare(aBuf, bBuf) == 1 } -func authentication(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - if serverSecret == "" { - next.ServeHTTP(w, r) - return - } +func authentication(secret string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + // Browser websocket not support custom header + if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" { + token := r.URL.Query().Get("token") + if !safeEuqal(token, secret) { + render.Status(r, http.StatusUnauthorized) + render.JSON(w, r, ErrUnauthorized) + return + } + next.ServeHTTP(w, r) + return + } - // Browser websocket not support custom header - if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" { - token := r.URL.Query().Get("token") - if !safeEuqal(token, serverSecret) { + header := r.Header.Get("Authorization") + bearer, token, found := strings.Cut(header, " ") + + hasInvalidHeader := bearer != "Bearer" + hasInvalidSecret := !found || !safeEuqal(token, secret) + if hasInvalidHeader || hasInvalidSecret { render.Status(r, http.StatusUnauthorized) render.JSON(w, r, ErrUnauthorized) return } next.ServeHTTP(w, r) - return } - - header := r.Header.Get("Authorization") - bearer, token, found := strings.Cut(header, " ") - - hasInvalidHeader := bearer != "Bearer" - hasInvalidSecret := !found || !safeEuqal(token, serverSecret) - if hasInvalidHeader || hasInvalidSecret { - render.Status(r, http.StatusUnauthorized) - render.JSON(w, r, ErrUnauthorized) - return - } - next.ServeHTTP(w, r) + return http.HandlerFunc(fn) } - return http.HandlerFunc(fn) } func hello(w http.ResponseWriter, r *http.Request) { diff --git a/hub/route/upgrade.go b/hub/route/upgrade.go index ea371798..beee77a2 100644 --- a/hub/route/upgrade.go +++ b/hub/route/upgrade.go @@ -1,13 +1,11 @@ package route import ( - "errors" "fmt" "net/http" "os" - "github.com/metacubex/mihomo/config" - "github.com/metacubex/mihomo/hub/updater" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/log" "github.com/go-chi/chi/v5" @@ -16,8 +14,11 @@ import ( func upgradeRouter() http.Handler { r := chi.NewRouter() - r.Post("/", upgradeCore) r.Post("/ui", updateUI) + if !embedMode { // disallow upgrade core/geo in embed mode + r.Post("/", upgradeCore) + r.Post("/geo", updateGeoDatabases) + } return r } @@ -31,7 +32,7 @@ func upgradeCore(w http.ResponseWriter, r *http.Request) { return } - err = updater.Update(execPath) + err = updater.UpdateCore(execPath) if err != nil { log.Warnln("%s", err) render.Status(r, http.StatusInternalServerError) @@ -48,17 +49,11 @@ func upgradeCore(w http.ResponseWriter, r *http.Request) { } func updateUI(w http.ResponseWriter, r *http.Request) { - err := config.UpdateUI() + err := updater.DefaultUiUpdater.DownloadUI() 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))) - } + log.Warnln("%s", err) + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(fmt.Sprintf("%s", err))) return } diff --git a/listener/auth/auth.go b/listener/auth/auth.go index 46f552b8..9e7632e8 100644 --- a/listener/auth/auth.go +++ b/listener/auth/auth.go @@ -4,12 +4,30 @@ import ( "github.com/metacubex/mihomo/component/auth" ) -var authenticator auth.Authenticator - -func Authenticator() auth.Authenticator { - return authenticator +type authStore struct { + authenticator auth.Authenticator } -func SetAuthenticator(au auth.Authenticator) { - authenticator = au +func (a *authStore) Authenticator() auth.Authenticator { + return a.authenticator } + +func (a *authStore) SetAuthenticator(authenticator auth.Authenticator) { + a.authenticator = authenticator +} + +func NewAuthStore(authenticator auth.Authenticator) auth.AuthStore { + return &authStore{authenticator} +} + +var Default auth.AuthStore = NewAuthStore(nil) + +type nilAuthStore struct{} + +func (a *nilAuthStore) Authenticator() auth.Authenticator { + return nil +} + +func (a *nilAuthStore) SetAuthenticator(authenticator auth.Authenticator) {} + +var Nil auth.AuthStore = (*nilAuthStore)(nil) // always return nil, even call SetAuthenticator() with a non-nil authenticator diff --git a/listener/autoredir/tcp.go b/listener/autoredir/tcp.go deleted file mode 100644 index 2b21b087..00000000 --- a/listener/autoredir/tcp.go +++ /dev/null @@ -1,95 +0,0 @@ -package autoredir - -import ( - "net" - "net/netip" - - "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 { - listener net.Listener - addr string - closed bool - additions []inbound.Addition - lookupFunc func(netip.AddrPort) (socks5.Addr, error) -} - -// RawAddress implements C.Listener -func (l *Listener) RawAddress() string { - return l.addr -} - -// Address implements C.Listener -func (l *Listener) Address() string { - return l.listener.Addr().String() -} - -// Close implements C.Listener -func (l *Listener) Close() error { - l.closed = true - return l.listener.Close() -} - -func (l *Listener) TCPAddr() netip.AddrPort { - return l.listener.Addr().(*net.TCPAddr).AddrPort() -} - -func (l *Listener) SetLookupFunc(lookupFunc func(netip.AddrPort) (socks5.Addr, error)) { - l.lookupFunc = lookupFunc -} - -func (l *Listener) handleRedir(conn net.Conn, tunnel C.Tunnel) { - if l.lookupFunc == nil { - log.Errorln("[Auto Redirect] lookup function is nil") - return - } - - target, err := l.lookupFunc(conn.RemoteAddr().(*net.TCPAddr).AddrPort()) - if err != nil { - log.Warnln("[Auto Redirect] %v", err) - _ = conn.Close() - return - } - - N.TCPKeepAlive(conn) - - tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.REDIR, l.additions...)) -} - -func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - if len(additions) == 0 { - additions = []inbound.Addition{ - inbound.WithInName("DEFAULT-REDIR"), - inbound.WithSpecialRules(""), - } - } - l, err := net.Listen("tcp", addr) - if err != nil { - return nil, err - } - rl := &Listener{ - listener: l, - addr: addr, - additions: additions, - } - - go func() { - for { - c, err := l.Accept() - if err != nil { - if rl.closed { - break - } - continue - } - go rl.handleRedir(c, tunnel) - } - }() - - return rl, nil -} diff --git a/listener/config/tun.go b/listener/config/tun.go index 1772c6f5..3901bb1d 100644 --- a/listener/config/tun.go +++ b/listener/config/tun.go @@ -3,7 +3,10 @@ package config import ( "net/netip" + "github.com/metacubex/mihomo/common/nnip" C "github.com/metacubex/mihomo/constant" + + "golang.org/x/exp/slices" ) func StringSliceToNetipPrefixSlice(ss []string) ([]netip.Prefix, error) { @@ -25,28 +28,180 @@ type Tun struct { DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"` AutoRoute bool `yaml:"auto-route" json:"auto-route"` AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` - RedirectToTun []string `yaml:"-" json:"-"` - MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` - GSO bool `yaml:"gso" json:"gso,omitempty"` - GSOMaxSize uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` - Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` - Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` - StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` + MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` + GSO bool `yaml:"gso" json:"gso,omitempty"` + GSOMaxSize uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` + Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` + Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` + IPRoute2TableIndex int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` + IPRoute2RuleIndex int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` + AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` + AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` + AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` + StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` + RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` + RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"` + RouteExcludeAddress []netip.Prefix `yaml:"route-exclude-address" json:"route-exclude-address,omitempty"` + RouteExcludeAddressSet []string `yaml:"route-exclude-address-set" json:"route-exclude-address-set,omitempty"` + IncludeInterface []string `yaml:"include-interface" json:"include-interface,omitempty"` + ExcludeInterface []string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` + IncludeUID []uint32 `yaml:"include-uid" json:"include-uid,omitempty"` + IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` + ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` + ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` + IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` + IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` + ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` + EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` + UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` + FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` + Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` Inet6RouteAddress []netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"` Inet4RouteExcludeAddress []netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"` Inet6RouteExcludeAddress []netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"` - IncludeInterface []string `yaml:"include-interface" json:"include-interface,omitempty"` - ExcludeInterface []string `yaml:"exclude-interface" json:"exclude-interface,omitempty"` - IncludeUID []uint32 `yaml:"include-uid" json:"include-uid,omitempty"` - IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` - ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` - ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` - IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` - IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` - ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` - EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` - UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` - FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` +} + +func (t *Tun) Sort() { + slices.Sort(t.DNSHijack) + + slices.SortFunc(t.Inet4Address, nnip.PrefixCompare) + slices.SortFunc(t.Inet6Address, nnip.PrefixCompare) + slices.SortFunc(t.RouteAddress, nnip.PrefixCompare) + slices.Sort(t.RouteAddressSet) + slices.SortFunc(t.RouteExcludeAddress, nnip.PrefixCompare) + slices.Sort(t.RouteExcludeAddressSet) + slices.Sort(t.IncludeInterface) + slices.Sort(t.ExcludeInterface) + slices.Sort(t.IncludeUID) + slices.Sort(t.IncludeUIDRange) + slices.Sort(t.ExcludeUID) + slices.Sort(t.ExcludeUIDRange) + slices.Sort(t.IncludeAndroidUser) + slices.Sort(t.IncludePackage) + slices.Sort(t.ExcludePackage) + + slices.SortFunc(t.Inet4RouteAddress, nnip.PrefixCompare) + slices.SortFunc(t.Inet6RouteAddress, nnip.PrefixCompare) + slices.SortFunc(t.Inet4RouteExcludeAddress, nnip.PrefixCompare) + slices.SortFunc(t.Inet6RouteExcludeAddress, nnip.PrefixCompare) +} + +func (t *Tun) Equal(other Tun) bool { + if t.Enable != other.Enable { + return false + } + if t.Device != other.Device { + return false + } + if t.Stack != other.Stack { + return false + } + if !slices.Equal(t.DNSHijack, other.DNSHijack) { + return false + } + if t.AutoRoute != other.AutoRoute { + return false + } + if t.AutoDetectInterface != other.AutoDetectInterface { + return false + } + + if t.MTU != other.MTU { + return false + } + if t.GSO != other.GSO { + return false + } + if t.GSOMaxSize != other.GSOMaxSize { + return false + } + if !slices.Equal(t.Inet4Address, other.Inet4Address) { + return false + } + if !slices.Equal(t.Inet6Address, other.Inet6Address) { + return false + } + if t.IPRoute2TableIndex != other.IPRoute2TableIndex { + return false + } + if t.IPRoute2RuleIndex != other.IPRoute2RuleIndex { + return false + } + if t.AutoRedirect != other.AutoRedirect { + return false + } + if t.AutoRedirectInputMark != other.AutoRedirectInputMark { + return false + } + if t.AutoRedirectOutputMark != other.AutoRedirectOutputMark { + return false + } + if t.StrictRoute != other.StrictRoute { + return false + } + if !slices.Equal(t.RouteAddress, other.RouteAddress) { + return false + } + if !slices.Equal(t.RouteAddressSet, other.RouteAddressSet) { + return false + } + if !slices.Equal(t.RouteExcludeAddress, other.RouteExcludeAddress) { + return false + } + if !slices.Equal(t.RouteExcludeAddressSet, other.RouteExcludeAddressSet) { + return false + } + if !slices.Equal(t.IncludeInterface, other.IncludeInterface) { + return false + } + if !slices.Equal(t.ExcludeInterface, other.ExcludeInterface) { + return false + } + if !slices.Equal(t.IncludeUID, other.IncludeUID) { + return false + } + if !slices.Equal(t.IncludeUIDRange, other.IncludeUIDRange) { + return false + } + if !slices.Equal(t.ExcludeUID, other.ExcludeUID) { + return false + } + if !slices.Equal(t.ExcludeUIDRange, other.ExcludeUIDRange) { + return false + } + if !slices.Equal(t.IncludeAndroidUser, other.IncludeAndroidUser) { + return false + } + if !slices.Equal(t.IncludePackage, other.IncludePackage) { + return false + } + if !slices.Equal(t.ExcludePackage, other.ExcludePackage) { + return false + } + if t.EndpointIndependentNat != other.EndpointIndependentNat { + return false + } + if t.UDPTimeout != other.UDPTimeout { + return false + } + if t.FileDescriptor != other.FileDescriptor { + return false + } + + if !slices.Equal(t.Inet4RouteAddress, other.Inet4RouteAddress) { + return false + } + if !slices.Equal(t.Inet6RouteAddress, other.Inet6RouteAddress) { + return false + } + if !slices.Equal(t.Inet4RouteExcludeAddress, other.Inet4RouteExcludeAddress) { + return false + } + if !slices.Equal(t.Inet6RouteExcludeAddress, other.Inet6RouteExcludeAddress) { + return false + } + + return true } diff --git a/listener/http/client.go b/listener/http/client.go index dfd1985f..0f084fca 100644 --- a/listener/http/client.go +++ b/listener/http/client.go @@ -13,7 +13,7 @@ import ( "github.com/metacubex/mihomo/transport/socks5" ) -func newClient(srcConn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) *http.Client { +func newClient(srcConn net.Conn, tunnel C.Tunnel, additions []inbound.Addition) *http.Client { // additions using slice let caller can change its value (without size) after newClient return return &http.Client{ Transport: &http.Transport{ // from http.DefaultTransport @@ -21,6 +21,7 @@ func newClient(srcConn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, + DisableCompression: true, // prevents the Transport add "Accept-Encoding: gzip" DialContext: func(context context.Context, network, address string) (net.Conn, error) { if network != "tcp" && network != "tcp4" && network != "tcp6" { return nil, errors.New("unsupported network " + network) diff --git a/listener/http/patch_android.go b/listener/http/patch_android.go deleted file mode 100644 index b1b8bfc7..00000000 --- a/listener/http/patch_android.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build android && cmfa - -package http - -import "net" - -func (l *Listener) Listener() net.Listener { - return l.listener -} diff --git a/listener/http/proxy.go b/listener/http/proxy.go index 4822eabc..5c08cd45 100644 --- a/listener/http/proxy.go +++ b/listener/http/proxy.go @@ -8,24 +8,32 @@ import ( "net/http" "strings" "sync" - _ "unsafe" "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/lru" N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/auth" C "github.com/metacubex/mihomo/constant" - authStore "github.com/metacubex/mihomo/listener/auth" "github.com/metacubex/mihomo/log" ) -//go:linkname registerOnHitEOF net/http.registerOnHitEOF -func registerOnHitEOF(rc io.ReadCloser, fn func()) +type bodyWrapper struct { + io.ReadCloser + once sync.Once + onHitEOF func() +} -//go:linkname requestBodyRemains net/http.requestBodyRemains -func requestBodyRemains(rc io.ReadCloser) bool +func (b *bodyWrapper) Read(p []byte) (n int, err error) { + n, err = b.ReadCloser.Read(p) + if err == io.EOF && b.onHitEOF != nil { + b.once.Do(b.onHitEOF) + } + return n, err +} -func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], additions ...inbound.Addition) { - client := newClient(c, tunnel, additions...) +func HandleConn(c net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { + additions = append(additions, inbound.Placeholder) // Add a placeholder for InUser + inUserIdx := len(additions) - 1 + client := newClient(c, tunnel, additions) defer client.CloseIdleConnections() ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -33,8 +41,10 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], conn := N.NewBufferedConn(c) + authenticator := store.Authenticator() keepAlive := true - trusted := cache == nil // disable authenticate if lru is nil + trusted := authenticator == nil // disable authenticate if lru is nil + lastUser := "" for keepAlive { peekMutex.Lock() @@ -50,11 +60,10 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], var resp *http.Response - if !trusted { - resp = authenticate(request, cache) - - trusted = resp == nil - } + var user string + resp, user = authenticate(request, authenticator) // always call authenticate function to get user + trusted = trusted || resp == nil + additions[inUserIdx] = inbound.WithInUser(user) if trusted { if request.Method == http.MethodConnect { @@ -81,6 +90,13 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], return // hijack connection } + // ensure there is a client with correct additions + // when the authenticated user changed, outbound client should close idle connections + if user != lastUser { + client.CloseIdleConnections() + lastUser = user + } + removeHopByHopHeaders(request.Header) removeExtraHTTPHostPort(request) @@ -99,10 +115,10 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], } }() } - if requestBodyRemains(request.Body) { - registerOnHitEOF(request.Body, startBackgroundRead) - } else { + if request.Body == nil || request.Body == http.NoBody { startBackgroundRead() + } else { + request.Body = &bodyWrapper{ReadCloser: request.Body, onHitEOF: startBackgroundRead} } resp, err = client.Do(request) if err != nil { @@ -130,33 +146,21 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], _ = conn.Close() } -func authenticate(request *http.Request, cache *lru.LruCache[string, bool]) *http.Response { - authenticator := authStore.Authenticator() - if inbound.SkipAuthRemoteAddress(request.RemoteAddr) { - authenticator = nil +func authenticate(request *http.Request, authenticator auth.Authenticator) (resp *http.Response, user string) { + credential := parseBasicProxyAuthorization(request) + if credential == "" && authenticator != nil { + resp = responseWith(request, http.StatusProxyAuthRequired) + resp.Header.Set("Proxy-Authenticate", "Basic") + return } - if authenticator != nil { - credential := parseBasicProxyAuthorization(request) - if credential == "" { - resp := responseWith(request, http.StatusProxyAuthRequired) - resp.Header.Set("Proxy-Authenticate", "Basic") - return resp - } - - authed, exist := cache.Get(credential) - if !exist { - user, pass, err := decodeBasicProxyAuthorization(credential) - authed = err == nil && authenticator.Verify(user, pass) - cache.Set(credential, authed) - } - if !authed { - log.Infoln("Auth failed from %s", request.RemoteAddr) - - return responseWith(request, http.StatusForbidden) - } + user, pass, err := decodeBasicProxyAuthorization(credential) + authed := authenticator == nil || (err == nil && authenticator.Verify(user, pass)) + if !authed { + log.Infoln("Auth failed from %s", request.RemoteAddr) + return responseWith(request, http.StatusForbidden), user } - - return nil + log.Debugln("Auth success from %s -> %s", request.RemoteAddr, user) + return } func responseWith(request *http.Request, statusCode int) *http.Response { diff --git a/listener/http/server.go b/listener/http/server.go index 8fc9da59..24f07e8b 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -4,9 +4,9 @@ import ( "net" "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/lru" + "github.com/metacubex/mihomo/component/auth" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" + authStore "github.com/metacubex/mihomo/listener/auth" ) type Listener struct { @@ -32,10 +32,20 @@ func (l *Listener) Close() error { } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - return NewWithAuthenticate(addr, tunnel, true, additions...) + return NewWithAuthenticator(addr, tunnel, authStore.Default, additions...) } +// NewWithAuthenticate +// never change type traits because it's used in CMFA func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) { + store := authStore.Default + if !authenticate { + store = authStore.Default + } + return NewWithAuthenticator(addr, tunnel, store, additions...) +} + +func NewWithAuthenticator(addr string, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) (*Listener, error) { isDefault := false if len(additions) == 0 { isDefault = true @@ -44,17 +54,12 @@ func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additi inbound.WithSpecialRules(""), } } - l, err := inbound.Listen("tcp", addr) + l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } - var c *lru.LruCache[string, bool] - if authenticate { - c = lru.New[string, bool](lru.WithAge[string, bool](30)) - } - hl := &Listener{ listener: l, addr: addr, @@ -68,18 +73,18 @@ func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additi } continue } - if features.CMFA { - if t, ok := conn.(*net.TCPConn); ok { - t.SetKeepAlive(false) - } - } - if isDefault { // only apply on default listener + + store := store + if isDefault || store == authStore.Default { // only apply on default listener if !inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) { _ = conn.Close() continue } + if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) { + store = authStore.Nil + } } - go HandleConn(conn, tunnel, c, additions...) + go HandleConn(conn, tunnel, store, additions...) } }() diff --git a/listener/inbound/auth.go b/listener/inbound/auth.go new file mode 100644 index 00000000..85e72494 --- /dev/null +++ b/listener/inbound/auth.go @@ -0,0 +1,31 @@ +package inbound + +import ( + "github.com/metacubex/mihomo/component/auth" + authStore "github.com/metacubex/mihomo/listener/auth" +) + +type AuthUser struct { + Username string `inbound:"username"` + Password string `inbound:"password"` +} + +type AuthUsers []AuthUser + +func (a AuthUsers) GetAuthStore() auth.AuthStore { + if a != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array + if len(a) == 0 { + return authStore.Nil + } + users := make([]auth.AuthUser, len(a)) + for i, user := range a { + users[i] = auth.AuthUser{ + User: user.Username, + Pass: user.Password, + } + } + authenticator := auth.NewAuthenticator(users) + return authStore.NewAuthStore(authenticator) + } + return authStore.Default +} diff --git a/listener/inbound/http.go b/listener/inbound/http.go index f5301f46..e20a9a23 100644 --- a/listener/inbound/http.go +++ b/listener/inbound/http.go @@ -8,6 +8,7 @@ import ( type HTTPOption struct { BaseOption + Users AuthUsers `inbound:"users,omitempty"` } func (o HTTPOption) Equal(config C.InboundConfig) bool { @@ -44,7 +45,7 @@ func (h *HTTP) Address() string { // Listen implements constant.InboundListener func (h *HTTP) Listen(tunnel C.Tunnel) error { var err error - h.l, err = http.New(h.RawAddress(), tunnel, h.Additions()...) + h.l, err = http.NewWithAuthenticator(h.RawAddress(), tunnel, h.config.Users.GetAuthStore(), h.Additions()...) if err != nil { return err } diff --git a/listener/inbound/mixed.go b/listener/inbound/mixed.go index fc643821..1d79929a 100644 --- a/listener/inbound/mixed.go +++ b/listener/inbound/mixed.go @@ -12,7 +12,8 @@ import ( type MixedOption struct { BaseOption - UDP bool `inbound:"udp,omitempty"` + Users AuthUsers `inbound:"users,omitempty"` + UDP bool `inbound:"udp,omitempty"` } func (o MixedOption) Equal(config C.InboundConfig) bool { @@ -52,7 +53,7 @@ func (m *Mixed) Address() string { // Listen implements constant.InboundListener func (m *Mixed) Listen(tunnel C.Tunnel) error { var err error - m.l, err = mixed.New(m.RawAddress(), tunnel, m.Additions()...) + m.l, err = mixed.NewWithAuthenticator(m.RawAddress(), tunnel, m.config.Users.GetAuthStore(), m.Additions()...) if err != nil { return err } diff --git a/listener/inbound/socks.go b/listener/inbound/socks.go index 7e10d93a..119eec82 100644 --- a/listener/inbound/socks.go +++ b/listener/inbound/socks.go @@ -9,7 +9,8 @@ import ( type SocksOption struct { BaseOption - UDP bool `inbound:"udp,omitempty"` + Users AuthUsers `inbound:"users,omitempty"` + UDP bool `inbound:"udp,omitempty"` } func (o SocksOption) Equal(config C.InboundConfig) bool { @@ -70,7 +71,7 @@ func (s *Socks) Address() string { // Listen implements constant.InboundListener func (s *Socks) Listen(tunnel C.Tunnel) error { var err error - if s.stl, err = socks.New(s.RawAddress(), tunnel, s.Additions()...); err != nil { + if s.stl, err = socks.NewWithAuthenticator(s.RawAddress(), tunnel, s.config.Users.GetAuthStore(), s.Additions()...); err != nil { return err } if s.udp { diff --git a/listener/inbound/tun.go b/listener/inbound/tun.go index a1fdebfa..1b0a6a80 100644 --- a/listener/inbound/tun.go +++ b/listener/inbound/tun.go @@ -18,28 +18,38 @@ type TunOption struct { AutoRoute bool `inbound:"auto-route,omitempty"` AutoDetectInterface bool `inbound:"auto-detect-interface,omitempty"` - MTU uint32 `inbound:"mtu,omitempty"` - GSO bool `inbound:"gso,omitempty"` - GSOMaxSize uint32 `inbound:"gso-max-size,omitempty"` - Inet4Address []string `inbound:"inet4_address,omitempty"` - Inet6Address []string `inbound:"inet6_address,omitempty"` - 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"` - IncludeInterface []string `inbound:"include-interface,omitempty"` - ExcludeInterface []string `inbound:"exclude-interface" json:"exclude-interface,omitempty"` - IncludeUID []uint32 `inbound:"include_uid,omitempty"` - IncludeUIDRange []string `inbound:"include_uid_range,omitempty"` - ExcludeUID []uint32 `inbound:"exclude_uid,omitempty"` - ExcludeUIDRange []string `inbound:"exclude_uid_range,omitempty"` - 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"` + MTU uint32 `inbound:"mtu,omitempty"` + GSO bool `inbound:"gso,omitempty"` + GSOMaxSize uint32 `inbound:"gso-max-size,omitempty"` + Inet4Address []string `inbound:"inet4-address,omitempty"` + Inet6Address []string `inbound:"inet6-address,omitempty"` + IPRoute2TableIndex int `inbound:"iproute2-table-index,omitempty"` + IPRoute2RuleIndex int `inbound:"iproute2-rule-index,omitempty"` + AutoRedirect bool `inbound:"auto-redirect,omitempty"` + AutoRedirectInputMark uint32 `inbound:"auto-redirect-input-mark,omitempty"` + AutoRedirectOutputMark uint32 `inbound:"auto-redirect-output-mark,omitempty"` + StrictRoute bool `inbound:"strict-route,omitempty"` + RouteAddress []string `inbound:"route-address,omitempty"` + RouteAddressSet []string `inbound:"route-address-set,omitempty"` + RouteExcludeAddress []string `inbound:"route-exclude-address,omitempty"` + RouteExcludeAddressSet []string `inbound:"route-exclude-address-set,omitempty"` + IncludeInterface []string `inbound:"include-interface,omitempty"` + ExcludeInterface []string `inbound:"exclude-interface,omitempty"` + IncludeUID []uint32 `inbound:"include-uid,omitempty"` + IncludeUIDRange []string `inbound:"include-uid-range,omitempty"` + ExcludeUID []uint32 `inbound:"exclude-uid,omitempty"` + ExcludeUIDRange []string `inbound:"exclude-uid-range,omitempty"` + IncludeAndroidUser []int `inbound:"include-android-user,omitempty"` + IncludePackage []string `inbound:"include-package,omitempty"` + ExcludePackage []string `inbound:"exclude-package,omitempty"` + EndpointIndependentNat bool `inbound:"endpoint-independent-nat,omitempty"` + UDPTimeout int64 `inbound:"udp-timeout,omitempty"` + FileDescriptor int `inbound:"file-descriptor,omitempty"` + + Inet4RouteAddress []string `inbound:"inet4-route-address,omitempty"` + Inet6RouteAddress []string `inbound:"inet6-route-address,omitempty"` + Inet4RouteExcludeAddress []string `inbound:"inet4-route-exclude-address,omitempty"` + Inet6RouteExcludeAddress []string `inbound:"inet6-route-exclude-address,omitempty"` } func (o TunOption) Equal(config C.InboundConfig) bool { @@ -62,6 +72,16 @@ func NewTun(options *TunOption) (*Tun, error) { if !exist { return nil, errors.New("invalid tun stack") } + + routeAddress, err := LC.StringSliceToNetipPrefixSlice(options.RouteAddress) + if err != nil { + return nil, err + } + routeExcludeAddress, err := LC.StringSliceToNetipPrefixSlice(options.RouteExcludeAddress) + if err != nil { + return nil, err + } + inet4Address, err := LC.StringSliceToNetipPrefixSlice(options.Inet4Address) if err != nil { return nil, err @@ -90,34 +110,44 @@ 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, - GSO: options.GSO, - GSOMaxSize: options.GSOMaxSize, - Inet4Address: inet4Address, - Inet6Address: inet6Address, - StrictRoute: options.StrictRoute, + Enable: true, + Device: options.Device, + Stack: stack, + DNSHijack: options.DNSHijack, + AutoRoute: options.AutoRoute, + AutoDetectInterface: options.AutoDetectInterface, + MTU: options.MTU, + GSO: options.GSO, + GSOMaxSize: options.GSOMaxSize, + Inet4Address: inet4Address, + Inet6Address: inet6Address, + IPRoute2TableIndex: options.IPRoute2TableIndex, + IPRoute2RuleIndex: options.IPRoute2RuleIndex, + AutoRedirect: options.AutoRedirect, + AutoRedirectInputMark: options.AutoRedirectInputMark, + AutoRedirectOutputMark: options.AutoRedirectOutputMark, + StrictRoute: options.StrictRoute, + RouteAddress: routeAddress, + RouteAddressSet: options.RouteAddressSet, + RouteExcludeAddress: routeExcludeAddress, + RouteExcludeAddressSet: options.RouteExcludeAddressSet, + IncludeInterface: options.IncludeInterface, + ExcludeInterface: options.ExcludeInterface, + IncludeUID: options.IncludeUID, + IncludeUIDRange: options.IncludeUIDRange, + ExcludeUID: options.ExcludeUID, + ExcludeUIDRange: options.ExcludeUIDRange, + IncludeAndroidUser: options.IncludeAndroidUser, + IncludePackage: options.IncludePackage, + ExcludePackage: options.ExcludePackage, + EndpointIndependentNat: options.EndpointIndependentNat, + UDPTimeout: options.UDPTimeout, + FileDescriptor: options.FileDescriptor, + Inet4RouteAddress: inet4RouteAddress, Inet6RouteAddress: inet6RouteAddress, Inet4RouteExcludeAddress: inet4RouteExcludeAddress, Inet6RouteExcludeAddress: inet6RouteExcludeAddress, - IncludeInterface: options.IncludeInterface, - ExcludeInterface: options.ExcludeInterface, - 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 } diff --git a/listener/inner/tcp.go b/listener/inner/tcp.go index dac3d721..ee34ada6 100644 --- a/listener/inner/tcp.go +++ b/listener/inner/tcp.go @@ -16,7 +16,7 @@ func New(t C.Tunnel) { tunnel = t } -func HandleTcp(address string) (conn net.Conn, err error) { +func HandleTcp(address string, proxy string) (conn net.Conn, err error) { if tunnel == nil { return nil, errors.New("tcp uninitialized") } @@ -28,6 +28,9 @@ func HandleTcp(address string) (conn net.Conn, err error) { metadata.Type = C.INNER metadata.DNSMode = C.DNSNormal metadata.Process = C.MihomoName + if proxy != "" { + metadata.SpecialProxy = proxy + } if h, port, err := net.SplitHostPort(address); err == nil { if port, err := strconv.ParseUint(port, 10, 16); err == nil { metadata.DstPort = uint16(port) diff --git a/listener/listener.go b/listener/listener.go index ac602971..2e25c8b8 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -2,16 +2,12 @@ package listener import ( "fmt" - "golang.org/x/exp/slices" "net" - "sort" "strconv" "strings" "sync" - "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" @@ -49,24 +45,19 @@ var ( shadowSocksListener C.MultiAddrListener vmessListener *sing_vmess.Listener tuicListener *tuic.Listener - autoRedirListener *autoredir.Listener - autoRedirProgram *ebpf.TcEBpfProgram - tcProgram *ebpf.TcEBpfProgram // lock for recreate function - socksMux sync.Mutex - httpMux sync.Mutex - redirMux sync.Mutex - tproxyMux sync.Mutex - mixedMux sync.Mutex - tunnelMux sync.Mutex - inboundMux sync.Mutex - tunMux sync.Mutex - ssMux sync.Mutex - vmessMux sync.Mutex - tuicMux sync.Mutex - autoRedirMux sync.Mutex - tcMux sync.Mutex + socksMux sync.Mutex + httpMux sync.Mutex + redirMux sync.Mutex + tproxyMux sync.Mutex + mixedMux sync.Mutex + tunnelMux sync.Mutex + inboundMux sync.Mutex + tunMux sync.Mutex + ssMux sync.Mutex + vmessMux sync.Mutex + tuicMux sync.Mutex LastTunConf LC.Tun LastTuicConf LC.TuicServer @@ -504,6 +495,8 @@ func ReCreateMixed(port int, tunnel C.Tunnel) { } func ReCreateTun(tunConf LC.Tun, tunnel C.Tunnel) { + tunConf.Sort() + tunMux.Lock() defer func() { LastTunConf = tunConf @@ -518,7 +511,7 @@ func ReCreateTun(tunConf LC.Tun, tunnel C.Tunnel) { } }() - if !hasTunConfigChange(&tunConf) { + if tunConf.Equal(LastTunConf) { if tunLister != nil { tunLister.FlushDefaultInterface() } @@ -540,95 +533,6 @@ func ReCreateTun(tunConf LC.Tun, tunnel C.Tunnel) { log.Infoln("[TUN] Tun adapter listening at: %s", tunLister.Address()) } -func ReCreateRedirToTun(ifaceNames []string) { - tcMux.Lock() - defer tcMux.Unlock() - - nicArr := ifaceNames - slices.Sort(nicArr) - nicArr = slices.Compact(nicArr) - - if tcProgram != nil { - tcProgram.Close() - tcProgram = nil - } - - if len(nicArr) == 0 { - return - } - - tunConf := GetTunConf() - - if !tunConf.Enable { - return - } - - program, err := ebpf.NewTcEBpfProgram(nicArr, tunConf.Device) - if err != nil { - log.Errorln("Attached tc ebpf program error: %v", err) - return - } - tcProgram = program - - log.Infoln("Attached tc ebpf program to interfaces %v", tcProgram.RawNICs()) -} - -func ReCreateAutoRedir(ifaceNames []string, tunnel C.Tunnel) { - autoRedirMux.Lock() - defer autoRedirMux.Unlock() - - var err error - defer func() { - if err != nil { - if autoRedirListener != nil { - _ = autoRedirListener.Close() - autoRedirListener = nil - } - if autoRedirProgram != nil { - autoRedirProgram.Close() - autoRedirProgram = nil - } - log.Errorln("Start auto redirect server error: %s", err.Error()) - } - }() - - nicArr := ifaceNames - slices.Sort(nicArr) - nicArr = slices.Compact(nicArr) - - if autoRedirListener != nil && autoRedirProgram != nil { - _ = autoRedirListener.Close() - autoRedirProgram.Close() - autoRedirListener = nil - autoRedirProgram = nil - } - - if len(nicArr) == 0 { - return - } - - defaultRouteInterfaceName, err := ebpf.GetAutoDetectInterface() - if err != nil { - return - } - - addr := genAddr("*", C.TcpAutoRedirPort, true) - - autoRedirListener, err = autoredir.New(addr, tunnel) - if err != nil { - return - } - - autoRedirProgram, err = ebpf.NewRedirEBpfProgram(nicArr, autoRedirListener.TCPAddr().Port(), defaultRouteInterfaceName) - if err != nil { - return - } - - autoRedirListener.SetLookupFunc(autoRedirProgram.Lookup) - - log.Infoln("Auto redirect proxy listening at: %s, attached tc ebpf program to interfaces %v", autoRedirListener.Address(), autoRedirProgram.RawNICs()) -} - func PatchTunnel(tunnels []LC.Tunnel, tunnel C.Tunnel) { tunnelMux.Lock() defer tunnelMux.Unlock() @@ -811,112 +715,6 @@ func genAddr(host string, port int, allowLan bool) string { return fmt.Sprintf("127.0.0.1:%d", port) } -func hasTunConfigChange(tunConf *LC.Tun) bool { - if LastTunConf.Enable != tunConf.Enable || - LastTunConf.Device != tunConf.Device || - LastTunConf.Stack != tunConf.Stack || - LastTunConf.AutoRoute != tunConf.AutoRoute || - LastTunConf.AutoDetectInterface != tunConf.AutoDetectInterface || - LastTunConf.MTU != tunConf.MTU || - LastTunConf.GSO != tunConf.GSO || - LastTunConf.GSOMaxSize != tunConf.GSOMaxSize || - LastTunConf.StrictRoute != tunConf.StrictRoute || - LastTunConf.EndpointIndependentNat != tunConf.EndpointIndependentNat || - LastTunConf.UDPTimeout != tunConf.UDPTimeout || - LastTunConf.FileDescriptor != tunConf.FileDescriptor { - return true - } - - if len(LastTunConf.DNSHijack) != len(tunConf.DNSHijack) { - return true - } - - sort.Slice(tunConf.DNSHijack, func(i, j int) bool { - return tunConf.DNSHijack[i] < tunConf.DNSHijack[j] - }) - - sort.Slice(tunConf.Inet4Address, func(i, j int) bool { - return tunConf.Inet4Address[i].String() < tunConf.Inet4Address[j].String() - }) - - sort.Slice(tunConf.Inet6Address, func(i, j int) bool { - return tunConf.Inet6Address[i].String() < tunConf.Inet6Address[j].String() - }) - - sort.Slice(tunConf.Inet4RouteAddress, func(i, j int) bool { - return tunConf.Inet4RouteAddress[i].String() < tunConf.Inet4RouteAddress[j].String() - }) - - sort.Slice(tunConf.Inet6RouteAddress, func(i, j int) bool { - 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.IncludeInterface, func(i, j int) bool { - return tunConf.IncludeInterface[i] < tunConf.IncludeInterface[j] - }) - - sort.Slice(tunConf.ExcludeInterface, func(i, j int) bool { - return tunConf.ExcludeInterface[i] < tunConf.ExcludeInterface[j] - }) - - sort.Slice(tunConf.IncludeUID, func(i, j int) bool { - return tunConf.IncludeUID[i] < tunConf.IncludeUID[j] - }) - - sort.Slice(tunConf.IncludeUIDRange, func(i, j int) bool { - return tunConf.IncludeUIDRange[i] < tunConf.IncludeUIDRange[j] - }) - - sort.Slice(tunConf.ExcludeUID, func(i, j int) bool { - return tunConf.ExcludeUID[i] < tunConf.ExcludeUID[j] - }) - - sort.Slice(tunConf.ExcludeUIDRange, func(i, j int) bool { - return tunConf.ExcludeUIDRange[i] < tunConf.ExcludeUIDRange[j] - }) - - sort.Slice(tunConf.IncludeAndroidUser, func(i, j int) bool { - return tunConf.IncludeAndroidUser[i] < tunConf.IncludeAndroidUser[j] - }) - - sort.Slice(tunConf.IncludePackage, func(i, j int) bool { - return tunConf.IncludePackage[i] < tunConf.IncludePackage[j] - }) - - sort.Slice(tunConf.ExcludePackage, func(i, j int) bool { - return tunConf.ExcludePackage[i] < tunConf.ExcludePackage[j] - }) - - if !slices.Equal(tunConf.DNSHijack, LastTunConf.DNSHijack) || - !slices.Equal(tunConf.Inet4Address, LastTunConf.Inet4Address) || - !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.IncludeInterface, LastTunConf.IncludeInterface) || - !slices.Equal(tunConf.ExcludeInterface, LastTunConf.ExcludeInterface) || - !slices.Equal(tunConf.IncludeUID, LastTunConf.IncludeUID) || - !slices.Equal(tunConf.IncludeUIDRange, LastTunConf.IncludeUIDRange) || - !slices.Equal(tunConf.ExcludeUID, LastTunConf.ExcludeUID) || - !slices.Equal(tunConf.ExcludeUIDRange, LastTunConf.ExcludeUIDRange) || - !slices.Equal(tunConf.IncludeAndroidUser, LastTunConf.IncludeAndroidUser) || - !slices.Equal(tunConf.IncludePackage, LastTunConf.IncludePackage) || - !slices.Equal(tunConf.ExcludePackage, LastTunConf.ExcludePackage) { - return true - } - - return false -} - func closeTunListener() { if tunLister != nil { tunLister.Close() diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go index 367b7a36..5ac63011 100644 --- a/listener/mixed/mixed.go +++ b/listener/mixed/mixed.go @@ -4,9 +4,10 @@ import ( "net" "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/lru" N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/auth" C "github.com/metacubex/mihomo/constant" + authStore "github.com/metacubex/mihomo/listener/auth" "github.com/metacubex/mihomo/listener/http" "github.com/metacubex/mihomo/listener/socks" "github.com/metacubex/mihomo/transport/socks4" @@ -16,7 +17,6 @@ import ( type Listener struct { listener net.Listener addr string - cache *lru.LruCache[string, bool] closed bool } @@ -37,6 +37,10 @@ func (l *Listener) Close() error { } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { + return NewWithAuthenticator(addr, tunnel, authStore.Default, additions...) +} + +func NewWithAuthenticator(addr string, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) (*Listener, error) { isDefault := false if len(additions) == 0 { isDefault = true @@ -45,6 +49,7 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener inbound.WithSpecialRules(""), } } + l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err @@ -53,7 +58,6 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener ml := &Listener{ listener: l, addr: addr, - cache: lru.New[string, bool](lru.WithAge[string, bool](30)), } go func() { for { @@ -64,22 +68,24 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener } continue } - if isDefault { // only apply on default listener + store := store + if isDefault || store == authStore.Default { // only apply on default listener if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { _ = c.Close() continue } + if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) { + store = authStore.Nil + } } - go handleConn(c, tunnel, ml.cache, additions...) + go handleConn(c, tunnel, store, additions...) } }() return ml, nil } -func handleConn(conn net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], additions ...inbound.Addition) { - N.TCPKeepAlive(conn) - +func handleConn(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { bufConn := N.NewBufferedConn(conn) head, err := bufConn.Peek(1) if err != nil { @@ -88,10 +94,10 @@ func handleConn(conn net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool switch head[0] { case socks4.Version: - socks.HandleSocks4(bufConn, tunnel, additions...) + socks.HandleSocks4(bufConn, tunnel, store, additions...) case socks5.Version: - socks.HandleSocks5(bufConn, tunnel, additions...) + socks.HandleSocks5(bufConn, tunnel, store, additions...) default: - http.HandleConn(bufConn, tunnel, cache, additions...) + http.HandleConn(bufConn, tunnel, store, additions...) } } diff --git a/listener/redir/tcp.go b/listener/redir/tcp.go index 8474a8e2..47363182 100644 --- a/listener/redir/tcp.go +++ b/listener/redir/tcp.go @@ -4,7 +4,7 @@ import ( "net" "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/keepalive" C "github.com/metacubex/mihomo/constant" ) @@ -37,10 +37,12 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener inbound.WithSpecialRules(""), } } + l, err := net.Listen("tcp", addr) if err != nil { return nil, err } + rl := &Listener{ listener: l, addr: addr, @@ -68,6 +70,6 @@ func handleRedir(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) conn.Close() return } - N.TCPKeepAlive(conn) + keepalive.TCPKeepAlive(conn) tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.REDIR, additions...)) } diff --git a/listener/shadowsocks/tcp.go b/listener/shadowsocks/tcp.go index c08667de..b150e4cb 100644 --- a/listener/shadowsocks/tcp.go +++ b/listener/shadowsocks/tcp.go @@ -22,7 +22,7 @@ type Listener struct { var _listener *Listener -func New(config LC.ShadowsocksServer, tunnel C.Tunnel) (*Listener, error) { +func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { pickCipher, err := core.PickCipher(config.Cipher, nil, config.Password) if err != nil { return nil, err @@ -36,7 +36,7 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel) (*Listener, error) { if config.Udp { //UDP - ul, err := NewUDP(addr, pickCipher, tunnel) + ul, err := NewUDP(addr, pickCipher, tunnel, additions...) if err != nil { return nil, err } @@ -59,8 +59,7 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel) (*Listener, error) { } continue } - N.TCPKeepAlive(c) - go sl.HandleConn(c, tunnel) + go sl.HandleConn(c, tunnel, additions...) } }() } diff --git a/listener/shadowsocks/udp.go b/listener/shadowsocks/udp.go index 4336db22..77932ed1 100644 --- a/listener/shadowsocks/udp.go +++ b/listener/shadowsocks/udp.go @@ -17,7 +17,7 @@ type UDPListener struct { closed bool } -func NewUDP(addr string, pickCipher core.Cipher, tunnel C.Tunnel) (*UDPListener, error) { +func NewUDP(addr string, pickCipher core.Cipher, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { l, err := net.ListenPacket("udp", addr) if err != nil { return nil, err @@ -42,7 +42,7 @@ func NewUDP(addr string, pickCipher core.Cipher, tunnel C.Tunnel) (*UDPListener, } continue } - handleSocksUDP(conn, tunnel, data, put, remoteAddr) + handleSocksUDP(conn, tunnel, data, put, remoteAddr, additions...) } }() diff --git a/listener/sing/sing.go b/listener/sing/sing.go index 4e31faeb..0d4bb926 100644 --- a/listener/sing/sing.go +++ b/listener/sing/sing.go @@ -129,6 +129,12 @@ func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, meta NetWork: C.TCP, Type: h.Type, } + if metadata.Source.IsIP() && metadata.Source.Fqdn == "" { + cMetadata.RawSrcAddr = metadata.Source.Unwrap().TCPAddr() + } + if metadata.Destination.IsIP() && metadata.Destination.Fqdn == "" { + cMetadata.RawDstAddr = metadata.Destination.Unwrap().TCPAddr() + } inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(metadata.Destination), inbound.WithSrcAddr(metadata.Source), inbound.WithInAddr(conn.LocalAddr())) inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...) inbound.ApplyAdditions(cMetadata, h.Additions...) @@ -185,6 +191,12 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. NetWork: C.UDP, Type: h.Type, } + if metadata.Source.IsIP() && metadata.Source.Fqdn == "" { + cMetadata.RawSrcAddr = metadata.Source.Unwrap().UDPAddr() + } + if dest.IsIP() && dest.Fqdn == "" { + cMetadata.RawDstAddr = dest.Unwrap().UDPAddr() + } inbound.ApplyAdditions(cMetadata, inbound.WithDstAddr(dest), inbound.WithSrcAddr(metadata.Source), inbound.WithInAddr(conn.LocalAddr())) inbound.ApplyAdditions(cMetadata, getAdditions(ctx)...) inbound.ApplyAdditions(cMetadata, h.Additions...) @@ -198,6 +210,12 @@ func (h *ListenerHandler) NewError(ctx context.Context, err error) { log.Warnln("%s listener get error: %+v", h.Type.String(), err) } +func (h *ListenerHandler) TypeMutation(typ C.Type) *ListenerHandler { + handler := *h + handler.Type = typ + return &handler +} + func ShouldIgnorePacketError(err error) bool { // ignore simple error if E.IsTimeout(err) || E.IsClosed(err) || E.IsCanceled(err) { diff --git a/listener/sing_shadowsocks/server.go b/listener/sing_shadowsocks/server.go index bd5002a4..5f2a4292 100644 --- a/listener/sing_shadowsocks/server.go +++ b/listener/sing_shadowsocks/server.go @@ -7,7 +7,6 @@ import ( "strings" "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" @@ -72,7 +71,7 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi sl.service, err = shadowaead_2022.NewServiceWithPassword(config.Cipher, config.Password, udpTimeout, h, ntp.Now) default: err = fmt.Errorf("shadowsocks: unsupported method: %s", config.Cipher) - return embedSS.New(config, tunnel) + return embedSS.New(config, tunnel, additions...) } if err != nil { return nil, err @@ -153,7 +152,6 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi } continue } - N.TCPKeepAlive(c) go sl.HandleConn(c, tunnel) } diff --git a/listener/sing_tun/dns.go b/listener/sing_tun/dns.go index 42926732..505f16ac 100644 --- a/listener/sing_tun/dns.go +++ b/listener/sing_tun/dns.go @@ -8,6 +8,7 @@ import ( "time" "github.com/metacubex/mihomo/component/resolver" + C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" @@ -124,3 +125,9 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. } return h.ListenerHandler.NewPacketConnection(ctx, conn, metadata) } + +func (h *ListenerHandler) TypeMutation(typ C.Type) *ListenerHandler { + handle := *h + handle.ListenerHandler = h.ListenerHandler.TypeMutation(typ) + return &handle +} diff --git a/listener/sing_tun/iface.go b/listener/sing_tun/iface.go new file mode 100644 index 00000000..543d206c --- /dev/null +++ b/listener/sing_tun/iface.go @@ -0,0 +1,75 @@ +package sing_tun + +import ( + "errors" + "net/netip" + + "github.com/metacubex/mihomo/component/iface" + + "github.com/sagernet/sing/common/control" +) + +type defaultInterfaceFinder struct{} + +var DefaultInterfaceFinder control.InterfaceFinder = (*defaultInterfaceFinder)(nil) + +func (f *defaultInterfaceFinder) Update() error { + iface.FlushCache() + return nil +} + +func (f *defaultInterfaceFinder) Interfaces() []control.Interface { + ifaces, err := iface.Interfaces() + if err != nil { + return nil + } + interfaces := make([]control.Interface, 0, len(ifaces)) + for _, _interface := range ifaces { + interfaces = append(interfaces, control.Interface(*_interface)) + } + + return interfaces +} + +var errNoSuchInterface = errors.New("no such network interface") + +func (f *defaultInterfaceFinder) InterfaceIndexByName(name string) (int, error) { + ifaces, err := iface.Interfaces() + if err != nil { + return 0, err + } + for _, netInterface := range ifaces { + if netInterface.Name == name { + return netInterface.Index, nil + } + } + return 0, errNoSuchInterface +} + +func (f *defaultInterfaceFinder) InterfaceNameByIndex(index int) (string, error) { + ifaces, err := iface.Interfaces() + if err != nil { + return "", err + } + for _, netInterface := range ifaces { + if netInterface.Index == index { + return netInterface.Name, nil + } + } + return "", errNoSuchInterface +} + +func (f *defaultInterfaceFinder) InterfaceByAddr(addr netip.Addr) (*control.Interface, error) { + ifaces, err := iface.Interfaces() + if err != nil { + return nil, err + } + for _, netInterface := range ifaces { + for _, prefix := range netInterface.Addresses { + if prefix.Contains(addr) { + return (*control.Interface)(netInterface), nil + } + } + } + return nil, errNoSuchInterface +} diff --git a/listener/sing_tun/redirect_linux.go b/listener/sing_tun/redirect_linux.go new file mode 100644 index 00000000..6ef6fc96 --- /dev/null +++ b/listener/sing_tun/redirect_linux.go @@ -0,0 +1,3 @@ +package sing_tun + +const supportRedirect = true diff --git a/listener/sing_tun/redirect_stub.go b/listener/sing_tun/redirect_stub.go new file mode 100644 index 00000000..d711af3c --- /dev/null +++ b/listener/sing_tun/redirect_stub.go @@ -0,0 +1,5 @@ +//go:build !linux + +package sing_tun + +const supportRedirect = false diff --git a/listener/sing_tun/server.go b/listener/sing_tun/server.go index cc26d37d..ba337b01 100644 --- a/listener/sing_tun/server.go +++ b/listener/sing_tun/server.go @@ -3,16 +3,21 @@ package sing_tun import ( "context" "fmt" + "io" "net" "net/netip" + "os" "runtime" "strconv" "strings" + "sync" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/iface" + "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/provider" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" @@ -22,9 +27,14 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/ranges" + + "go4.org/netipx" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" ) var InterfaceName = "Meta" +var EnforceBindInterface = false type Listener struct { closed bool @@ -39,8 +49,21 @@ type Listener struct { networkUpdateMonitor tun.NetworkUpdateMonitor defaultInterfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager + autoRedirect tun.AutoRedirect + autoRedirectOutputMark int32 + + ruleUpdateCallbackCloser io.Closer + ruleUpdateMutex sync.Mutex + routeAddressMap map[string]*netipx.IPSet + routeExcludeAddressMap map[string]*netipx.IPSet + routeAddressSet []*netipx.IPSet + routeExcludeAddressSet []*netipx.IPSet + + dnsServerIp []string } +var emptyAddressSet = []*netipx.IPSet{{}} + func CalculateInterfaceName(name string) (tunName string) { if runtime.GOOS == "darwin" { tunName = "utun" @@ -54,15 +77,25 @@ func CalculateInterfaceName(name string) (tunName string) { if err != nil { return } - var tunIndex int + tunIndex := 0 + indexArr := make([]int, 0, len(interfaces)) for _, netInterface := range interfaces { if strings.HasPrefix(netInterface.Name, tunName) { index, parseErr := strconv.ParseInt(netInterface.Name[len(tunName):], 10, 16) if parseErr == nil { - tunIndex = int(index) + 1 + indexArr = append(indexArr, int(index)) } } } + slices.Sort(indexArr) + indexArr = slices.Compact(indexArr) + for _, index := range indexArr { + if index == tunIndex { + tunIndex += 1 + } else { // indexArr already sorted and distinct, so this tunIndex nobody used + break + } + } tunName = F.ToString(tunName, tunIndex) return } @@ -94,14 +127,45 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis inbound.WithSpecialRules(""), } } + ctx := context.TODO() + rpTunnel := tunnel.(provider.Tunnel) if options.GSOMaxSize == 0 { options.GSOMaxSize = 65536 } + if !supportRedirect { + options.AutoRedirect = false + } tunName := options.Device - if tunName == "" || !checkTunName(tunName) { + if options.FileDescriptor == 0 && (tunName == "" || !checkTunName(tunName)) { tunName = CalculateInterfaceName(InterfaceName) options.Device = tunName } + routeAddress := options.RouteAddress + if len(options.Inet4RouteAddress) > 0 { + routeAddress = append(routeAddress, options.Inet4RouteAddress...) + } + if len(options.Inet6RouteAddress) > 0 { + routeAddress = append(routeAddress, options.Inet6RouteAddress...) + } + inet4RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) + routeExcludeAddress := options.RouteExcludeAddress + if len(options.Inet4RouteExcludeAddress) > 0 { + routeExcludeAddress = append(routeExcludeAddress, options.Inet4RouteExcludeAddress...) + } + if len(options.Inet6RouteExcludeAddress) > 0 { + routeExcludeAddress = append(routeExcludeAddress, options.Inet6RouteExcludeAddress...) + } + inet4RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) + inet6RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { + return it.Addr().Is6() + }) tunMTU := options.MTU if tunMTU == 0 { tunMTU = 9000 @@ -112,6 +176,22 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis } else { udpTimeout = int64(sing.UDPTimeout.Seconds()) } + tableIndex := options.IPRoute2TableIndex + if tableIndex == 0 { + tableIndex = tun.DefaultIPRoute2TableIndex + } + ruleIndex := options.IPRoute2RuleIndex + if ruleIndex == 0 { + ruleIndex = tun.DefaultIPRoute2RuleIndex + } + inputMark := options.AutoRedirectInputMark + if inputMark == 0 { + inputMark = tun.DefaultAutoRedirectInputMark + } + outputMark := options.AutoRedirectOutputMark + if outputMark == 0 { + outputMark = tun.DefaultAutoRedirectOutputMark + } includeUID := uidToRange(options.IncludeUID) if len(options.IncludeUIDRange) > 0 { var err error @@ -143,12 +223,16 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis dnsAdds = append(dnsAdds, addrPort) } + + var dnsServerIp []string for _, a := range options.Inet4Address { addrPort := netip.AddrPortFrom(a.Addr().Next(), 53) + dnsServerIp = append(dnsServerIp, a.Addr().Next().String()) dnsAdds = append(dnsAdds, addrPort) } for _, a := range options.Inet6Address { addrPort := netip.AddrPortFrom(a.Addr().Next(), 53) + dnsServerIp = append(dnsServerIp, a.Addr().Next().String()) dnsAdds = append(dnsAdds, addrPort) } @@ -169,6 +253,7 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis closed: false, options: options, handler: handler, + tunName: tunName, } defer func() { if err != nil { @@ -177,31 +262,41 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis } }() - networkUpdateMonitor, err := tun.NewNetworkUpdateMonitor(log.SingLogger) - if err != nil { - err = E.Cause(err, "create NetworkUpdateMonitor") - return - } - l.networkUpdateMonitor = networkUpdateMonitor - err = networkUpdateMonitor.Start() - if err != nil { - err = E.Cause(err, "start NetworkUpdateMonitor") - return - } + interfaceFinder := DefaultInterfaceFinder - 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) { - l.FlushDefaultInterface() - }) - err = defaultInterfaceMonitor.Start() - if err != nil { - err = E.Cause(err, "start DefaultInterfaceMonitor") - return + var networkUpdateMonitor tun.NetworkUpdateMonitor + var defaultInterfaceMonitor tun.DefaultInterfaceMonitor + if options.AutoRoute || options.AutoDetectInterface { // don't start NetworkUpdateMonitor because netlink banned by google on Android14+ + networkUpdateMonitor, err = tun.NewNetworkUpdateMonitor(log.SingLogger) + if err != nil { + err = E.Cause(err, "create NetworkUpdateMonitor") + return + } + l.networkUpdateMonitor = networkUpdateMonitor + err = networkUpdateMonitor.Start() + if err != nil { + err = E.Cause(err, "start NetworkUpdateMonitor") + return + } + + overrideAndroidVPN := true + if disable, _ := strconv.ParseBool(os.Getenv("DISABLE_OVERRIDE_ANDROID_VPN")); disable { + overrideAndroidVPN = false + } + defaultInterfaceMonitor, err = tun.NewDefaultInterfaceMonitor(networkUpdateMonitor, log.SingLogger, tun.DefaultInterfaceMonitorOptions{InterfaceFinder: interfaceFinder, OverrideAndroidVPN: overrideAndroidVPN}) + if err != nil { + err = E.Cause(err, "create DefaultInterfaceMonitor") + return + } + l.defaultInterfaceMonitor = defaultInterfaceMonitor + defaultInterfaceMonitor.RegisterCallback(func(event int) { + l.FlushDefaultInterface() + }) + err = defaultInterfaceMonitor.Start() + if err != nil { + err = E.Cause(err, "start DefaultInterfaceMonitor") + return + } } tunOptions := tun.Options{ @@ -211,11 +306,15 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis Inet4Address: options.Inet4Address, Inet6Address: options.Inet6Address, AutoRoute: options.AutoRoute, + IPRoute2TableIndex: tableIndex, + IPRoute2RuleIndex: ruleIndex, + AutoRedirectInputMark: inputMark, + AutoRedirectOutputMark: outputMark, StrictRoute: options.StrictRoute, - Inet4RouteAddress: options.Inet4RouteAddress, - Inet6RouteAddress: options.Inet6RouteAddress, - Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress, - Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress, + Inet4RouteAddress: inet4RouteAddress, + Inet6RouteAddress: inet6RouteAddress, + Inet4RouteExcludeAddress: inet4RouteExcludeAddress, + Inet6RouteExcludeAddress: inet6RouteExcludeAddress, IncludeInterface: options.IncludeInterface, ExcludeInterface: options.ExcludeInterface, IncludeUID: includeUID, @@ -225,7 +324,56 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis ExcludePackage: options.ExcludePackage, FileDescriptor: options.FileDescriptor, InterfaceMonitor: defaultInterfaceMonitor, - TableIndex: 2022, + } + + if options.AutoRedirect { + l.routeAddressMap = make(map[string]*netipx.IPSet) + l.routeExcludeAddressMap = make(map[string]*netipx.IPSet) + + if !options.AutoRoute { + return nil, E.New("`auto-route` is required by `auto-redirect`") + } + disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) + l.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ + TunOptions: &tunOptions, + Context: ctx, + Handler: handler.TypeMutation(C.REDIR), + Logger: log.SingLogger, + NetworkMonitor: l.networkUpdateMonitor, + InterfaceFinder: interfaceFinder, + TableName: "mihomo", + DisableNFTables: dErr == nil && disableNFTables, + RouteAddressSet: &l.routeAddressSet, + RouteExcludeAddressSet: &l.routeExcludeAddressSet, + }) + if err != nil { + err = E.Cause(err, "initialize auto redirect") + return + } + + var markMode bool + for _, routeAddressSet := range options.RouteAddressSet { + rp, loaded := rpTunnel.RuleProviders()[routeAddressSet] + if !loaded { + err = E.New("parse route-address-set: rule-set not found: ", routeAddressSet) + return + } + l.updateRule(rp, false, false) + markMode = true + } + for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { + rp, loaded := rpTunnel.RuleProviders()[routeExcludeAddressSet] + if !loaded { + err = E.New("parse route-exclude_address-set: rule-set not found: ", routeExcludeAddressSet) + return + } + l.updateRule(rp, true, false) + markMode = true + } + if markMode { + tunOptions.AutoRedirectMarkMode = true + } + } err = l.buildAndroidRules(&tunOptions) @@ -239,14 +387,20 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis return } + l.dnsServerIp = dnsServerIp + // after tun.New sing-tun has set DNS to TUN interface + resolver.AddSystemDnsBlacklist(dnsServerIp...) + stackOptions := tun.StackOptions{ - Context: context.TODO(), + Context: ctx, Tun: tunIf, TunOptions: tunOptions, EndpointIndependentNat: options.EndpointIndependentNat, UDPTimeout: udpTimeout, Handler: handler, Logger: log.SingLogger, + InterfaceFinder: interfaceFinder, + EnforceBindInterface: EnforceBindInterface, } if options.FileDescriptor > 0 { @@ -256,26 +410,101 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis } } l.tunIf = tunIf - l.tunStack, err = tun.NewStack(strings.ToLower(options.Stack.String()), stackOptions) + + tunStack, err := tun.NewStack(strings.ToLower(options.Stack.String()), stackOptions) if err != nil { return } - err = l.tunStack.Start() + err = tunStack.Start() if err != nil { return } + l.tunStack = tunStack + + if l.autoRedirect != nil { + if len(l.options.RouteAddressSet) > 0 && len(l.routeAddressSet) == 0 { + l.routeAddressSet = emptyAddressSet // without this we can't call UpdateRouteAddressSet after Start + } + if len(l.options.RouteExcludeAddressSet) > 0 && len(l.routeExcludeAddressSet) == 0 { + l.routeExcludeAddressSet = emptyAddressSet // without this we can't call UpdateRouteAddressSet after Start + } + err = l.autoRedirect.Start() + if err != nil { + err = E.Cause(err, "auto redirect") + return + } + if tunOptions.AutoRedirectMarkMode { + l.autoRedirectOutputMark = int32(outputMark) + dialer.DefaultRoutingMark.Store(l.autoRedirectOutputMark) + l.autoRedirect.UpdateRouteAddressSet() + l.ruleUpdateCallbackCloser = rpTunnel.RuleUpdateCallback().Register(l.ruleUpdateCallback) + } + } //l.openAndroidHotspot(tunOptions) - l.addrStr = fmt.Sprintf("%s(%s,%s), mtu: %d, auto route: %v, ip stack: %s", - tunName, tunOptions.Inet4Address, tunOptions.Inet6Address, tunMTU, options.AutoRoute, options.Stack) + if !l.options.AutoDetectInterface { + resolver.ResetConnection() + } + + if options.FileDescriptor != 0 { + tunName = fmt.Sprintf("%s(fd=%d)", tunName, options.FileDescriptor) + } + l.addrStr = fmt.Sprintf("%s(%s,%s), mtu: %d, auto route: %v, auto redir: %v, ip stack: %s", + tunName, tunOptions.Inet4Address, tunOptions.Inet6Address, tunMTU, options.AutoRoute, options.AutoRedirect, options.Stack) return } +func (l *Listener) ruleUpdateCallback(ruleProvider provider.RuleProvider) { + name := ruleProvider.Name() + if slices.Contains(l.options.RouteAddressSet, name) { + l.updateRule(ruleProvider, false, true) + return + } + if slices.Contains(l.options.RouteExcludeAddressSet, name) { + l.updateRule(ruleProvider, true, true) + return + } +} + +type toIpCidr interface { + ToIpCidr() *netipx.IPSet +} + +func (l *Listener) updateRule(ruleProvider provider.RuleProvider, exclude bool, update bool) { + l.ruleUpdateMutex.Lock() + defer l.ruleUpdateMutex.Unlock() + name := ruleProvider.Name() + switch rp := ruleProvider.Strategy().(type) { + case toIpCidr: + if !exclude { + ipCidr := rp.ToIpCidr() + if ipCidr != nil { + l.routeAddressMap[name] = ipCidr + } else { + delete(l.routeAddressMap, name) + } + l.routeAddressSet = maps.Values(l.routeAddressMap) + } else { + ipCidr := rp.ToIpCidr() + if ipCidr != nil { + l.routeExcludeAddressMap[name] = ipCidr + } else { + delete(l.routeExcludeAddressMap, name) + } + l.routeExcludeAddressSet = maps.Values(l.routeExcludeAddressMap) + } + default: + return + } + if update && l.autoRedirect != nil { + l.autoRedirect.UpdateRouteAddressSet() + } +} + func (l *Listener) FlushDefaultInterface() { - if l.options.AutoDetectInterface { - targetInterface := dialer.DefaultInterface.Load() + if l.options.AutoDetectInterface && l.defaultInterfaceMonitor != nil { for _, destination := range []netip.Addr{netip.IPv4Unspecified(), netip.IPv6Unspecified(), netip.MustParseAddr("1.1.1.1")} { autoDetectInterfaceName := l.defaultInterfaceMonitor.DefaultInterfaceName(destination) if autoDetectInterfaceName == l.tunName { @@ -283,17 +512,17 @@ func (l *Listener) FlushDefaultInterface() { } else if autoDetectInterfaceName == "" || autoDetectInterfaceName == "" { log.Warnln("[TUN] Auto detect interface by %s get empty name.", destination.String()) } else { - targetInterface = autoDetectInterfaceName - if old := dialer.DefaultInterface.Load(); old != targetInterface { - log.Warnln("[TUN] default interface changed by monitor, %s => %s", old, targetInterface) - - dialer.DefaultInterface.Store(targetInterface) - + if old := dialer.DefaultInterface.Swap(autoDetectInterfaceName); old != autoDetectInterfaceName { + log.Warnln("[TUN] default interface changed by monitor, %s => %s", old, autoDetectInterfaceName) iface.FlushCache() + resolver.ResetConnection() // reset resolver's connection after default interface changed } return } } + if dialer.DefaultInterface.CompareAndSwap("", "") { + log.Warnln("[TUN] Auto detect interface failed, set '' to DefaultInterface to avoid lookback") + } } } @@ -316,11 +545,11 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges. } var start, end uint64 var err error - start, err = strconv.ParseUint(uidRange[:subIndex], 10, 32) + start, err = strconv.ParseUint(uidRange[:subIndex], 0, 32) if err != nil { return nil, E.Cause(err, "parse range start") } - end, err = strconv.ParseUint(uidRange[subIndex+1:], 10, 32) + end, err = strconv.ParseUint(uidRange[subIndex+1:], 0, 32) if err != nil { return nil, E.Cause(err, "parse range end") } @@ -331,9 +560,15 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges. func (l *Listener) Close() error { l.closed = true + resolver.RemoveSystemDnsBlacklist(l.dnsServerIp...) + if l.autoRedirectOutputMark != 0 { + dialer.DefaultRoutingMark.CompareAndSwap(l.autoRedirectOutputMark, 0) + } return common.Close( + l.ruleUpdateCallbackCloser, l.tunStack, l.tunIf, + l.autoRedirect, l.defaultInterfaceMonitor, l.networkUpdateMonitor, l.packageManager, diff --git a/listener/sing_tun/server_android.go b/listener/sing_tun/server_android.go index ac41282d..d8240534 100644 --- a/listener/sing_tun/server_android.go +++ b/listener/sing_tun/server_android.go @@ -1,29 +1,82 @@ +//go:build android && !cmfa + package sing_tun import ( + "errors" + "runtime" + "sync" + + "github.com/metacubex/mihomo/component/process" + "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/log" - tun "github.com/metacubex/sing-tun" + + "github.com/metacubex/sing-tun" "github.com/sagernet/netlink" "golang.org/x/sys/unix" - "runtime" ) -func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { - packageManager, err := tun.NewPackageManager(l.handler) +type packageManagerCallback struct{} + +func (cb *packageManagerCallback) OnPackagesUpdated(packageCount int, sharedCount int) {} + +func newPackageManager() (tun.PackageManager, error) { + packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ + Callback: &packageManagerCallback{}, + Logger: log.SingLogger, + }) if err != nil { - return err + return nil, err } err = packageManager.Start() + if err != nil { + return nil, err + } + return packageManager, nil +} + +var ( + globalPM tun.PackageManager + pmOnce sync.Once + pmErr error +) + +func getPackageManager() (tun.PackageManager, error) { + pmOnce.Do(func() { + globalPM, pmErr = newPackageManager() + }) + return globalPM, pmErr +} + +func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { + packageManager, err := getPackageManager() if err != nil { return err } - l.packageManager = packageManager tunOptions.BuildAndroidRules(packageManager, l.handler) return nil } -func (h *ListenerHandler) OnPackagesUpdated(packages int, sharedUsers int) { - return +func findPackageName(metadata *constant.Metadata) (string, error) { + packageManager, err := getPackageManager() + if err != nil { + return "", err + } + uid := metadata.Uid + if sharedPackage, loaded := packageManager.SharedPackageByID(uid % 100000); loaded { + return sharedPackage, nil + } + if packageName, loaded := packageManager.PackageByID(uid % 100000); loaded { + return packageName, nil + } + return "", errors.New("package not found") +} + +func init() { + if !features.CMFA { + process.DefaultPackageNameResolver = findPackageName + } } func (l *Listener) openAndroidHotspot(tunOptions tun.Options) { diff --git a/listener/sing_tun/server_notandroid.go b/listener/sing_tun/server_notandroid.go index 6b30ee03..10fd3997 100644 --- a/listener/sing_tun/server_notandroid.go +++ b/listener/sing_tun/server_notandroid.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build !android || cmfa package sing_tun diff --git a/listener/sing_tun/server_windows.go b/listener/sing_tun/server_windows.go index 8da21287..325cf096 100644 --- a/listener/sing_tun/server_windows.go +++ b/listener/sing_tun/server_windows.go @@ -3,6 +3,7 @@ package sing_tun import ( "time" + "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/log" tun "github.com/metacubex/sing-tun" @@ -27,4 +28,9 @@ func tunNew(options tun.Options) (tunIf tun.Tun, err error) { func init() { tun.TunnelType = InterfaceName + + if features.WindowsMajorVersion < 10 { + // to resolve "bind: The requested address is not valid in its context" + EnforceBindInterface = true + } } diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index ce422b16..7a0afa0b 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -121,7 +121,6 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } continue } - N.TCPKeepAlive(c) go sl.HandleConn(c, tunnel) } diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go index b6ea023a..cc66613e 100644 --- a/listener/socks/tcp.go +++ b/listener/socks/tcp.go @@ -6,6 +6,7 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/auth" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" "github.com/metacubex/mihomo/transport/socks4" @@ -35,6 +36,10 @@ func (l *Listener) Close() error { } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { + return NewWithAuthenticator(addr, tunnel, authStore.Default, additions...) +} + +func NewWithAuthenticator(addr string, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) (*Listener, error) { isDefault := false if len(additions) == 0 { isDefault = true @@ -43,6 +48,7 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener inbound.WithSpecialRules(""), } } + l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err @@ -61,21 +67,24 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener } continue } - if isDefault { // only apply on default listener + store := store + if isDefault || store == authStore.Default { // only apply on default listener if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { _ = c.Close() continue } + if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) { + store = authStore.Nil + } } - go handleSocks(c, tunnel, additions...) + go handleSocks(c, tunnel, store, additions...) } }() return sl, nil } -func handleSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - N.TCPKeepAlive(conn) +func handleSocks(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { bufConn := N.NewBufferedConn(conn) head, err := bufConn.Peek(1) if err != nil { @@ -85,33 +94,28 @@ func handleSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) switch head[0] { case socks4.Version: - HandleSocks4(bufConn, tunnel, additions...) + HandleSocks4(bufConn, tunnel, store, additions...) case socks5.Version: - HandleSocks5(bufConn, tunnel, additions...) + HandleSocks5(bufConn, tunnel, store, additions...) default: conn.Close() } } -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) +func HandleSocks4(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { + authenticator := store.Authenticator() + addr, _, user, err := socks4.ServerHandshake(conn, authenticator) if err != nil { conn.Close() return } + additions = append(additions, inbound.WithInUser(user)) tunnel.HandleTCPConn(inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...)) } -func HandleSocks5(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - authenticator := authStore.Authenticator() - if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) { - authenticator = nil - } - target, command, err := socks5.ServerHandshake(conn, authenticator) +func HandleSocks5(conn net.Conn, tunnel C.Tunnel, store auth.AuthStore, additions ...inbound.Addition) { + authenticator := store.Authenticator() + target, command, user, err := socks5.ServerHandshake(conn, authenticator) if err != nil { conn.Close() return @@ -121,5 +125,6 @@ func HandleSocks5(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) io.Copy(io.Discard, conn) return } + additions = append(additions, inbound.WithInUser(user)) tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.SOCKS5, additions...)) } diff --git a/listener/tproxy/packet.go b/listener/tproxy/packet.go index e4852665..b038d954 100644 --- a/listener/tproxy/packet.go +++ b/listener/tproxy/packet.go @@ -105,9 +105,9 @@ func listenLocalConn(rAddr, lAddr net.Addr, tunnel C.Tunnel) (*net.UDPConn, erro buf := pool.Get(pool.UDPBufferSize) br, err := lc.Read(buf) if err != nil { - pool.Put(buf) if errors.Is(err, net.ErrClosed) { log.Debugln("TProxy local conn listener exit.. rAddr=%s lAddr=%s", rAddr.String(), lAddr.String()) + pool.Put(buf) return } } diff --git a/listener/tproxy/tproxy.go b/listener/tproxy/tproxy.go index efb144a9..6056047a 100644 --- a/listener/tproxy/tproxy.go +++ b/listener/tproxy/tproxy.go @@ -4,7 +4,7 @@ import ( "net" "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/keepalive" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/transport/socks5" ) @@ -33,7 +33,9 @@ func (l *Listener) Close() error { func (l *Listener) handleTProxy(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { target := socks5.ParseAddrToSocksAddr(conn.LocalAddr()) - N.TCPKeepAlive(conn) + keepalive.TCPKeepAlive(conn) + // TProxy's conn.LocalAddr() is target address, so we set from l.listener + additions = append([]inbound.Addition{inbound.WithInAddr(l.listener.Addr())}, additions...) tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.TPROXY, additions...)) } diff --git a/listener/tproxy/tproxy_iptables.go b/listener/tproxy/tproxy_iptables.go index 6c6e2cc8..bc26b125 100644 --- a/listener/tproxy/tproxy_iptables.go +++ b/listener/tproxy/tproxy_iptables.go @@ -119,9 +119,7 @@ func CleanupTProxyIPTables() { log.Warnln("Cleanup tproxy linux iptables") - if int(dialer.DefaultRoutingMark.Load()) == 2158 { - dialer.DefaultRoutingMark.Store(0) - } + dialer.DefaultRoutingMark.CompareAndSwap(2158, 0) if _, err := cmd.ExecCmd("iptables -t mangle -L mihomo_divert"); err != nil { return diff --git a/listener/tunnel/tcp.go b/listener/tunnel/tcp.go index 794dc8ac..7c916a38 100644 --- a/listener/tunnel/tcp.go +++ b/listener/tunnel/tcp.go @@ -5,7 +5,6 @@ import ( "net" "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" ) @@ -35,7 +34,6 @@ func (l *Listener) Close() error { } 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...)) } diff --git a/log/level.go b/log/level.go index ea06ee4b..a4c6ecbd 100644 --- a/log/level.go +++ b/log/level.go @@ -3,6 +3,7 @@ package log import ( "encoding/json" "errors" + "strings" ) // LogLevelMapping is a mapping for LogLevel enum @@ -28,7 +29,7 @@ type LogLevel int func (l *LogLevel) UnmarshalYAML(unmarshal func(any) error) error { var tp string unmarshal(&tp) - level, exist := LogLevelMapping[tp] + level, exist := LogLevelMapping[strings.ToLower(tp)] if !exist { return errors.New("invalid mode") } @@ -40,7 +41,7 @@ func (l *LogLevel) UnmarshalYAML(unmarshal func(any) error) error { func (l *LogLevel) UnmarshalJSON(data []byte) error { var tp string json.Unmarshal(data, &tp) - level, exist := LogLevelMapping[tp] + level, exist := LogLevelMapping[strings.ToLower(tp)] if !exist { return errors.New("invalid mode") } @@ -48,9 +49,14 @@ func (l *LogLevel) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON serialize LogLevel with json -func (l LogLevel) MarshalJSON() ([]byte, error) { - return json.Marshal(l.String()) +// UnmarshalText unserialize LogLevel +func (l *LogLevel) UnmarshalText(data []byte) error { + level, exist := LogLevelMapping[strings.ToLower(string(data))] + if !exist { + return errors.New("invalid mode") + } + *l = level + return nil } // MarshalYAML serialize LogLevel with yaml @@ -58,6 +64,16 @@ func (l LogLevel) MarshalYAML() (any, error) { return l.String(), nil } +// MarshalJSON serialize LogLevel with json +func (l LogLevel) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +// MarshalText serialize LogLevel +func (l LogLevel) MarshalText() ([]byte, error) { + return []byte(l.String()), nil +} + func (l LogLevel) String() string { switch l { case INFO: diff --git a/log/log.go b/log/log.go index 6f565e7c..f1c68b42 100644 --- a/log/log.go +++ b/log/log.go @@ -20,7 +20,7 @@ func init() { log.SetLevel(log.DebugLevel) log.SetFormatter(&log.TextFormatter{ FullTimestamp: true, - TimestampFormat: "2006-01-02T15:04:05.999999999Z07:00", + TimestampFormat: "2006-01-02T15:04:05.000000000Z07:00", EnvironmentOverrideColors: true, }) } diff --git a/main.go b/main.go index 748fa2e3..9a2222df 100644 --- a/main.go +++ b/main.go @@ -1,45 +1,55 @@ package main import ( + "context" + "encoding/base64" "flag" "fmt" + "io" + "net" "os" "os/signal" "path/filepath" "runtime" "strings" - "sync" "syscall" - "time" + "github.com/metacubex/mihomo/component/geodata" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/rules/provider" "go.uber.org/automaxprocs/maxprocs" ) var ( - version bool - testConfig bool - geodataMode bool - homeDir string - configFile string - externalUI string - externalController string - secret string - updateGeoMux sync.Mutex - updatingGeo = false + version bool + testConfig bool + geodataMode bool + homeDir string + configFile string + configString string + configBytes []byte + externalUI string + externalController string + externalControllerUnix string + externalControllerPipe string + secret string ) func init() { flag.StringVar(&homeDir, "d", os.Getenv("CLASH_HOME_DIR"), "set configuration directory") flag.StringVar(&configFile, "f", os.Getenv("CLASH_CONFIG_FILE"), "specify configuration file") + flag.StringVar(&configString, "config", os.Getenv("CLASH_CONFIG_STRING"), "specify base64-encoded configuration string") flag.StringVar(&externalUI, "ext-ui", os.Getenv("CLASH_OVERRIDE_EXTERNAL_UI_DIR"), "override external ui directory") flag.StringVar(&externalController, "ext-ctl", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER"), "override external controller address") + flag.StringVar(&externalControllerUnix, "ext-ctl-unix", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_UNIX"), "override external controller unix address") + flag.StringVar(&externalControllerPipe, "ext-ctl-pipe", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_PIPE"), "override external controller pipe address") flag.StringVar(&secret, "secret", os.Getenv("CLASH_OVERRIDE_SECRET"), "override secret for RESTful API") flag.BoolVar(&geodataMode, "m", false, "set geodata mode") flag.BoolVar(&version, "v", false, "show current version of mihomo") @@ -48,7 +58,19 @@ func init() { } func main() { + // Defensive programming: panic when code mistakenly calls net.DefaultResolver + net.DefaultResolver.PreferGo = true + net.DefaultResolver.Dial = func(ctx context.Context, network, address string) (net.Conn, error) { + panic("should never be called") + } + _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) + + if len(os.Args) > 1 && os.Args[1] == "convert-ruleset" { + provider.ConvertMain(os.Args[2:]) + return + } + if version { fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) @@ -67,29 +89,53 @@ func main() { C.SetHomeDir(homeDir) } - if configFile != "" { - if !filepath.IsAbs(configFile) { - currentDir, _ := os.Getwd() - configFile = filepath.Join(currentDir, configFile) + if geodataMode { + geodata.SetGeodataMode(true) + } + + if configString != "" { + var err error + configBytes, err = base64.StdEncoding.DecodeString(configString) + if err != nil { + log.Fatalln("Initial configuration error: %s", err.Error()) + return + } + } else if configFile == "-" { + var err error + configBytes, err = io.ReadAll(os.Stdin) + if err != nil { + log.Fatalln("Initial configuration error: %s", err.Error()) + return } } else { - configFile = filepath.Join(C.Path.HomeDir(), C.Path.Config()) - } - C.SetConfig(configFile) + if configFile != "" { + if !filepath.IsAbs(configFile) { + currentDir, _ := os.Getwd() + configFile = filepath.Join(currentDir, configFile) + } + } else { + configFile = filepath.Join(C.Path.HomeDir(), C.Path.Config()) + } + C.SetConfig(configFile) - if geodataMode { - C.GeodataMode = true - } - - if err := config.Init(C.Path.HomeDir()); err != nil { - log.Fatalln("Initial configuration directory error: %s", err.Error()) + if err := config.Init(C.Path.HomeDir()); err != nil { + log.Fatalln("Initial configuration directory error: %s", err.Error()) + } } if testConfig { - if _, err := executor.Parse(); err != nil { - log.Errorln(err.Error()) - fmt.Printf("configuration file %s test failed\n", C.Path.Config()) - os.Exit(1) + if len(configBytes) != 0 { + if _, err := executor.ParseWithBytes(configBytes); err != nil { + log.Errorln(err.Error()) + fmt.Println("configuration test failed") + os.Exit(1) + } + } else { + if _, err := executor.Parse(); err != nil { + log.Errorln(err.Error()) + fmt.Printf("configuration file %s test failed\n", C.Path.Config()) + os.Exit(1) + } } fmt.Printf("configuration file %s test is successful\n", C.Path.Config()) return @@ -102,23 +148,22 @@ func main() { if externalController != "" { options = append(options, hub.WithExternalController(externalController)) } + if externalControllerUnix != "" { + options = append(options, hub.WithExternalControllerUnix(externalControllerUnix)) + } + if externalControllerPipe != "" { + options = append(options, hub.WithExternalControllerPipe(externalControllerPipe)) + } if secret != "" { options = append(options, hub.WithSecret(secret)) } - if err := hub.Parse(options...); err != nil { + if err := hub.Parse(configBytes, options...); err != nil { log.Fatalln("Parse config error: %s", err.Error()) } - if C.GeoAutoUpdate { - ticker := time.NewTicker(time.Duration(C.GeoUpdateInterval) * time.Hour) - - log.Infoln("[GEO] Start update GEO database every %d hours", C.GeoUpdateInterval) - go func() { - for range ticker.C { - updateGeoDatabases() - } - }() + if updater.GeoAutoUpdate() { + updater.RegisterGeoUpdater() } defer executor.Shutdown() @@ -132,47 +177,9 @@ func main() { case <-termSign: return case <-hupSign: - if cfg, err := executor.ParseWithPath(C.Path.Config()); err == nil { - executor.ApplyConfig(cfg, true) - } else { + if err := hub.Parse(configBytes, options...); err != nil { log.Errorln("Parse config error: %s", err.Error()) } } } } - -func updateGeoDatabases() { - log.Infoln("[GEO] Start updating GEO database") - updateGeoMux.Lock() - - if updatingGeo { - updateGeoMux.Unlock() - log.Infoln("[GEO] GEO database is updating, skip") - return - } - - updatingGeo = true - updateGeoMux.Unlock() - - go func() { - defer func() { - updatingGeo = false - }() - - log.Infoln("[GEO] Updating GEO database") - - if err := config.UpdateGeoDatabases(); err != nil { - log.Errorln("[GEO] update GEO database error: %s", err.Error()) - return - } - - cfg, err := executor.ParseWithPath(C.Path.Config()) - if err != nil { - log.Errorln("[GEO] update GEO database failed: %s", err.Error()) - return - } - - log.Infoln("[GEO] Update GEO database success, apply new config") - executor.ApplyConfig(cfg, false) - }() -} diff --git a/rules/common/base.go b/rules/common/base.go index d912107c..1abbe72c 100644 --- a/rules/common/base.go +++ b/rules/common/base.go @@ -2,11 +2,20 @@ package common import ( "errors" + + C "github.com/metacubex/mihomo/constant" + + "golang.org/x/exp/slices" ) var ( errPayload = errors.New("payloadRule error") - noResolve = "no-resolve" +) + +// params +var ( + NoResolve = "no-resolve" + Src = "src" ) type Base struct { @@ -20,11 +29,16 @@ func (b *Base) ShouldResolveIP() bool { return false } -func HasNoResolve(params []string) bool { - for _, p := range params { - if p == noResolve { - return true - } +func (b *Base) ProviderNames() []string { return nil } + +func ParseParams(params []string) (isSrc bool, noResolve bool) { + isSrc = slices.Contains(params, Src) + if isSrc { + noResolve = true + } else { + noResolve = slices.Contains(params, NoResolve) } - return false + return } + +type ParseRuleFunc func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (C.Rule, error) diff --git a/rules/common/domain.go b/rules/common/domain.go index 23f21185..306eb65f 100644 --- a/rules/common/domain.go +++ b/rules/common/domain.go @@ -4,6 +4,7 @@ import ( "strings" C "github.com/metacubex/mihomo/constant" + "golang.org/x/net/idna" ) type Domain struct { @@ -29,9 +30,10 @@ func (d *Domain) Payload() string { } func NewDomain(domain string, adapter string) *Domain { + punycode, _ := idna.ToASCII(strings.ToLower(domain)) return &Domain{ Base: &Base{}, - domain: strings.ToLower(domain), + domain: punycode, adapter: adapter, } } diff --git a/rules/common/domain_keyword.go b/rules/common/domain_keyword.go index ec01293a..9d6f1c15 100644 --- a/rules/common/domain_keyword.go +++ b/rules/common/domain_keyword.go @@ -4,6 +4,7 @@ import ( "strings" C "github.com/metacubex/mihomo/constant" + "golang.org/x/net/idna" ) type DomainKeyword struct { @@ -30,9 +31,10 @@ func (dk *DomainKeyword) Payload() string { } func NewDomainKeyword(keyword string, adapter string) *DomainKeyword { + punycode, _ := idna.ToASCII(strings.ToLower(keyword)) return &DomainKeyword{ Base: &Base{}, - keyword: strings.ToLower(keyword), + keyword: punycode, adapter: adapter, } } diff --git a/rules/common/domain_regex.go b/rules/common/domain_regex.go index 3d961542..d214a772 100644 --- a/rules/common/domain_regex.go +++ b/rules/common/domain_regex.go @@ -1,14 +1,14 @@ package common import ( - "regexp" - C "github.com/metacubex/mihomo/constant" + + "github.com/dlclark/regexp2" ) type DomainRegex struct { *Base - regex *regexp.Regexp + regex *regexp2.Regexp adapter string } @@ -18,7 +18,8 @@ func (dr *DomainRegex) RuleType() C.RuleType { func (dr *DomainRegex) Match(metadata *C.Metadata) (bool, string) { domain := metadata.RuleHost() - return dr.regex.MatchString(domain), dr.adapter + match, _ := dr.regex.MatchString(domain) + return match, dr.adapter } func (dr *DomainRegex) Adapter() string { @@ -30,7 +31,7 @@ func (dr *DomainRegex) Payload() string { } func NewDomainRegex(regex string, adapter string) (*DomainRegex, error) { - r, err := regexp.Compile(regex) + r, err := regexp2.Compile(regex, regexp2.IgnoreCase) if err != nil { return nil, err } diff --git a/rules/common/domain_suffix.go b/rules/common/domain_suffix.go index b7b1794d..c5b87208 100644 --- a/rules/common/domain_suffix.go +++ b/rules/common/domain_suffix.go @@ -4,6 +4,7 @@ import ( "strings" C "github.com/metacubex/mihomo/constant" + "golang.org/x/net/idna" ) type DomainSuffix struct { @@ -30,9 +31,10 @@ func (ds *DomainSuffix) Payload() string { } func NewDomainSuffix(suffix string, adapter string) *DomainSuffix { + punycode, _ := idna.ToASCII(strings.ToLower(suffix)) return &DomainSuffix{ Base: &Base{}, - suffix: strings.ToLower(suffix), + suffix: punycode, adapter: adapter, } } diff --git a/rules/common/geoip.go b/rules/common/geoip.go index 223a7904..61fae504 100644 --- a/rules/common/geoip.go +++ b/rules/common/geoip.go @@ -1,7 +1,9 @@ package common import ( + "errors" "fmt" + "net/netip" "strings" "github.com/metacubex/mihomo/component/geodata" @@ -10,62 +12,145 @@ import ( "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" + + "golang.org/x/exp/slices" ) type GEOIP struct { *Base - country string - adapter string - noResolveIP bool - geoIPMatcher *router.GeoIPMatcher - recodeSize int + country string + adapter string + noResolveIP bool + isSourceIP bool } var _ C.Rule = (*GEOIP)(nil) func (g *GEOIP) RuleType() C.RuleType { + if g.isSourceIP { + return C.SrcGEOIP + } return C.GEOIP } func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) { ip := metadata.DstIP + if g.isSourceIP { + ip = metadata.SrcIP + } if !ip.IsValid() { return false, "" } if g.country == "lan" { - return ip.IsPrivate() || - ip.IsUnspecified() || - ip.IsLoopback() || - ip.IsMulticast() || - ip.IsLinkLocalUnicast() || - resolver.IsFakeBroadcastIP(ip), g.adapter + return g.isLan(ip), g.adapter } - for _, code := range metadata.DstGeoIP { - if g.country == code { - return true, g.adapter - } - } - - if !C.GeodataMode { - if metadata.DstGeoIP != nil { - return false, g.adapter - } - metadata.DstGeoIP = mmdb.IPInstance().LookupCode(ip.AsSlice()) - for _, code := range metadata.DstGeoIP { - if g.country == code { + if geodata.GeodataMode() { + if g.isSourceIP { + if slices.Contains(metadata.SrcGeoIP, g.country) { + return true, g.adapter + } + } else { + if slices.Contains(metadata.DstGeoIP, g.country) { return true, g.adapter } } - return false, g.adapter + matcher, err := g.getIPMatcher() + if err != nil { + return false, "" + } + match := matcher.Match(ip) + if match { + if g.isSourceIP { + metadata.SrcGeoIP = append(metadata.SrcGeoIP, g.country) + } else { + metadata.DstGeoIP = append(metadata.DstGeoIP, g.country) + } + } + return match, g.adapter } - match := g.geoIPMatcher.Match(ip) - if match { - metadata.DstGeoIP = append(metadata.DstGeoIP, g.country) + if g.isSourceIP { + if metadata.SrcGeoIP != nil { + return slices.Contains(metadata.SrcGeoIP, g.country), g.adapter + } + } else { + if metadata.DstGeoIP != nil { + return slices.Contains(metadata.DstGeoIP, g.country), g.adapter + } } - return match, g.adapter + codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) + if g.isSourceIP { + metadata.SrcGeoIP = codes + } else { + metadata.DstGeoIP = codes + } + if slices.Contains(codes, g.country) { + return true, g.adapter + } + return false, "" +} + +// MatchIp implements C.IpMatcher +func (g *GEOIP) MatchIp(ip netip.Addr) bool { + if !ip.IsValid() { + return false + } + + if g.country == "lan" { + return g.isLan(ip) + } + + if geodata.GeodataMode() { + matcher, err := g.getIPMatcher() + if err != nil { + return false + } + return matcher.Match(ip) + } + + codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) + return slices.Contains(codes, g.country) +} + +// MatchIp implements C.IpMatcher +func (g dnsFallbackFilter) MatchIp(ip netip.Addr) bool { + if !ip.IsValid() { + return false + } + + if g.isLan(ip) { // compatible with original behavior + return false + } + + if geodata.GeodataMode() { + matcher, err := g.getIPMatcher() + if err != nil { + return false + } + return !matcher.Match(ip) + } + + codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) + return !slices.Contains(codes, g.country) +} + +type dnsFallbackFilter struct { + *GEOIP +} + +func (g *GEOIP) DnsFallbackFilter() C.IpMatcher { // for dns.fallback-filter.geoip + return dnsFallbackFilter{GEOIP: g} +} + +func (g *GEOIP) isLan(ip netip.Addr) bool { + return ip.IsPrivate() || + ip.IsUnspecified() || + ip.IsLoopback() || + ip.IsMulticast() || + ip.IsLinkLocalUnicast() || + resolver.IsFakeBroadcastIP(ip) } func (g *GEOIP) Adapter() string { @@ -84,44 +169,56 @@ func (g *GEOIP) GetCountry() string { return g.country } -func (g *GEOIP) GetIPMatcher() *router.GeoIPMatcher { - return g.geoIPMatcher +func (g *GEOIP) GetIPMatcher() (router.IPMatcher, error) { + if geodata.GeodataMode() { + return g.getIPMatcher() + } + return nil, errors.New("not geodata mode") +} + +func (g *GEOIP) getIPMatcher() (router.IPMatcher, error) { + geoIPMatcher, err := geodata.LoadGeoIPMatcher(g.country) + if err != nil { + return nil, fmt.Errorf("[GeoIP] %w", err) + } + return geoIPMatcher, nil + } func (g *GEOIP) GetRecodeSize() int { - return g.recodeSize + if matcher, err := g.GetIPMatcher(); err == nil { + return matcher.Count() + } + return 0 } -func NewGEOIP(country string, adapter string, noResolveIP bool) (*GEOIP, error) { +func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP, error) { + country = strings.ToLower(country) + + geoip := &GEOIP{ + Base: &Base{}, + country: country, + adapter: adapter, + noResolveIP: noResolveIP, + isSourceIP: isSrc, + } + + if country == "lan" { + return geoip, nil + } + if err := geodata.InitGeoIP(); err != nil { log.Errorln("can't initial GeoIP: %s", err) return nil, err } - country = strings.ToLower(country) - if !C.GeodataMode || country == "lan" { - geoip := &GEOIP{ - Base: &Base{}, - country: country, - adapter: adapter, - noResolveIP: noResolveIP, + if geodata.GeodataMode() { + geoIPMatcher, err := geoip.getIPMatcher() // test load + if err != nil { + return nil, err } - return geoip, nil + log.Infoln("Finished initial GeoIP rule %s => %s, records: %d", country, adapter, geoIPMatcher.Count()) } - geoIPMatcher, size, err := geodata.LoadGeoIPMatcher(country) - if err != nil { - return nil, fmt.Errorf("[GeoIP] %w", err) - } - - log.Infoln("Start initial GeoIP rule %s => %s, records: %d", country, adapter, size) - geoip := &GEOIP{ - Base: &Base{}, - country: country, - adapter: adapter, - noResolveIP: noResolveIP, - geoIPMatcher: geoIPMatcher, - recodeSize: size, - } return geoip, nil } diff --git a/rules/common/geosite.go b/rules/common/geosite.go index 1e3c1ab5..851bc8a4 100644 --- a/rules/common/geosite.go +++ b/rules/common/geosite.go @@ -15,7 +15,6 @@ type GEOSITE struct { *Base country string adapter string - matcher router.DomainMatcher recodeSize int } @@ -24,11 +23,19 @@ func (gs *GEOSITE) RuleType() C.RuleType { } func (gs *GEOSITE) Match(metadata *C.Metadata) (bool, string) { - domain := metadata.RuleHost() + return gs.MatchDomain(metadata.RuleHost()), gs.adapter +} + +// MatchDomain implements C.DomainMatcher +func (gs *GEOSITE) MatchDomain(domain string) bool { if len(domain) == 0 { - return false, "" + return false } - return gs.matcher.ApplyDomain(domain), gs.adapter + matcher, err := gs.GetDomainMatcher() + if err != nil { + return false + } + return matcher.ApplyDomain(domain) } func (gs *GEOSITE) Adapter() string { @@ -39,12 +46,19 @@ func (gs *GEOSITE) Payload() string { return gs.country } -func (gs *GEOSITE) GetDomainMatcher() router.DomainMatcher { - return gs.matcher +func (gs *GEOSITE) GetDomainMatcher() (router.DomainMatcher, error) { + matcher, err := geodata.LoadGeoSiteMatcher(gs.country) + if err != nil { + return nil, fmt.Errorf("load GeoSite data error, %w", err) + } + return matcher, nil } func (gs *GEOSITE) GetRecodeSize() int { - return gs.recodeSize + if matcher, err := gs.GetDomainMatcher(); err == nil { + return matcher.Count() + } + return 0 } func NewGEOSITE(country string, adapter string) (*GEOSITE, error) { @@ -53,21 +67,19 @@ func NewGEOSITE(country string, adapter string) (*GEOSITE, error) { return nil, err } - matcher, size, err := geodata.LoadGeoSiteMatcher(country) - if err != nil { - return nil, fmt.Errorf("load GeoSite data error, %s", err.Error()) - } - - log.Infoln("Start initial GeoSite rule %s => %s, records: %d", country, adapter, size) - geoSite := &GEOSITE{ - Base: &Base{}, - country: country, - adapter: adapter, - matcher: matcher, - recodeSize: size, + Base: &Base{}, + country: country, + adapter: adapter, } + matcher, err := geoSite.GetDomainMatcher() // test load + if err != nil { + return nil, err + } + + log.Infoln("Finished initial GeoSite rule %s => %s, records: %d", country, adapter, matcher.Count()) + return geoSite, nil } diff --git a/rules/common/ipasn.go b/rules/common/ipasn.go index 1fce8af4..7d554103 100644 --- a/rules/common/ipasn.go +++ b/rules/common/ipasn.go @@ -1,8 +1,6 @@ package common import ( - "strconv" - "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/mmdb" C "github.com/metacubex/mihomo/constant" @@ -14,24 +12,32 @@ type ASN struct { asn string adapter string noResolveIP bool + isSourceIP bool } func (a *ASN) Match(metadata *C.Metadata) (bool, string) { ip := metadata.DstIP + if a.isSourceIP { + ip = metadata.SrcIP + } if !ip.IsValid() { return false, "" } - result := mmdb.ASNInstance().LookupASN(ip.AsSlice()) + asn, aso := mmdb.ASNInstance().LookupASN(ip.AsSlice()) + if a.isSourceIP { + metadata.SrcIPASN = asn + " " + aso + } else { + metadata.DstIPASN = asn + " " + aso + } - asnNumber := strconv.FormatUint(uint64(result.AutonomousSystemNumber), 10) - metadata.DstIPASN = asnNumber + " " + result.AutonomousSystemOrganization - - match := a.asn == asnNumber - return match, a.adapter + return a.asn == asn, a.adapter } func (a *ASN) RuleType() C.RuleType { + if a.isSourceIP { + return C.SrcIPASN + } return C.IPASN } @@ -51,8 +57,7 @@ func (a *ASN) GetASN() string { return a.asn } -func NewIPASN(asn string, adapter string, noResolveIP bool) (*ASN, error) { - C.ASNEnable = true +func NewIPASN(asn string, adapter string, isSrc, noResolveIP bool) (*ASN, error) { if err := geodata.InitASN(); err != nil { log.Errorln("can't initial ASN: %s", err) return nil, err @@ -63,5 +68,6 @@ func NewIPASN(asn string, adapter string, noResolveIP bool) (*ASN, error) { asn: asn, adapter: adapter, noResolveIP: noResolveIP, + isSourceIP: isSrc, }, nil } diff --git a/rules/common/ipcidr.go b/rules/common/ipcidr.go index 663c9397..9c159502 100644 --- a/rules/common/ipcidr.go +++ b/rules/common/ipcidr.go @@ -40,7 +40,7 @@ func (i *IPCIDR) Match(metadata *C.Metadata) (bool, string) { if i.isSourceIP { ip = metadata.SrcIP } - return ip.IsValid() && i.ipnet.Contains(ip), i.adapter + return ip.IsValid() && i.ipnet.Contains(ip.WithZone("")), i.adapter } func (i *IPCIDR) Adapter() string { diff --git a/rules/common/process.go b/rules/common/process.go index ce643594..8932e946 100644 --- a/rules/common/process.go +++ b/rules/common/process.go @@ -4,6 +4,8 @@ import ( "strings" C "github.com/metacubex/mihomo/constant" + + "github.com/dlclark/regexp2" ) type Process struct { @@ -11,21 +13,36 @@ type Process struct { adapter string process string nameOnly bool + regexp *regexp2.Regexp } func (ps *Process) RuleType() C.RuleType { if ps.nameOnly { - return C.Process + if ps.regexp != nil { + return C.ProcessNameRegex + } + return C.ProcessName } + if ps.regexp != nil { + return C.ProcessPathRegex + } return C.ProcessPath } func (ps *Process) Match(metadata *C.Metadata) (bool, string) { if ps.nameOnly { + if ps.regexp != nil { + match, _ := ps.regexp.MatchString(metadata.Process) + return match, ps.adapter + } return strings.EqualFold(metadata.Process, ps.process), ps.adapter } + if ps.regexp != nil { + match, _ := ps.regexp.MatchString(metadata.ProcessPath) + return match, ps.adapter + } return strings.EqualFold(metadata.ProcessPath, ps.process), ps.adapter } @@ -41,11 +58,20 @@ func (ps *Process) ShouldFindProcess() bool { return true } -func NewProcess(process string, adapter string, nameOnly bool) (*Process, error) { +func NewProcess(process string, adapter string, nameOnly bool, regex bool) (*Process, error) { + var r *regexp2.Regexp + var err error + if regex { + r, err = regexp2.Compile(process, regexp2.IgnoreCase) + if err != nil { + return nil, err + } + } return &Process{ Base: &Base{}, adapter: adapter, process: process, nameOnly: nameOnly, + regexp: r, }, nil } diff --git a/rules/logic/logic.go b/rules/logic/logic.go index af8c31a4..f7b5a987 100644 --- a/rules/logic/logic.go +++ b/rules/logic/logic.go @@ -2,29 +2,29 @@ package logic import ( "fmt" - list "github.com/bahlo/generic-list-go" "regexp" "strings" + "sync" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/rules/common" + + list "github.com/bahlo/generic-list-go" ) type Logic struct { *common.Base - payload string - adapter string - ruleType C.RuleType - rules []C.Rule - subRules map[string][]C.Rule - needIP bool - needProcess bool + payload string + adapter string + ruleType C.RuleType + rules []C.Rule + subRules map[string][]C.Rule + + payloadOnce sync.Once } -type ParseRuleFunc func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (C.Rule, error) - -func NewSubRule(payload, adapter string, subRules map[string][]C.Rule, parseRule ParseRuleFunc) (*Logic, error) { - logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.SubRules} +func NewSubRule(payload, adapter string, subRules map[string][]C.Rule, parseRule common.ParseRuleFunc) (*Logic, error) { + logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.SubRules, subRules: subRules} err := logic.parsePayload(fmt.Sprintf("(%s)", payload), parseRule) if err != nil { return nil, err @@ -33,19 +33,10 @@ func NewSubRule(payload, adapter string, subRules map[string][]C.Rule, parseRule if len(logic.rules) != 1 { return nil, fmt.Errorf("Sub-Rule rule must contain one rule") } - for _, rule := range subRules[adapter] { - if rule.ShouldResolveIP() { - logic.needIP = true - } - if rule.ShouldFindProcess() { - logic.needProcess = true - } - } - logic.subRules = subRules return logic, nil } -func NewNOT(payload string, adapter string, parseRule ParseRuleFunc) (*Logic, error) { +func NewNOT(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) { logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.NOT} err := logic.parsePayload(payload, parseRule) if err != nil { @@ -55,52 +46,24 @@ func NewNOT(payload string, adapter string, parseRule ParseRuleFunc) (*Logic, er if len(logic.rules) != 1 { return nil, fmt.Errorf("not rule must contain one rule") } - logic.needIP = logic.rules[0].ShouldResolveIP() - logic.needProcess = logic.rules[0].ShouldFindProcess() - logic.payload = fmt.Sprintf("(!(%s,%s))", logic.rules[0].RuleType(), logic.rules[0].Payload()) return logic, nil } -func NewOR(payload string, adapter string, parseRule ParseRuleFunc) (*Logic, error) { +func NewOR(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) { logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.OR} err := logic.parsePayload(payload, parseRule) if err != nil { return nil, err } - - payloads := make([]string, 0, len(logic.rules)) - for _, rule := range logic.rules { - payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) - if rule.ShouldResolveIP() { - logic.needIP = true - } - if rule.ShouldFindProcess() { - logic.needProcess = true - } - } - logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " || ")) - return logic, nil } -func NewAND(payload string, adapter string, parseRule ParseRuleFunc) (*Logic, error) { + +func NewAND(payload string, adapter string, parseRule common.ParseRuleFunc) (*Logic, error) { logic := &Logic{Base: &common.Base{}, payload: payload, adapter: adapter, ruleType: C.AND} err := logic.parsePayload(payload, parseRule) if err != nil { return nil, err } - - payloads := make([]string, 0, len(logic.rules)) - for _, rule := range logic.rules { - payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) - if rule.ShouldResolveIP() { - logic.needIP = true - } - if rule.ShouldFindProcess() { - logic.needProcess = true - } - } - logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " && ")) - return logic, nil } @@ -114,7 +77,7 @@ func (r Range) containRange(preStart, preEnd int) bool { return preStart < r.start && preEnd > r.end } -func (logic *Logic) payloadToRule(subPayload string, parseRule ParseRuleFunc) (C.Rule, error) { +func (logic *Logic) payloadToRule(subPayload string, parseRule common.ParseRuleFunc) (C.Rule, error) { splitStr := strings.SplitN(subPayload, ",", 2) if len(splitStr) < 2 { return nil, fmt.Errorf("[%s] format is error", subPayload) @@ -195,7 +158,7 @@ func (logic *Logic) findSubRuleRange(payload string, ruleRanges []Range) []Range return subRuleRange } -func (logic *Logic) parsePayload(payload string, parseRule ParseRuleFunc) error { +func (logic *Logic) parsePayload(payload string, parseRule common.ParseRuleFunc) error { regex, err := regexp.Compile("\\(.*\\)") if err != nil { return err @@ -217,13 +180,6 @@ func (logic *Logic) parsePayload(payload string, parseRule ParseRuleFunc) error return err } - if rule.ShouldResolveIP() { - logic.needIP = true - } - if rule.ShouldFindProcess() { - logic.needProcess = true - } - rules = append(rules, rule) } @@ -243,7 +199,7 @@ func matchSubRules(metadata *C.Metadata, name string, subRules map[string][]C.Ru for _, rule := range subRules[name] { if m, a := rule.Match(metadata); m { if rule.RuleType() == C.SubRules { - matchSubRules(metadata, rule.Adapter(), subRules) + return matchSubRules(metadata, rule.Adapter(), subRules) } else { return m, a } @@ -278,9 +234,9 @@ func (logic *Logic) Match(metadata *C.Metadata) (bool, string) { } } return true, logic.adapter + default: + return false, "" } - - return false, "" } func (logic *Logic) Adapter() string { @@ -288,13 +244,63 @@ func (logic *Logic) Adapter() string { } func (logic *Logic) Payload() string { + logic.payloadOnce.Do(func() { // a little bit expensive, so only computed once + switch logic.ruleType { + case C.NOT: + logic.payload = fmt.Sprintf("(!(%s,%s))", logic.rules[0].RuleType(), logic.rules[0].Payload()) + case C.OR: + payloads := make([]string, 0, len(logic.rules)) + for _, rule := range logic.rules { + payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) + } + logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " || ")) + case C.AND: + payloads := make([]string, 0, len(logic.rules)) + for _, rule := range logic.rules { + payloads = append(payloads, fmt.Sprintf("(%s,%s)", rule.RuleType().String(), rule.Payload())) + } + logic.payload = fmt.Sprintf("(%s)", strings.Join(payloads, " && ")) + default: + } + }) return logic.payload } func (logic *Logic) ShouldResolveIP() bool { - return logic.needIP + if logic.ruleType == C.SubRules { + for _, rule := range logic.subRules[logic.adapter] { + if rule.ShouldResolveIP() { + return true + } + } + } + for _, rule := range logic.rules { + if rule.ShouldResolveIP() { + return true + } + } + return false } func (logic *Logic) ShouldFindProcess() bool { - return logic.needProcess + if logic.ruleType == C.SubRules { + for _, rule := range logic.subRules[logic.adapter] { + if rule.ShouldFindProcess() { + return true + } + } + } + for _, rule := range logic.rules { + if rule.ShouldFindProcess() { + return true + } + } + return false +} + +func (logic *Logic) ProviderNames() (names []string) { + for _, rule := range logic.rules { + names = append(names, rule.ProviderNames()...) + } + return } diff --git a/rules/parser.go b/rules/parser.go index b69cc4fd..83325433 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -22,19 +22,23 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] case "GEOSITE": parsed, parseErr = RC.NewGEOSITE(payload, target) case "GEOIP": - noResolve := RC.HasNoResolve(params) - parsed, parseErr = RC.NewGEOIP(payload, target, noResolve) - case "IP-CIDR", "IP-CIDR6": - noResolve := RC.HasNoResolve(params) - parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRNoResolve(noResolve)) + isSrc, noResolve := RC.ParseParams(params) + parsed, parseErr = RC.NewGEOIP(payload, target, isSrc, noResolve) + case "SRC-GEOIP": + parsed, parseErr = RC.NewGEOIP(payload, target, true, true) case "IP-ASN": - noResolve := RC.HasNoResolve(params) - parsed, parseErr = RC.NewIPASN(payload, target, noResolve) + isSrc, noResolve := RC.ParseParams(params) + parsed, parseErr = RC.NewIPASN(payload, target, isSrc, noResolve) + case "SRC-IP-ASN": + parsed, parseErr = RC.NewIPASN(payload, target, true, true) + case "IP-CIDR", "IP-CIDR6": + isSrc, noResolve := RC.ParseParams(params) + parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(isSrc), RC.WithIPCIDRNoResolve(noResolve)) case "SRC-IP-CIDR": parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(true), RC.WithIPCIDRNoResolve(true)) case "IP-SUFFIX": - noResolve := RC.HasNoResolve(params) - parsed, parseErr = RC.NewIPSuffix(payload, target, false, noResolve) + isSrc, noResolve := RC.ParseParams(params) + parsed, parseErr = RC.NewIPSuffix(payload, target, isSrc, noResolve) case "SRC-IP-SUFFIX": parsed, parseErr = RC.NewIPSuffix(payload, target, true, true) case "SRC-PORT": @@ -46,9 +50,13 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] case "DSCP": parsed, parseErr = RC.NewDSCP(payload, target) case "PROCESS-NAME": - parsed, parseErr = RC.NewProcess(payload, target, true) + parsed, parseErr = RC.NewProcess(payload, target, true, false) case "PROCESS-PATH": - parsed, parseErr = RC.NewProcess(payload, target, false) + parsed, parseErr = RC.NewProcess(payload, target, false, false) + case "PROCESS-NAME-REGEX": + parsed, parseErr = RC.NewProcess(payload, target, true, true) + case "PROCESS-PATH-REGEX": + parsed, parseErr = RC.NewProcess(payload, target, false, true) case "NETWORK": parsed, parseErr = RC.NewNetworkType(payload, target) case "UID": @@ -68,8 +76,8 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] case "NOT": parsed, parseErr = logic.NewNOT(payload, target, ParseRule) case "RULE-SET": - noResolve := RC.HasNoResolve(params) - parsed, parseErr = RP.NewRuleSet(payload, target, noResolve) + isSrc, noResolve := RC.ParseParams(params) + parsed, parseErr = RP.NewRuleSet(payload, target, isSrc, noResolve) case "MATCH": parsed = RC.NewMatch(target) parseErr = nil @@ -83,3 +91,5 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] return } + +var _ RC.ParseRuleFunc = ParseRule diff --git a/rules/provider/classical_strategy.go b/rules/provider/classical_strategy.go index f8042164..205a8e59 100644 --- a/rules/provider/classical_strategy.go +++ b/rules/provider/classical_strategy.go @@ -2,9 +2,11 @@ package provider import ( "fmt" - C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" "strings" + + C "github.com/metacubex/mihomo/constant" + P "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/log" ) type classicalStrategy struct { @@ -15,6 +17,10 @@ type classicalStrategy struct { parse func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) } +func (c *classicalStrategy) Behavior() P.RuleBehavior { + return P.Classical +} + func (c *classicalStrategy) Match(metadata *C.Metadata) bool { for _, rule := range c.rules { if m, _ := rule.Match(metadata); m { @@ -76,7 +82,7 @@ func ruleParse(ruleRaw string) (string, string, []string) { } else if len(item) == 2 { return item[0], item[1], nil } else if len(item) > 2 { - if item[0] == "NOT" || item[0] == "OR" || item[0] == "AND" || item[0] == "SUB-RULE" { + if item[0] == "NOT" || item[0] == "OR" || item[0] == "AND" || item[0] == "SUB-RULE" || item[0] == "DOMAIN-REGEX" || item[0] == "PROCESS-NAME-REGEX" || item[0] == "PROCESS-PATH-REGEX" { return item[0], strings.Join(item[1:len(item)], ","), nil } else { return item[0], item[1], item[2:] diff --git a/rules/provider/domain_strategy.go b/rules/provider/domain_strategy.go index c0787d58..b893f038 100644 --- a/rules/provider/domain_strategy.go +++ b/rules/provider/domain_strategy.go @@ -1,9 +1,16 @@ package provider import ( + "errors" + "io" + "strings" + "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" + P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" + + "golang.org/x/exp/slices" ) type domainStrategy struct { @@ -12,6 +19,10 @@ type domainStrategy struct { domainSet *trie.DomainSet } +func (d *domainStrategy) Behavior() P.RuleBehavior { + return P.Domain +} + func (d *domainStrategy) ShouldFindProcess() bool { return false } @@ -35,6 +46,10 @@ func (d *domainStrategy) Reset() { } func (d *domainStrategy) Insert(rule string) { + if strings.ContainsRune(rule, '/') { + log.Warnln("invalid domain:[%s]", rule) + return + } err := d.domainTrie.Insert(rule, struct{}{}) if err != nil { log.Warnln("invalid domain:[%s]", rule) @@ -48,6 +63,45 @@ func (d *domainStrategy) FinishInsert() { d.domainTrie = nil } +func (d *domainStrategy) FromMrs(r io.Reader, count int) error { + domainSet, err := trie.ReadDomainSetBin(r) + if err != nil { + return err + } + d.count = count + d.domainSet = domainSet + return nil +} + +func (d *domainStrategy) WriteMrs(w io.Writer) error { + if d.domainSet == nil { + return errors.New("nil domainSet") + } + return d.domainSet.WriteBin(w) +} + +func (d *domainStrategy) DumpMrs(f func(key string) bool) { + if d.domainSet != nil { + var keys []string + d.domainSet.Foreach(func(key string) bool { + keys = append(keys, key) + return true + }) + slices.Sort(keys) + + for _, key := range keys { + if _, ok := slices.BinarySearch(keys, "+."+key); ok { + continue // ignore the rules added by trie internal processing + } + if !f(key) { + return + } + } + } +} + +var _ mrsRuleStrategy = (*domainStrategy)(nil) + func NewDomainStrategy() *domainStrategy { return &domainStrategy{} } diff --git a/rules/provider/ipcidr_strategy.go b/rules/provider/ipcidr_strategy.go index c93facd9..9efffed9 100644 --- a/rules/provider/ipcidr_strategy.go +++ b/rules/provider/ipcidr_strategy.go @@ -1,9 +1,16 @@ package provider import ( + "errors" + "io" + "net/netip" + "github.com/metacubex/mihomo/component/cidr" C "github.com/metacubex/mihomo/constant" + P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" + + "go4.org/netipx" ) type ipcidrStrategy struct { @@ -13,6 +20,10 @@ type ipcidrStrategy struct { //trie *trie.IpCidrTrie } +func (i *ipcidrStrategy) Behavior() P.RuleBehavior { + return P.IPCIDR +} + func (i *ipcidrStrategy) ShouldFindProcess() bool { return false } @@ -52,6 +63,38 @@ func (i *ipcidrStrategy) FinishInsert() { i.cidrSet.Merge() } +func (i *ipcidrStrategy) FromMrs(r io.Reader, count int) error { + cidrSet, err := cidr.ReadIpCidrSet(r) + if err != nil { + return err + } + i.count = count + i.cidrSet = cidrSet + if i.count > 0 { + i.shouldResolveIP = true + } + return nil +} + +func (i *ipcidrStrategy) WriteMrs(w io.Writer) error { + if i.cidrSet == nil { + return errors.New("nil cidrSet") + } + return i.cidrSet.WriteBin(w) +} + +func (i *ipcidrStrategy) DumpMrs(f func(key string) bool) { + if i.cidrSet != nil { + i.cidrSet.Foreach(func(prefix netip.Prefix) bool { + return f(prefix.String()) + }) + } +} + +func (i *ipcidrStrategy) ToIpCidr() *netipx.IPSet { + return i.cidrSet.ToIPSet() +} + func NewIPCidrStrategy() *ipcidrStrategy { return &ipcidrStrategy{} } diff --git a/rules/provider/mrs_converter.go b/rules/provider/mrs_converter.go new file mode 100644 index 00000000..dbbe51cb --- /dev/null +++ b/rules/provider/mrs_converter.go @@ -0,0 +1,120 @@ +package provider + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "os" + + P "github.com/metacubex/mihomo/constant/provider" + + "github.com/klauspost/compress/zstd" +) + +func ConvertToMrs(buf []byte, behavior P.RuleBehavior, format P.RuleFormat, w io.Writer) (err error) { + strategy := newStrategy(behavior, nil) + strategy, err = rulesParse(buf, strategy, format) + if err != nil { + return err + } + if strategy.Count() == 0 { + return errors.New("empty rule") + } + if _strategy, ok := strategy.(mrsRuleStrategy); ok { + if format == P.MrsRule { // export to TextRule + _strategy.DumpMrs(func(key string) bool { + _, err = fmt.Fprintln(w, key) + if err != nil { + return false + } + return true + }) + return nil + } + + var encoder *zstd.Encoder + encoder, err = zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) + if err != nil { + return err + } + defer func() { + zstdErr := encoder.Close() + if err == nil { + err = zstdErr + } + }() + + // header + _, err = encoder.Write(MrsMagicBytes[:]) + if err != nil { + return err + } + + // behavior + _behavior := []byte{behavior.Byte()} + _, err = encoder.Write(_behavior[:]) + if err != nil { + return err + } + + // count + count := int64(_strategy.Count()) + err = binary.Write(encoder, binary.BigEndian, count) + if err != nil { + return err + } + + // extra (reserved for future using) + var extra []byte + err = binary.Write(encoder, binary.BigEndian, int64(len(extra))) + if err != nil { + return err + } + _, err = encoder.Write(extra) + if err != nil { + return err + } + + return _strategy.WriteMrs(encoder) + } else { + return ErrInvalidFormat + } +} + +func ConvertMain(args []string) { + if len(args) > 3 { + behavior, err := P.ParseBehavior(args[0]) + if err != nil { + panic(err) + } + format, err := P.ParseRuleFormat(args[1]) + if err != nil { + panic(err) + } + source := args[2] + target := args[3] + + sourceFile, err := os.ReadFile(source) + if err != nil { + panic(err) + } + + targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + panic(err) + } + + err = ConvertToMrs(sourceFile, behavior, format, targetFile) + if err != nil { + panic(err) + } + + err = targetFile.Close() + if err != nil { + panic(err) + } + } else { + panic("Usage: convert-ruleset ") + } +} diff --git a/rules/provider/mrs_reader.go b/rules/provider/mrs_reader.go new file mode 100644 index 00000000..66f62127 --- /dev/null +++ b/rules/provider/mrs_reader.go @@ -0,0 +1,72 @@ +package provider + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/klauspost/compress/zstd" +) + +var MrsMagicBytes = [4]byte{'M', 'R', 'S', 1} // MRSv1 + +func rulesMrsParse(buf []byte, strategy ruleStrategy) (ruleStrategy, error) { + if _strategy, ok := strategy.(mrsRuleStrategy); ok { + reader, err := zstd.NewReader(bytes.NewReader(buf)) + if err != nil { + return nil, err + } + defer reader.Close() + + // header + var header [4]byte + _, err = io.ReadFull(reader, header[:]) + if err != nil { + return nil, err + } + if header != MrsMagicBytes { + return nil, fmt.Errorf("invalid MrsMagic bytes") + } + + // behavior + var _behavior [1]byte + _, err = io.ReadFull(reader, _behavior[:]) + if err != nil { + return nil, err + } + if _behavior[0] != strategy.Behavior().Byte() { + return nil, fmt.Errorf("invalid behavior") + } + + // count + var count int64 + err = binary.Read(reader, binary.BigEndian, &count) + if err != nil { + return nil, err + } + + // extra (reserved for future using) + var length int64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 0 { + return nil, errors.New("length is invalid") + } + if length > 0 { + extra := make([]byte, length) + _, err = io.ReadFull(reader, extra) + if err != nil { + return nil, err + } + } + + err = _strategy.FromMrs(reader, int(count)) + return strategy, err + } else { + return nil, ErrInvalidFormat + } +} diff --git a/rules/provider/parse.go b/rules/provider/parse.go index a867d570..97d513fc 100644 --- a/rules/provider/parse.go +++ b/rules/provider/parse.go @@ -8,8 +8,8 @@ import ( "github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/constant/features" P "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/rules/common" ) var ( @@ -17,42 +17,30 @@ var ( ) type ruleProviderSchema struct { - Type string `provider:"type"` - Behavior string `provider:"behavior"` - Path string `provider:"path,omitempty"` - URL string `provider:"url,omitempty"` - Format string `provider:"format,omitempty"` - Interval int `provider:"interval,omitempty"` + Type string `provider:"type"` + Behavior string `provider:"behavior"` + Path string `provider:"path,omitempty"` + URL string `provider:"url,omitempty"` + Proxy string `provider:"proxy,omitempty"` + Format string `provider:"format,omitempty"` + Interval int `provider:"interval,omitempty"` + SizeLimit int64 `provider:"size-limit,omitempty"` + Payload []string `provider:"payload,omitempty"` } -func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(tp, payload, target string, params []string, subRules map[string][]C.Rule) (parsed C.Rule, parseErr error)) (P.RuleProvider, error) { +func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRuleFunc) (P.RuleProvider, error) { schema := &ruleProviderSchema{} decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) if err := decoder.Decode(mapping, schema); err != nil { return nil, err } - var behavior P.RuleBehavior - - switch schema.Behavior { - case "domain": - behavior = P.Domain - case "ipcidr": - behavior = P.IPCIDR - case "classical": - behavior = P.Classical - default: - return nil, fmt.Errorf("unsupported behavior type: %s", schema.Behavior) + behavior, err := P.ParseBehavior(schema.Behavior) + if err != nil { + return nil, err } - - 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) + format, err := P.ParseRuleFormat(schema.Format) + if err != nil { + return nil, err } var vehicle P.Vehicle @@ -61,17 +49,16 @@ func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(t path := C.Path.Resolve(schema.Path) vehicle = resource.NewFileVehicle(path) case "http": + path := C.Path.GetPathByHash("rules", schema.URL) if schema.Path != "" { - path := C.Path.Resolve(schema.Path) - if !features.CMFA && !C.Path.IsSafePath(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) } - + vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, nil, resource.DefaultHttpTimeout, schema.SizeLimit) + case "inline": + return NewInlineProvider(name, behavior, schema.Payload, parse), nil default: return nil, fmt.Errorf("unsupported vehicle type: %s", schema.Type) } diff --git a/rules/provider/patch_android.go b/rules/provider/patch_android.go deleted file mode 100644 index 2bd5ffc8..00000000 --- a/rules/provider/patch_android.go +++ /dev/null @@ -1,27 +0,0 @@ -//go:build android && cmfa - -package provider - -import "time" - -var ( - suspended bool -) - -type UpdatableProvider interface { - UpdatedAt() time.Time -} - -func (f *ruleSetProvider) UpdatedAt() time.Time { - return f.Fetcher.UpdatedAt -} - -func (rp *ruleSetProvider) Close() error { - rp.Fetcher.Destroy() - - return nil -} - -func Suspend(s bool) { - suspended = s -} diff --git a/rules/provider/provider.go b/rules/provider/provider.go index adc2e44a..5c456832 100644 --- a/rules/provider/provider.go +++ b/rules/provider/provider.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "errors" - "gopkg.in/yaml.v3" + "io" "runtime" "strings" "time" @@ -13,21 +13,15 @@ import ( "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/rules/common" + + "gopkg.in/yaml.v3" ) -var ( - ruleProviders = map[string]P.RuleProvider{} -) +var tunnel P.Tunnel -type ruleSetProvider struct { - *resource.Fetcher[any] - behavior P.RuleBehavior - format P.RuleFormat - strategy ruleStrategy -} - -type RuleSetProvider struct { - *ruleSetProvider +func SetTunnel(t P.Tunnel) { + tunnel = t } type RulePayload struct { @@ -39,7 +33,19 @@ type RulePayload struct { Rules []string `yaml:"rules"` } +type providerForApi struct { + Behavior string `json:"behavior"` + Format string `json:"format"` + Name string `json:"name"` + RuleCount int `json:"ruleCount"` + Type string `json:"type"` + VehicleType string `json:"vehicleType"` + UpdatedAt time.Time `json:"updatedAt"` + Payload []string `json:"payload,omitempty"` +} + type ruleStrategy interface { + Behavior() P.RuleBehavior Match(metadata *C.Metadata) bool Count() int ShouldResolveIP() bool @@ -49,98 +55,111 @@ type ruleStrategy interface { FinishInsert() } -func RuleProviders() map[string]P.RuleProvider { - return ruleProviders +type mrsRuleStrategy interface { + ruleStrategy + FromMrs(r io.Reader, count int) error + WriteMrs(w io.Writer) error + DumpMrs(f func(key string) bool) } -func SetRuleProvider(ruleProvider P.RuleProvider) { - if ruleProvider != nil { - ruleProviders[(ruleProvider).Name()] = ruleProvider - } +type baseProvider struct { + behavior P.RuleBehavior + strategy ruleStrategy } -func (rp *ruleSetProvider) Type() P.ProviderType { +func (bp *baseProvider) Type() P.ProviderType { return P.Rule } -func (rp *ruleSetProvider) Initial() error { - elm, err := rp.Fetcher.Initial() - if err != nil { - return err - } - - rp.OnUpdate(elm) - return nil +func (bp *baseProvider) Behavior() P.RuleBehavior { + return bp.behavior } -func (rp *ruleSetProvider) Update() error { - elm, same, err := rp.Fetcher.Update() - if err == nil && !same { - rp.OnUpdate(elm) - return nil - } +func (bp *baseProvider) Count() int { + return bp.strategy.Count() +} +func (bp *baseProvider) Match(metadata *C.Metadata) bool { + return bp.strategy != nil && bp.strategy.Match(metadata) +} + +func (bp *baseProvider) ShouldResolveIP() bool { + return bp.strategy.ShouldResolveIP() +} + +func (bp *baseProvider) ShouldFindProcess() bool { + return bp.strategy.ShouldFindProcess() +} + +func (bp *baseProvider) Strategy() any { + return bp.strategy +} + +type ruleSetProvider struct { + baseProvider + *resource.Fetcher[ruleStrategy] + format P.RuleFormat +} + +type RuleSetProvider struct { + *ruleSetProvider +} + +func (rp *ruleSetProvider) Initial() error { + _, err := rp.Fetcher.Initial() return err } -func (rp *ruleSetProvider) Behavior() P.RuleBehavior { - return rp.behavior -} - -func (rp *ruleSetProvider) Match(metadata *C.Metadata) bool { - return rp.strategy != nil && rp.strategy.Match(metadata) -} - -func (rp *ruleSetProvider) ShouldResolveIP() bool { - return rp.strategy.ShouldResolveIP() -} - -func (rp *ruleSetProvider) ShouldFindProcess() bool { - return rp.strategy.ShouldFindProcess() -} - -func (rp *ruleSetProvider) AsRule(adaptor string) C.Rule { - panic("implement me") +func (rp *ruleSetProvider) Update() error { + _, _, err := rp.Fetcher.Update() + return err } 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(), - "updatedAt": rp.UpdatedAt, - "vehicleType": rp.VehicleType().String(), + providerForApi{ + Behavior: rp.behavior.String(), + Format: rp.format.String(), + Name: rp.Fetcher.Name(), + RuleCount: rp.strategy.Count(), + Type: rp.Type().String(), + UpdatedAt: rp.UpdatedAt(), + VehicleType: rp.VehicleType().String(), }) } -func 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 { +func (rp *RuleSetProvider) Close() error { + runtime.SetFinalizer(rp, nil) + return rp.ruleSetProvider.Close() +} + +func NewRuleSetProvider(name string, behavior P.RuleBehavior, format P.RuleFormat, interval time.Duration, vehicle P.Vehicle, parse common.ParseRuleFunc) P.RuleProvider { rp := &ruleSetProvider{ - behavior: behavior, - format: format, + baseProvider: baseProvider{ + behavior: behavior, + }, + format: format, } - onUpdate := func(elm interface{}) { - strategy := elm.(ruleStrategy) + onUpdate := func(strategy ruleStrategy) { rp.strategy = strategy + tunnel.RuleUpdateCallback().Emit(rp) } 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) + rp.Fetcher = resource.NewFetcher(name, interval, vehicle, func(bytes []byte) (ruleStrategy, error) { + return rulesParse(bytes, newStrategy(behavior, parse), format) + }, onUpdate) wrapper := &RuleSetProvider{ rp, } - final := func(provider *RuleSetProvider) { _ = rp.Fetcher.Destroy() } - runtime.SetFinalizer(wrapper, final) + runtime.SetFinalizer(wrapper, (*RuleSetProvider).Close) return wrapper } -func newStrategy(behavior P.RuleBehavior, 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 common.ParseRuleFunc) ruleStrategy { switch behavior { case P.Domain: strategy := NewDomainStrategy() @@ -156,10 +175,16 @@ func newStrategy(behavior P.RuleBehavior, parse func(tp, payload, target string, } } -var ErrNoPayload = errors.New("file must have a `payload` field") +var ( + ErrNoPayload = errors.New("file must have a `payload` field") + ErrInvalidFormat = errors.New("invalid format") +) -func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (any, error) { +func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStrategy, error) { strategy.Reset() + if format == P.MrsRule { + return rulesMrsParse(buf, strategy) + } schema := &RulePayload{} @@ -176,15 +201,14 @@ func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (any, er 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 + s = len(buf) // stop loop in next step + if firstLineLength == 0 && format == P.YamlRule { // no head or only one line body return nil, ErrNoPayload } } var str string switch format { case P.TextRule: - firstLineLength = -1 // don't return ErrNoPayload when read last line str = string(line) str = strings.TrimSpace(str) if len(str) == 0 { @@ -234,6 +258,8 @@ func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (any, er if len(schema.Payload) > 0 { str = schema.Payload[0] } + default: + return nil, ErrInvalidFormat } if str == "" { @@ -247,3 +273,76 @@ func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (any, er return strategy, nil } + +func rulesParseInline(rs []string, strategy ruleStrategy) ruleStrategy { + strategy.Reset() + for _, r := range rs { + if r != "" { + strategy.Insert(r) + } + } + strategy.FinishInsert() + return strategy +} + +type InlineProvider struct { + *inlineProvider +} + +type inlineProvider struct { + baseProvider + name string + updateAt time.Time + payload []string +} + +func (i *inlineProvider) Name() string { + return i.name +} + +func (i *inlineProvider) Initial() error { + return nil +} + +func (i *inlineProvider) Update() error { + // make api update happy + i.updateAt = time.Now() + return nil +} + +func (i *inlineProvider) VehicleType() P.VehicleType { + return P.Inline +} + +func (i *inlineProvider) MarshalJSON() ([]byte, error) { + return json.Marshal( + providerForApi{ + Behavior: i.behavior.String(), + Name: i.Name(), + RuleCount: i.strategy.Count(), + Type: i.Type().String(), + VehicleType: i.VehicleType().String(), + UpdatedAt: i.updateAt, + Payload: i.payload, + }) +} + +func NewInlineProvider(name string, behavior P.RuleBehavior, payload []string, parse common.ParseRuleFunc) P.RuleProvider { + ip := &inlineProvider{ + baseProvider: baseProvider{ + behavior: behavior, + strategy: newStrategy(behavior, parse), + }, + payload: payload, + name: name, + updateAt: time.Now(), + } + ip.strategy = rulesParseInline(payload, ip.strategy) + + wrapper := &InlineProvider{ + ip, + } + + //runtime.SetFinalizer(wrapper, (*InlineProvider).Close) + return wrapper +} diff --git a/rules/provider/rule_set.go b/rules/provider/rule_set.go index 1d940188..2ad0bd3d 100644 --- a/rules/provider/rule_set.go +++ b/rules/provider/rule_set.go @@ -1,7 +1,8 @@ package provider import ( - "fmt" + "net/netip" + C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/rules/common" @@ -11,13 +12,19 @@ type RuleSet struct { *common.Base ruleProviderName string adapter string - ruleProvider P.RuleProvider + isSrc bool noResolveIP bool shouldFindProcess bool } func (rs *RuleSet) ShouldFindProcess() bool { - return rs.shouldFindProcess || rs.getProviders().ShouldFindProcess() + if rs.shouldFindProcess { + return true + } + if provider, ok := rs.getProvider(); ok { + return provider.ShouldFindProcess() + } + return false } func (rs *RuleSet) RuleType() C.RuleType { @@ -25,7 +32,26 @@ func (rs *RuleSet) RuleType() C.RuleType { } func (rs *RuleSet) Match(metadata *C.Metadata) (bool, string) { - return rs.getProviders().Match(metadata), rs.adapter + if provider, ok := rs.getProvider(); ok { + if rs.isSrc { + metadata.SwapSrcDst() + defer metadata.SwapSrcDst() + } + return provider.Match(metadata), rs.adapter + } + return false, "" +} + +// MatchDomain implements C.DomainMatcher +func (rs *RuleSet) MatchDomain(domain string) bool { + ok, _ := rs.Match(&C.Metadata{Host: domain}) + return ok +} + +// MatchIp implements C.IpMatcher +func (rs *RuleSet) MatchIp(ip netip.Addr) bool { + ok, _ := rs.Match(&C.Metadata{DstIP: ip}) + return ok } func (rs *RuleSet) Adapter() string { @@ -33,31 +59,35 @@ func (rs *RuleSet) Adapter() string { } func (rs *RuleSet) Payload() string { - return rs.getProviders().Name() + return rs.ruleProviderName } func (rs *RuleSet) ShouldResolveIP() bool { - return !rs.noResolveIP && rs.getProviders().ShouldResolveIP() -} -func (rs *RuleSet) getProviders() P.RuleProvider { - if rs.ruleProvider == nil { - rp := RuleProviders()[rs.ruleProviderName] - rs.ruleProvider = rp + if rs.noResolveIP { + return false } - - return rs.ruleProvider + if provider, ok := rs.getProvider(); ok { + return provider.ShouldResolveIP() + } + return false } -func NewRuleSet(ruleProviderName string, adapter string, noResolveIP bool) (*RuleSet, error) { - rp, ok := RuleProviders()[ruleProviderName] - if !ok { - return nil, fmt.Errorf("rule set %s not found", ruleProviderName) - } - return &RuleSet{ +func (rs *RuleSet) ProviderNames() []string { + return []string{rs.ruleProviderName} +} + +func (rs *RuleSet) getProvider() (P.RuleProvider, bool) { + pp, ok := tunnel.RuleProviders()[rs.ruleProviderName] + return pp, ok +} + +func NewRuleSet(ruleProviderName string, adapter string, isSrc bool, noResolveIP bool) (*RuleSet, error) { + rs := &RuleSet{ Base: &common.Base{}, ruleProviderName: ruleProviderName, adapter: adapter, - ruleProvider: rp, + isSrc: isSrc, noResolveIP: noResolveIP, - }, nil + } + return rs, nil } diff --git a/test/go.mod b/test/go.mod index 92374886..f364c2c8 100644 --- a/test/go.mod +++ b/test/go.mod @@ -22,7 +22,6 @@ require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/cilium/ebpf v0.12.3 // indirect github.com/coreos/go-iptables v0.7.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect diff --git a/test/go.sum b/test/go.sum index af34b356..05a23ce9 100644 --- a/test/go.sum +++ b/test/go.sum @@ -19,8 +19,6 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2 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.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= -github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= 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= diff --git a/transport/hysteria/conns/udp/hop.go b/transport/hysteria/conns/udp/hop.go index eb0732f0..0a888749 100644 --- a/transport/hysteria/conns/udp/hop.go +++ b/transport/hysteria/conns/udp/hop.go @@ -12,7 +12,7 @@ import ( "github.com/metacubex/mihomo/transport/hysteria/obfs" "github.com/metacubex/mihomo/transport/hysteria/utils" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) const ( @@ -86,7 +86,7 @@ func NewObfsUDPHopClientPacketConn(server string, serverPorts string, hopInterva serverAddrs: serverAddrs, hopInterval: hopInterval, obfs: obfs, - addrIndex: fastrand.Intn(len(serverAddrs)), + addrIndex: randv2.IntN(len(serverAddrs)), recvQueue: make(chan *udpPacket, packetQueueSize), closeChan: make(chan struct{}), bufPool: sync.Pool{ @@ -177,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 = fastrand.Intn(len(c.serverAddrs)) + c.addrIndex = randv2.IntN(len(c.serverAddrs)) } func (c *ObfsUDPHopClientPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { diff --git a/transport/hysteria/conns/wechat/obfs.go b/transport/hysteria/conns/wechat/obfs.go index 4266d268..c5ec47ee 100644 --- a/transport/hysteria/conns/wechat/obfs.go +++ b/transport/hysteria/conns/wechat/obfs.go @@ -9,7 +9,7 @@ import ( "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/hysteria/obfs" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) const udpBufferSize = 65535 @@ -31,7 +31,7 @@ func NewObfsWeChatUDPConn(orig net.PacketConn, obfs obfs.Obfuscator) *ObfsWeChat obfs: obfs, readBuf: make([]byte, udpBufferSize), writeBuf: make([]byte, udpBufferSize), - sn: fastrand.Uint32() & 0xFFFF, + sn: randv2.Uint32() & 0xFFFF, } } diff --git a/transport/hysteria/core/client.go b/transport/hysteria/core/client.go index 199fe0d4..782948c0 100644 --- a/transport/hysteria/core/client.go +++ b/transport/hysteria/core/client.go @@ -19,7 +19,7 @@ import ( "github.com/lunixbochs/struc" "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/congestion" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) var ( @@ -289,7 +289,10 @@ func (c *Client) DialUDP(dialer utils.PacketDialer) (UDPConn, error) { func (c *Client) Close() error { c.reconnectMutex.Lock() defer c.reconnectMutex.Unlock() - err := c.quicSession.CloseWithError(closeErrorCodeGeneric, "") + var err error + if c.quicSession != nil { + err = c.quicSession.CloseWithError(closeErrorCodeGeneric, "") + } c.closed = true return err } @@ -405,8 +408,8 @@ func (c *quicPktConn) WriteTo(p []byte, addr string) error { var errSize *quic.DatagramTooLargeError if errors.As(err, &errSize) { // need to frag - msg.MsgID = uint16(fastrand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 - fragMsgs := fragUDPMessage(msg, int(errSize.PeerMaxDatagramFrameSize)) + msg.MsgID = uint16(randv2.IntN(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 + fragMsgs := fragUDPMessage(msg, int(errSize.MaxDatagramPayloadSize)) for _, fragMsg := range fragMsgs { msgBuf.Reset() _ = struc.Pack(&msgBuf, &fragMsg) diff --git a/transport/hysteria/obfs/xplus.go b/transport/hysteria/obfs/xplus.go index 171bf281..4d1e3f29 100644 --- a/transport/hysteria/obfs/xplus.go +++ b/transport/hysteria/obfs/xplus.go @@ -1,9 +1,8 @@ package obfs import ( + "crypto/rand" "crypto/sha256" - - "github.com/zhangyunhao116/fastrand" ) // [salt][obfuscated payload] @@ -35,7 +34,7 @@ func (x *XPlusObfuscator) Deobfuscate(in []byte, out []byte) int { } func (x *XPlusObfuscator) Obfuscate(in []byte, out []byte) int { - _, _ = fastrand.Read(out[:saltLen]) // salt + _, _ = rand.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/transport/client.go b/transport/hysteria/transport/client.go index f5cc9f07..91876ea9 100644 --- a/transport/hysteria/transport/client.go +++ b/transport/hysteria/transport/client.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "fmt" "net" - "strings" "time" "github.com/metacubex/quic-go" @@ -16,9 +15,7 @@ import ( "github.com/metacubex/mihomo/transport/hysteria/utils" ) -type ClientTransport struct { - Dialer *net.Dialer -} +type ClientTransport struct{} func (ct *ClientTransport) quicPacketConn(proto string, rAddr net.Addr, serverPorts string, obfs obfsPkg.Obfuscator, hopInterval time.Duration, dialer utils.PacketDialer) (net.PacketConn, error) { server := rAddr.String() @@ -86,23 +83,3 @@ func (ct *ClientTransport) QUICDial(proto string, server string, serverPorts str } return qs, nil } - -func (ct *ClientTransport) DialTCP(raddr *net.TCPAddr) (*net.TCPConn, error) { - conn, err := ct.Dialer.Dial("tcp", raddr.String()) - if err != nil { - return nil, err - } - return conn.(*net.TCPConn), nil -} - -func (ct *ClientTransport) ListenUDP() (*net.UDPConn, error) { - return net.ListenUDP("udp", nil) -} - -func isMultiPortAddr(addr string) bool { - _, portStr, err := net.SplitHostPort(addr) - if err == nil && (strings.Contains(portStr, ",") || strings.Contains(portStr, "-")) { - return true - } - return false -} diff --git a/transport/shadowsocks/core/cipher.go b/transport/shadowsocks/core/cipher.go index 44b2e8d4..7cb12c45 100644 --- a/transport/shadowsocks/core/cipher.go +++ b/transport/shadowsocks/core/cipher.go @@ -34,6 +34,11 @@ const ( aeadAes256Gcm = "AEAD_AES_256_GCM" aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305" aeadXChacha20Poly1305 = "AEAD_XCHACHA20_POLY1305" + aeadChacha8Poly1305 = "AEAD_CHACHA8_POLY1305" + aeadXChacha8Poly1305 = "AEAD_XCHACHA8_POLY1305" + aeadAes128Ccm = "AEAD_AES_128_CCM" + aeadAes192Ccm = "AEAD_AES_192_CCM" + aeadAes256Ccm = "AEAD_AES_256_CCM" ) // List of AEAD ciphers: key size in bytes and constructor @@ -46,6 +51,11 @@ var aeadList = map[string]struct { aeadAes256Gcm: {32, shadowaead.AESGCM}, aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305}, aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305}, + aeadChacha8Poly1305: {32, shadowaead.Chacha8Poly1305}, + aeadXChacha8Poly1305: {32, shadowaead.XChacha8Poly1305}, + aeadAes128Ccm: {16, shadowaead.AESCCM}, + aeadAes192Ccm: {24, shadowaead.AESCCM}, + aeadAes256Ccm: {32, shadowaead.AESCCM}, } // List of stream ciphers: key size in bytes and constructor @@ -95,6 +105,16 @@ func PickCipher(name string, key []byte, password string) (Cipher, error) { name = aeadAes192Gcm case "AES-256-GCM": name = aeadAes256Gcm + case "CHACHA8-IETF-POLY1305": + name = aeadChacha8Poly1305 + case "XCHACHA8-IETF-POLY1305": + name = aeadXChacha8Poly1305 + case "AES-128-CCM": + name = aeadAes128Ccm + case "AES-192-CCM": + name = aeadAes192Ccm + case "AES-256-CCM": + name = aeadAes256Ccm } if choice, ok := aeadList[name]; ok { diff --git a/transport/shadowsocks/shadowaead/cipher.go b/transport/shadowsocks/shadowaead/cipher.go index 3cf75749..7981d5b1 100644 --- a/transport/shadowsocks/shadowaead/cipher.go +++ b/transport/shadowsocks/shadowaead/cipher.go @@ -7,6 +7,8 @@ import ( "io" "strconv" + "github.com/metacubex/chacha" + "gitlab.com/go-extension/aes-ccm" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" ) @@ -75,6 +77,25 @@ func AESGCM(psk []byte) (Cipher, error) { return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil } +func aesCCM(key []byte) (cipher.AEAD, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return ccm.NewCCM(blk) +} + +// AESCCM creates a new Cipher with a pre-shared key. len(psk) must be +// one of 16, 24, or 32 to select AES-128/196/256-GCM. +func AESCCM(psk []byte) (Cipher, error) { + switch l := len(psk); l { + case 16, 24, 32: // AES 128/196/256 + default: + return nil, aes.KeySizeError(l) + } + return &metaCipher{psk: psk, makeAEAD: aesCCM}, nil +} + // Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) // must be 32. func Chacha20Poly1305(psk []byte) (Cipher, error) { @@ -92,3 +113,21 @@ func XChacha20Poly1305(psk []byte) (Cipher, error) { } return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil } + +// Chacha8Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func Chacha8Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha.KeySize { + return nil, KeySizeError(chacha.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha.NewChaCha8IETFPoly1305}, nil +} + +// XChacha8Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func XChacha8Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha.KeySize { + return nil, KeySizeError(chacha.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha.NewXChaCha20IETFPoly1305}, nil +} diff --git a/transport/shadowsocks/shadowstream/old_chacha20.go b/transport/shadowsocks/shadowstream/old_chacha20.go index 65737fcc..eb61232e 100644 --- a/transport/shadowsocks/shadowstream/old_chacha20.go +++ b/transport/shadowsocks/shadowstream/old_chacha20.go @@ -2,7 +2,8 @@ package shadowstream import ( "crypto/cipher" - "github.com/aead/chacha20/chacha" + + "github.com/metacubex/chacha" ) type chacha20key []byte @@ -11,7 +12,7 @@ func (k chacha20key) IVSize() int { return chacha.NonceSize } func (k chacha20key) Encrypter(iv []byte) cipher.Stream { - c, _ := chacha.NewCipher(iv, k, 20) + c, _ := chacha.NewChaCha20(iv, k) return c } func (k chacha20key) Decrypter(iv []byte) cipher.Stream { diff --git a/transport/simple-obfs/http.go b/transport/simple-obfs/http.go index 681c1864..9c3f8e00 100644 --- a/transport/simple-obfs/http.go +++ b/transport/simple-obfs/http.go @@ -2,6 +2,7 @@ package obfs import ( "bytes" + "crypto/rand" "encoding/base64" "fmt" "io" @@ -10,7 +11,7 @@ import ( "github.com/metacubex/mihomo/common/pool" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) // HTTPObfs is shadowsocks http simple-obfs implementation @@ -64,9 +65,12 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) { func (ho *HTTPObfs) Write(b []byte) (int, error) { if ho.firstRequest { randBytes := make([]byte, 16) - 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", fastrand.Int()%54, fastrand.Int()%2)) + rand.Read(randBytes) + req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:])) + if err != nil { + return 0, err + } + req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", randv2.Int()%54, randv2.Int()%2)) req.Header.Set("Upgrade", "websocket") req.Header.Set("Connection", "Upgrade") req.Host = ho.host @@ -75,7 +79,7 @@ func (ho *HTTPObfs) Write(b []byte) (int, error) { } req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes)) req.ContentLength = int64(len(b)) - err := req.Write(ho.Conn) + err = req.Write(ho.Conn) ho.firstRequest = false return len(b), err } diff --git a/transport/simple-obfs/tls.go b/transport/simple-obfs/tls.go index 78317f0a..a0cbc350 100644 --- a/transport/simple-obfs/tls.go +++ b/transport/simple-obfs/tls.go @@ -2,14 +2,13 @@ package obfs import ( "bytes" + "crypto/rand" "encoding/binary" "io" "net" "time" "github.com/metacubex/mihomo/common/pool" - - "github.com/zhangyunhao116/fastrand" ) const ( @@ -127,8 +126,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) - fastrand.Read(random) - fastrand.Read(sessionID) + rand.Read(random) + rand.Read(sessionID) buf := &bytes.Buffer{} diff --git a/transport/sing-shadowtls/shadowtls.go b/transport/sing-shadowtls/shadowtls.go index 982d847a..2012e20a 100644 --- a/transport/sing-shadowtls/shadowtls.go +++ b/transport/sing-shadowtls/shadowtls.go @@ -9,9 +9,9 @@ import ( tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/mihomo/log" + utls "github.com/metacubex/utls" "github.com/sagernet/sing-shadowtls" sing_common "github.com/sagernet/sing/common" - utls "github.com/sagernet/utls" ) const ( diff --git a/transport/socks4/socks4.go b/transport/socks4/socks4.go index 9533a1c0..50708eda 100644 --- a/transport/socks4/socks4.go +++ b/transport/socks4/socks4.go @@ -43,7 +43,7 @@ var ( var subnet = netip.PrefixFrom(netip.IPv4Unspecified(), 24) -func ServerHandshake(rw io.ReadWriter, authenticator auth.Authenticator) (addr string, command Command, err error) { +func ServerHandshake(rw io.ReadWriter, authenticator auth.Authenticator) (addr string, command Command, user string, err error) { var req [8]byte if _, err = io.ReadFull(rw, req[:]); err != nil { return @@ -73,6 +73,7 @@ func ServerHandshake(rw io.ReadWriter, authenticator auth.Authenticator) (addr s if userID, err = readUntilNull(rw); err != nil { return } + user = string(userID) if isReservedIP(dstIP) { var target []byte @@ -90,7 +91,7 @@ func ServerHandshake(rw io.ReadWriter, authenticator auth.Authenticator) (addr s } // SOCKS4 only support USERID auth. - if authenticator == nil || authenticator.Verify(string(userID), "") { + if authenticator == nil || authenticator.Verify(user, "") { code = RequestGranted } else { code = RequestIdentdMismatched diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index c97c370c..61b555f4 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -106,7 +106,7 @@ type User struct { } // ServerHandshake fast-tracks SOCKS initialization to get target address to connect on server side. -func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr, command Command, err error) { +func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr, command Command, user string, err error) { // Read RFC 1928 for request and reply structure and sizes. buf := make([]byte, MaxAddrLen) // read VER, NMETHODS, METHODS @@ -141,7 +141,7 @@ func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr, if _, err = io.ReadFull(rw, authBuf[:userLen]); err != nil { return } - user := string(authBuf[:userLen]) + user = string(authBuf[:userLen]) // Get password if _, err = rw.Read(header[:1]); err != nil { diff --git a/transport/ssr/obfs/http_simple.go b/transport/ssr/obfs/http_simple.go index 359ca342..d59b490d 100644 --- a/transport/ssr/obfs/http_simple.go +++ b/transport/ssr/obfs/http_simple.go @@ -10,7 +10,7 @@ import ( "github.com/metacubex/mihomo/common/pool" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) func init() { @@ -82,7 +82,7 @@ func (c *httpConn) Write(b []byte) (int, error) { bLength := len(b) headDataLength := bLength if bLength-headLength > 64 { - headDataLength = headLength + fastrand.Intn(65) + headDataLength = headLength + randv2.IntN(65) } headData := b[:headDataLength] b = b[headDataLength:] @@ -100,7 +100,7 @@ func (c *httpConn) Write(b []byte) (int, error) { } } hosts := strings.Split(host, ",") - host = hosts[fastrand.Intn(len(hosts))] + host = hosts[randv2.IntN(len(hosts))] buf := pool.GetBuffer() defer pool.PutBuffer(buf) @@ -119,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[fastrand.Intn(len(userAgent))]) + buf.WriteString(userAgent[randv2.IntN(len(userAgent))]) buf.WriteString("\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\n") if c.post { packBoundary(buf) @@ -147,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[fastrand.Intn(62)]) + buf.WriteByte(set[randv2.IntN(62)]) } buf.WriteString("\r\n") } diff --git a/transport/ssr/obfs/random_head.go b/transport/ssr/obfs/random_head.go index 9a2072fe..a5ad2dab 100644 --- a/transport/ssr/obfs/random_head.go +++ b/transport/ssr/obfs/random_head.go @@ -1,13 +1,14 @@ package obfs import ( + "crypto/rand" "encoding/binary" "hash/crc32" "net" "github.com/metacubex/mihomo/common/pool" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) func init() { @@ -54,10 +55,10 @@ func (c *randomHeadConn) Write(b []byte) (int, error) { c.buf = append(c.buf, b...) if !c.hasSentHeader { c.hasSentHeader = true - dataLength := fastrand.Intn(96) + 4 + dataLength := randv2.IntN(96) + 4 buf := pool.Get(dataLength + 4) defer pool.Put(buf) - fastrand.Read(buf[:dataLength]) + rand.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 d5e3ca88..d5955edc 100644 --- a/transport/ssr/obfs/tls1.2_ticket_auth.go +++ b/transport/ssr/obfs/tls1.2_ticket_auth.go @@ -3,6 +3,7 @@ package obfs import ( "bytes" "crypto/hmac" + "crypto/rand" "encoding/binary" "net" "strings" @@ -11,7 +12,7 @@ import ( "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/transport/ssr/tools" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) func init() { @@ -26,7 +27,7 @@ type tls12Ticket struct { func newTLS12Ticket(b *Base) Obfs { r := &tls12Ticket{Base: b, authData: &authData{}} - fastrand.Read(r.clientID[:]) + rand.Read(r.clientID[:]) return r } @@ -91,7 +92,7 @@ func (c *tls12TicketConn) Write(b []byte) (int, error) { buf := pool.GetBuffer() defer pool.PutBuffer(buf) for len(b) > 2048 { - size := fastrand.Intn(4096) + 100 + size := randv2.IntN(4096) + 100 if len(b) < size { size = len(b) } @@ -197,7 +198,7 @@ func packSNIData(buf *bytes.Buffer, u string) { } func (c *tls12TicketConn) packTicketBuf(buf *bytes.Buffer, u string) { - length := 16 * (fastrand.Intn(17) + 8) + length := 16 * (randv2.IntN(17) + 8) buf.Write([]byte{0, 0x23}) binary.Write(buf, binary.BigEndian, uint16(length)) tools.AppendRandBytes(buf, length) @@ -222,6 +223,6 @@ func (t *tls12Ticket) getHost() string { host = "" } hosts := strings.Split(host, ",") - host = hosts[fastrand.Intn(len(hosts))] + host = hosts[randv2.IntN(len(hosts))] return host } diff --git a/transport/ssr/protocol/auth_aes128_sha1.go b/transport/ssr/protocol/auth_aes128_sha1.go index 6ee4160e..cfd55510 100644 --- a/transport/ssr/protocol/auth_aes128_sha1.go +++ b/transport/ssr/protocol/auth_aes128_sha1.go @@ -2,6 +2,7 @@ package protocol import ( "bytes" + "crypto/rand" "encoding/binary" "math" "net" @@ -13,7 +14,7 @@ import ( "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/ssr/tools" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) type ( @@ -66,7 +67,7 @@ func (a *authAES128) initUserData() { } if len(a.userKey) == 0 { a.userKey = a.Key - fastrand.Read(a.userID[:]) + rand.Read(a.userID[:]) } } @@ -200,7 +201,7 @@ func (a *authAES128) packData(poolBuf *bytes.Buffer, data []byte, fullDataLength } func trapezoidRandom(max int, d float64) int { - base := fastrand.Float64() + base := randv2.Float64() if d-0 > 1e-6 { a := 1 - d base = (math.Sqrt(a*a+4*d*base) - a) / (2 * d) @@ -221,10 +222,10 @@ func (a *authAES128) getRandDataLengthForPackData(dataLength, fullDataLength int if revLength > -1460 { return trapezoidRandom(revLength+1460, -0.3) } - return fastrand.Intn(32) + return randv2.IntN(32) } if dataLength > 900 { - return fastrand.Intn(revLength) + return randv2.IntN(revLength) } return trapezoidRandom(revLength, -0.3) } @@ -249,7 +250,7 @@ func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) { copy(macKey, a.iv) copy(macKey[len(a.iv):], a.Key) - poolBuf.WriteByte(byte(fastrand.Intn(256))) + poolBuf.WriteByte(byte(randv2.IntN(256))) poolBuf.Write(a.hmac(macKey, poolBuf.Bytes())[:6]) poolBuf.Write(a.userID[:]) err := a.authData.putEncryptedData(poolBuf, a.userKey, [2]int{packedAuthDataLength, randDataLength}, a.salt) @@ -265,9 +266,9 @@ func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) { func (a *authAES128) getRandDataLengthForPackAuthData(size int) int { if size > 400 { - return fastrand.Intn(512) + return randv2.IntN(512) } - return fastrand.Intn(1024) + return randv2.IntN(1024) } func (a *authAES128) packRandData(poolBuf *bytes.Buffer, size int) { diff --git a/transport/ssr/protocol/auth_sha1_v4.go b/transport/ssr/protocol/auth_sha1_v4.go index ed1a39f1..1c616c3f 100644 --- a/transport/ssr/protocol/auth_sha1_v4.go +++ b/transport/ssr/protocol/auth_sha1_v4.go @@ -11,7 +11,7 @@ import ( "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/transport/ssr/tools" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) func init() { @@ -178,7 +178,7 @@ func (a *authSHA1V4) getRandDataLength(size int) int { return 0 } if size > 400 { - return fastrand.Intn(256) + return randv2.IntN(256) } - return fastrand.Intn(512) + return randv2.IntN(512) } diff --git a/transport/ssr/protocol/base.go b/transport/ssr/protocol/base.go index e26a6587..79870177 100644 --- a/transport/ssr/protocol/base.go +++ b/transport/ssr/protocol/base.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/aes" "crypto/cipher" + "crypto/rand" "encoding/base64" "encoding/binary" "sync" @@ -13,7 +14,7 @@ import ( "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/shadowsocks/core" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) type Base struct { @@ -38,8 +39,8 @@ func (a *authData) next() *authData { a.mutex.Lock() defer a.mutex.Unlock() if a.connectionID > 0xff000000 || a.connectionID == 0 { - fastrand.Read(a.clientID[:]) - a.connectionID = fastrand.Uint32() & 0xffffff + rand.Read(a.clientID[:]) + a.connectionID = randv2.Uint32() & 0xffffff } a.connectionID++ copy(r.clientID[:], a.clientID[:]) diff --git a/transport/ssr/protocol/protocol.go b/transport/ssr/protocol/protocol.go index a04e6bd4..ad6bf6ba 100644 --- a/transport/ssr/protocol/protocol.go +++ b/transport/ssr/protocol/protocol.go @@ -8,7 +8,7 @@ import ( N "github.com/metacubex/mihomo/common/net" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) var ( @@ -71,7 +71,7 @@ func getHeadSize(b []byte, defaultValue int) int { func getDataLength(b []byte) int { bLength := len(b) - dataLength := getHeadSize(b, 30) + fastrand.Intn(32) + dataLength := getHeadSize(b, 30) + randv2.IntN(32) if bLength < dataLength { return bLength } diff --git a/transport/trojan/trojan.go b/transport/trojan/trojan.go index 09be1124..17f403c1 100644 --- a/transport/trojan/trojan.go +++ b/transport/trojan/trojan.go @@ -129,6 +129,12 @@ func (t *Trojan) StreamWebsocketConn(ctx context.Context, conn net.Conn, wsOptio ServerName: t.option.ServerName, } + var err error + tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, t.option.Fingerprint) + if err != nil { + return nil, err + } + return vmess.StreamWebsocketConn(ctx, conn, &vmess.WebsocketConfig{ Host: wsOptions.Host, Port: wsOptions.Port, diff --git a/transport/tuic/common/congestion.go b/transport/tuic/common/congestion.go index 485e2e6a..a0e2be68 100644 --- a/transport/tuic/common/congestion.go +++ b/transport/tuic/common/congestion.go @@ -22,7 +22,7 @@ func SetCongestionController(quicConn quic.Connection, cc string, cwnd int) { quicConn.SetCongestionControl( congestion.NewCubicSender( congestion.DefaultClock{}, - congestion.GetInitialPacketSize(quicConn.RemoteAddr()), + congestion.GetInitialPacketSize(quicConn), false, ), ) @@ -30,7 +30,7 @@ func SetCongestionController(quicConn quic.Connection, cc string, cwnd int) { quicConn.SetCongestionControl( congestion.NewCubicSender( congestion.DefaultClock{}, - congestion.GetInitialPacketSize(quicConn.RemoteAddr()), + congestion.GetInitialPacketSize(quicConn), true, ), ) @@ -38,7 +38,7 @@ func SetCongestionController(quicConn quic.Connection, cc string, cwnd int) { quicConn.SetCongestionControl( congestion.NewBBRSender( congestion.DefaultClock{}, - congestion.GetInitialPacketSize(quicConn.RemoteAddr()), + congestion.GetInitialPacketSize(quicConn), c.ByteCount(cwnd)*congestion.InitialMaxDatagramSize, congestion.DefaultBBRMaxCongestionWindow*congestion.InitialMaxDatagramSize, ), @@ -49,7 +49,7 @@ func SetCongestionController(quicConn quic.Connection, cc string, cwnd int) { quicConn.SetCongestionControl( congestionv2.NewBbrSender( congestionv2.DefaultClock{}, - congestionv2.GetInitialPacketSize(quicConn.RemoteAddr()), + congestionv2.GetInitialPacketSize(quicConn), c.ByteCount(cwnd), ), ) diff --git a/transport/tuic/congestion/bbr_sender.go b/transport/tuic/congestion/bbr_sender.go index 8c18c616..5a863362 100644 --- a/transport/tuic/congestion/bbr_sender.go +++ b/transport/tuic/congestion/bbr_sender.go @@ -5,34 +5,23 @@ package congestion import ( "fmt" "math" - "net" "time" + "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/congestion" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) const ( // InitialMaxDatagramSize is the default maximum packet size used in QUIC for congestion window computations in bytes. - InitialMaxDatagramSize = 1252 - InitialPacketSizeIPv4 = 1252 - InitialPacketSizeIPv6 = 1232 + InitialMaxDatagramSize = 1280 + InitialPacketSize = 1280 InitialCongestionWindow = 32 DefaultBBRMaxCongestionWindow = 10000 ) -func GetInitialPacketSize(addr net.Addr) congestion.ByteCount { - 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 { - if udpAddr.IP.To4() != nil { - maxSize = InitialPacketSizeIPv4 - } else { - maxSize = InitialPacketSizeIPv6 - } - } - return congestion.ByteCount(maxSize) +func GetInitialPacketSize(quicConn quic.Connection) congestion.ByteCount { + return congestion.ByteCount(quicConn.Config().InitialPacketSize) } var ( @@ -727,7 +716,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 = fastrand.Int() % (GainCycleLength - 1) + b.cycleCurrentOffset = randv2.Int() % (GainCycleLength - 1) if b.cycleCurrentOffset >= 1 { b.cycleCurrentOffset += 1 } diff --git a/transport/tuic/congestion/cubic.go b/transport/tuic/congestion/cubic.go index dd491a32..a9bed43a 100644 --- a/transport/tuic/congestion/cubic.go +++ b/transport/tuic/congestion/cubic.go @@ -21,7 +21,7 @@ const ( cubeCongestionWindowScale = 410 cubeFactor congestion.ByteCount = 1 << cubeScale / cubeCongestionWindowScale / maxDatagramSize // TODO: when re-enabling cubic, make sure to use the actual packet size here - maxDatagramSize = congestion.ByteCount(InitialPacketSizeIPv4) + maxDatagramSize = congestion.ByteCount(InitialPacketSize) ) const defaultNumConnections = 1 diff --git a/transport/tuic/congestion_v2/bbr_sender.go b/transport/tuic/congestion_v2/bbr_sender.go index 084f85b1..d8852fbc 100644 --- a/transport/tuic/congestion_v2/bbr_sender.go +++ b/transport/tuic/congestion_v2/bbr_sender.go @@ -4,12 +4,12 @@ package congestion import ( "fmt" - "net" "time" + "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/congestion" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) // BbrSender implements BBR congestion control algorithm. BBR aims to estimate @@ -30,7 +30,7 @@ const ( // 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) + defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSize) // The gain used for the STARTUP, equal to 2/ln(2). defaultHighGain = 2.885 @@ -62,7 +62,7 @@ const ( // Flag. defaultStartupFullLossCount = 8 quicBbr2DefaultLossThreshold = 0.02 - maxBbrBurstPackets = 3 + maxBbrBurstPackets = 10 ) type bbrMode int @@ -334,6 +334,8 @@ func (b *bbrSender) OnPacketSent( } b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) + + b.maybeAppLimited(bytesInFlight) } // CanSend implements the SendAlgorithm interface. @@ -413,8 +415,6 @@ func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, even // packet in lost_packets. var lastPacketSendState sendTimeState - b.maybeApplimited(priorInFlight) - // Update bytesInFlight b.bytesInFlight = priorInFlight for _, p := range ackedPackets { @@ -541,7 +541,7 @@ func (b *bbrSender) setDrainGain(drainGain float64) { b.drainGain = drainGain } -// What's the current estimated bandwidth in bytes per second. +// Get the current bandwidth estimate. Note that Bandwidth is in bits per second. func (b *bbrSender) bandwidthEstimate() Bandwidth { return b.maxBandwidth.GetBest() } @@ -620,7 +620,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 = int(fastrand.Int31n(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1) + b.cycleCurrentOffset = int(randv2.Int32N(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1) if b.cycleCurrentOffset >= 1 { b.cycleCurrentOffset += 1 } @@ -700,14 +700,13 @@ func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeSta } } -func (b *bbrSender) maybeApplimited(bytesInFlight congestion.ByteCount) { +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 { + if availableBytes > maxBbrBurstPackets*b.maxDatagramSize { b.sampler.OnAppLimited() } } @@ -931,16 +930,6 @@ func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.B 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 - } +func GetInitialPacketSize(quicConn quic.Connection) congestion.ByteCount { + return congestion.ByteCount(quicConn.Config().InitialPacketSize) } diff --git a/transport/tuic/congestion_v2/pacer.go b/transport/tuic/congestion_v2/pacer.go index ecaf3d11..174b3dbe 100644 --- a/transport/tuic/congestion_v2/pacer.go +++ b/transport/tuic/congestion_v2/pacer.go @@ -21,8 +21,8 @@ type Pacer struct { func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer { p := &Pacer{ - budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSizeIPv4, - maxDatagramSize: congestion.InitialPacketSizeIPv4, + budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSize, + maxDatagramSize: congestion.InitialPacketSize, getBandwidth: getBandwidth, } return p diff --git a/transport/tuic/v4/client.go b/transport/tuic/v4/client.go index 5c9c889c..62b419b7 100644 --- a/transport/tuic/v4/client.go +++ b/transport/tuic/v4/client.go @@ -20,8 +20,8 @@ import ( "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/quic-go" + "github.com/metacubex/randv2" "github.com/puzpuzpuz/xsync/v3" - "github.com/zhangyunhao116/fastrand" ) type ClientOption struct { @@ -367,7 +367,7 @@ func (t *clientImpl) ListenPacketWithDialer(ctx context.Context, metadata *C.Met pipe1, pipe2 := N.Pipe() var connId uint32 for { - connId = fastrand.Uint32() + connId = randv2.Uint32() _, loaded := t.udpInputMap.LoadOrStore(connId, pipe1) if !loaded { break diff --git a/transport/tuic/v4/packet.go b/transport/tuic/v4/packet.go index 8f5bb5b3..47484e78 100644 --- a/transport/tuic/v4/packet.go +++ b/transport/tuic/v4/packet.go @@ -123,7 +123,7 @@ func (q *quicStreamPacketConn) WaitReadFrom() (data []byte, put func(), addr net func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { if q.udpRelayMode != common.QUIC && len(p) > q.maxUdpRelayPacketSize { - return 0, &quic.DatagramTooLargeError{PeerMaxDatagramFrameSize: int64(q.maxUdpRelayPacketSize)} + return 0, &quic.DatagramTooLargeError{MaxDatagramPayloadSize: int64(q.maxUdpRelayPacketSize)} } if q.closed { return 0, net.ErrClosed diff --git a/transport/tuic/v5/client.go b/transport/tuic/v5/client.go index 89454add..a3c13d2b 100644 --- a/transport/tuic/v5/client.go +++ b/transport/tuic/v5/client.go @@ -20,8 +20,8 @@ import ( "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/quic-go" + "github.com/metacubex/randv2" "github.com/puzpuzpuz/xsync/v3" - "github.com/zhangyunhao116/fastrand" ) type ClientOption struct { @@ -351,7 +351,7 @@ func (t *clientImpl) ListenPacketWithDialer(ctx context.Context, metadata *C.Met pipe1, pipe2 := N.Pipe() var connId uint16 for { - connId = uint16(fastrand.Intn(0xFFFF)) + connId = uint16(randv2.IntN(0xFFFF)) _, loaded := t.udpInputMap.LoadOrStore(connId, pipe1) if !loaded { break diff --git a/transport/tuic/v5/packet.go b/transport/tuic/v5/packet.go index 86f839a5..8281a11e 100644 --- a/transport/tuic/v5/packet.go +++ b/transport/tuic/v5/packet.go @@ -12,7 +12,7 @@ import ( "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/quic-go" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) type quicStreamPacketConn struct { @@ -137,7 +137,7 @@ func (q *quicStreamPacketConn) WaitReadFrom() (data []byte, put func(), addr net func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { if len(p) > 0xffff { // uint16 max - return 0, &quic.DatagramTooLargeError{PeerMaxDatagramFrameSize: 0xffff} + return 0, &quic.DatagramTooLargeError{MaxDatagramPayloadSize: 0xffff} } if q.closed { return 0, net.ErrClosed @@ -157,7 +157,7 @@ func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err erro if err != nil { return } - pktId := uint16(fastrand.Uint32()) + pktId := uint16(randv2.Uint32()) packet := NewPacket(q.connId, pktId, 1, 0, uint16(len(p)), address, p) switch q.udpRelayMode { case common.QUIC: @@ -189,7 +189,7 @@ func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err erro var tooLarge *quic.DatagramTooLargeError if errors.As(err, &tooLarge) { - err = fragWriteNative(q.quicConn, packet, buf, int(tooLarge.PeerMaxDatagramFrameSize)-PacketOverHead) + err = fragWriteNative(q.quicConn, packet, buf, int(tooLarge.MaxDatagramPayloadSize)-PacketOverHead) } if err != nil { return diff --git a/transport/vless/vision/conn.go b/transport/vless/vision/conn.go index 5ad28134..69ccfba0 100644 --- a/transport/vless/vision/conn.go +++ b/transport/vless/vision/conn.go @@ -14,7 +14,7 @@ import ( "github.com/metacubex/mihomo/log" "github.com/gofrs/uuid/v5" - utls "github.com/sagernet/utls" + utls "github.com/metacubex/utls" ) var ( diff --git a/transport/vless/vision/padding.go b/transport/vless/vision/padding.go index e5f9dc85..dd9cb261 100644 --- a/transport/vless/vision/padding.go +++ b/transport/vless/vision/padding.go @@ -8,7 +8,7 @@ import ( "github.com/metacubex/mihomo/log" "github.com/gofrs/uuid/v5" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) const ( @@ -25,9 +25,9 @@ func WriteWithPadding(buffer *buf.Buffer, p []byte, command byte, userUUID *uuid if contentLen < 900 { if paddingTLS { //log.Debugln("long padding") - paddingLen = fastrand.Int31n(500) + 900 - contentLen + paddingLen = randv2.Int32N(500) + 900 - contentLen } else { - paddingLen = fastrand.Int31n(256) + paddingLen = randv2.Int32N(256) } } if userUUID != nil { @@ -49,9 +49,9 @@ func ApplyPadding(buffer *buf.Buffer, command byte, userUUID *uuid.UUID, padding if contentLen < 900 { if paddingTLS { //log.Debugln("long padding") - paddingLen = fastrand.Int31n(500) + 900 - contentLen + paddingLen = randv2.Int32N(500) + 900 - contentLen } else { - paddingLen = fastrand.Int31n(256) + paddingLen = randv2.Int32N(256) } } diff --git a/transport/vless/vision/vision.go b/transport/vless/vision/vision.go index 09299b23..3b9e9379 100644 --- a/transport/vless/vision/vision.go +++ b/transport/vless/vision/vision.go @@ -14,8 +14,8 @@ import ( tlsC "github.com/metacubex/mihomo/component/tls" "github.com/gofrs/uuid/v5" + utls "github.com/metacubex/utls" "github.com/sagernet/sing/common" - utls "github.com/sagernet/utls" ) var ErrNotTLS13 = errors.New("XTLS Vision based on TLS 1.3 outer connection") diff --git a/transport/vmess/conn.go b/transport/vmess/conn.go index 292137ab..b65a447d 100644 --- a/transport/vmess/conn.go +++ b/transport/vmess/conn.go @@ -6,6 +6,7 @@ import ( "crypto/cipher" "crypto/hmac" "crypto/md5" + "crypto/rand" "crypto/sha256" "encoding/binary" "errors" @@ -14,7 +15,7 @@ import ( "net" "time" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" "golang.org/x/crypto/chacha20poly1305" ) @@ -72,7 +73,7 @@ func (vc *Conn) sendRequest() error { buf.WriteByte(vc.respV) buf.WriteByte(OptionChunkStream) - p := fastrand.Intn(16) + p := randv2.IntN(16) // P Sec Reserve Cmd buf.WriteByte(byte(p<<4) | byte(vc.security)) buf.WriteByte(0) @@ -90,7 +91,7 @@ func (vc *Conn) sendRequest() error { // padding if p > 0 { padding := make([]byte, p) - fastrand.Read(padding) + rand.Read(padding) buf.Write(padding) } @@ -196,7 +197,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) - fastrand.Read(randBytes) + rand.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 f91c2766..a39ec5d9 100644 --- a/transport/vmess/h2.go +++ b/transport/vmess/h2.go @@ -7,7 +7,7 @@ import ( "net/http" "net/url" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" "golang.org/x/net/http2" ) @@ -27,7 +27,7 @@ type H2Config struct { func (hc *h2Conn) establishConn() error { preader, pwriter := io.Pipe() - host := hc.cfg.Hosts[fastrand.Intn(len(hc.cfg.Hosts))] + host := hc.cfg.Hosts[randv2.IntN(len(hc.cfg.Hosts))] path := hc.cfg.Path // TODO: connect use VMess Host instead of H2 Host req := http.Request{ diff --git a/transport/vmess/http.go b/transport/vmess/http.go index 6da9759e..3c66fe6b 100644 --- a/transport/vmess/http.go +++ b/transport/vmess/http.go @@ -3,6 +3,7 @@ package vmess import ( "bufio" "bytes" + "errors" "fmt" "net" "net/http" @@ -10,7 +11,7 @@ import ( "github.com/metacubex/mihomo/common/utils" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) type httpConn struct { @@ -54,16 +55,23 @@ func (hc *httpConn) Write(b []byte) (int, error) { return hc.Conn.Write(b) } - path := hc.cfg.Path[fastrand.Intn(len(hc.cfg.Path))] + if len(hc.cfg.Path) == 0 { + return -1, errors.New("path is empty") + } + + path := hc.cfg.Path[randv2.IntN(len(hc.cfg.Path))] host := hc.cfg.Host if header := hc.cfg.Headers["Host"]; len(header) != 0 { - host = header[fastrand.Intn(len(header))] + host = header[randv2.IntN(len(header))] } u := fmt.Sprintf("http://%s%s", net.JoinHostPort(host, "80"), path) - req, _ := http.NewRequest(utils.EmptyOr(hc.cfg.Method, http.MethodGet), u, bytes.NewBuffer(b)) + req, err := http.NewRequest(utils.EmptyOr(hc.cfg.Method, http.MethodGet), u, bytes.NewBuffer(b)) + if err != nil { + return 0, err + } for key, list := range hc.cfg.Headers { - req.Header.Set(key, list[fastrand.Intn(len(list))]) + req.Header.Set(key, list[randv2.IntN(len(list))]) } req.ContentLength = int64(len(b)) if err := req.Write(hc.Conn); err != nil { diff --git a/transport/vmess/vmess.go b/transport/vmess/vmess.go index 7c587c6a..22c77ab7 100644 --- a/transport/vmess/vmess.go +++ b/transport/vmess/vmess.go @@ -8,7 +8,7 @@ import ( "github.com/metacubex/mihomo/common/utils" "github.com/gofrs/uuid/v5" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) // Version of vmess @@ -78,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 := fastrand.Intn(len(c.user)) + r := randv2.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 f6914199..ca3a57b7 100644 --- a/transport/vmess/websocket.go +++ b/transport/vmess/websocket.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "crypto/rand" "crypto/sha1" "crypto/tls" "encoding/base64" @@ -25,7 +26,7 @@ import ( "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" - "github.com/zhangyunhao116/fastrand" + "github.com/metacubex/randv2" ) type websocketConn struct { @@ -150,7 +151,7 @@ func (wsc *websocketConn) WriteBuffer(buffer *buf.Buffer) error { } if wsc.state.ClientSide() { - maskKey := fastrand.Uint32() + maskKey := randv2.Uint32() binary.LittleEndian.PutUint32(header[1+payloadBitLength:], maskKey) N.MaskWebSocket(maskKey, data) } @@ -398,7 +399,7 @@ func streamWebsocketConn(ctx context.Context, conn net.Conn, c *WebsocketConfig, const nonceKeySize = 16 // NOTE: bts does not escape. bts := make([]byte, nonceKeySize) - if _, err = fastrand.Read(bts); err != nil { + if _, err = rand.Read(bts); err != nil { return nil, fmt.Errorf("rand read error: %w", err) } secKey = base64.StdEncoding.EncodeToString(bts) diff --git a/tunnel/connection.go b/tunnel/connection.go index e96545e8..1ea0678c 100644 --- a/tunnel/connection.go +++ b/tunnel/connection.go @@ -1,16 +1,109 @@ package tunnel import ( + "context" "errors" "net" "net/netip" "time" + "github.com/metacubex/mihomo/common/lru" N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) +type packetSender struct { + ctx context.Context + cancel context.CancelFunc + ch chan C.PacketAdapter + cache *lru.LruCache[string, netip.Addr] +} + +// newPacketSender return a chan based C.PacketSender +// It ensures that packets can be sent sequentially and without blocking +func newPacketSender() C.PacketSender { + ctx, cancel := context.WithCancel(context.Background()) + ch := make(chan C.PacketAdapter, senderCapacity) + return &packetSender{ + ctx: ctx, + cancel: cancel, + ch: ch, + cache: lru.New[string, netip.Addr](lru.WithSize[string, netip.Addr](senderCapacity)), + } +} + +func (s *packetSender) Process(pc C.PacketConn, proxy C.WriteBackProxy) { + for { + select { + case <-s.ctx.Done(): + return // sender closed + case packet := <-s.ch: + if proxy != nil { + proxy.UpdateWriteBack(packet) + } + if err := s.ResolveUDP(packet.Metadata()); err != nil { + log.Warnln("[UDP] Resolve Ip error: %s", err) + } else { + _ = handleUDPToRemote(packet, pc, packet.Metadata()) + } + packet.Drop() + } + } +} + +func (s *packetSender) dropAll() { + for { + select { + case data := <-s.ch: + data.Drop() // drop all data still in chan + default: + return // no data, exit goroutine + } + } +} + +func (s *packetSender) Send(packet C.PacketAdapter) { + select { + case <-s.ctx.Done(): + packet.Drop() // sender closed before Send() + return + default: + } + + select { + case s.ch <- packet: + // put ok, so don't drop packet, will process by other side of chan + case <-s.ctx.Done(): + packet.Drop() // sender closed when putting data to chan + default: + packet.Drop() // chan is full + } +} + +func (s *packetSender) Close() { + s.cancel() + s.dropAll() +} + +func (s *packetSender) ResolveUDP(metadata *C.Metadata) (err error) { + // local resolve UDP dns + if !metadata.Resolved() { + ip, ok := s.cache.Get(metadata.Host) + if !ok { + ip, err = resolver.ResolveIP(s.ctx, metadata.Host) + if err != nil { + return err + } + s.cache.Set(metadata.Host, ip) + } + + metadata.DstIP = ip + } + return nil +} + func handleUDPToRemote(packet C.UDPPacket, pc C.PacketConn, metadata *C.Metadata) error { addr := metadata.UDPAddr() if addr == nil { @@ -26,8 +119,9 @@ func handleUDPToRemote(packet C.UDPPacket, pc C.PacketConn, metadata *C.Metadata return nil } -func handleUDPToLocal(writeBack C.WriteBack, pc N.EnhancePacketConn, key string, oAddrPort netip.AddrPort, fAddr netip.Addr) { +func handleUDPToLocal(writeBack C.WriteBack, pc N.EnhancePacketConn, sender C.PacketSender, key string, oAddrPort netip.AddrPort, fAddr netip.Addr) { defer func() { + sender.Close() _ = pc.Close() closeAllLocalCoon(key) natTable.Delete(key) diff --git a/tunnel/dns_dialer.go b/tunnel/dns_dialer.go new file mode 100644 index 00000000..1839869b --- /dev/null +++ b/tunnel/dns_dialer.go @@ -0,0 +1,186 @@ +package tunnel + +// WARNING: all function in this file should only be using in dns module + +import ( + "context" + "fmt" + "net" + "strings" + + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/resolver" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/tunnel/statistic" +) + +const DnsRespectRules = "RULES" + +type DNSDialer struct { + r resolver.Resolver + proxyAdapter C.ProxyAdapter + proxyName string + opts []dialer.Option +} + +func NewDNSDialer(r resolver.Resolver, proxyAdapter C.ProxyAdapter, proxyName string, opts ...dialer.Option) *DNSDialer { + return &DNSDialer{r: r, proxyAdapter: proxyAdapter, proxyName: proxyName, opts: opts} +} + +func (d *DNSDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + r := d.r + proxyName := d.proxyName + proxyAdapter := d.proxyAdapter + opts := d.opts + var rule C.Rule + metadata := &C.Metadata{ + NetWork: C.TCP, + Type: C.INNER, + } + err := metadata.SetRemoteAddress(addr) // tcp can resolve host by remote + if err != nil { + return nil, err + } + if !strings.Contains(network, "tcp") { + metadata.NetWork = C.UDP + if !metadata.Resolved() { + // udp must resolve host first + dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) + if err != nil { + return nil, err + } + metadata.DstIP = dstIP + } + } + + if proxyAdapter == nil && len(proxyName) != 0 { + if proxyName == DnsRespectRules { + if !metadata.Resolved() { + // resolve here before resolveMetadata to avoid its inner resolver.ResolveIP + dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) + if err != nil { + return nil, err + } + metadata.DstIP = dstIP + } + proxyAdapter, rule, err = resolveMetadata(metadata) + if err != nil { + return nil, err + } + } else { + var ok bool + proxyAdapter, ok = Proxies()[proxyName] + if !ok { + opts = append(opts, dialer.WithInterface(proxyName)) + } + } + } + + if metadata.NetWork == C.TCP { + if proxyAdapter == nil { + opts = append(opts, dialer.WithResolver(r)) + return dialer.DialContext(ctx, network, addr, opts...) + } + + if proxyAdapter.IsL3Protocol(metadata) { // L3 proxy should resolve domain before to avoid loopback + if !metadata.Resolved() { + dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) + if err != nil { + return nil, err + } + metadata.DstIP = dstIP + } + metadata.Host = "" // clear host to avoid double resolve in proxy + } + + conn, err := proxyAdapter.DialContext(ctx, metadata, opts...) + if err != nil { + logMetadataErr(metadata, rule, proxyAdapter, err) + return nil, err + } + logMetadata(metadata, rule, conn) + + conn = statistic.NewTCPTracker(conn, statistic.DefaultManager, metadata, rule, 0, 0, false) + + return conn, nil + } else { + if proxyAdapter == nil { + return dialer.DialContext(ctx, network, metadata.AddrPort().String(), opts...) + } + + if !proxyAdapter.SupportUDP() { + return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) + } + + packetConn, err := proxyAdapter.ListenPacketContext(ctx, metadata, opts...) + if err != nil { + logMetadataErr(metadata, rule, proxyAdapter, err) + return nil, err + } + logMetadata(metadata, rule, packetConn) + + packetConn = statistic.NewUDPTracker(packetConn, statistic.DefaultManager, metadata, rule, 0, 0, false) + + return N.NewBindPacketConn(packetConn, metadata.UDPAddr()), nil + } + +} + +func (d *DNSDialer) ListenPacket(ctx context.Context, network, addr string) (net.PacketConn, error) { + r := d.r + proxyAdapter := d.proxyAdapter + proxyName := d.proxyName + opts := d.opts + metadata := &C.Metadata{ + NetWork: C.UDP, + Type: C.INNER, + } + err := metadata.SetRemoteAddress(addr) + if err != nil { + return nil, err + } + if !metadata.Resolved() { + // udp must resolve host first + dstIP, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) + if err != nil { + return nil, err + } + metadata.DstIP = dstIP + } + + var rule C.Rule + if proxyAdapter == nil { + if proxyName == DnsRespectRules { + proxyAdapter, rule, err = resolveMetadata(metadata) + if err != nil { + return nil, err + } + } else { + var ok bool + proxyAdapter, ok = Proxies()[proxyName] + if !ok { + opts = append(opts, dialer.WithInterface(proxyName)) + } + } + } + + if proxyAdapter == nil { + return dialer.NewDialer(opts...).ListenPacket(ctx, network, "", metadata.AddrPort()) + } + + if !proxyAdapter.SupportUDP() { + return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) + } + + packetConn, err := proxyAdapter.ListenPacketContext(ctx, metadata, opts...) + if err != nil { + logMetadataErr(metadata, rule, proxyAdapter, err) + return nil, err + } + logMetadata(metadata, rule, packetConn) + + packetConn = statistic.NewUDPTracker(packetConn, statistic.DefaultManager, metadata, rule, 0, 0, false) + + return packetConn, nil +} diff --git a/tunnel/mode.go b/tunnel/mode.go index 9c3e0fc0..a4d8a0e3 100644 --- a/tunnel/mode.go +++ b/tunnel/mode.go @@ -21,18 +21,6 @@ const ( Direct ) -// UnmarshalJSON unserialize Mode -func (m *TunnelMode) UnmarshalJSON(data []byte) error { - var tp string - json.Unmarshal(data, &tp) - mode, exist := ModeMapping[strings.ToLower(tp)] - if !exist { - return errors.New("invalid mode") - } - *m = mode - return nil -} - // UnmarshalYAML unserialize Mode with yaml func (m *TunnelMode) UnmarshalYAML(unmarshal func(any) error) error { var tp string @@ -45,9 +33,26 @@ func (m *TunnelMode) UnmarshalYAML(unmarshal func(any) error) error { return nil } -// MarshalJSON serialize Mode -func (m TunnelMode) MarshalJSON() ([]byte, error) { - return json.Marshal(m.String()) +// UnmarshalJSON unserialize Mode +func (m *TunnelMode) UnmarshalJSON(data []byte) error { + var tp string + json.Unmarshal(data, &tp) + mode, exist := ModeMapping[strings.ToLower(tp)] + if !exist { + return errors.New("invalid mode") + } + *m = mode + return nil +} + +// UnmarshalText unserialize Mode +func (m *TunnelMode) UnmarshalText(data []byte) error { + mode, exist := ModeMapping[strings.ToLower(string(data))] + if !exist { + return errors.New("invalid mode") + } + *m = mode + return nil } // MarshalYAML serialize TunnelMode with yaml @@ -55,6 +60,16 @@ func (m TunnelMode) MarshalYAML() (any, error) { return m.String(), nil } +// MarshalJSON serialize Mode +func (m TunnelMode) MarshalJSON() ([]byte, error) { + return json.Marshal(m.String()) +} + +// MarshalText serialize Mode +func (m TunnelMode) MarshalText() ([]byte, error) { + return []byte(m.String()), nil +} + func (m TunnelMode) String() string { switch m { case Global: diff --git a/tunnel/statistic/manager.go b/tunnel/statistic/manager.go index 08747118..3f2770c2 100644 --- a/tunnel/statistic/manager.go +++ b/tunnel/statistic/manager.go @@ -7,7 +7,7 @@ import ( "github.com/metacubex/mihomo/common/atomic" "github.com/puzpuzpuz/xsync/v3" - "github.com/shirou/gopsutil/v3/process" + "github.com/shirou/gopsutil/v4/process" ) var DefaultManager *Manager @@ -114,10 +114,8 @@ func (m *Manager) handle() { ticker := time.NewTicker(time.Second) for range ticker.C { - m.uploadBlip.Store(m.uploadTemp.Load()) - m.uploadTemp.Store(0) - m.downloadBlip.Store(m.downloadTemp.Load()) - m.downloadTemp.Store(0) + m.uploadBlip.Store(m.uploadTemp.Swap(0)) + m.downloadBlip.Store(m.downloadTemp.Swap(0)) } } diff --git a/tunnel/statistic/tracker.go b/tunnel/statistic/tracker.go index 0bf7995d..ca592d97 100644 --- a/tunnel/statistic/tracker.go +++ b/tunnel/statistic/tracker.go @@ -117,24 +117,19 @@ func (tt *tcpTracker) Upstream() any { } 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 { - return "" + if addr != nil { + if addrPort, err := netip.ParseAddrPort(addr.String()); err == nil && addrPort.Addr().IsValid() { + return addrPort.Addr().String() } } + if conn != nil { + return conn.RemoteDestination() + } + return "" } func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule, uploadTotal int64, downloadTotal int64, pushToManager bool) *tcpTracker { - if conn != nil { - metadata.RemoteDst = parseRemoteDestination(conn.RemoteAddr(), conn) - } + metadata.RemoteDst = parseRemoteDestination(conn.RemoteAddr(), conn) t := &tcpTracker{ Conn: conn, diff --git a/tunnel/status.go b/tunnel/status.go index d81dd45e..388d597b 100644 --- a/tunnel/status.go +++ b/tunnel/status.go @@ -22,18 +22,6 @@ const ( 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 @@ -46,9 +34,26 @@ func (s *TunnelStatus) UnmarshalYAML(unmarshal func(any) error) error { return nil } -// MarshalJSON serialize Status -func (s TunnelStatus) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) +// UnmarshalJSON unserialize Status +func (s *TunnelStatus) UnmarshalJSON(data []byte) error { + var tp string + json.Unmarshal(data, &tp) + status, exist := StatusMapping[strings.ToLower(tp)] + if !exist { + return errors.New("invalid status") + } + *s = status + return nil +} + +// UnmarshalText unserialize Status +func (s *TunnelStatus) UnmarshalText(data []byte) error { + status, exist := StatusMapping[strings.ToLower(string(data))] + if !exist { + return errors.New("invalid status") + } + *s = status + return nil } // MarshalYAML serialize TunnelMode with yaml @@ -56,6 +61,16 @@ func (s TunnelStatus) MarshalYAML() (any, error) { return s.String(), nil } +// MarshalJSON serialize Status +func (s TunnelStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +// MarshalText serialize Status +func (s TunnelStatus) MarshalText() ([]byte, error) { + return []byte(s.String()), nil +} + func (s TunnelStatus) String() string { switch s { case Suspend: diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index f8fdcf11..a4486df7 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -8,10 +8,13 @@ import ( "net/netip" "path/filepath" "runtime" + "strings" "sync" "time" N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/loopback" "github.com/metacubex/mihomo/component/nat" P "github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/component/resolver" @@ -25,19 +28,29 @@ import ( "github.com/metacubex/mihomo/tunnel/statistic" ) +const ( + queueCapacity = 64 // chan capacity tcpQueue and udpQueue + senderCapacity = 128 // chan capacity of PacketSender +) + var ( - status = newAtomicStatus(Suspend) - tcpQueue = make(chan C.ConnContext, 200) - udpQueue = make(chan C.PacketAdapter, 200) - natTable = nat.New() - rules []C.Rule - listeners = make(map[string]C.InboundListener) - subRules map[string][]C.Rule - proxies = make(map[string]C.Proxy) - providers map[string]provider.ProxyProvider - ruleProviders map[string]provider.RuleProvider - sniffingEnable = false - configMux sync.RWMutex + status = newAtomicStatus(Suspend) + udpInit sync.Once + udpQueues []chan C.PacketAdapter + natTable = nat.New() + rules []C.Rule + listeners = make(map[string]C.InboundListener) + subRules map[string][]C.Rule + proxies = make(map[string]C.Proxy) + providers map[string]provider.ProxyProvider + ruleProviders map[string]provider.RuleProvider + configMux sync.RWMutex + + // for compatibility, lazy init + tcpQueue chan C.ConnContext + tcpInOnce sync.Once + udpQueue chan C.PacketAdapter + udpInOnce sync.Once // Outbound Rule mode = Rule @@ -48,22 +61,51 @@ var ( findProcessMode P.FindProcessMode fakeIPRange netip.Prefix + + snifferDispatcher *sniffer.Dispatcher + sniffingEnable = false + + ruleUpdateCallback = utils.NewCallback[provider.RuleProvider]() ) type tunnel struct{} -var Tunnel C.Tunnel = tunnel{} +var Tunnel = tunnel{} +var _ C.Tunnel = Tunnel +var _ provider.Tunnel = Tunnel func (t tunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) { connCtx := icontext.NewConnContext(conn, metadata) handleTCPConn(connCtx) } +func initUDP() { + numUDPWorkers := 4 + if num := runtime.GOMAXPROCS(0); num > numUDPWorkers { + numUDPWorkers = num + } + + udpQueues = make([]chan C.PacketAdapter, numUDPWorkers) + for i := 0; i < numUDPWorkers; i++ { + queue := make(chan C.PacketAdapter, queueCapacity) + udpQueues[i] = queue + go processUDP(queue) + } +} + func (t tunnel) HandleUDPPacket(packet C.UDPPacket, metadata *C.Metadata) { + udpInit.Do(initUDP) + packetAdapter := C.NewPacketAdapter(packet, metadata) + key := packetAdapter.Key() + + hash := utils.MapHash(key) + queueNo := uint(hash) % uint(len(udpQueues)) + select { - case udpQueue <- packetAdapter: + case udpQueues[queueNo] <- packetAdapter: default: + packet.Drop() } } @@ -71,6 +113,18 @@ func (t tunnel) NatTable() C.NatTable { return natTable } +func (t tunnel) Providers() map[string]provider.ProxyProvider { + return providers +} + +func (t tunnel) RuleProviders() map[string]provider.RuleProvider { + return ruleProviders +} + +func (t tunnel) RuleUpdateCallback() *utils.Callback[provider.RuleProvider] { + return ruleUpdateCallback +} + func OnSuspend() { status.Store(Suspend) } @@ -96,7 +150,7 @@ func FakeIPRange() netip.Prefix { } func SetSniffing(b bool) { - if sniffer.Dispatcher.Enable() { + if snifferDispatcher.Enable() { configMux.Lock() sniffingEnable = b configMux.Unlock() @@ -107,19 +161,31 @@ func IsSniffing() bool { return sniffingEnable } -func init() { - go process() -} - // TCPIn return fan-in queue // Deprecated: using Tunnel instead func TCPIn() chan<- C.ConnContext { + tcpInOnce.Do(func() { + tcpQueue = make(chan C.ConnContext, queueCapacity) + go func() { + for connCtx := range tcpQueue { + go handleTCPConn(connCtx) + } + }() + }) return tcpQueue } // UDPIn return fan-in udp queue // Deprecated: using Tunnel instead func UDPIn() chan<- C.PacketAdapter { + udpInOnce.Do(func() { + udpQueue = make(chan C.PacketAdapter, queueCapacity) + go func() { + for packet := range udpQueue { + Tunnel.HandleUDPPacket(packet, packet.Metadata()) + } + }() + }) return udpQueue } @@ -189,9 +255,9 @@ func UpdateListeners(newListeners map[string]C.InboundListener) { listeners = newListeners } -func UpdateSniffer(dispatcher *sniffer.SnifferDispatcher) { +func UpdateSniffer(dispatcher *sniffer.Dispatcher) { configMux.Lock() - sniffer.Dispatcher = dispatcher + snifferDispatcher = dispatcher sniffingEnable = dispatcher.Enable() configMux.Unlock() } @@ -206,6 +272,10 @@ func SetMode(m TunnelMode) { mode = m } +func FindProcessMode() P.FindProcessMode { + return findProcessMode +} + // SetFindProcessMode replace SetAlwaysFindProcess // always find process info if legacyAlways = true or mode.Always() = true, may be increase many memory func SetFindProcessMode(mode P.FindProcessMode) { @@ -217,29 +287,6 @@ func isHandle(t C.Type) bool { return status == Running || (status == Inner && t == C.INNER) } -// processUDP starts a loop to handle udp packet -func processUDP() { - queue := udpQueue - for conn := range queue { - handleUDPConn(conn) - } -} - -func process() { - numUDPWorkers := 4 - if num := runtime.GOMAXPROCS(0); num > numUDPWorkers { - numUDPWorkers = num - } - for i := 0; i < numUDPWorkers; i++ { - go processUDP() - } - - queue := tcpQueue - for conn := range queue { - go handleTCPConn(conn) - } -} - func needLookupIP(metadata *C.Metadata) bool { return resolver.MappingEnabled() && metadata.Host == "" && metadata.DstIP.IsValid() } @@ -299,6 +346,13 @@ func resolveMetadata(metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err erro return } +// processUDP starts a loop to handle udp packet +func processUDP(queue chan C.PacketAdapter) { + for conn := range queue { + handleUDPConn(conn) + } +} + func handleUDPConn(packet C.PacketAdapter) { if !isHandle(packet.Metadata().Type) { packet.Drop() @@ -324,114 +378,62 @@ func handleUDPConn(packet C.PacketAdapter) { return } - if sniffer.Dispatcher.Enable() && sniffingEnable { - sniffer.Dispatcher.UDPSniff(packet) + if sniffingEnable && snifferDispatcher.Enable() { + snifferDispatcher.UDPSniff(packet) } - // local resolve UDP dns - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(context.Background(), metadata.Host) - if err != nil { - return - } - metadata.DstIP = ip - } - - key := packet.LocalAddr().String() - - handle := func() bool { - pc, proxy := natTable.Get(key) - if pc != nil { - if proxy != nil { - proxy.UpdateWriteBack(packet) + key := packet.Key() + sender, loaded := natTable.GetOrCreate(key, newPacketSender) + if !loaded { + dial := func() (C.PacketConn, C.WriteBackProxy, error) { + if err := sender.ResolveUDP(metadata); err != nil { + log.Warnln("[UDP] Resolve Ip error: %s", err) + return nil, nil, err } - _ = handleUDPToRemote(packet, pc, metadata) - return true - } - return false - } - if handle() { - packet.Drop() - return - } + proxy, rule, err := resolveMetadata(metadata) + if err != nil { + log.Warnln("[UDP] Parse metadata failed: %s", err.Error()) + return nil, nil, err + } - cond, loaded := natTable.GetOrCreateLock(key) + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout) + defer cancel() + rawPc, err := retry(ctx, func(ctx context.Context) (C.PacketConn, error) { + return proxy.ListenPacketContext(ctx, metadata.Pure()) + }, func(err error) { + logMetadataErr(metadata, rule, proxy, err) + }) + if err != nil { + return nil, nil, err + } + logMetadata(metadata, rule, rawPc) - go func() { - defer packet.Drop() + pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule, 0, 0, true) - if loaded { - cond.L.Lock() - cond.Wait() - handle() - cond.L.Unlock() - return + if rawPc.Chains().Last() == "REJECT-DROP" { + _ = pc.Close() + return nil, nil, errors.New("rejected drop packet") + } + + oAddrPort := metadata.AddrPort() + writeBackProxy := nat.NewWriteBackProxy(packet) + + go handleUDPToLocal(writeBackProxy, pc, sender, key, oAddrPort, fAddr) + return pc, writeBackProxy, nil } - defer func() { - natTable.DeleteLock(key) - cond.Broadcast() + go func() { + pc, proxy, err := dial() + if err != nil { + sender.Close() + natTable.Delete(key) + return + } + sender.Process(pc, proxy) }() - - proxy, rule, err := resolveMetadata(metadata) - if err != nil { - log.Warnln("[UDP] Parse metadata failed: %s", err.Error()) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout) - defer cancel() - rawPc, err := retry(ctx, func(ctx context.Context) (C.PacketConn, error) { - return proxy.ListenPacketContext(ctx, metadata.Pure()) - }, func(err error) { - if rule == nil { - log.Warnln( - "[UDP] dial %s %s --> %s error: %s", - proxy.Name(), - metadata.SourceDetail(), - metadata.RemoteAddress(), - err.Error(), - ) - } else { - log.Warnln("[UDP] dial %s (match %s/%s) %s --> %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.SourceDetail(), metadata.RemoteAddress(), err.Error()) - } - }) - if err != nil { - return - } - - pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule, 0, 0, true) - - switch true { - case metadata.SpecialProxy != "": - log.Infoln("[UDP] %s --> %s using %s", metadata.SourceDetail(), metadata.RemoteAddress(), metadata.SpecialProxy) - case rule != nil: - if rule.Payload() != "" { - log.Infoln("[UDP] %s --> %s match %s using %s", metadata.SourceDetail(), metadata.RemoteAddress(), fmt.Sprintf("%s(%s)", rule.RuleType().String(), rule.Payload()), rawPc.Chains().String()) - if rawPc.Chains().Last() == "REJECT-DROP" { - pc.Close() - return - } - } else { - log.Infoln("[UDP] %s --> %s match %s using %s", metadata.SourceDetail(), metadata.RemoteAddress(), rule.Payload(), rawPc.Chains().String()) - } - case mode == Global: - log.Infoln("[UDP] %s --> %s using GLOBAL", metadata.SourceDetail(), metadata.RemoteAddress()) - case mode == Direct: - log.Infoln("[UDP] %s --> %s using DIRECT", metadata.SourceDetail(), metadata.RemoteAddress()) - default: - log.Infoln("[UDP] %s --> %s doesn't match any rule using DIRECT", metadata.SourceDetail(), metadata.RemoteAddress()) - } - - oAddrPort := metadata.AddrPort() - writeBackProxy := nat.NewWriteBackProxy(packet) - natTable.Set(key, pc, writeBackProxy) - - go handleUDPToLocal(writeBackProxy, pc, key, oAddrPort, fAddr) - - handle() - }() + } + sender.Send(packet) // nonblocking } func handleTCPConn(connCtx C.ConnContext) { @@ -458,10 +460,10 @@ func handleTCPConn(connCtx C.ConnContext) { conn := connCtx.Conn() conn.ResetPeeked() // reset before sniffer - if sniffer.Dispatcher.Enable() && sniffingEnable { + if sniffingEnable && snifferDispatcher.Enable() { // Try to sniff a domain when `preHandleMetadata` failed, this is usually // caused by a "Fake DNS record missing" error when enhanced-mode is fake-ip. - if sniffer.Dispatcher.TCPSniff(conn, metadata) { + if snifferDispatcher.TCPSniff(conn, metadata) { // we now have a domain name preHandleFailed = false } @@ -538,48 +540,18 @@ func handleTCPConn(connCtx C.ConnContext) { } return }, func(err error) { - if rule == nil { - log.Warnln( - "[TCP] dial %s %s --> %s error: %s", - proxy.Name(), - metadata.SourceDetail(), - metadata.RemoteAddress(), - err.Error(), - ) - } else { - log.Warnln("[TCP] dial %s (match %s/%s) %s --> %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.SourceDetail(), metadata.RemoteAddress(), err.Error()) - } + logMetadataErr(metadata, rule, proxy, err) }) if err != nil { return } + logMetadata(metadata, rule, remoteConn) - remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule, 0, int64(peekLen), true) + remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule, int64(peekLen), 0, true) defer func(remoteConn C.Conn) { _ = remoteConn.Close() }(remoteConn) - switch true { - case metadata.SpecialProxy != "": - log.Infoln("[TCP] %s --> %s using %s", metadata.SourceDetail(), metadata.RemoteAddress(), metadata.SpecialProxy) - case rule != nil: - if rule.Payload() != "" { - log.Infoln("[TCP] %s --> %s match %s using %s", metadata.SourceDetail(), metadata.RemoteAddress(), fmt.Sprintf("%s(%s)", rule.RuleType().String(), rule.Payload()), remoteConn.Chains().String()) - } else { - log.Infoln("[TCP] %s --> %s match %s using %s", metadata.SourceDetail(), metadata.RemoteAddress(), rule.RuleType().String(), remoteConn.Chains().String()) - } - case mode == Global: - log.Infoln("[TCP] %s --> %s using GLOBAL", metadata.SourceDetail(), metadata.RemoteAddress()) - case mode == Direct: - log.Infoln("[TCP] %s --> %s using DIRECT", metadata.SourceDetail(), metadata.RemoteAddress()) - default: - log.Infoln( - "[TCP] %s --> %s doesn't match any rule using DIRECT", - metadata.SourceDetail(), - metadata.RemoteAddress(), - ) - } - _ = conn.SetReadDeadline(time.Now()) // stop unfinished peek peekMutex.Lock() defer peekMutex.Unlock() @@ -587,6 +559,33 @@ func handleTCPConn(connCtx C.ConnContext) { handleSocket(conn, remoteConn) } +func logMetadataErr(metadata *C.Metadata, rule C.Rule, proxy C.ProxyAdapter, err error) { + if rule == nil { + log.Warnln("[%s] dial %s %s --> %s error: %s", strings.ToUpper(metadata.NetWork.String()), proxy.Name(), metadata.SourceDetail(), metadata.RemoteAddress(), err.Error()) + } else { + log.Warnln("[%s] dial %s (match %s/%s) %s --> %s error: %s", strings.ToUpper(metadata.NetWork.String()), proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.SourceDetail(), metadata.RemoteAddress(), err.Error()) + } +} + +func logMetadata(metadata *C.Metadata, rule C.Rule, remoteConn C.Connection) { + switch { + case metadata.SpecialProxy != "": + log.Infoln("[%s] %s --> %s using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), metadata.SpecialProxy) + case rule != nil: + if rule.Payload() != "" { + log.Infoln("[%s] %s --> %s match %s using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), fmt.Sprintf("%s(%s)", rule.RuleType().String(), rule.Payload()), remoteConn.Chains().String()) + } else { + log.Infoln("[%s] %s --> %s match %s using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), rule.RuleType().String(), remoteConn.Chains().String()) + } + case mode == Global: + log.Infoln("[%s] %s --> %s using GLOBAL", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress()) + case mode == Direct: + log.Infoln("[%s] %s --> %s using DIRECT", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress()) + default: + log.Infoln("[%s] %s --> %s doesn't match any rule using %s", strings.ToUpper(metadata.NetWork.String()), metadata.SourceDetail(), metadata.RemoteAddress(), remoteConn.Chains().Last()) + } +} + func shouldResolveIP(rule C.Rule, metadata *C.Metadata) bool { return rule.ShouldResolveIP() && metadata.Host != "" && !metadata.DstIP.IsValid() } @@ -596,7 +595,7 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { defer configMux.RUnlock() var ( resolved bool - attemptProcessLookup = true + attemptProcessLookup = metadata.Type != C.INNER ) if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok { @@ -626,17 +625,21 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { // normal check for process uid, path, err := P.FindProcessName(metadata.NetWork.String(), metadata.SrcIP, int(metadata.SrcPort)) if err != nil { - log.Debugln("[Process] find process %s error: %v", metadata.String(), err) + log.Debugln("[Process] find process error for %s: %v", metadata.String(), err) } else { metadata.Process = filepath.Base(path) metadata.ProcessPath = path metadata.Uid = uid + + if pkg, err := P.FindPackageName(metadata); err == nil { // for android (not CMFA) package names + metadata.Process = pkg + } } } else { // check package names pkg, err := P.FindPackageName(metadata) if err != nil { - log.Debugln("[Process] find process %s error: %v", metadata.String(), err) + log.Debugln("[Process] find process error for %s: %v", metadata.String(), err) } else { metadata.Process = pkg } @@ -694,6 +697,9 @@ func shouldStopRetry(err error) bool { if errors.Is(err, resolver.ErrIPv6Disabled) { return true } + if errors.Is(err, loopback.ErrReject) { + return true + } return false }