Compare commits

..

36 Commits

Author SHA1 Message Date
Larvan2
81947304bc Merge branch 'refs/heads/Alpha' into Meta 2024-04-29 14:22:39 +08:00
Larvan2
e27d7c010f Merge branch 'Alpha' into Meta 2024-03-29 19:50:16 +08:00
Larvan2
582e94ff4d Merge branch 'Alpha' into Meta 2024-03-27 18:43:06 +08:00
Larvan2
a034421a58 Merge branch 'Alpha' into Meta 2024-02-02 20:15:27 +08:00
Larvan2
dc2108c174 Merge branch 'Alpha' into Meta 2024-01-02 15:21:37 +08:00
wwqgtxx
bda71dbfa1 Merge branch 'Alpha' into Meta 2023-12-03 08:44:30 +08:00
wwqgtxx
253b023442 Merge branch 'Alpha' into Meta 2023-11-03 21:59:44 +08:00
wwqgtxx
8293b7fdae Merge branch 'Beta' into Meta
# Conflicts:
#	.github/workflows/docker.yaml
#	.github/workflows/prerelease.yml
2023-02-19 01:25:34 +08:00
wwqgtxx
0ba415866e Merge branch 'Alpha' into Beta 2023-02-19 01:25:01 +08:00
Larvan2
53b41ca166 Chore: Add action for deleting old workflow 2023-01-30 18:17:22 +08:00
metacubex
8a75f78e63 chore: adjust Dockerfile 2023-01-12 02:24:12 +08:00
metacubex
d9692c6366 Merge branch 'Beta' into Meta 2023-01-12 02:15:14 +08:00
metacubex
f4b0062dfc Merge branch 'Alpha' into Beta 2023-01-12 02:14:49 +08:00
metacubex
b9ffc82e53 Merge branch 'Beta' into Meta 2023-01-12 01:33:56 +08:00
metacubex
78aaea6a45 Merge branch 'Alpha' into Beta 2023-01-12 01:33:16 +08:00
cubemaze
3645fbf161 Merge pull request #327 from Rasphino/Meta
Update flake.nix hash
2023-01-08 00:29:29 +08:00
Rasphino
a1d0f22132 fix: update flake.nix hash 2023-01-07 23:38:32 +08:00
metacubex
fa73b0f4bf Merge remote-tracking branch 'origin/Beta' into Meta 2023-01-01 19:41:36 +08:00
metacubex
3b76a8b839 Merge remote-tracking branch 'origin/Alpha' into Beta 2023-01-01 19:40:36 +08:00
cubemaze
667f42dcdc Merge pull request #282 from tdjnodj/Meta
Update README.md
2022-12-03 17:23:51 +08:00
tdjnodj
dfbe09860f Update README.md 2022-12-03 17:17:08 +08:00
metacubex
9e20f9c26a chore: update dependencies 2022-11-28 20:33:10 +08:00
Skimmle
f968d0cb82 chore: update github action 2022-11-26 20:16:12 +08:00
metacubex
2ad84f4379 Merge branch 'Beta' into Meta 2022-11-02 18:08:22 +08:00
metacubex
c7aa16426f Merge branch 'Alpha' into Beta 2022-11-02 18:07:29 +08:00
Skyxim
5987f8e3b5 Merge branch 'Beta' into Meta 2022-08-29 13:08:29 +08:00
Skyxim
3a8eb72de2 Merge branch 'Alpha' into Beta 2022-08-29 13:08:22 +08:00
zhudan
33abbdfd24 Merge pull request #174 from MetaCubeX/Alpha
Alpha
2022-08-29 11:24:07 +08:00
zhudan
0703d6cbff Merge pull request #173 from MetaCubeX/Alpha
Alpha
2022-08-29 11:22:14 +08:00
Skyxim
10d2d14938 Merge branch 'Beta' into Meta
# Conflicts:
#	rules/provider/classical_strategy.go
2022-07-02 10:41:41 +08:00
wwqgtxx
691cf1d8d6 Merge pull request #94 from bash99/Meta
Update README.md
2022-06-15 19:15:51 +08:00
bash99
d1decb8e58 Update README.md
add permissions for systemctl services
clash-dashboard change to updated one
2022-06-15 14:00:05 +08:00
Skyxim
7d04904109 fix: leak dns when domain in hosts list 2022-06-11 18:51:26 +08:00
Skyxim
a5acd3aa97 refactor: clear linkname,reduce cycle dependencies,transport init geosite function 2022-06-11 18:51:22 +08:00
Skyxim
eea9a12560 fix: 规则匹配默认策略组返回错误 2022-06-09 14:18:35 +08:00
adlyq
0a4570b55c fix: group filter touch provider 2022-06-09 14:18:29 +08:00
278 changed files with 10679 additions and 8474 deletions

16
.github/workflows/Delete.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Delete old workflow runs
on:
schedule:
- cron: '0 0 1 * *'
# Run monthly, at 00:00 on the 1st day of month.
jobs:
del_runs:
runs-on: ubuntu-latest
steps:
- name: Delete workflow runs
uses: GitRML/delete-workflow-runs@main
with:
token: ${{ secrets.AUTH_PAT }}
repository: ${{ github.repository }}
retain_days: 30

View File

@@ -1,10 +1,6 @@
name: Build
on:
workflow_dispatch:
inputs:
version:
description: "Tag version to release"
required: true
push:
paths-ignore:
- "docs/**"
@@ -17,8 +13,9 @@ on:
pull_request_target:
branches:
- Alpha
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
@@ -40,10 +37,10 @@ jobs:
- { 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, 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: 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: mips64, output: mips64 }
- { goos: linux, goarch: mips64le, output: mips64le }
- { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1' }
@@ -67,29 +64,11 @@ 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' }
@@ -107,7 +86,7 @@ jobs:
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.goarch != 'loong64' }}
uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version: '1.22'
- name: Set up Go
if: ${{ matrix.jobs.goversion != '' && matrix.jobs.goarch != 'loong64' }}
@@ -115,71 +94,28 @@ jobs:
with:
go-version: ${{ matrix.jobs.goversion }}
- name: Set up Go1.22 loongarch abi1
- name: Set up Go1.21 loongarch abi1
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
run: |
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.22.4/go1.22.4.linux-amd64-abi1.tar.gz
sudo tar zxf go1.22.4.linux-amd64-abi1.tar.gz -C /usr/local
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
echo "/usr/local/go/bin" >> $GITHUB_PATH
- name: Set up Go1.22 loongarch abi2
- name: Set up Go1.21 loongarch abi2
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '2' }}
run: |
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.22.4/go1.22.4.linux-amd64-abi2.tar.gz
sudo tar zxf go1.22.4.linux-amd64-abi2.tar.gz -C /usr/local
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
# 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: |
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.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }}
run: echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
if: ${{github.ref_name=='Alpha'}}
run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{ github.event_name != 'workflow_dispatch' && github.ref_name == 'Alpha' }}
run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
if: ${{github.ref_name=='' || github.ref_type=='tag'}}
run: echo "VERSION=$(git describe --tags)" >> $GITHUB_ENV
shell: bash
- name: Set Time Variable
@@ -187,14 +123,13 @@ 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: r27
ndk-version: r26c
- name: Set NDK path
if: ${{ matrix.jobs.goos == 'android' }}
@@ -207,8 +142,6 @@ jobs:
if: ${{ matrix.jobs.test == 'test' }}
run: |
go test ./...
echo "---test with_gvisor---"
go test ./... -tags "with_gvisor" -count=1
- name: Update CA
run: |
@@ -221,10 +154,10 @@ jobs:
GOOS: ${{matrix.jobs.goos}}
GOARCH: ${{matrix.jobs.goarch}}
GOAMD64: ${{matrix.jobs.goamd64}}
GOARM: ${{matrix.jobs.goarm}}
GOMIPS: ${{matrix.jobs.gomips}}
GOARM: ${{matrix.jobs.arm}}
GOMIPS: ${{matrix.jobs.mips}}
run: |
go env
echo $CGO_ENABLED
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
@@ -241,10 +174,6 @@ 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
@@ -309,7 +238,7 @@ jobs:
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: "${{ matrix.jobs.goos }}-${{ matrix.jobs.output }}"
name: ${{ matrix.jobs.goos }}-${{ matrix.jobs.output }}
path: |
mihomo*.gz
mihomo*.deb
@@ -319,7 +248,7 @@ jobs:
Upload-Prerelease:
permissions: write-all
if: ${{ github.event_name != 'workflow_dispatch' && github.ref_type == 'branch' && !startsWith(github.event_name, 'pull_request') }}
if: ${{ github.ref_type == 'branch' && !startsWith(github.event_name, 'pull_request') }}
needs: [build]
runs-on: ubuntu-latest
steps:
@@ -370,62 +299,44 @@ jobs:
Upload-Release:
permissions: write-all
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }}
if: ${{ github.ref_type=='tag' }}
needs: [build]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: Meta
fetch-depth: '0'
fetch-tags: 'true'
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- 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: Get tags
run: |
echo "CURRENTVERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
git fetch --tags
echo "PREVERSION=$(git describe --tags --abbrev=0 HEAD^)" >> $GITHUB_ENV
- name: Force push Alpha branch to Meta
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git fetch origin Alpha:Alpha
git push origin Alpha:Meta --force
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate release notes
run: |
cp ./.github/genReleaseNote.sh ./
bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION}
rm ./genReleaseNote.sh
- 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 }}
- uses: actions/download-artifact@v4
with:
path: bin/
merge-multiple: true
- 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
- 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
Docker:
if: ${{ !startsWith(github.event_name, 'pull_request') }}
@@ -454,35 +365,20 @@ 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
if: ${{ github.event_name != 'workflow_dispatch' }}
id: meta_alpha
id: meta
uses: docker/metadata-action@v5
with:
images: '${{ env.REGISTRY }}/${{ github.repository }}'
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }}
id: meta_release
uses: docker/metadata-action@v5
with:
images: '${{ env.REGISTRY }}/${{ github.repository }}'
tags: |
${{ github.event.inputs.version }}
flavor: |
latest=true
labels: org.opencontainers.image.version=${{ github.event.inputs.version }}
images: ${{ env.REGISTRY }}/${{ github.repository }}
- name: Show files
run: |
ls .
ls bin/
- name: login to docker REGISTRY
uses: docker/login-action@v3
with:
@@ -493,7 +389,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
if: ${{ github.event_name != 'workflow_dispatch' }}
id: build-and-push
uses: docker/build-push-action@v5
with:
context: .
@@ -504,20 +400,5 @@ jobs:
linux/amd64
linux/arm64
linux/arm/v7
tags: ${{ steps.meta_alpha.outputs.tags }}
labels: ${{ steps.meta_alpha.outputs.labels }}
- name: Build and push Docker image
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }}
uses: docker/build-push-action@v5
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 }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -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 && chmod +x mihomo && echo "$FILE_NAME" > /mihomo-config/test
mv bin/$FILE_NAME mihomo.gz && gzip -d mihomo.gz && echo "$FILE_NAME" > /mihomo-config/test
FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/mihomo"
@@ -23,4 +23,5 @@ VOLUME ["/root/.config/mihomo/"]
COPY --from=builder /mihomo-config/ /root/.config/mihomo/
COPY --from=builder /mihomo/mihomo /mihomo
RUN chmod +x /mihomo
ENTRYPOINT [ "/mihomo" ]

View File

@@ -163,3 +163,7 @@ 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 ./...

View File

@@ -98,3 +98,4 @@ 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)

View File

@@ -2,7 +2,6 @@ package adapter
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
@@ -10,16 +9,13 @@ 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"
)
@@ -41,11 +37,6 @@ 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 {
@@ -239,7 +230,6 @@ 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{
@@ -262,18 +252,10 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
if unifiedDelay {
second := time.Now()
var ignoredErr error
var secondResp *http.Response
secondResp, ignoredErr = client.Do(req)
if ignoredErr == nil {
resp = secondResp
resp, err = client.Do(req)
if err == nil {
_ = 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.")
}
}
}

View File

@@ -69,5 +69,3 @@ func WithDSCP(dscp uint8) Addition {
metadata.DSCP = dscp
}
}
func Placeholder(metadata *C.Metadata) {}

View File

@@ -11,8 +11,6 @@ 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

View File

@@ -3,9 +3,6 @@ package inbound
import (
"context"
"net"
"sync"
"github.com/metacubex/mihomo/component/keepalive"
"github.com/metacubex/tfo-go"
)
@@ -14,47 +11,20 @@ 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) {
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)
})
}

View File

@@ -1,14 +0,0 @@
//go:build !windows
package inbound
import (
"net"
"os"
)
const SupportNamedPipe = false
func ListenNamedPipe(path string) (net.Listener, error) {
return nil, os.ErrInvalid
}

View File

@@ -1,32 +0,0 @@
package inbound
import (
"net"
"os"
"github.com/metacubex/wireguard-go/ipc/namedpipe"
"golang.org/x/sys/windows"
)
const SupportNamedPipe = true
// windowsSDDL is the Security Descriptor set on the namedpipe.
// It provides read/write access to all users and the local system.
const windowsSDDL = "D:PAI(A;OICI;GWGR;;;BU)(A;OICI;GWGR;;;SY)"
func ListenNamedPipe(path string) (net.Listener, error) {
sddl := os.Getenv("LISTEN_NAMEDPIPE_SDDL")
if sddl == "" {
sddl = windowsSDDL
}
securityDescriptor, err := windows.SecurityDescriptorFromString(sddl)
if err != nil {
return nil, err
}
namedpipeLC := namedpipe.ListenConfig{
SecurityDescriptor: securityDescriptor,
InputBufferSize: 256 * 1024,
OutputBufferSize: 256 * 1024,
}
return namedpipeLC.Listen(path)
}

View File

@@ -8,7 +8,3 @@ const multipathTCPAvailable = false
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
}
func getMultiPathTCP(listenConfig *net.ListenConfig) bool {
return false
}

View File

@@ -9,7 +9,3 @@ const multipathTCPAvailable = true
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
listenConfig.SetMultipathTCP(open)
}
func getMultiPathTCP(listenConfig *net.ListenConfig) bool {
return listenConfig.MultipathTCP()
}

View File

@@ -3,18 +3,15 @@ package outbound
import (
"context"
"errors"
"os"
"strconv"
"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"
"github.com/metacubex/mihomo/constant/features"
)
var DisableLoopBackDetector, _ = strconv.ParseBool(os.Getenv("DISABLE_LOOPBACK_DETECTOR"))
type Direct struct {
*Base
loopBack *loopback.Detector
@@ -27,45 +24,38 @@ type DirectOption struct {
// DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
if !features.CMFA && !DisableLoopBackDetector {
if err := d.loopBack.CheckConn(metadata); err != nil {
return nil, err
}
if err := d.loopBack.CheckConn(metadata); err != nil {
return nil, err
}
opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver))
opts = append(opts, dialer.WithResolver(resolver.DefaultResolver))
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 !features.CMFA && !DisableLoopBackDetector {
if err := d.loopBack.CheckPacketConn(metadata); err != nil {
return nil, err
}
if err := d.loopBack.CheckPacketConn(metadata); err != nil {
return nil, err
}
// net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DefaultResolver)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", metadata.AddrPort())
pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort))
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{

View File

@@ -7,11 +7,13 @@ 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"
@@ -74,6 +76,7 @@ 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)

View File

@@ -7,7 +7,6 @@ import (
"fmt"
"net"
"net/netip"
"runtime"
"strconv"
"time"
@@ -15,7 +14,6 @@ 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"
@@ -45,8 +43,6 @@ 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) {
@@ -55,7 +51,7 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts .
return nil, err
}
return NewConn(CN.NewRefConn(tcpConn, h), h), nil
return NewConn(tcpConn, h), nil
}
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
@@ -63,13 +59,13 @@ func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata
if err != nil {
return nil, err
}
return newPacketConn(CN.NewRefPacketConn(&hyPacketConn{udpConn}, h), h), nil
return newPacketConn(&hyPacketConn{udpConn}, h), nil
}
func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer {
return &hyDialerWithContext{
ctx: context.Background(),
hyDialer: func(network string, rAddr net.Addr) (net.PacketConn, error) {
hyDialer: func(network string) (net.PacketConn, error) {
var err error
var cDialer C.Dialer = dialer.NewDialer(h.Base.DialOptions(opts...)...)
if len(h.option.DialerProxy) > 0 {
@@ -78,7 +74,7 @@ func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.Pack
return nil, err
}
}
rAddrPort, _ := netip.ParseAddrPort(rAddr.String())
rAddrPort, _ := netip.ParseAddrPort(h.Addr())
return cDialer.ListenPacket(ctx, network, "", rAddrPort)
},
remoteAddr: func(addr string) (net.Addr, error) {
@@ -131,7 +127,11 @@ func (c *HysteriaOption) Speed() (uint64, uint64, error) {
}
func NewHysteria(option HysteriaOption) (*Hysteria, error) {
clientTransport := &transport.ClientTransport{}
clientTransport := &transport.ClientTransport{
Dialer: &net.Dialer{
Timeout: 8 * time.Second,
},
}
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
ports := option.Ports
@@ -218,7 +218,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
if err != nil {
return nil, fmt.Errorf("hysteria %s create error: %w", addr, err)
}
outbound := &Hysteria{
return &Hysteria{
Base: &Base{
name: option.Name,
addr: addr,
@@ -231,19 +231,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
},
option: &option,
client: client,
}
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)
}
}, nil
}
type hyPacketConn struct {
@@ -280,7 +268,7 @@ func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
}
type hyDialerWithContext struct {
hyDialer func(network string, rAddr net.Addr) (net.PacketConn, error)
hyDialer func(network string) (net.PacketConn, error)
ctx context.Context
remoteAddr func(host string) (net.Addr, error)
}
@@ -290,7 +278,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, rAddr)
return h.hyDialer(network)
}
func (h *hyDialerWithContext) Context() context.Context {

View File

@@ -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,8 +38,6 @@ type Hysteria2 struct {
option *Hysteria2Option
client *hysteria2.Client
dialer proxydialer.SingDialer
closeCh chan struct{} // for test
}
type Hysteria2Option struct {
@@ -91,9 +89,6 @@ func closeHysteria2(h *Hysteria2) {
if h.client != nil {
_ = h.client.CloseWithError(errors.New("proxy removed"))
}
if h.closeCh != nil {
close(h.closeCh)
}
}
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
@@ -170,7 +165,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[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
return resolveUDPAddrWithPrefer(ctx, "udp", serverAddress[fastrand.Intn(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
}
if option.HopInterval == 0 {

View File

@@ -1,38 +0,0 @@
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")
}
}

View File

@@ -1,39 +0,0 @@
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")
}
}

View File

@@ -37,7 +37,7 @@ func NewRejectWithOption(option RejectOption) *Reject {
return &Reject{
Base: &Base{
name: option.Name,
tp: C.Reject,
tp: C.Direct,
udp: true,
},
}

View File

@@ -149,6 +149,7 @@ 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)
@@ -165,6 +166,12 @@ 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 {
@@ -172,12 +179,6 @@ 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
@@ -272,7 +273,6 @@ 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

View File

@@ -80,6 +80,7 @@ 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)

View File

@@ -6,6 +6,7 @@ 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"
@@ -93,6 +94,7 @@ 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)
@@ -120,6 +122,7 @@ 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)
@@ -204,7 +207,8 @@ func NewSnell(option SnellOption) (*Snell, error) {
if err != nil {
return nil, err
}
N.TCPKeepAlive(c)
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
})
}

View File

@@ -10,6 +10,7 @@ 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"
@@ -81,6 +82,7 @@ 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)
@@ -126,6 +128,7 @@ 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{

View File

@@ -17,7 +17,7 @@ import (
"github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/randv2"
"github.com/zhangyunhao116/fastrand"
"golang.org/x/crypto/ssh"
)
@@ -77,6 +77,7 @@ 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)
@@ -179,10 +180,10 @@ func NewSsh(option SshOption) (*Ssh, error) {
}
version := "SSH-2.0-OpenSSH_"
if randv2.IntN(2) == 0 {
version += "7." + strconv.Itoa(randv2.IntN(10))
if fastrand.Intn(2) == 0 {
version += "7." + strconv.Itoa(fastrand.Intn(10))
} else {
version += "8." + strconv.Itoa(randv2.IntN(9))
version += "8." + strconv.Itoa(fastrand.Intn(9))
}
config.ClientVersion = version

View File

@@ -3,19 +3,18 @@ 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"
)
@@ -30,8 +29,6 @@ type Trojan struct {
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
ssCipher core.Cipher
}
type TrojanOption struct {
@@ -49,17 +46,9 @@ 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)
@@ -106,10 +95,6 @@ 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
@@ -127,10 +112,6 @@ 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
@@ -153,6 +134,7 @@ 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)
@@ -179,11 +161,6 @@ 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
@@ -210,15 +187,12 @@ 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
@@ -283,20 +257,6 @@ 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
@@ -311,6 +271,7 @@ 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
}

View File

@@ -55,7 +55,7 @@ func resolveUDPAddr(ctx context.Context, network, address string) (*net.UDPAddr,
return nil, err
}
ip, err := resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
ip, err := resolver.ResolveProxyServerHost(ctx, host)
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.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host)
case C.IPv6Only:
ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver)
ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host)
case C.IPv6Prefer:
var ips []netip.Addr
ips, err = resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
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.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
if err == nil {
for _, addr := range ips {
if addr.Is4() {

View File

@@ -262,6 +262,7 @@ 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)
@@ -326,6 +327,7 @@ 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)
@@ -503,14 +505,17 @@ func NewVless(option VlessOption) (*Vless, error) {
var addons *vless.Addons
if option.Network != "ws" && len(option.Flow) >= 16 {
option.Flow = option.Flow[:16]
if option.Flow != vless.XRV {
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:
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 {
@@ -572,6 +577,7 @@ 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
}

View File

@@ -179,7 +179,6 @@ 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,
@@ -209,7 +208,6 @@ 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,
@@ -312,6 +310,7 @@ 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)
@@ -372,6 +371,7 @@ 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)
@@ -471,6 +471,7 @@ 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
}

View File

@@ -12,7 +12,6 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/metacubex/mihomo/common/atomic"
CN "github.com/metacubex/mihomo/common/net"
@@ -24,27 +23,22 @@ 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 wireguardGoDevice
device *device.Device
tunDevice wireguard.Device
dialer proxydialer.SingDialer
resolver resolver.Resolver
resolver *dns.Resolver
refP *refProxyAdapter
initOk atomic.Bool
@@ -54,10 +48,6 @@ type WireGuard struct {
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
}
@@ -73,14 +63,10 @@ type WireGuardOption struct {
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 {
@@ -92,18 +78,6 @@ type WireGuardPeerOption struct {
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
}
@@ -263,20 +237,14 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
if err != nil {
return nil, E.Cause(err, "create WireGuard device")
}
logger := &device.Logger{
outbound.device = device.NewDevice(context.Background(), outbound.tunDevice, outbound.bind, &device.Logger{
Verbosef: func(format string, args ...interface{}) {
log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
},
Errorf: func(format string, args ...interface{}) {
log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
},
}
if option.AmneziaWGOption != nil {
outbound.bind.SetParseReserved(false) // AmneziaWG don't need parse reserved
outbound.device = amnezia.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers)
} else {
outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers)
}
}, option.Workers)
var has6 bool
for _, address := range outbound.localPrefixes {
@@ -319,15 +287,6 @@ func (w *WireGuard) resolve(ctx context.Context, address M.Socksaddr) (netip.Add
}
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
}
@@ -342,23 +301,70 @@ func (w *WireGuard) init0(ctx context.Context) error {
}
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
ipcConf := "private_key=" + w.option.PrivateKey
if len(w.option.Peers) > 0 {
for i, peer := range w.option.Peers {
destination, err := w.resolve(ctx, peer.Addr())
if err != nil {
// !!! do not set initErr here !!!
// let us can retry domain resolve in next time
return E.Cause(err, "resolve endpoint domain for peer ", i)
}
ipcConf += "\npublic_key=" + peer.PublicKey
ipcConf += "\nendpoint=" + destination.String()
if peer.PreSharedKey != "" {
ipcConf += "\npreshared_key=" + peer.PreSharedKey
}
for _, allowedIP := range peer.AllowedIPs {
ipcConf += "\nallowed_ip=" + allowedIP
}
if len(peer.Reserved) > 0 {
var reserved [3]uint8
copy(reserved[:], w.option.Reserved)
w.bind.SetReservedForEndpoint(destination, reserved)
}
}
} else {
ipcConf += "\npublic_key=" + w.option.PublicKey
destination, err := w.resolve(ctx, w.connectAddr)
if err != nil {
// !!! do not set initErr here !!!
// let us can retry domain resolve in next time
return E.Cause(err, "resolve endpoint domain")
}
w.bind.SetConnectAddr(destination)
ipcConf += "\nendpoint=" + destination.String()
if w.option.PreSharedKey != "" {
ipcConf += "\npreshared_key=" + w.option.PreSharedKey
}
var has4, has6 bool
for _, address := range w.localPrefixes {
if address.Addr().Is4() {
has4 = true
} else {
has6 = true
}
}
if has4 {
ipcConf += "\nallowed_ip=0.0.0.0/0"
}
if has6 {
ipcConf += "\nallowed_ip=::/0"
}
}
if w.option.PersistentKeepalive != 0 {
ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", w.option.PersistentKeepalive)
}
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)
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 {
@@ -370,128 +376,11 @@ func (w *WireGuard) init0(ctx context.Context) error {
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)
}

View File

@@ -29,7 +29,6 @@ func TestWireGuardGC(t *testing.T) {
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

View File

@@ -48,7 +48,7 @@ type GroupBaseOption struct {
func NewGroupBase(opt GroupBaseOption) *GroupBase {
var excludeFilterReg *regexp2.Regexp
if opt.excludeFilter != "" {
excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, regexp2.None)
excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, 0)
}
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, regexp2.None)
filterReg := regexp2.MustCompile(filter, 0)
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.MatchString(name); mat {
if mat, _ := filterReg.FindStringMatch(name); mat != nil {
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.MatchString(name); mat {
if mat, _ := filterReg.FindStringMatch(name); mat != nil {
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.MatchString(name); mat {
if mat, _ := gb.excludeFilterReg.FindStringMatch(name); mat != nil {
continue
}
newProxies = append(newProxies, p)

View File

@@ -204,7 +204,8 @@ func strategyStickySessions(url string) strategyFn {
for i := 1; i < maxRetry; i++ {
proxy := proxies[nowIdx]
if proxy.AliveForTestUrl(url) {
if !has || nowIdx != idx {
if nowIdx != idx {
lruCache.Delete(key)
lruCache.Set(key, nowIdx)
}
@@ -214,6 +215,7 @@ func strategyStickySessions(url string) strategyFn {
}
}
lruCache.Delete(key)
lruCache.Set(key, 0)
return proxies[0]
}

View File

@@ -5,8 +5,6 @@ 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"
@@ -69,28 +67,10 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
}
if groupOption.IncludeAllProviders {
groupOption.Use = AllProviders
groupOption.Use = append(groupOption.Use, AllProviders...)
}
if groupOption.IncludeAllProxies {
if groupOption.Filter != "" {
var filterRegs []*regexp2.Regexp
for _, filter := range strings.Split(groupOption.Filter, "`") {
filterReg := regexp2.MustCompile(filter, regexp2.None)
filterRegs = append(filterRegs, filterReg)
}
for _, p := range AllProxies {
for _, filterReg := range filterRegs {
if mat, _ := filterReg.MatchString(p); mat {
groupOption.Proxies = append(groupOption.Proxies, p)
}
}
}
} else {
groupOption.Proxies = append(groupOption.Proxies, AllProxies...)
}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
groupOption.Proxies = []string{"COMPATIBLE"}
}
groupOption.Proxies = append(groupOption.Proxies, AllProxies...)
}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {

View File

@@ -9,7 +9,6 @@ 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 {
@@ -150,7 +149,6 @@ 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{

View File

@@ -4,7 +4,3 @@ type SelectAble interface {
Set(string) error
ForceSet(name string)
}
var _ SelectAble = (*Fallback)(nil)
var _ SelectAble = (*URLTest)(nil)
var _ SelectAble = (*Selector)(nil)

View File

@@ -27,8 +27,6 @@ type extraOption struct {
}
type HealthCheck struct {
ctx context.Context
ctxCancel context.CancelFunc
url string
extra map[string]*extraOption
mu sync.Mutex
@@ -38,6 +36,7 @@ type HealthCheck struct {
lazy bool
expectedStatus utils.IntRanges[uint16]
lastTouch atomic.TypedValue[time.Time]
done chan struct{}
singleDo *singledo.Single[struct{}]
timeout time.Duration
}
@@ -60,7 +59,7 @@ func (hc *HealthCheck) process() {
} else {
log.Debugln("Skip once health check because we are lazy")
}
case <-hc.ctx.Done():
case <-hc.done:
ticker.Stop()
hc.stop()
return
@@ -147,7 +146,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](hc.ctx, batch.WithConcurrencyNum[bool](10))
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
// execute default health check
option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus}
@@ -182,21 +181,21 @@ func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *ex
filters = append(filters, filter)
}
filterReg = regexp2.MustCompile(strings.Join(filters, "|"), regexp2.None)
filterReg = regexp2.MustCompile(strings.Join(filters, "|"), 0)
}
}
for _, proxy := range hc.proxies {
// skip proxies that do not require health check
if filterReg != nil {
if match, _ := filterReg.MatchString(proxy.Name()); !match {
if match, _ := filterReg.FindStringMatch(proxy.Name()); match == nil {
continue
}
}
p := proxy
b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(hc.ctx, hc.timeout)
ctx, cancel := context.WithTimeout(context.Background(), hc.timeout)
defer cancel()
log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid)
_, _ = p.URLTest(ctx, url, expectedStatus)
@@ -207,7 +206,7 @@ func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *ex
}
func (hc *HealthCheck) close() {
hc.ctxCancel()
hc.done <- struct{}{}
}
func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck {
@@ -218,11 +217,8 @@ 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,
@@ -230,6 +226,7 @@ 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),
}
}

View File

@@ -1,7 +1,6 @@
package provider
import (
"encoding"
"errors"
"fmt"
"time"
@@ -10,9 +9,8 @@ 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 (
@@ -29,20 +27,8 @@ 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"`
@@ -52,8 +38,6 @@ 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 {
@@ -107,11 +91,11 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
path := C.Path.GetPathByHash("proxies", schema.URL)
if schema.Path != "" {
path = C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) {
if !features.CMFA && !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path)
}
}
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout)
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header)
default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
}

View File

@@ -0,0 +1,36 @@
//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
}

View File

@@ -1,10 +1,11 @@
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"net/http"
"runtime"
"strings"
"time"
@@ -12,10 +13,11 @@ import (
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/common/convert"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/profile/cachefile"
mihomoHttp "github.com/metacubex/mihomo/component/http"
"github.com/metacubex/mihomo/component/resource"
C "github.com/metacubex/mihomo/constant"
types "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel/statistic"
"github.com/dlclark/regexp2"
@@ -51,7 +53,7 @@ func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
"proxies": pp.Proxies(),
"testUrl": pp.healthCheck.url,
"expectedStatus": pp.healthCheck.expectedStatus.String(),
"updatedAt": pp.UpdatedAt(),
"updatedAt": pp.UpdatedAt,
"subscriptionInfo": pp.subscriptionInfo,
})
}
@@ -69,18 +71,20 @@ func (pp *proxySetProvider) HealthCheck() {
}
func (pp *proxySetProvider) Update() error {
_, _, err := pp.Fetcher.Update()
elm, same, err := pp.Fetcher.Update()
if err == nil && !same {
pp.OnUpdate(elm)
}
return err
}
func (pp *proxySetProvider) Initial() error {
_, err := pp.Fetcher.Initial()
elm, err := pp.Fetcher.Initial()
if err != nil {
return err
}
if subscriptionInfo := cachefile.Cache().GetSubscriptionInfo(pp.Name()); subscriptionInfo != "" {
pp.SetSubscriptionInfo(subscriptionInfo)
}
pp.OnUpdate(elm)
pp.getSubscriptionInfo()
pp.closeAllConnections()
return nil
}
@@ -93,10 +97,6 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies
}
func (pp *proxySetProvider) Count() int {
return len(pp.proxies)
}
func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch()
}
@@ -117,14 +117,38 @@ func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
}
}
func (pp *proxySetProvider) SetSubscriptionInfo(userInfo string) {
pp.subscriptionInfo = NewSubscriptionInfo(userInfo)
}
func (pp *proxySetProvider) SetProvider(provider types.ProxyProvider) {
if httpVehicle, ok := pp.Vehicle().(*resource.HTTPVehicle); ok {
httpVehicle.SetProvider(provider)
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.HttpRequestWithProxy(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil, pp.Vehicle().Proxy())
if err != nil {
return
}
defer resp.Body.Close()
userInfoStr := strings.TrimSpace(resp.Header.Get("subscription-userinfo"))
if userInfoStr == "" {
resp2, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
http.MethodGet, http.Header{"User-Agent": {"Quantumultx"}}, nil, pp.Vehicle().Proxy())
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() {
@@ -139,13 +163,13 @@ func (pp *proxySetProvider) closeAllConnections() {
})
}
func (pp *proxySetProvider) Close() error {
pp.healthCheck.close()
return pp.Fetcher.Close()
func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close()
_ = pd.Fetcher.Destroy()
}
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, regexp2.None)
excludeFilterReg, err := regexp2.Compile(excludeFilter, 0)
if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
}
@@ -156,7 +180,7 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, exc
var filterRegs []*regexp2.Regexp
for _, filter := range strings.Split(filter, "`") {
filterReg, err := regexp2.Compile(filter, regexp2.None)
filterReg, err := regexp2.Compile(filter, 0)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
@@ -175,33 +199,20 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, exc
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, excludeTypeArray, filterRegs, excludeFilterReg, dialerProxy, override), proxiesOnUpdate(pd))
pd.Fetcher = fetcher
wrapper := &ProxySetProvider{pd}
if httpVehicle, ok := vehicle.(*resource.HTTPVehicle); ok {
httpVehicle.SetProvider(wrapper)
}
runtime.SetFinalizer(wrapper, (*ProxySetProvider).Close)
runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper, nil
}
func (pp *ProxySetProvider) Close() error {
runtime.SetFinalizer(pp, nil)
return pp.proxySetProvider.Close()
}
func (pp *ProxySetProvider) SetProvider(provider types.ProxyProvider) {
pp.proxySetProvider.SetProvider(provider)
}
// CompatibleProvider for auto gc
type CompatibleProvider struct {
*compatibleProvider
}
type compatibleProvider struct {
name string
healthCheck *HealthCheck
subscriptionInfo *SubscriptionInfo
proxies []C.Proxy
version uint32
name string
healthCheck *HealthCheck
proxies []C.Proxy
version uint32
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
@@ -250,10 +261,6 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) Count() int {
return len(cp.proxies)
}
func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch()
}
@@ -266,13 +273,8 @@ func (cp *compatibleProvider) RegisterHealthCheckTask(url string, expectedStatus
cp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
}
func (cp *compatibleProvider) Close() error {
cp.healthCheck.close()
return nil
}
func (cp *compatibleProvider) SetSubscriptionInfo(userInfo string) {
cp.subscriptionInfo = NewSubscriptionInfo(userInfo)
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
@@ -291,19 +293,15 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
}
wrapper := &CompatibleProvider{pd}
runtime.SetFinalizer(wrapper, (*CompatibleProvider).Close)
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
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()
}
}
@@ -358,12 +356,12 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray
continue
}
if len(excludeFilter) > 0 {
if mat, _ := excludeFilterReg.MatchString(name); mat {
if mat, _ := excludeFilterReg.FindStringMatch(name); mat != nil {
continue
}
}
if len(filter) > 0 {
if mat, _ := filterReg.MatchString(name); !mat {
if mat, _ := filterReg.FindStringMatch(name); mat == nil {
continue
}
}
@@ -375,33 +373,37 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray
mapping["dialer-proxy"] = dialerProxy
}
val := reflect.ValueOf(override)
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
if field.IsNil() {
continue
}
fieldName := strings.Split(val.Type().Field(i).Tag.Get("provider"), ",")[0]
switch fieldName {
case "additional-prefix":
name := mapping["name"].(string)
mapping["name"] = *field.Interface().(*string) + name
case "additional-suffix":
name := mapping["name"].(string)
mapping["name"] = name + *field.Interface().(*string)
case "proxy-name":
// Iterate through all naming replacement rules and perform the replacements.
for _, expr := range override.ProxyName {
name := mapping["name"].(string)
newName, err := expr.Pattern.Replace(name, expr.Target, 0, -1)
if err != nil {
return nil, fmt.Errorf("proxy name replace error: %w", err)
}
mapping["name"] = newName
}
default:
mapping[fieldName] = field.Elem().Interface()
}
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
}
proxy, err := adapter.ParseProxy(mapping)

View File

@@ -1,11 +1,8 @@
package provider
import (
"fmt"
"strconv"
"strings"
"github.com/metacubex/mihomo/log"
)
type SubscriptionInfo struct {
@@ -15,45 +12,28 @@ type SubscriptionInfo struct {
Expire int64
}
func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo) {
userinfo = strings.ReplaceAll(strings.ToLower(userinfo), " ", "")
func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo, err error) {
userinfo = strings.ToLower(userinfo)
userinfo = strings.ReplaceAll(userinfo, " ", "")
si = new(SubscriptionInfo)
for _, field := range strings.Split(userinfo, ";") {
name, value, ok := strings.Cut(field, "=")
if !ok {
continue
}
intValue, err := parseValue(value)
if err != nil {
log.Warnln("[Provider] get subscription-userinfo: %e", err)
continue
}
switch name {
switch name, value, _ := strings.Cut(field, "="); name {
case "upload":
si.Upload = intValue
si.Upload, err = strconv.ParseInt(value, 10, 64)
case "download":
si.Download = intValue
si.Download, err = strconv.ParseInt(value, 10, 64)
case "total":
si.Total = intValue
si.Total, err = strconv.ParseInt(value, 10, 64)
case "expire":
si.Expire = intValue
if value == "" {
si.Expire = 0
} else {
si.Expire, err = strconv.ParseInt(value, 10, 64)
}
}
if err != nil {
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)
return
}

View File

@@ -33,8 +33,15 @@ 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]{}
arc.Clear()
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]),
}
for _, option := range options {
option(arc)
@@ -42,19 +49,6 @@ 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) {

View File

@@ -333,7 +333,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "ws", "httpupgrade":
headers := make(map[string]any)
wsOpts := make(map[string]any)
wsOpts["path"] = "/"
wsOpts["path"] = []string{"/"}
if host, ok := values["host"]; ok && host != "" {
headers["Host"] = host.(string)
}

View File

@@ -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[randv2.IntN(hostsLen)]
return prefix + hostsSuffix[fastrand.Intn(hostsLen)]
}
func RandUserAgent() string {
return userAgents[randv2.IntN(uaLen)]
return userAgents[fastrand.Intn(uaLen)]
}
func SetUserAgent(header http.Header) {

View File

@@ -68,8 +68,10 @@ 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]{}
lc.Clear()
lc := &LruCache[K, V]{
lru: list.New[*entry[K, V]](),
cache: make(map[K]*list.Element[*entry[K, V]]),
}
for _, option := range options {
option(lc)
@@ -78,14 +80,6 @@ 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) {
@@ -229,10 +223,6 @@ 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)
}
@@ -256,32 +246,13 @@ func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
}
}
// 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) {
func (c *LruCache[K, V]) Clear() error {
c.mu.Lock()
defer c.mu.Unlock()
if el := c.get(key); el != nil {
actual, ok = el.value, true
}
if newValue, del := valueFn(actual, ok); del {
if ok { // data not in cache, so needn't delete
c.delete(key)
}
return lo.Empty[V](), false
} else {
c.set(key, newValue)
return newValue, true
}
c.cache = make(map[K]*list.Element[*entry[K, V]])
return nil
}
type entry[K comparable, V any] struct {

View File

@@ -4,8 +4,11 @@ import (
"fmt"
"net"
"strings"
"time"
)
var KeepAliveInterval = 15 * time.Second
func SplitNetworkType(s string) (string, string, error) {
var (
shecme string
@@ -44,3 +47,10 @@ func SplitHostPort(s string) (host, port string, hasPort bool, err error) {
host, port, err = net.SplitHostPort(temp)
return
}
func TCPKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(KeepAliveInterval)
}
}

View File

@@ -51,23 +51,3 @@ 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())
}

View File

@@ -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(randv2.Int())
msb(fastrand.Int())
}
}

View File

@@ -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.RLock()
defer q.lock.RUnlock()
q.lock.Lock()
defer q.lock.Unlock()
return int64(len(q.items))
}

View File

@@ -1,224 +0,0 @@
// 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()
}

View File

@@ -3,7 +3,6 @@ package structure
// references: https://github.com/mitchellh/mapstructure
import (
"encoding"
"encoding/base64"
"fmt"
"reflect"
@@ -87,41 +86,35 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
}
func (d *Decoder) decode(name string, data any, val reflect.Value) error {
for {
kind := val.Kind()
if kind == reflect.Pointer && val.IsNil() {
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)
}
switch kind {
case reflect.Pointer:
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
if ok, err := d.decodeTextUnmarshaller(name, data, val); ok {
return err
}
switch {
case isInt(kind):
return d.decodeInt(name, data, val)
case isUint(kind):
return d.decodeUint(name, data, val)
case isFloat(kind):
return d.decodeFloat(name, data, val)
}
switch kind {
case reflect.Pointer:
val = val.Elem()
continue
case reflect.String:
return d.decodeString(name, data, val)
case reflect.Bool:
return d.decodeBool(name, data, val)
case reflect.Slice:
return d.decodeSlice(name, data, val)
case reflect.Map:
return d.decodeMap(name, data, val)
case reflect.Interface:
return d.setInterface(name, data, val)
case reflect.Struct:
return d.decodeStruct(name, data, val)
default:
return fmt.Errorf("type %s not support", val.Kind().String())
}
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())
}
}
@@ -560,25 +553,3 @@ 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
}

View File

@@ -1,7 +1,6 @@
package structure
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
@@ -180,90 +179,3 @@ 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)
}

View File

@@ -1,50 +0,0 @@
package utils
import (
"io"
"sync"
list "github.com/bahlo/generic-list-go"
)
type Callback[T any] struct {
list list.List[func(T)]
mutex sync.RWMutex
}
func NewCallback[T any]() *Callback[T] {
return &Callback[T]{}
}
func (c *Callback[T]) Register(item func(T)) io.Closer {
c.mutex.Lock()
defer c.mutex.Unlock()
element := c.list.PushBack(item)
return &callbackCloser[T]{
element: element,
callback: c,
}
}
func (c *Callback[T]) Emit(item T) {
c.mutex.RLock()
defer c.mutex.RUnlock()
for element := c.list.Front(); element != nil; element = element.Next() {
go element.Value(item)
}
}
type callbackCloser[T any] struct {
element *list.Element[func(T)]
callback *Callback[T]
once sync.Once
}
func (c *callbackCloser[T]) Close() error {
c.once.Do(func() {
c.callback.mutex.Lock()
defer c.callback.mutex.Unlock()
c.callback.list.Remove(c.element)
})
return nil
}

View File

@@ -1,62 +0,0 @@
package utils
import (
"crypto/md5"
"encoding/hex"
"errors"
)
// HashType warps hash array inside struct
// someday can change to other hash algorithm simply
type HashType struct {
md5 [md5.Size]byte // MD5
}
func MakeHash(data []byte) HashType {
return HashType{md5.Sum(data)}
}
func (h HashType) Equal(hash HashType) bool {
return h.md5 == hash.md5
}
func (h HashType) Bytes() []byte {
return h.md5[:]
}
func (h HashType) String() string {
return hex.EncodeToString(h.Bytes())
}
func (h HashType) MarshalText() ([]byte, error) {
return []byte(h.String()), nil
}
func (h *HashType) UnmarshalText(data []byte) error {
if hex.DecodedLen(len(data)) != md5.Size {
return errors.New("invalid hash length")
}
_, err := hex.Decode(h.md5[:], data)
return err
}
func (h HashType) MarshalBinary() ([]byte, error) {
return h.md5[:], nil
}
func (h *HashType) UnmarshalBinary(data []byte) error {
if len(data) != md5.Size {
return errors.New("invalid hash length")
}
copy(h.md5[:], data)
return nil
}
func (h HashType) Len() int {
return len(h.md5)
}
func (h HashType) IsValid() bool {
var zero HashType
return h != zero
}

View File

@@ -2,39 +2,19 @@ package utils
import (
"github.com/gofrs/uuid/v5"
"github.com/metacubex/randv2"
"github.com/zhangyunhao116/fastrand"
)
type unsafeRandReader struct{}
type fastRandReader struct{}
func (r unsafeRandReader) Read(p []byte) (n int, err error) {
// modify from https://github.com/golang/go/blob/587c3847da81aa7cfc3b3db2677c8586c94df13a/src/runtime/rand.go#L70-L89
// Inspired by wyrand.
n = len(p)
v := randv2.Uint64()
for len(p) > 0 {
v ^= 0xa0761d6478bd642f
v *= 0xe7037ed1a0b428db
size := 8
if len(p) < 8 {
size = len(p)
}
for i := 0; i < size; i++ {
p[i] ^= byte(v >> (8 * i))
}
p = p[size:]
v = v>>32 | v<<32
}
return
func (r fastRandReader) Read(p []byte) (int, error) {
return fastrand.Read(p)
}
var UnsafeRandReader = unsafeRandReader{}
var UnsafeUUIDGenerator = uuid.NewGenWithOptions(uuid.WithRandomReader(UnsafeRandReader))
var UnsafeUUIDGenerator = uuid.NewGenWithOptions(uuid.WithRandomReader(fastRandReader{}))
func NewUUIDV1() uuid.UUID {
u, _ := UnsafeUUIDGenerator.NewV1() // unsafeRandReader wouldn't cause error, so ignore err is safe
u, _ := UnsafeUUIDGenerator.NewV1() // fastrand.Read wouldn't cause error, so ignore err is safe
return u
}
@@ -43,7 +23,7 @@ func NewUUIDV3(ns uuid.UUID, name string) uuid.UUID {
}
func NewUUIDV4() uuid.UUID {
u, _ := UnsafeUUIDGenerator.NewV4() // unsafeRandReader wouldn't cause error, so ignore err is safe
u, _ := UnsafeUUIDGenerator.NewV4() // fastrand.Read wouldn't cause error, so ignore err is safe
return u
}
@@ -52,12 +32,12 @@ func NewUUIDV5(ns uuid.UUID, name string) uuid.UUID {
}
func NewUUIDV6() uuid.UUID {
u, _ := UnsafeUUIDGenerator.NewV6() // unsafeRandReader wouldn't cause error, so ignore err is safe
u, _ := UnsafeUUIDGenerator.NewV6() // fastrand.Read wouldn't cause error, so ignore err is safe
return u
}
func NewUUIDV7() uuid.UUID {
u, _ := UnsafeUUIDGenerator.NewV7() // unsafeRandReader wouldn't cause error, so ignore err is safe
u, _ := UnsafeUUIDGenerator.NewV7() // fastrand.Read wouldn't cause error, so ignore err is safe
return u
}

View File

@@ -5,11 +5,6 @@ type Authenticator interface {
Users() []string
}
type AuthStore interface {
Authenticator() Authenticator
SetAuthenticator(Authenticator)
}
type AuthUser struct {
User string
Pass string

View File

@@ -67,6 +67,9 @@ func ResetCertificate() {
}
func getCertPool() *x509.CertPool {
if len(trustCerts) == 0 {
return nil
}
if globalCertPool == nil {
mutex.Lock()
defer mutex.Unlock()

View File

@@ -1,14 +0,0 @@
package ca
import (
"github.com/metacubex/mihomo/constant/features"
)
func init() {
// crypto/x509: certificate validation in Windows fails to validate IP in SAN
// https://github.com/golang/go/issues/37176
// As far as I can tell this is still the case on most older versions of Windows (but seems to be fixed in 10)
if features.WindowsMajorVersion < 10 && len(_CaCertificates) > 0 {
DisableSystemCa = true
}
}

View File

@@ -43,20 +43,12 @@ func (set *IpCidrSet) IsContainForString(ipString string) bool {
}
func (set *IpCidrSet) IsContain(ip netip.Addr) bool {
return set.ToIPSet().Contains(ip.WithZone(""))
}
// MatchIp implements C.IpMatcher
func (set *IpCidrSet) MatchIp(ip netip.Addr) bool {
if set.IsEmpty() {
return false
}
return set.IsContain(ip)
return set.toIPSet().Contains(ip.WithZone(""))
}
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
@@ -65,23 +57,7 @@ func (set *IpCidrSet) Merge() error {
return nil
}
func (set *IpCidrSet) IsEmpty() bool {
return set == nil || len(set.rr) == 0
}
func (set *IpCidrSet) Foreach(f func(prefix netip.Prefix) bool) {
for _, r := range set.rr {
for _, prefix := range r.Prefixes() {
if !f(prefix) {
return
}
}
}
}
// ToIPSet not safe convert to *netipx.IPSet
// be careful, must be used after Merge
func (set *IpCidrSet) ToIPSet() *netipx.IPSet {
func (set *IpCidrSet) toIPSet() *netipx.IPSet {
return (*netipx.IPSet)(unsafe.Pointer(set))
}

View File

@@ -1,77 +0,0 @@
package cidr
import (
"encoding/binary"
"errors"
"io"
"net/netip"
"go4.org/netipx"
)
func (ss *IpCidrSet) WriteBin(w io.Writer) (err error) {
// version
_, err = w.Write([]byte{1})
if err != nil {
return err
}
// rr
err = binary.Write(w, binary.BigEndian, int64(len(ss.rr)))
if err != nil {
return err
}
for _, r := range ss.rr {
err = binary.Write(w, binary.BigEndian, r.From().As16())
if err != nil {
return err
}
err = binary.Write(w, binary.BigEndian, r.To().As16())
if err != nil {
return err
}
}
return nil
}
func ReadIpCidrSet(r io.Reader) (ss *IpCidrSet, err error) {
// version
version := make([]byte, 1)
_, err = io.ReadFull(r, version)
if err != nil {
return nil, err
}
if version[0] != 1 {
return nil, errors.New("version is invalid")
}
ss = NewIpCidrSet()
var length int64
// rr
err = binary.Read(r, binary.BigEndian, &length)
if err != nil {
return nil, err
}
if length < 1 {
return nil, errors.New("length is invalid")
}
ss.rr = make([]netipx.IPRange, length)
for i := int64(0); i < length; i++ {
var a16 [16]byte
err = binary.Read(r, binary.BigEndian, &a16)
if err != nil {
return nil, err
}
from := netip.AddrFrom16(a16).Unmap()
err = binary.Read(r, binary.BigEndian, &a16)
if err != nil {
return nil, err
}
to := netip.AddrFrom16(a16).Unmap()
ss.rr[i] = netipx.IPRangeFrom(from, to)
}
return ss, nil
}

View File

@@ -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"
)
@@ -79,29 +79,29 @@ func DialContext(ctx context.Context, network, address string, options ...Option
}
func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort, options ...Option) (net.PacketConn, error) {
if features.CMFA && DefaultSocketHook != nil {
return listenPacketHooked(ctx, network, address)
}
cfg := applyOptions(options...)
lc := &net.ListenConfig{}
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.addrReuse {
addrReuseToListenConfig(lc)
}
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)
}
if cfg.routingMark != 0 {
bindMarkToListenConfig(cfg.routingMark, lc, network, address)
}
return lc.ListenPacket(ctx, network, address)
@@ -127,6 +127,10 @@ 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 {
destination, port = lookupIP4P(destination, port)
@@ -145,31 +149,24 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
}
dialer := netDialer.(*net.Dialer)
keepalive.SetNetDialer(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)
}
if opt.mpTcp {
setMultiPathTCP(dialer)
}
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
socketHookToToDialer(dialer)
} else {
if opt.interfaceName != "" {
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)
}
if opt.tfo {
return dialTFO(ctx, *dialer, network, address)
}
return dialer.DialContext(ctx, network, address)
}
@@ -340,18 +337,26 @@ 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":
ips, err = resolver.LookupIPv4WithResolver(ctx, host, preferResolver)
if preferResolver == nil {
ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host)
} else {
ips, err = resolver.LookupIPv4WithResolver(ctx, host, preferResolver)
}
case "tcp6", "udp6":
ips, err = resolver.LookupIPv6WithResolver(ctx, host, preferResolver)
if preferResolver == nil {
ips, err = resolver.LookupIPv6ProxyServerHost(ctx, host)
} else {
ips, err = resolver.LookupIPv6WithResolver(ctx, host, preferResolver)
}
default:
ips, err = resolver.LookupIPWithResolver(ctx, host, preferResolver)
if preferResolver == nil {
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
} else {
ips, err = resolver.LookupIPWithResolver(ctx, host, preferResolver)
}
}
if err != nil {
return nil, "-1", fmt.Errorf("dns resolve failed: %w", err)
@@ -373,12 +378,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 := d.Opt // make a copy
opt := WithOption(d.Opt)
if rAddrPort.Addr().Unmap().IsLoopback() {
// avoid "The requested address is not valid in its context."
WithInterface("")(&opt)
opt = WithInterface("")
}
return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, rAddrPort, WithOption(opt))
return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, rAddrPort, opt)
}
func NewDialer(options ...Option) Dialer {

View File

@@ -0,0 +1,39 @@
//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)
}

View File

@@ -0,0 +1,22 @@
//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
}

View File

@@ -0,0 +1,29 @@
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)
}

View File

@@ -1,27 +0,0 @@
package dialer
import (
"context"
"net"
"syscall"
)
// SocketControl
// never change type traits because it's used in CMFA
type SocketControl func(network, address string, conn syscall.RawConn) error
// DefaultSocketHook
// never change type traits because it's used in CMFA
var DefaultSocketHook SocketControl
func socketHookToToDialer(dialer *net.Dialer) {
addControlToDialer(dialer, func(ctx context.Context, network, address string, c syscall.RawConn) error {
return DefaultSocketHook(network, address, c)
})
}
func socketHookToListenConfig(lc *net.ListenConfig) {
addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error {
return DefaultSocketHook(network, address, c)
})
}

View File

@@ -9,8 +9,6 @@ import (
"github.com/metacubex/tfo-go"
)
var DisableTFO = false
type tfoConn struct {
net.Conn
closed bool

View File

@@ -1,11 +0,0 @@
package dialer
import "github.com/metacubex/mihomo/constant/features"
func init() {
// According to MSDN, this option is available since Windows 10, 1607
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms738596(v=vs.85).aspx
if features.WindowsMajorVersion < 10 || (features.WindowsMajorVersion == 10 && features.WindowsBuildNumber < 14393) {
DisableTFO = true
}
}

View File

@@ -0,0 +1,99 @@
/* 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__ */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
/* 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

342
component/ebpf/bpf/redir.c Normal file
View File

@@ -0,0 +1,342 @@
#include <stdint.h>
#include <stdbool.h>
//#include <linux/types.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
//#include <linux/if_packet.h>
//#include <linux/if_vlan.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <linux/tcp.h>
//#include <linux/udp.h>
#include <linux/pkt_cls.h>
#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";

103
component/ebpf/bpf/tc.c Normal file
View File

@@ -0,0 +1,103 @@
#include <stdbool.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
//#include <linux/tcp.h>
//#include <linux/udp.h>
#include <linux/pkt_cls.h>
#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";

View File

@@ -0,0 +1,13 @@
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)
}

View File

@@ -0,0 +1,12 @@
//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 }

View File

@@ -0,0 +1,15 @@
//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) }

33
component/ebpf/ebpf.go Normal file
View File

@@ -0,0 +1,33 @@
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
}

View File

@@ -0,0 +1,137 @@
//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))
}
}

View File

@@ -0,0 +1,21 @@
//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")
}

View File

@@ -0,0 +1,216 @@
//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
}

View File

@@ -0,0 +1,139 @@
// 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

Binary file not shown.

View File

@@ -0,0 +1,139 @@
// 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

Binary file not shown.

View File

@@ -0,0 +1,120 @@
// 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

Binary file not shown.

View File

@@ -0,0 +1,120 @@
//
//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

Binary file not shown.

View File

@@ -0,0 +1,147 @@
//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")
}

View File

@@ -7,32 +7,46 @@ import (
)
type cachefileStore struct {
cache *cachefile.FakeIpStore
cache *cachefile.CacheFile
}
// GetByHost implements store.GetByHost
func (c *cachefileStore) GetByHost(host string) (netip.Addr, bool) {
return c.cache.GetByHost(host)
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
}
}
// PutByHost implements store.PutByHost
func (c *cachefileStore) PutByHost(host string, ip netip.Addr) {
c.cache.PutByHost(host, ip)
c.cache.PutFakeip([]byte(host), ip.AsSlice())
}
// GetByIP implements store.GetByIP
func (c *cachefileStore) GetByIP(ip netip.Addr) (string, bool) {
return c.cache.GetByIP(ip)
elm := c.cache.GetFakeip(ip.AsSlice())
if elm == nil {
return "", false
}
return string(elm), true
}
// PutByIP implements store.PutByIP
func (c *cachefileStore) PutByIP(ip netip.Addr, host string) {
c.cache.PutByIP(ip, host)
c.cache.PutFakeip(ip.AsSlice(), []byte(host))
}
// DelByIP implements store.DelByIP
func (c *cachefileStore) DelByIP(ip netip.Addr) {
c.cache.DelByIP(ip)
addr := ip.AsSlice()
c.cache.DelFakeipPair(addr, c.cache.GetFakeip(addr))
}
// Exist implements store.Exist
@@ -49,7 +63,3 @@ 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()}
}

View File

@@ -67,9 +67,8 @@ func (m *memoryStore) CloneTo(store store) {
// FlushFakeIP implements store.FlushFakeIP
func (m *memoryStore) FlushFakeIP() error {
m.cacheIP.Clear()
m.cacheHost.Clear()
return nil
_ = m.cacheIP.Clear()
return m.cacheHost.Clear()
}
func newMemoryStore(size int) *memoryStore {

View File

@@ -8,7 +8,7 @@ import (
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/component/profile/cachefile"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/component/trie"
)
const (
@@ -35,8 +35,7 @@ type Pool struct {
offset netip.Addr
cycle bool
mux sync.Mutex
host []C.DomainMatcher
mode C.FilterMode
host *trie.DomainTrie[struct{}]
ipnet netip.Prefix
store store
}
@@ -67,20 +66,10 @@ func (p *Pool) LookBack(ip netip.Addr) (string, bool) {
// ShouldSkipped return if domain should be skipped
func (p *Pool) ShouldSkipped(domain string) bool {
should := p.shouldSkipped(domain)
if p.mode == C.FilterWhiteList {
return !should
if p.host == nil {
return false
}
return should
}
func (p *Pool) shouldSkipped(domain string) bool {
for _, matcher := range p.host {
if matcher.MatchDomain(domain) {
return true
}
}
return false
return p.host.Search(domain) != nil
}
// Exist returns if given ip exists in fake-ip pool
@@ -165,8 +154,7 @@ func (p *Pool) restoreState() {
type Options struct {
IPNet netip.Prefix
Host []C.DomainMatcher
Mode C.FilterMode
Host *trie.DomainTrie[struct{}]
// Size sets the maximum number of entries in memory
// and does not work if Persistence is true
@@ -197,11 +185,12 @@ 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 = newCachefileStore(cachefile.Cache())
pool.store = &cachefileStore{
cache: cachefile.Cache(),
}
} else {
pool.store = newMemoryStore(options.Size)
}

View File

@@ -9,9 +9,8 @@ import (
"github.com/metacubex/mihomo/component/profile/cachefile"
"github.com/metacubex/mihomo/component/trie"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/bbolt"
"github.com/sagernet/bbolt"
"github.com/stretchr/testify/assert"
)
@@ -43,7 +42,9 @@ func createCachefileStore(options Options) (*Pool, string, error) {
return nil, "", err
}
pool.store = newCachefileStore(&cachefile.CacheFile{DB: db})
pool.store = &cachefileStore{
cache: &cachefile.CacheFile{DB: db},
}
return pool, f.Name(), nil
}
@@ -61,13 +62,13 @@ func TestPool_Basic(t *testing.T) {
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.Equal(t, pool.Lookup("foo.com"), netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5}))
assert.True(t, 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.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.Equal(t, pool.Gateway(), netip.AddrFrom4([4]byte{192, 168, 0, 1}))
assert.Equal(t, pool.Broadcast(), netip.AddrFrom4([4]byte{192, 168, 0, 15}))
assert.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.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})))
@@ -89,13 +90,13 @@ func TestPool_BasicV6(t *testing.T) {
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.Equal(t, first, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
assert.Equal(t, pool.Lookup("foo.com"), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
assert.Equal(t, last, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805"))
assert.True(t, 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.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.Equal(t, pool.Gateway(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801"))
assert.Equal(t, pool.Broadcast(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff"))
assert.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.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")))
@@ -141,20 +142,19 @@ func TestPool_CycleUsed(t *testing.T) {
}
baz := pool.Lookup("baz.com")
next := pool.Lookup("foo.com")
assert.Equal(t, foo, baz)
assert.Equal(t, next, bar)
assert.True(t, foo == baz)
assert.True(t, next == bar)
}
}
func TestPool_Skip(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/29")
tree := trie.New[struct{}]()
assert.NoError(t, tree.Insert("example.com", struct{}{}))
assert.False(t, tree.IsEmpty())
tree.Insert("example.com", struct{}{})
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
Host: []C.DomainMatcher{tree.NewDomainSet()},
Host: tree,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
@@ -162,28 +162,6 @@ 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"))
}
}
@@ -199,7 +177,7 @@ func TestPool_MaxCacheSize(t *testing.T) {
pool.Lookup("baz.com")
next := pool.Lookup("foo.com")
assert.NotEqual(t, first, next)
assert.False(t, first == next)
}
func TestPool_DoubleMapping(t *testing.T) {
@@ -229,7 +207,7 @@ func TestPool_DoubleMapping(t *testing.T) {
assert.False(t, bazExist)
assert.True(t, barExist)
assert.NotEqual(t, bazIP, newBazIP)
assert.False(t, bazIP == newBazIP)
}
func TestPool_Clone(t *testing.T) {
@@ -241,8 +219,8 @@ func TestPool_Clone(t *testing.T) {
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5}))
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5}))
newPool, _ := New(Options{
IPNet: ipnet,
@@ -287,13 +265,13 @@ func TestPool_FlushFileCache(t *testing.T) {
baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com")
assert.Equal(t, foo, fox)
assert.Equal(t, foo, next)
assert.NotEqual(t, foo, baz)
assert.Equal(t, bar, bax)
assert.Equal(t, bar, baz)
assert.NotEqual(t, bar, next)
assert.Equal(t, baz, nero)
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)
}
}
@@ -316,11 +294,11 @@ func TestPool_FlushMemoryCache(t *testing.T) {
baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com")
assert.Equal(t, foo, fox)
assert.Equal(t, foo, next)
assert.NotEqual(t, foo, baz)
assert.Equal(t, bar, bax)
assert.Equal(t, bar, baz)
assert.NotEqual(t, bar, next)
assert.Equal(t, baz, nero)
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)
}

View File

@@ -7,7 +7,7 @@ import (
)
type AttributeList struct {
matcher []BooleanMatcher
matcher []AttributeMatcher
}
func (al *AttributeList) Match(domain *router.Domain) bool {
@@ -23,14 +23,6 @@ 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 {

View File

@@ -6,10 +6,8 @@ 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"
@@ -20,57 +18,36 @@ 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 InitGeoSite() error {
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 {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
log.Infoln("Download GeoSite.dat finish")
initGeoSite = false
}
if !initGeoSite {
if err := Verify(C.GeositeName); err != nil {
log.Warnln("GeoSite.dat invalid, remove and download: %s", err)
if err := os.Remove(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error())
}
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
}
initGeoSite = true
}
return nil
}
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) {
func downloadGeoSite(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)
resp, err := mihomoHttp.HttpRequest(ctx, C.GeoSiteUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
if err != nil {
return
}
@@ -86,41 +63,30 @@ func downloadToPath(url string, path string) (err error) {
return err
}
func InitGeoSite() error {
geoSiteEnable.Store(true)
initGeoSiteMutex.Lock()
defer initGeoSiteMutex.Unlock()
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
log.Infoln("Can't find GeoSite.dat, start download")
if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
log.Infoln("Download GeoSite.dat finish")
initGeoSite = false
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
}
if !initGeoSite {
if err := Verify(C.GeositeName); err != nil {
log.Warnln("GeoSite.dat invalid, remove and download: %s", err)
if err := os.Remove(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error())
}
if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
}
initGeoSite = true
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
return nil
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func InitGeoIP() error {
geoIpEnable.Store(true)
initGeoIPMutex.Lock()
defer initGeoIPMutex.Unlock()
if GeodataMode() {
if C.GeodataMode {
if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) {
log.Infoln("Can't find GeoIP.dat, start download")
if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil {
if err := downloadGeoIP(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
}
log.Infoln("Download GeoIP.dat finish")
@@ -133,7 +99,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 := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil {
if err := downloadGeoIP(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
}
}
@@ -144,7 +110,7 @@ func InitGeoIP() error {
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
log.Infoln("Can't find MMDB, start download")
if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil {
if err := mmdb.DownloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
}
@@ -155,7 +121,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 := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil {
if err := mmdb.DownloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
}
@@ -165,12 +131,9 @@ 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 := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil {
if err := mmdb.DownloadASN(C.Path.ASN()); err != nil {
return fmt.Errorf("can't download ASN.mmdb: %s", err.Error())
}
log.Infoln("Download ASN.mmdb finish")
@@ -182,7 +145,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 := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil {
if err := mmdb.DownloadASN(C.Path.ASN()); err != nil {
return fmt.Errorf("can't download ASN: %s", err.Error())
}
}
@@ -190,15 +153,3 @@ func InitASN() error {
}
return nil
}
func GeoIpEnable() bool {
return geoIpEnable.Load()
}
func GeoSiteEnable() bool {
return geoSiteEnable.Load()
}
func ASNEnable() bool {
return asnEnable.Load()
}

View File

@@ -33,13 +33,12 @@ func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) {
type DomainMatcher interface {
ApplyDomain(string) bool
Count() int
}
type succinctDomainMatcher struct {
set *trie.DomainSet
otherMatchers []strmatcher.Matcher
count int
not bool
}
func (m *succinctDomainMatcher) ApplyDomain(domain string) bool {
@@ -52,17 +51,16 @@ func (m *succinctDomainMatcher) ApplyDomain(domain string) bool {
}
}
}
if m.not {
isMatched = !isMatched
}
return isMatched
}
func (m *succinctDomainMatcher) Count() int {
return m.count
}
func NewSuccinctMatcherGroup(domains []*Domain) (DomainMatcher, error) {
func NewSuccinctMatcherGroup(domains []*Domain, not bool) (DomainMatcher, error) {
t := trie.New[struct{}]()
m := &succinctDomainMatcher{
count: len(domains),
not: not,
}
for _, d := range domains {
switch d.Type {
@@ -92,10 +90,10 @@ func NewSuccinctMatcherGroup(domains []*Domain) (DomainMatcher, error) {
type v2rayDomainMatcher struct {
matchers strmatcher.IndexMatcher
count int
not bool
}
func NewMphMatcherGroup(domains []*Domain) (DomainMatcher, error) {
func NewMphMatcherGroup(domains []*Domain, not bool) (DomainMatcher, error) {
g := strmatcher.NewMphMatcherGroup()
for _, d := range domains {
matcherType, f := matcherTypeMap[d.Type]
@@ -110,80 +108,119 @@ func NewMphMatcherGroup(domains []*Domain) (DomainMatcher, error) {
g.Build()
return &v2rayDomainMatcher{
matchers: g,
count: len(domains),
not: not,
}, nil
}
func (m *v2rayDomainMatcher) ApplyDomain(domain string) bool {
return len(m.matchers.Match(strings.ToLower(domain))) > 0
}
func (m *v2rayDomainMatcher) Count() int {
return m.count
}
type notDomainMatcher struct {
DomainMatcher
}
func (m notDomainMatcher) ApplyDomain(domain string) bool {
return !m.DomainMatcher.ApplyDomain(domain)
}
func NewNotDomainMatcherGroup(matcher DomainMatcher) DomainMatcher {
return notDomainMatcher{matcher}
}
type IPMatcher interface {
Match(ip netip.Addr) bool
Count() int
}
type geoIPMatcher struct {
cidrSet *cidr.IpCidrSet
count int
}
// Match returns true if the given ip is included by the GeoIP.
func (m *geoIPMatcher) Match(ip netip.Addr) bool {
return m.cidrSet.IsContain(ip)
}
func (m *geoIPMatcher) Count() int {
return m.count
}
func NewGeoIPMatcher(cidrList []*CIDR) (IPMatcher, error) {
m := &geoIPMatcher{
cidrSet: cidr.NewIpCidrSet(),
count: len(cidrList),
isMatched := len(m.matchers.Match(strings.ToLower(domain))) > 0
if m.not {
isMatched = !isMatched
}
for _, cidr := range cidrList {
return isMatched
}
type GeoIPMatcher struct {
countryCode string
reverseMatch bool
cidrSet *cidr.IpCidrSet
}
func (m *GeoIPMatcher) Init(cidrs []*CIDR) error {
for _, cidr := range cidrs {
addr, ok := netip.AddrFromSlice(cidr.Ip)
if !ok {
return nil, fmt.Errorf("error when loading GeoIP: invalid IP: %s", cidr.Ip)
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 nil, fmt.Errorf("error when loading GeoIP: %w", err)
return fmt.Errorf("error when loading GeoIP: %w", err)
}
}
err := m.cidrSet.Merge()
return m.cidrSet.Merge()
}
func (m *GeoIPMatcher) SetReverseMatch(isReverseMatch bool) {
m.reverseMatch = isReverseMatch
}
// 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
}
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
}
}
}
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)
if err != nil {
return nil, err
}
return m, nil
return matcher, nil
}
type notIPMatcher struct {
IPMatcher
func (m *MultiGeoIPMatcher) ApplyIp(ip netip.Addr) bool {
for _, matcher := range m.matchers {
if matcher.Match(ip) {
return true
}
}
return false
}
func (m notIPMatcher) Match(ip netip.Addr) bool {
return !m.IPMatcher.Match(ip)
}
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)
}
func NewNotIpMatcherGroup(matcher IPMatcher) IPMatcher {
return notIPMatcher{matcher}
matcher := &MultiGeoIPMatcher{
matchers: matchers,
}
return matcher, nil
}

View File

@@ -5,7 +5,8 @@ import (
"fmt"
"strings"
"github.com/metacubex/mihomo/common/singleflight"
"golang.org/x/sync/singleflight"
"github.com/metacubex/mihomo/component/geodata/router"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
@@ -13,6 +14,8 @@ import (
var (
geoMode bool
AutoUpdate bool
UpdateInterval int
geoLoaderName = "memconservative"
geoSiteMatcher = "succinct"
)
@@ -23,6 +26,14 @@ func GeodataMode() bool {
return geoMode
}
func GeoAutoUpdate() bool {
return AutoUpdate
}
func GeoUpdateInterval() int {
return UpdateInterval
}
func LoaderName() string {
return geoLoaderName
}
@@ -34,6 +45,12 @@ 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" {
@@ -54,22 +71,21 @@ 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 loadGeoSiteMatcherListSF = singleflight.Group[[]*router.Domain]{StoreResult: true}
var loadGeoSiteMatcherSF = singleflight.Group[router.DomainMatcher]{StoreResult: true}
var loadGeoSiteMatcherSF = singleflight.Group{}
func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, error) {
func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, int, error) {
if countryCode == "" {
return nil, fmt.Errorf("country code could not be empty")
return nil, 0, fmt.Errorf("country code could not be empty")
}
not := false
@@ -81,84 +97,73 @@ func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, error) {
parts := strings.Split(countryCode, "@")
if len(parts) == 0 {
return nil, errors.New("empty rule")
return nil, 0, errors.New("empty rule")
}
listName := strings.TrimSpace(parts[0])
attrVal := parts[1:]
attrs := parseAttrs(attrVal)
if listName == "" {
return nil, fmt.Errorf("empty listname in rule: %s", countryCode)
return nil, 0, fmt.Errorf("empty listname in rule: %s", countryCode)
}
matcherName := listName
if !attrs.IsEmpty() {
matcherName += "@" + attrs.String()
}
matcher, err, shared := loadGeoSiteMatcherSF.Do(matcherName, func() (router.DomainMatcher, error) {
log.Infoln("Load GeoSite rule: %s", matcherName)
domains, err, shared := loadGeoSiteMatcherListSF.Do(listName, func() ([]*router.Domain, error) {
geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil {
return nil, err
}
return geoLoader.LoadGeoSite(listName)
})
v, err, shared := loadGeoSiteMatcherSF.Do(listName, func() (interface{}, error) {
geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil {
if !shared {
loadGeoSiteMatcherListSF.Forget(listName) // don't store the error result
}
return nil, err
}
if attrs.IsEmpty() {
if strings.Contains(countryCode, "@") {
log.Warnln("empty attribute list: %s", countryCode)
}
} else {
filteredDomains := make([]*router.Domain, 0, len(domains))
hasAttrMatched := false
for _, domain := range domains {
if attrs.Match(domain) {
hasAttrMatched = true
filteredDomains = append(filteredDomains, domain)
}
}
if !hasAttrMatched {
log.Warnln("attribute match no rule: geosite: %s", countryCode)
}
domains = filteredDomains
}
/**
linear: linear algorithm
matcher, err := router.NewDomainMatcher(domains)
mphminimal perfect hash algorithm
*/
if geoSiteMatcher == "mph" {
return router.NewMphMatcherGroup(domains)
} else {
return router.NewSuccinctMatcherGroup(domains)
}
return geoLoader.LoadGeoSite(listName)
})
if err != nil {
if !shared {
loadGeoSiteMatcherSF.Forget(matcherName) // don't store the error result
loadGeoSiteMatcherSF.Forget(listName) // don't store the error result
}
return nil, err
return nil, 0, err
}
if not {
matcher = router.NewNotDomainMatcherGroup(matcher)
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
}
return matcher, nil
/**
linear: linear algorithm
matcher, err := router.NewDomainMatcher(domains)
mphminimal 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
}
var loadGeoIPMatcherSF = singleflight.Group[router.IPMatcher]{StoreResult: true}
var loadGeoIPMatcherSF = singleflight.Group{}
func LoadGeoIPMatcher(country string) (router.IPMatcher, error) {
func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) {
if len(country) == 0 {
return nil, fmt.Errorf("country code could not be empty")
return nil, 0, fmt.Errorf("country code could not be empty")
}
not := false
@@ -168,36 +173,35 @@ func LoadGeoIPMatcher(country string) (router.IPMatcher, error) {
}
country = strings.ToLower(country)
matcher, err, shared := loadGeoIPMatcherSF.Do(country, func() (router.IPMatcher, error) {
log.Infoln("Load GeoIP rule: %s", country)
v, err, shared := loadGeoIPMatcherSF.Do(country, func() (interface{}, error) {
geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil {
return nil, err
}
cidrList, err := geoLoader.LoadGeoIP(country)
if err != nil {
return nil, err
}
return router.NewGeoIPMatcher(cidrList)
return geoLoader.LoadGeoIP(country)
})
if err != nil {
if !shared {
loadGeoIPMatcherSF.Forget(country) // don't store the error result
log.Warnln("Load GeoIP rule: %s", country)
}
return nil, err
return nil, 0, err
}
if not {
matcher = router.NewNotIpMatcherGroup(matcher)
records := v.([]*router.CIDR)
geoIP := &router.GeoIP{
CountryCode: country,
Cidr: records,
ReverseMatch: not,
}
return matcher, nil
matcher, err := router.NewGeoIPMatcher(geoIP)
if err != nil {
return nil, 0, err
}
return matcher, len(records), nil
}
func ClearGeoSiteCache() {
loadGeoSiteMatcherListSF.Reset()
loadGeoSiteMatcherSF.Reset()
}
func ClearGeoIPCache() {
loadGeoIPMatcherSF.Reset()
func ClearCache() {
loadGeoSiteMatcherSF = singleflight.Group{}
loadGeoIPMatcherSF = singleflight.Group{}
}

View File

@@ -12,22 +12,10 @@ import (
"time"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/listener/inner"
)
var (
ua string
)
func UA() string {
return ua
}
func SetUA(UA string) {
ua = UA
}
func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) {
return HttpRequestWithProxy(ctx, url, method, header, body, "")
}
@@ -47,7 +35,7 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st
}
if _, ok := header["User-Agent"]; !ok {
req.Header.Set("User-Agent", UA())
req.Header.Set("User-Agent", C.UA)
}
if err != nil {
@@ -72,7 +60,8 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st
if conn, err := inner.HandleTcp(address, specialProxy); err == nil {
return conn, nil
} else {
return dialer.DialContext(ctx, network, address)
d := net.Dialer{}
return d.DialContext(ctx, network, address)
}
},
TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}),

View File

@@ -11,9 +11,8 @@ import (
type Interface struct {
Index int
MTU int
Name string
Addresses []netip.Prefix
Addrs []netip.Prefix
HardwareAddr net.HardwareAddr
}
@@ -62,9 +61,8 @@ func Interfaces() (map[string]*Interface, error) {
r[iface.Name] = &Interface{
Index: iface.Index,
MTU: iface.MTU,
Name: iface.Name,
Addresses: ipNets,
Addrs: ipNets,
HardwareAddr: iface.HardwareAddr,
}
}
@@ -94,7 +92,7 @@ func IsLocalIp(ip netip.Addr) (bool, error) {
return false, err
}
for _, iface := range ifaces {
for _, addr := range iface.Addresses {
for _, addr := range iface.Addrs {
if addr.Contains(ip) {
return true, nil
}
@@ -122,7 +120,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.Addresses {
for _, addr := range iface.Addrs {
if !accept(addr) {
continue
}

View File

@@ -1,65 +0,0 @@
package keepalive
import (
"net"
"runtime"
"time"
"github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/utils"
)
var (
keepAliveIdle = atomic.NewTypedValue[time.Duration](0 * time.Second)
keepAliveInterval = atomic.NewTypedValue[time.Duration](0 * time.Second)
disableKeepAlive = atomic.NewBool(false)
SetDisableKeepAliveCallback = utils.NewCallback[bool]()
)
func SetKeepAliveIdle(t time.Duration) {
keepAliveIdle.Store(t)
}
func SetKeepAliveInterval(t time.Duration) {
keepAliveInterval.Store(t)
}
func KeepAliveIdle() time.Duration {
return keepAliveIdle.Load()
}
func KeepAliveInterval() time.Duration {
return keepAliveInterval.Load()
}
func SetDisableKeepAlive(disable bool) {
if runtime.GOOS == "android" {
setDisableKeepAlive(false)
} 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)
}
}

View File

@@ -1,30 +0,0 @@
//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()
}
}

Some files were not shown because too many files have changed in this diff Show More