diff --git a/.github/mihomo.service b/.github/mihomo.service new file mode 100644 index 0000000000..a884059f99 --- /dev/null +++ b/.github/mihomo.service @@ -0,0 +1,17 @@ +[Unit] +Description=mihomo Daemon, Another Clash Kernel. +After=network.target NetworkManager.service systemd-networkd.service iwd.service + +[Service] +Type=simple +LimitNPROC=500 +LimitNOFILE=1000000 +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH +Restart=always +ExecStartPre=/usr/bin/sleep 2s +ExecStart=/usr/bin/mihomo -d /etc/mihomo +ExecReload=/bin/kill -HUP $MAINPID + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6bd6dad050..bad84cd1b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,334 +21,335 @@ concurrency: env: REGISTRY: docker.io jobs: - Build: - permissions: write-all + build: runs-on: ubuntu-latest strategy: - fail-fast: false matrix: - job: - - { - type: "WithoutCGO", - target: "linux-amd64 linux-amd64-compatible", - id: "1", - } - - { - type: "WithoutCGO", - target: "linux-armv5 linux-armv6 linux-armv7", - id: "2", - } - - { - type: "WithoutCGO", - target: "linux-arm64 linux-mips64 linux-mips64le", - id: "3", - } - - { - type: "WithoutCGO", - target: "linux-mips-softfloat linux-mips-hardfloat linux-mipsle-softfloat linux-mipsle-hardfloat", - id: "4", - } - - { type: "WithoutCGO", target: "linux-386 linux-riscv64 linux-loong64", id: "5" } - - { - type: "WithoutCGO", - target: "freebsd-386 freebsd-amd64 freebsd-arm64", - id: "6", - } - - { - type: "WithoutCGO", - target: "windows-amd64-compatible windows-amd64 windows-386", - id: "7", - } - - { - type: "WithoutCGO", - target: "windows-arm64 windows-arm32v7", - id: "8", - } - - { - type: "WithoutCGO", - target: "darwin-amd64 darwin-arm64 android-arm64", - id: "9", - } - # only for test - - { type: "WithoutCGO-GO120", target: "linux-amd64 linux-amd64-compatible",id: "1" } - # Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016. - - { type: "WithoutCGO-GO120", target: "windows-amd64-compatible windows-amd64 windows-386",id: "2" } - # Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later. - - { type: "WithoutCGO-GO120", target: "darwin-amd64 darwin-arm64 android-arm64",id: "3" } -# - { type: "WithCGO", target: "windows/*", id: "1" } -# - { type: "WithCGO", target: "linux/386", id: "2" } -# - { type: "WithCGO", target: "linux/amd64", id: "3" } -# - { type: "WithCGO", target: "linux/arm64,linux/riscv64", id: "4" } -# - { type: "WithCGO", target: "linux/arm,", id: "5" } -# - { type: "WithCGO", target: "linux/arm-6,linux/arm-7", id: "6" } -# - { type: "WithCGO", target: "linux/mips,linux/mipsle", id: "7" } -# - { type: "WithCGO", target: "linux/mips64", id: "8" } -# - { type: "WithCGO", target: "linux/mips64le", id: "9" } -# - { type: "WithCGO", target: "darwin-10.16/*", id: "10" } -# - { type: "WithCGO", target: "android", id: "11" } - - steps: - - name: Check out code into the Go module directory - uses: actions/checkout@v3 - - - name: Set variables - run: echo "VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - shell: bash - - - name: Set variables - if: ${{github.ref_name=='Alpha'}} - run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV - shell: bash - - - name: Set variables - if: ${{github.ref_name=='Beta'}} - run: echo "VERSION=beta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV - shell: bash - - - name: Set variables - if: ${{github.ref_name=='Meta'}} - run: echo "VERSION=meta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV - shell: bash - - - name: Set variables - if: ${{github.ref_name=='' || github.ref_type=='tag'}} - run: echo "VERSION=$(git describe --tags)" >> $GITHUB_ENV - shell: bash - - - name: Set ENV - run: | - sudo timedatectl set-timezone "Asia/Shanghai" - echo "BUILDTIME=$(date)" >> $GITHUB_ENV - shell: bash + jobs: + - { goos: darwin, goarch: arm64, output: arm64 } + - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible } + - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 } + + - { goos: linux, goarch: '386', output: '386' } + - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, test: test } + - { goos: linux, goarch: amd64, goamd64: v3, output: amd64 } + - { goos: linux, goarch: arm64, output: arm64 } + - { goos: linux, goarch: arm, goarm: '7', output: armv7 } + - { goos: linux, goarch: mips, mips: hardfloat, output: mips-hardfloat } + - { goos: linux, goarch: mips, mips: softfloat, output: mips-softfloat } + - { goos: linux, goarch: mipsle, mips: hardfloat, output: mipsle-hardfloat } + - { goos: linux, goarch: mipsle, mips: softfloat, output: mipsle-softfloat } + - { goos: linux, goarch: mips64, output: mips64 } + - { goos: linux, goarch: mips64le, output: mips64le } + - { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1' } + - { goos: linux, goarch: loong64, output: loong64-abi2, abi: '2' } + - { goos: linux, goarch: riscv64, output: riscv64 } + - { goos: linux, goarch: s390x, output: s390x } + + - { goos: windows, goarch: '386', output: '386' } + - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible } + - { goos: windows, goarch: amd64, goamd64: v3, output: amd64 } + - { goos: windows, goarch: arm, goarm: '7', output: armv7 } + - { goos: windows, goarch: arm64, output: arm64 } + + - { goos: freebsd, goarch: '386', output: '386' } + - { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-compatible } + - { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64 } + - { goos: freebsd, goarch: arm64, output: arm64 } + + - { goos: android, goarch: '386', ndk: i686-linux-android34, output: '386' } + - { goos: android, goarch: amd64, ndk: x86_64-linux-android34, output: amd64 } + - { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 } + - { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 } - - name: Set ENV - run: | - echo "TAGS=with_gvisor,with_lwip" >> $GITHUB_ENV - echo "LDFLAGS=-X 'github.com/metacubex/mihomo/constant.Version=${VERSION}' -X 'github.com/metacubex/mihomo/constant.BuildTime=${BUILDTIME}' -w -s -buildid=" >> $GITHUB_ENV - echo "GOTOOLCHAIN=local" >> $GITHUB_ENV - shell: bash - - - name: Setup Go - if: ${{ matrix.job.type!='WithoutCGO-GO120' }} - uses: actions/setup-go@v4 - with: - go-version: "1.21" - check-latest: true - - - name: Setup Go - if: ${{ matrix.job.type=='WithoutCGO-GO120' }} - uses: actions/setup-go@v4 - with: - go-version: "1.20" - check-latest: true - - - name: Test - if: ${{ matrix.job.id=='1' && matrix.job.type!='WithCGO' }} - run: | - go test ./... - - - name: Build WithoutCGO - if: ${{ matrix.job.type!='WithCGO' }} - env: - NAME: mihomo - BINDIR: bin - run: make -j$(($(nproc) + 1)) ${{ matrix.job.target }} - - - uses: nttld/setup-ndk@v1 - if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }} - id: setup-ndk - with: - ndk-version: r26b - add-to-path: true - - - name: Build Android - if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }} - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - run: | - mkdir bin - CC=${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang - CGO_ENABLED=1 CC=${CC} GOARCH=arm64 GOOS=android go build -tags ${TAGS} -trimpath -ldflags "${LDFLAGS}" -o bin/${NAME}-android-arm64 + # 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' } - - name: Set up xgo - if: ${{ matrix.job.type=='WithCGO' && matrix.job.target!='android' }} - run: | - docker pull techknowlogick/xgo:latest - go install src.techknowlogick.com/xgo@latest + # Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later. + - { goos: darwin, goarch: arm64, output: arm64-go120, goversion: '1.20' } + - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' } + - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } - - name: Build by xgo - if: ${{ matrix.job.type=='WithCGO' && matrix.job.target!='android' }} - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - run: | - mkdir bin - xgo --targets="${{ matrix.job.target }}" --tags="${TAGS}" -ldflags="${LDFLAGS}" --out bin/${NAME} ./ + # only for test + - { goos: linux, goarch: '386', output: '386-go120', goversion: '1.20' } + - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20', test: test } + - { goos: linux, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } - - name: Rename - if: ${{ matrix.job.type=='WithCGO' }} - run: | - cd bin - ls -la - cp ../.github/rename-cgo.sh ./ - bash ./rename-cgo.sh - rm ./rename-cgo.sh - ls -la - cd .. - - - name: Rename - if: ${{ matrix.job.type=='WithoutCGO-GO120' }} - run: | - cd bin - ls -la - cp ../.github/rename-go120.sh ./ - bash ./rename-go120.sh - rm ./rename-go120.sh - ls -la - cd .. - - - name: Zip - if: ${{ success() }} - run: | - cd bin - ls -la - chmod +x * - cp ../.github/release.sh ./ - bash ./release.sh - rm ./release.sh - ls -la - cd .. - - - name: Save version - run: echo ${VERSION} > bin/version.txt - shell: bash - - - uses: actions/upload-artifact@v3 - if: ${{ success() }} - with: - name: artifact - path: bin/ + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + if: ${{ matrix.jobs.goversion == '' && matrix.jobs.goarch != 'loong64' }} + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Set up Go + if: ${{ matrix.jobs.goversion != '' && matrix.jobs.goarch != 'loong64' }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.jobs.goversion }} + + - name: Set up Go1.21 loongarch abi1 + if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }} + run: | + wget -q https://github.com/xishang0128/loongarch64-golang/releases/download/1.21.5/go1.21.5.linux-amd64-abi1.tar.gz + sudo tar zxf go1.21.5.linux-amd64-abi1.tar.gz -C /usr/local + echo "/usr/local/go/bin" >> $GITHUB_PATH + + - name: Set up Go1.21 loongarch abi2 + if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '2' }} + run: | + wget -q https://github.com/xishang0128/loongarch64-golang/releases/download/1.21.5/go1.21.5.linux-amd64-abi2.tar.gz + sudo tar zxf go1.21.5.linux-amd64-abi2.tar.gz -C /usr/local + echo "/usr/local/go/bin" >> $GITHUB_PATH + + - name: Set variables + if: ${{github.ref_name=='Alpha'}} + run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV + shell: bash + + - name: Set variables + if: ${{github.ref_name=='' || github.ref_type=='tag'}} + run: echo "VERSION=$(git describe --tags)" >> $GITHUB_ENV + shell: bash + + - name: Set Time Variable + run: | + echo "BUILDTIME=$(date)" >> $GITHUB_ENV + echo "CGO_ENABLED=0" >> $GITHUB_ENV + echo "BUILDTAG=-extldflags --static" >> $GITHUB_ENV + + - name: Setup NDK + if: ${{ matrix.jobs.goos == 'android' }} + uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r26c + + - name: Set NDK path + if: ${{ matrix.jobs.goos == 'android' }} + run: | + echo "CC=${{steps.setup-ndk.outputs.ndk-path}}/toolchains/llvm/prebuilt/linux-x86_64/bin/${{matrix.jobs.ndk}}-clang" >> $GITHUB_ENV + echo "CGO_ENABLED=1" >> $GITHUB_ENV + echo "BUILDTAG=" >> $GITHUB_ENV + + - name: Test + if: ${{ matrix.jobs.test == 'test' }} + run: | + go test ./... + + - name: Build core + env: + GOOS: ${{matrix.jobs.goos}} + GOARCH: ${{matrix.jobs.goarch}} + GOAMD64: ${{matrix.jobs.goamd64}} + GOARM: ${{matrix.jobs.arm}} + GOMIPS: ${{matrix.jobs.mips}} + run: | + 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 + zip -r mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.zip mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}.exe + else + cp mihomo mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} + gzip -c mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.gz + rm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} + fi + + - name: Create DEB package + if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') }} + run: | + sudo apt-get install dpkg + if [ "${{matrix.jobs.abi}}" = "1" ]; then + ARCH=loongarch64 + else + ARCH=${{matrix.jobs.goarch}} + fi + mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN + mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin + mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo + mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/systemd/system/ + mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo + + cp mihomo mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin/mihomo + cp LICENSE mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo/ + cp .github/mihomo.service mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/systemd/system/ + + cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo/config.yaml < mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN/control < + Homepage: https://wiki.metacubex.one/ + Description: The universal proxy platform. + EOF + + dpkg-deb -Z gzip --build mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION} + + - name: Convert DEB to RPM + if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') }} + run: | + sudo apt-get install -y alien + alien --to-rpm --scripts mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb + mv mihomo*.rpm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.rpm + + # - name: Convert DEB to PKG + # if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') && !contains(matrix.jobs.goarch, 'loong64') }} + # run: | + # docker pull archlinux + # docker run --rm -v ./:/mnt archlinux bash -c " + # pacman -Syu pkgfile base-devel --noconfirm + # curl -L https://github.com/helixarch/debtap/raw/master/debtap > /usr/bin/debtap + # chmod 755 /usr/bin/debtap + # debtap -u + # debtap -Q /mnt/mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb + # " + # mv mihomo*.pkg.tar.zst mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.pkg.tar.zst + + - name: Save version + run: | + echo ${VERSION} > version.txt + shell: bash + + - name: Archive production artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.jobs.goos }}-${{ matrix.jobs.output }} + path: | + mihomo*.gz + mihomo*.deb + mihomo*.rpm + mihomo*.zip + version.txt Upload-Prerelease: permissions: write-all if: ${{ github.ref_type == 'branch' && !startsWith(github.event_name, 'pull_request') }} - needs: [Build] + needs: [build] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v3 - with: - name: artifact - path: bin/ - - - name: Display structure of downloaded files - run: ls -R - working-directory: bin - - - name: Delete current release assets - uses: 8Mi-Tech/delete-release-assets-action@main - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - tag: Prerelease-${{ github.ref_name }} - deleteOnlyFromDrafts: false - - - name: Set Env - run: | - echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV - shell: bash - - - name: Tag Repo - uses: richardsimko/update-tag@v1.0.6 - with: - tag_name: Prerelease-${{ github.ref_name }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - run: | - cat > release.txt << 'EOF' - Release created at ${{ env.BUILDTIME }} - Synchronize ${{ github.ref_name }} branch code updates, keeping only the latest version -
- [我应该下载哪个文件? / Which file should I download?](https://github.com/MetaCubeX/mihomo/wiki/FAQ) - [二进制文件筛选 / Binary file selector] (https://metacubex.github.io/Meta-Docs/startup/#_1) - [查看文档 / Docs](https://metacubex.github.io/Meta-Docs/) - EOF - - - name: Upload Prerelease - uses: softprops/action-gh-release@v1 - if: ${{ success() }} - with: - tag_name: Prerelease-${{ github.ref_name }} - files: | - bin/* - prerelease: true - generate_release_notes: true - body_path: release.txt + - name: Download all workflow run artifacts + uses: actions/download-artifact@v4 + with: + path: bin/ + merge-multiple: true + + - name: Delete current release assets + uses: 8Mi-Tech/delete-release-assets-action@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: Prerelease-${{ github.ref_name }} + deleteOnlyFromDrafts: false + - name: Set Env + run: | + echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV + shell: bash + + - name: Tag Repo + uses: richardsimko/update-tag@v1 + with: + tag_name: Prerelease-${{ github.ref_name }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - run: | + cat > release.txt << 'EOF' + Release created at ${{ env.BUILDTIME }} + Synchronize ${{ github.ref_name }} branch code updates, keeping only the latest version +
+ [我应该下载哪个文件? / Which file should I download?](https://github.com/MetaCubeX/mihomo/wiki/FAQ) + [二进制文件筛选 / Binary file selector](https://metacubex.github.io/Meta-Docs/startup/#_1) + [查看文档 / Docs](https://metacubex.github.io/Meta-Docs/) + EOF + + - name: Upload Prerelease + uses: softprops/action-gh-release@v1 + if: ${{ success() }} + with: + tag_name: Prerelease-${{ github.ref_name }} + files: | + bin/* + prerelease: true + generate_release_notes: true + body_path: release.txt Upload-Release: permissions: write-all if: ${{ github.ref_type=='tag' }} - needs: [Build] + needs: [build] runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - 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: Generate release notes - run: | + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - 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: Generate release notes + run: | cp ./.github/genReleaseNote.sh ./ bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION} rm ./genReleaseNote.sh - - uses: actions/download-artifact@v3 - with: - name: artifact - path: bin/ + - uses: actions/download-artifact@v4 + with: + path: bin/ + merge-multiple: true - - name: Display structure of downloaded files - run: ls -R - working-directory: bin + - name: Display structure of downloaded files + run: ls -R + working-directory: bin - - name: Upload Release - uses: softprops/action-gh-release@v1 - if: ${{ success() }} - with: - tag_name: ${{ github.ref_name }} - files: bin/* - generate_release_notes: true - body_path: release.md + - name: 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') }} permissions: write-all - needs: [Build] + needs: [build] runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: artifact path: bin/ + merge-multiple: true - name: Display structure of downloaded files run: ls -R working-directory: bin - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Setup Docker buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: version: latest @@ -356,7 +357,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ github.repository }} diff --git a/Dockerfile b/Dockerfile index c9cd56b7ab..64b33cf748 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY docker/file-name.sh /mihomo/file-name.sh WORKDIR /mihomo COPY bin/ bin/ RUN FILE_NAME=`sh file-name.sh` && echo $FILE_NAME && \ - FILE_NAME=`ls bin/ | egrep "$FILE_NAME.*"|awk NR==1` && echo $FILE_NAME && \ + FILE_NAME=`ls bin/ | egrep "$FILE_NAME.gz"|awk NR==1` && echo $FILE_NAME && \ mv bin/$FILE_NAME mihomo.gz && gzip -d mihomo.gz && echo "$FILE_NAME" > /mihomo-config/test FROM alpine:latest LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/mihomo" diff --git a/Makefile b/Makefile index f6ffcae533..59bec41ebf 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ GOBUILD=CGO_ENABLED=0 go build -tags with_gvisor -trimpath -ldflags '-X "github. -w -s -buildid=' PLATFORM_LIST = \ + darwin-amd64-compatible \ darwin-amd64 \ darwin-arm64 \ linux-amd64-compatible \ diff --git a/adapter/inbound/listen.go b/adapter/inbound/listen.go index 8b7b5fb2e3..18dc1bc242 100644 --- a/adapter/inbound/listen.go +++ b/adapter/inbound/listen.go @@ -4,7 +4,7 @@ import ( "context" "net" - "github.com/sagernet/tfo-go" + "github.com/metacubex/tfo-go" ) var ( diff --git a/adapter/outbound/dns.go b/adapter/outbound/dns.go new file mode 100644 index 0000000000..21a5b2b77f --- /dev/null +++ b/adapter/outbound/dns.go @@ -0,0 +1,159 @@ +package outbound + +import ( + "context" + "net" + "time" + + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/resolver" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" +) + +type Dns struct { + *Base +} + +type DnsOption struct { + BasicOption + Name string `proxy:"name"` +} + +// DialContext implements C.ProxyAdapter +func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { + left, right := N.Pipe() + go resolver.RelayDnsConn(context.Background(), right, 0) + return NewConn(left, d), nil +} + +// ListenPacketContext implements C.ProxyAdapter +func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { + log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort()) + + ctx, cancel := context.WithCancel(context.Background()) + + return newPacketConn(&dnsPacketConn{ + response: make(chan dnsPacket, 1), + ctx: ctx, + cancel: cancel, + }, d), nil +} + +type dnsPacket struct { + data []byte + put func() + addr net.Addr +} + +// dnsPacketConn implements net.PacketConn +type dnsPacketConn struct { + response chan dnsPacket + ctx context.Context + cancel context.CancelFunc +} + +func (d *dnsPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { + select { + case packet := <-d.response: + return packet.data, packet.put, packet.addr, nil + case <-d.ctx.Done(): + return nil, nil, nil, net.ErrClosed + } +} + +func (d *dnsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + select { + case packet := <-d.response: + n = copy(p, packet.data) + if packet.put != nil { + packet.put() + } + return n, packet.addr, nil + case <-d.ctx.Done(): + return 0, nil, net.ErrClosed + } +} + +func (d *dnsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + select { + case <-d.ctx.Done(): + return 0, net.ErrClosed + default: + } + + if len(p) > resolver.SafeDnsPacketSize { + // wtf??? + return len(p), nil + } + + ctx, cancel := context.WithTimeout(d.ctx, resolver.DefaultDnsRelayTimeout) + defer cancel() + + buf := pool.Get(resolver.SafeDnsPacketSize) + put := func() { _ = pool.Put(buf) } + copy(buf, p) // avoid p be changed after WriteTo returned + + go func() { // don't block the WriteTo function + buf, err = resolver.RelayDnsPacket(ctx, buf[:len(p)], buf) + if err != nil { + put() + return + } + + packet := dnsPacket{ + data: buf, + put: put, + addr: addr, + } + select { + case d.response <- packet: + break + case <-d.ctx.Done(): + put() + } + }() + return len(p), nil +} + +func (d *dnsPacketConn) Close() error { + d.cancel() + return nil +} + +func (*dnsPacketConn) LocalAddr() net.Addr { + return &net.UDPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 53, + Zone: "", + } +} + +func (*dnsPacketConn) SetDeadline(t time.Time) error { + return nil +} + +func (*dnsPacketConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (*dnsPacketConn) SetWriteDeadline(t time.Time) error { + return nil +} + +func NewDnsWithOption(option DnsOption) *Dns { + return &Dns{ + Base: &Base{ + name: option.Name, + tp: C.Dns, + udp: true, + tfo: option.TFO, + mpTcp: option.MPTCP, + iface: option.Interface, + rmark: option.RoutingMark, + prefer: C.NewDNSPrefer(option.IPVersion), + }, + } +} diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index 47272ec806..0ad7c2147a 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -8,23 +8,30 @@ import ( "net" "runtime" "strconv" + "time" CN "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" tuicCommon "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/sing-quic/hysteria2" M "github.com/sagernet/sing/common/metadata" + "github.com/zhangyunhao116/fastrand" ) func init() { hysteria2.SetCongestionController = tuicCommon.SetCongestionController } +const minHopInterval = 5 +const defaultHopInterval = 30 + type Hysteria2 struct { *Base @@ -37,7 +44,9 @@ type Hysteria2Option struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` - Port int `proxy:"port"` + Port int `proxy:"port,omitempty"` + Ports string `proxy:"ports,omitempty"` + HopInterval int `proxy:"hop-interval,omitempty"` Up string `proxy:"up,omitempty"` Down string `proxy:"down,omitempty"` Password string `proxy:"password,omitempty"` @@ -129,7 +138,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { clientOptions := hysteria2.ClientOptions{ Context: context.TODO(), Dialer: singDialer, - ServerAddress: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)), + Logger: log.SingLogger, SendBPS: StringToBps(option.Up), ReceiveBPS: StringToBps(option.Down), SalamanderPassword: salamanderPassword, @@ -138,6 +147,37 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { UDPDisabled: false, CWND: option.CWND, UdpMTU: option.UdpMTU, + ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) { + return resolveUDPAddrWithPrefer(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion)) + }, + } + + var ranges utils.IntRanges[uint16] + var serverAddress []string + if option.Ports != "" { + ranges, err = utils.NewUnsignedRanges[uint16](option.Ports) + if err != nil { + return nil, err + } + ranges.Range(func(port uint16) bool { + serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port)))) + return true + }) + if len(serverAddress) > 0 { + clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) { + return resolveUDPAddrWithPrefer(ctx, "udp", serverAddress[fastrand.Intn(len(serverAddress))], C.NewDNSPrefer(option.IPVersion)) + } + + if option.HopInterval == 0 { + option.HopInterval = defaultHopInterval + } else if option.HopInterval < minHopInterval { + option.HopInterval = minHopInterval + } + clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second + } + } + if option.Port == 0 && len(serverAddress) == 0 { + return nil, errors.New("invalid port") } client, err := hysteria2.NewClient(clientOptions) diff --git a/adapter/outbound/ssh.go b/adapter/outbound/ssh.go new file mode 100644 index 0000000000..a0efabca4e --- /dev/null +++ b/adapter/outbound/ssh.go @@ -0,0 +1,208 @@ +package outbound + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "net" + "os" + "runtime" + "strconv" + "strings" + "sync" + + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/proxydialer" + C "github.com/metacubex/mihomo/constant" + + "github.com/zhangyunhao116/fastrand" + "golang.org/x/crypto/ssh" +) + +type Ssh struct { + *Base + + option *SshOption + client *sshClient // using a standalone struct to avoid its inner loop invalidate the Finalizer +} + +type SshOption struct { + BasicOption + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + UserName string `proxy:"username"` + Password string `proxy:"password,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` + PrivateKeyPassphrase string `proxy:"private-key-passphrase,omitempty"` + HostKey []string `proxy:"host-key,omitempty"` + HostKeyAlgorithms []string `proxy:"host-key-algorithms,omitempty"` +} + +func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { + var cDialer C.Dialer = dialer.NewDialer(s.Base.DialOptions(opts...)...) + if len(s.option.DialerProxy) > 0 { + cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer) + if err != nil { + return nil, err + } + } + client, err := s.client.connect(ctx, cDialer, s.addr) + if err != nil { + return nil, err + } + c, err := client.DialContext(ctx, "tcp", metadata.RemoteAddress()) + if err != nil { + return nil, err + } + + return NewConn(N.NewRefConn(c, s), s), nil +} + +type sshClient struct { + config *ssh.ClientConfig + client *ssh.Client + cMutex sync.Mutex +} + +func (s *sshClient) connect(ctx context.Context, cDialer C.Dialer, addr string) (client *ssh.Client, err error) { + s.cMutex.Lock() + defer s.cMutex.Unlock() + if s.client != nil { + return s.client, nil + } + c, err := cDialer.DialContext(ctx, "tcp", addr) + if err != nil { + return nil, err + } + N.TCPKeepAlive(c) + + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + + if ctx.Done() != nil { + done := N.SetupContextForConn(ctx, c) + defer done(&err) + } + + clientConn, chans, reqs, err := ssh.NewClientConn(c, addr, s.config) + if err != nil { + return nil, err + } + client = ssh.NewClient(clientConn, chans, reqs) + + s.client = client + + go func() { + _ = client.Wait() // wait shutdown + _ = client.Close() + s.cMutex.Lock() + defer s.cMutex.Unlock() + if s.client == client { + s.client = nil + } + }() + + return client, nil +} + +func (s *sshClient) Close() error { + s.cMutex.Lock() + defer s.cMutex.Unlock() + if s.client != nil { + return s.client.Close() + } + return nil +} + +func closeSsh(s *Ssh) { + _ = s.client.Close() +} + +func NewSsh(option SshOption) (*Ssh, error) { + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + + config := ssh.ClientConfig{ + User: option.UserName, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + HostKeyAlgorithms: option.HostKeyAlgorithms, + } + + if option.PrivateKey != "" { + var b []byte + var err error + if strings.Contains(option.PrivateKey, "PRIVATE KEY") { + b = []byte(option.PrivateKey) + } else { + b, err = os.ReadFile(C.Path.Resolve(option.PrivateKey)) + if err != nil { + return nil, err + } + } + var pKey ssh.Signer + if option.PrivateKeyPassphrase != "" { + pKey, err = ssh.ParsePrivateKeyWithPassphrase(b, []byte(option.PrivateKeyPassphrase)) + } else { + pKey, err = ssh.ParsePrivateKey(b) + } + if err != nil { + return nil, err + } + + config.Auth = append(config.Auth, ssh.PublicKeys(pKey)) + } + + if option.Password != "" { + config.Auth = append(config.Auth, ssh.Password(option.Password)) + } + + if len(option.HostKey) != 0 { + keys := make([]ssh.PublicKey, len(option.HostKey)) + for i, hostKey := range option.HostKey { + key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey)) + if err != nil { + return nil, fmt.Errorf("parse host key :%s", key) + } + keys[i] = key + } + config.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error { + serverKey := key.Marshal() + for _, hostKey := range keys { + if bytes.Equal(serverKey, hostKey.Marshal()) { + return nil + } + } + return fmt.Errorf("host key mismatch, server send :%s %s", key.Type(), base64.StdEncoding.EncodeToString(serverKey)) + } + } + + version := "SSH-2.0-OpenSSH_" + if fastrand.Intn(2) == 0 { + version += "7." + strconv.Itoa(fastrand.Intn(10)) + } else { + version += "8." + strconv.Itoa(fastrand.Intn(9)) + } + config.ClientVersion = version + + outbound := &Ssh{ + Base: &Base{ + name: option.Name, + addr: addr, + tp: C.Ssh, + udp: false, + iface: option.Interface, + rmark: option.RoutingMark, + prefer: C.NewDNSPrefer(option.IPVersion), + }, + option: &option, + client: &sshClient{ + config: &config, + }, + } + runtime.SetFinalizer(outbound, closeSsh) + + return outbound, nil +} diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index ceeb52a51a..43b4aa214a 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -373,7 +373,7 @@ func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metada }, M.SocksaddrFromNet(metadata.UDPAddr())), ), v), nil } - return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil + return newPacketConn(N.NewThreadSafePacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), v), nil } // SupportUOT implements C.ProxyAdapter diff --git a/adapter/outbound/wireguard.go b/adapter/outbound/wireguard.go index 7c021c87ac..fe1f69fae0 100644 --- a/adapter/outbound/wireguard.go +++ b/adapter/outbound/wireguard.go @@ -13,6 +13,7 @@ import ( "strings" "sync" + "github.com/metacubex/mihomo/common/atomic" CN "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" @@ -37,8 +38,7 @@ type WireGuard struct { device *device.Device tunDevice wireguard.Device dialer proxydialer.SingDialer - startOnce sync.Once - startErr error + init func(ctx context.Context) error resolver *dns.Resolver refP *refProxyAdapter } @@ -47,6 +47,8 @@ type WireGuardOption struct { BasicOption WireGuardPeerOption Name string `proxy:"name"` + Ip string `proxy:"ip,omitempty"` + Ipv6 string `proxy:"ipv6,omitempty"` PrivateKey string `proxy:"private-key"` Workers int `proxy:"workers,omitempty"` MTU int `proxy:"mtu,omitempty"` @@ -62,8 +64,6 @@ type WireGuardOption struct { type WireGuardPeerOption struct { Server string `proxy:"server"` Port int `proxy:"port"` - Ip string `proxy:"ip,omitempty"` - Ipv6 string `proxy:"ipv6,omitempty"` PublicKey string `proxy:"public-key,omitempty"` PreSharedKey string `proxy:"pre-shared-key,omitempty"` Reserved []uint8 `proxy:"reserved,omitempty"` @@ -98,7 +98,7 @@ func (option WireGuardPeerOption) Addr() M.Socksaddr { return M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)) } -func (option WireGuardPeerOption) Prefixes() ([]netip.Prefix, error) { +func (option WireGuardOption) Prefixes() ([]netip.Prefix, error) { localPrefixes := make([]netip.Prefix, 0, 2) if len(option.Ip) > 0 { if !strings.Contains(option.Ip, "/") { @@ -141,6 +141,19 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { } runtime.SetFinalizer(outbound, closeWireGuard) + resolv := func(ctx context.Context, address M.Socksaddr) (netip.AddrPort, error) { + if address.Addr.IsValid() { + return address.AddrPort(), nil + } + udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", address.String(), outbound.prefer) + if err != nil { + return netip.AddrPort{}, err + } + // net.ResolveUDPAddr maybe return 4in6 address, so unmap at here + addrPort := udpAddr.AddrPort() + return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()), nil + } + var reserved [3]uint8 if len(option.Reserved) > 0 { if len(option.Reserved) != 3 { @@ -158,9 +171,12 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { connectAddr = option.Addr() } } - outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, outbound.dialer, isConnect, connectAddr, reserved) + outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, outbound.dialer, isConnect, connectAddr.AddrPort(), reserved) - var localPrefixes []netip.Prefix + localPrefixes, err := option.Prefixes() + if err != nil { + return nil, err + } var privateKey string { @@ -170,95 +186,145 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { } privateKey = hex.EncodeToString(bytes) } - ipcConf := "private_key=" + privateKey - if peersLen := len(option.Peers); peersLen > 0 { - localPrefixes = make([]netip.Prefix, 0, peersLen*2) + + if len(option.Peers) > 0 { for i, peer := range option.Peers { - var peerPublicKey, preSharedKey string - { - bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey) - if err != nil { - return nil, E.Cause(err, "decode public key for peer ", i) - } - peerPublicKey = hex.EncodeToString(bytes) + bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode public key for peer ", i) } + peer.PublicKey = hex.EncodeToString(bytes) + if peer.PreSharedKey != "" { bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey) if err != nil { return nil, E.Cause(err, "decode pre shared key for peer ", i) } - preSharedKey = hex.EncodeToString(bytes) - } - destination := peer.Addr() - ipcConf += "\npublic_key=" + peerPublicKey - ipcConf += "\nendpoint=" + destination.String() - if preSharedKey != "" { - ipcConf += "\npreshared_key=" + preSharedKey + peer.PreSharedKey = hex.EncodeToString(bytes) } + if len(peer.AllowedIPs) == 0 { return nil, E.New("missing allowed_ips for peer ", i) } - for _, allowedIP := range peer.AllowedIPs { - ipcConf += "\nallowed_ip=" + allowedIP - } + if len(peer.Reserved) > 0 { if len(peer.Reserved) != 3 { return nil, E.New("invalid reserved value for peer ", i, ", required 3 bytes, got ", len(peer.Reserved)) } - copy(reserved[:], option.Reserved) - outbound.bind.SetReservedForEndpoint(destination, reserved) } - prefixes, err := peer.Prefixes() - if err != nil { - return nil, err - } - localPrefixes = append(localPrefixes, prefixes...) } } else { - var peerPublicKey, preSharedKey string { bytes, err := base64.StdEncoding.DecodeString(option.PublicKey) if err != nil { return nil, E.Cause(err, "decode peer public key") } - peerPublicKey = hex.EncodeToString(bytes) + option.PublicKey = hex.EncodeToString(bytes) } if option.PreSharedKey != "" { bytes, err := base64.StdEncoding.DecodeString(option.PreSharedKey) if err != nil { return nil, E.Cause(err, "decode pre shared key") } - preSharedKey = hex.EncodeToString(bytes) + option.PreSharedKey = hex.EncodeToString(bytes) + } + } + + var ( + initOk atomic.Bool + initMutex sync.Mutex + initErr error + ) + + outbound.init = func(ctx context.Context) error { + if initOk.Load() { + return nil } - ipcConf += "\npublic_key=" + peerPublicKey - ipcConf += "\nendpoint=" + connectAddr.String() - if preSharedKey != "" { - ipcConf += "\npreshared_key=" + preSharedKey + initMutex.Lock() + defer initMutex.Unlock() + // double check like sync.Once + if initOk.Load() { + return nil } - var err error - localPrefixes, err = option.Prefixes() - if err != nil { - return nil, err + if initErr != nil { + return initErr } - var has4, has6 bool - for _, address := range localPrefixes { - if address.Addr().Is4() { - has4 = true - } else { - has6 = true + + outbound.bind.ResetReservedForEndpoint() + ipcConf := "private_key=" + privateKey + if len(option.Peers) > 0 { + for i, peer := range option.Peers { + destination, err := resolv(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 { + copy(reserved[:], option.Reserved) + outbound.bind.SetReservedForEndpoint(destination, reserved) + } + } + } else { + ipcConf += "\npublic_key=" + option.PublicKey + destination, err := resolv(ctx, 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") + } + outbound.bind.SetConnectAddr(destination) + ipcConf += "\nendpoint=" + destination.String() + if option.PreSharedKey != "" { + ipcConf += "\npreshared_key=" + option.PreSharedKey + } + var has4, has6 bool + for _, address := range localPrefixes { + if address.Addr().Is4() { + has4 = true + } else { + has6 = true + } + } + if has4 { + ipcConf += "\nallowed_ip=0.0.0.0/0" + } + if has6 { + ipcConf += "\nallowed_ip=::/0" } } - if has4 { - ipcConf += "\nallowed_ip=0.0.0.0/0" + + if option.PersistentKeepalive != 0 { + ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", option.PersistentKeepalive) } - if has6 { - ipcConf += "\nallowed_ip=::/0" + + if debug.Enabled { + log.SingLogger.Trace(fmt.Sprintf("[WG](%s) created wireguard ipc conf: \n %s", option.Name, ipcConf)) + } + err = outbound.device.IpcSet(ipcConf) + if err != nil { + initErr = E.Cause(err, "setup wireguard") + return initErr + } + + err = outbound.tunDevice.Start() + if err != nil { + initErr = err + return initErr } - } - if option.PersistentKeepalive != 0 { - ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", option.PersistentKeepalive) + initOk.Store(true) + return nil } + mtu := option.MTU if mtu == 0 { mtu = 1408 @@ -266,7 +332,6 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { if len(localPrefixes) == 0 { return nil, E.New("missing local address") } - var err error outbound.tunDevice, err = wireguard.NewStackDevice(localPrefixes, uint32(mtu)) if err != nil { return nil, E.Cause(err, "create WireGuard device") @@ -279,14 +344,6 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) }, }, option.Workers) - if debug.Enabled { - log.SingLogger.Trace(fmt.Sprintf("[WG](%s) created wireguard ipc conf: \n %s", option.Name, ipcConf)) - } - err = outbound.device.IpcSet(ipcConf) - if err != nil { - return nil, E.Cause(err, "setup wireguard") - } - //err = outbound.tunDevice.Start() var has6 bool for _, address := range localPrefixes { @@ -326,11 +383,8 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts options := w.Base.DialOptions(opts...) w.dialer.SetDialer(dialer.NewDialer(options...)) var conn net.Conn - w.startOnce.Do(func() { - w.startErr = w.tunDevice.Start() - }) - if w.startErr != nil { - return nil, w.startErr + if err = w.init(ctx); err != nil { + return nil, err } if !metadata.Resolved() || w.resolver != nil { r := resolver.DefaultResolver @@ -358,11 +412,8 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat options := w.Base.DialOptions(opts...) w.dialer.SetDialer(dialer.NewDialer(options...)) var pc net.PacketConn - w.startOnce.Do(func() { - w.startErr = w.tunDevice.Start() - }) - if w.startErr != nil { - return nil, w.startErr + if err = w.init(ctx); err != nil { + return nil, err } if err != nil { return nil, err diff --git a/adapter/outboundgroup/fallback.go b/adapter/outboundgroup/fallback.go index 4c8a22474c..9387f7dee8 100644 --- a/adapter/outboundgroup/fallback.go +++ b/adapter/outboundgroup/fallback.go @@ -164,6 +164,8 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) option.Filter, option.ExcludeFilter, option.ExcludeType, + option.TestTimeout, + option.MaxFailedTimes, providers, }), disableUDP: option.DisableUDP, diff --git a/adapter/outboundgroup/groupbase.go b/adapter/outboundgroup/groupbase.go index 0ea3685bec..b39ee3a659 100644 --- a/adapter/outboundgroup/groupbase.go +++ b/adapter/outboundgroup/groupbase.go @@ -31,14 +31,18 @@ type GroupBase struct { failedTesting atomic.Bool proxies [][]C.Proxy versions []atomic.Uint32 + TestTimeout int + maxFailedTimes int } type GroupBaseOption struct { outbound.BaseOption - filter string - excludeFilter string - excludeType string - providers []provider.ProxyProvider + filter string + excludeFilter string + excludeType string + TestTimeout int + maxFailedTimes int + providers []provider.ProxyProvider } func NewGroupBase(opt GroupBaseOption) *GroupBase { @@ -66,6 +70,15 @@ func NewGroupBase(opt GroupBaseOption) *GroupBase { excludeTypeArray: excludeTypeArray, providers: opt.providers, failedTesting: atomic.NewBool(false), + TestTimeout: opt.TestTimeout, + maxFailedTimes: opt.maxFailedTimes, + } + + if gb.TestTimeout == 0 { + gb.TestTimeout = 5000 + } + if gb.maxFailedTimes == 0 { + gb.maxFailedTimes = 5 } gb.proxies = make([][]C.Proxy, len(opt.providers)) @@ -240,13 +253,13 @@ func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error) { log.Debugln("ProxyGroup: %s first failed", gb.Name()) gb.failedTime = time.Now() } else { - if time.Since(gb.failedTime) > gb.failedTimeoutInterval() { + if time.Since(gb.failedTime) > time.Duration(gb.TestTimeout)*time.Millisecond { gb.failedTimes = 0 return } log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes) - if gb.failedTimes >= gb.maxFailedTimes() { + if gb.failedTimes >= gb.maxFailedTimes { log.Warnln("because %s failed multiple times, active health check", gb.Name()) gb.healthCheck() } @@ -275,20 +288,8 @@ func (gb *GroupBase) healthCheck() { gb.failedTimes = 0 } -func (gb *GroupBase) failedIntervalTime() int64 { - return 5 * time.Second.Milliseconds() -} - func (gb *GroupBase) onDialSuccess() { if !gb.failedTesting.Load() { gb.failedTimes = 0 } } - -func (gb *GroupBase) maxFailedTimes() int { - return 5 -} - -func (gb *GroupBase) failedTimeoutInterval() time.Duration { - return 5 * time.Second -} diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go index 976a2e89fa..4cb0db004f 100644 --- a/adapter/outboundgroup/loadbalance.go +++ b/adapter/outboundgroup/loadbalance.go @@ -266,6 +266,8 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide option.Filter, option.ExcludeFilter, option.ExcludeType, + option.TestTimeout, + option.MaxFailedTimes, providers, }), strategyFn: strategyFn, diff --git a/adapter/outboundgroup/parser.go b/adapter/outboundgroup/parser.go index 959041ddad..74947587f7 100644 --- a/adapter/outboundgroup/parser.go +++ b/adapter/outboundgroup/parser.go @@ -29,6 +29,7 @@ type GroupCommonOption struct { URL string `group:"url,omitempty"` Interval int `group:"interval,omitempty"` TestTimeout int `group:"timeout,omitempty"` + MaxFailedTimes int `group:"max-failed-times,omitempty"` Lazy bool `group:"lazy,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"` Filter string `group:"filter,omitempty"` @@ -64,20 +65,15 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide groupOption.IncludeAllProviders = true groupOption.IncludeAllProxies = true } - var GroupUse []string - var GroupProxies []string + if groupOption.IncludeAllProviders { - GroupUse = append(GroupUse, AllProviders...) - } else { - GroupUse = groupOption.Use + groupOption.Use = append(groupOption.Use, AllProviders...) } if groupOption.IncludeAllProxies { - GroupProxies = append(groupOption.Proxies, AllProxies...) - } else { - GroupProxies = groupOption.Proxies + groupOption.Proxies = append(groupOption.Proxies, AllProxies...) } - if len(GroupProxies) == 0 && len(GroupUse) == 0 { + if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { return nil, fmt.Errorf("%s: %w", groupName, errMissProxy) } @@ -91,17 +87,9 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide status = "*" } groupOption.ExpectedStatus = status - testUrl := groupOption.URL - if groupOption.Type != "select" && groupOption.Type != "relay" { - if groupOption.URL == "" { - groupOption.URL = C.DefaultTestURL - testUrl = groupOption.URL - } - } - - if len(GroupProxies) != 0 { - ps, err := getProxies(proxyMap, GroupProxies) + if len(groupOption.Proxies) != 0 { + ps, err := getProxies(proxyMap, groupOption.Proxies) if err != nil { return nil, fmt.Errorf("%s: %w", groupName, err) } @@ -115,9 +103,12 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide if groupOption.Interval == 0 { groupOption.Interval = 300 } + if groupOption.URL == "" { + groupOption.URL = C.DefaultTestURL + } } - hc := provider.NewHealthCheck(ps, testUrl, uint(groupOption.TestTimeout), uint(groupOption.Interval), groupOption.Lazy, expectedStatus) + hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.TestTimeout), uint(groupOption.Interval), groupOption.Lazy, expectedStatus) pd, err := provider.NewCompatibleProvider(groupName, ps, hc) if err != nil { @@ -128,18 +119,28 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide providersMap[groupName] = pd } - if len(GroupUse) != 0 { - list, err := getProviders(providersMap, GroupUse) + if len(groupOption.Use) != 0 { + list, err := getProviders(providersMap, groupOption.Use) if err != nil { return nil, fmt.Errorf("%s: %w", groupName, err) } - // different proxy groups use different test URL - addTestUrlToProviders(list, testUrl, expectedStatus, groupOption.Filter, uint(groupOption.Interval)) + if groupOption.URL == "" { + for _, p := range list { + if p.HealthCheckURL() != "" { + groupOption.URL = p.HealthCheckURL() + } + break + } + + if groupOption.URL == "" { + groupOption.URL = C.DefaultTestURL + } + } + // different proxy groups use different test URL + addTestUrlToProviders(list, groupOption.URL, expectedStatus, groupOption.Filter, uint(groupOption.Interval)) providers = append(providers, list...) - } else { - groupOption.Filter = "" } var group C.ProxyAdapter diff --git a/adapter/outboundgroup/relay.go b/adapter/outboundgroup/relay.go index 6a8e8bb1be..07fbcd9588 100644 --- a/adapter/outboundgroup/relay.go +++ b/adapter/outboundgroup/relay.go @@ -160,6 +160,8 @@ func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Re "", "", "", + 5000, + 5, providers, }), Hidden: option.Hidden, diff --git a/adapter/outboundgroup/selector.go b/adapter/outboundgroup/selector.go index 3ac740f414..20eca70ffd 100644 --- a/adapter/outboundgroup/selector.go +++ b/adapter/outboundgroup/selector.go @@ -114,6 +114,8 @@ func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) option.Filter, option.ExcludeFilter, option.ExcludeType, + option.TestTimeout, + option.MaxFailedTimes, providers, }), selected: "COMPATIBLE", diff --git a/adapter/outboundgroup/urltest.go b/adapter/outboundgroup/urltest.go index 8439772c51..5da44f3813 100644 --- a/adapter/outboundgroup/urltest.go +++ b/adapter/outboundgroup/urltest.go @@ -235,6 +235,8 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o option.Filter, option.ExcludeFilter, option.ExcludeType, + option.TestTimeout, + option.MaxFailedTimes, providers, }), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), diff --git a/adapter/parser.go b/adapter/parser.go index 1d363c1f95..c64ee13a48 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -120,6 +120,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { break } proxy = outbound.NewDirectWithOption(*directOption) + case "dns": + dnsOptions := &outbound.DnsOption{} + err = decoder.Decode(mapping, dnsOptions) + if err != nil { + break + } + proxy = outbound.NewDnsWithOption(*dnsOptions) case "reject": rejectOption := &outbound.RejectOption{} err = decoder.Decode(mapping, rejectOption) @@ -127,6 +134,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { break } proxy = outbound.NewRejectWithOption(*rejectOption) + case "ssh": + sshOption := &outbound.SshOption{} + err = decoder.Decode(mapping, sshOption) + if err != nil { + break + } + proxy, err = outbound.NewSsh(*sshOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } diff --git a/adapter/provider/healthcheck.go b/adapter/provider/healthcheck.go index fbc1d14f21..bfbcf8d904 100644 --- a/adapter/provider/healthcheck.go +++ b/adapter/provider/healthcheck.go @@ -211,8 +211,8 @@ func (hc *HealthCheck) close() { func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck { if url == "" { - // expectedStatus = nil - url = C.DefaultTestURL + expectedStatus = nil + interval = 0 } if timeout == 0 { timeout = 5000 diff --git a/adapter/provider/parser.go b/adapter/provider/parser.go index 88b36b7c45..2e4366691e 100644 --- a/adapter/provider/parser.go +++ b/adapter/provider/parser.go @@ -28,14 +28,16 @@ type healthCheckSchema struct { } type OverrideSchema struct { - UDP *bool `provider:"udp,omitempty"` - Up *string `provider:"up,omitempty"` - Down *string `provider:"down,omitempty"` - DialerProxy *string `provider:"dialer-proxy,omitempty"` - SkipCertVerify *bool `provider:"skip-cert-verify,omitempty"` - Interface *string `provider:"interface-name,omitempty"` - RoutingMark *int `provider:"routing-mark,omitempty"` - IPVersion *string `provider:"ip-version,omitempty"` + UDP *bool `provider:"udp,omitempty"` + Up *string `provider:"up,omitempty"` + Down *string `provider:"down,omitempty"` + DialerProxy *string `provider:"dialer-proxy,omitempty"` + SkipCertVerify *bool `provider:"skip-cert-verify,omitempty"` + Interface *string `provider:"interface-name,omitempty"` + RoutingMark *int `provider:"routing-mark,omitempty"` + IPVersion *string `provider:"ip-version,omitempty"` + AdditionalPrefix *string `provider:"additional-prefix,omitempty"` + AdditionalSuffix *string `provider:"additional-suffix,omitempty"` } type proxyProviderSchema struct { diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go index d710d3a433..2715a30972 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -46,18 +46,13 @@ type proxySetProvider struct { } func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { - expectedStatus := "*" - if pp.healthCheck.expectedStatus != nil { - expectedStatus = pp.healthCheck.expectedStatus.ToString() - } - return json.Marshal(map[string]any{ "name": pp.Name(), "type": pp.Type().String(), "vehicleType": pp.VehicleType().String(), "proxies": pp.Proxies(), "testUrl": pp.healthCheck.url, - "expectedStatus": expectedStatus, + "expectedStatus": pp.healthCheck.expectedStatus.String(), "updatedAt": pp.UpdatedAt, "subscriptionInfo": pp.subscriptionInfo, }) @@ -106,6 +101,10 @@ func (pp *proxySetProvider) Touch() { pp.healthCheck.touch() } +func (pp *proxySetProvider) HealthCheckURL() string { + return pp.healthCheck.url +} + func (pp *proxySetProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { pp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval) } @@ -217,18 +216,13 @@ type compatibleProvider struct { } func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { - expectedStatus := "*" - if cp.healthCheck.expectedStatus != nil { - expectedStatus = cp.healthCheck.expectedStatus.ToString() - } - return json.Marshal(map[string]any{ "name": cp.Name(), "type": cp.Type().String(), "vehicleType": cp.VehicleType().String(), "proxies": cp.Proxies(), "testUrl": cp.healthCheck.url, - "expectedStatus": expectedStatus, + "expectedStatus": cp.healthCheck.expectedStatus.String(), }) } @@ -271,6 +265,10 @@ func (cp *compatibleProvider) Touch() { cp.healthCheck.touch() } +func (cp *compatibleProvider) HealthCheckURL() string { + return cp.healthCheck.url +} + func (cp *compatibleProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { cp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval) } @@ -399,6 +397,14 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray 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) if err != nil { diff --git a/android_tz.go b/android_tz.go new file mode 100644 index 0000000000..82fc38e315 --- /dev/null +++ b/android_tz.go @@ -0,0 +1,21 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89 + +package main + +// #include +import "C" +import "time" + +func init() { + var currentT C.time_t + var currentTM C.struct_tm + C.time(¤tT) + C.localtime_r(¤tT, ¤tTM) + tzOffset := int(currentTM.tm_gmtoff) + tz := C.GoString(currentTM.tm_zone) + time.Local = time.FixedZone(tz, tzOffset) +} diff --git a/common/atomic/value.go b/common/atomic/value.go index cbc6c5b8c3..36623b3ee1 100644 --- a/common/atomic/value.go +++ b/common/atomic/value.go @@ -15,20 +15,31 @@ type TypedValue[T any] struct { value atomic.Value } +// tValue is a struct with determined type to resolve atomic.Value usages with interface types +// https://github.com/golang/go/issues/22550 +// +// The intention to have an atomic value store for errors. However, running this code panics: +// panic: sync/atomic: store of inconsistently typed value into Value +// This is because atomic.Value requires that the underlying concrete type be the same (which is a reasonable expectation for its implementation). +// When going through the atomic.Value.Store method call, the fact that both these are of the error interface is lost. +type tValue[T any] struct { + value T +} + func (t *TypedValue[T]) Load() T { value := t.value.Load() if value == nil { return DefaultValue[T]() } - return value.(T) + return value.(tValue[T]).value } func (t *TypedValue[T]) Store(value T) { - t.value.Store(value) + t.value.Store(tValue[T]{value}) } func (t *TypedValue[T]) Swap(new T) T { - old := t.value.Swap(new) + old := t.value.Swap(tValue[T]{new}) if old == nil { return DefaultValue[T]() } @@ -36,7 +47,7 @@ func (t *TypedValue[T]) Swap(new T) T { } func (t *TypedValue[T]) CompareAndSwap(old, new T) bool { - return t.value.CompareAndSwap(old, new) + return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) } func (t *TypedValue[T]) MarshalJSON() ([]byte, error) { diff --git a/common/net/deadline/conn.go b/common/net/deadline/conn.go index e8446ce2b9..fdf9334fd6 100644 --- a/common/net/deadline/conn.go +++ b/common/net/deadline/conn.go @@ -26,6 +26,11 @@ type Conn struct { resultCh chan *connReadResult } +func IsConn(conn any) bool { + _, ok := conn.(*Conn) + return ok +} + func NewConn(conn net.Conn) *Conn { c := &Conn{ ExtendedConn: bufio.NewExtendedConn(conn), diff --git a/common/net/deadline/pipe_sing.go b/common/net/deadline/pipe_sing.go index 20721fadbd..0f6d378da7 100644 --- a/common/net/deadline/pipe_sing.go +++ b/common/net/deadline/pipe_sing.go @@ -215,3 +215,8 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) { return nil, os.ErrDeadlineExceeded } } + +func IsPipe(conn any) bool { + _, ok := conn.(*pipe) + return ok +} diff --git a/common/net/earlyconn.go b/common/net/earlyconn.go index c9a428194b..82d392eeb0 100644 --- a/common/net/earlyconn.go +++ b/common/net/earlyconn.go @@ -3,10 +3,9 @@ package net import ( "net" "sync" - "sync/atomic" - "unsafe" "github.com/metacubex/mihomo/common/buf" + "github.com/metacubex/mihomo/common/once" ) type earlyConn struct { @@ -44,8 +43,7 @@ func (conn *earlyConn) Upstream() any { } func (conn *earlyConn) Success() bool { - // atomic visit sync.Once.done - return atomic.LoadUint32((*uint32)(unsafe.Pointer(&conn.resOnce))) == 1 && conn.resErr == nil + return once.Done(&conn.resOnce) && conn.resErr == nil } func (conn *earlyConn) ReaderReplaceable() bool { diff --git a/common/net/sing.go b/common/net/sing.go index 3296ad5ba9..d726f4409d 100644 --- a/common/net/sing.go +++ b/common/net/sing.go @@ -23,6 +23,12 @@ type ExtendedReader = network.ExtendedReader var WriteBuffer = bufio.WriteBuffer func NewDeadlineConn(conn net.Conn) ExtendedConn { + if deadline.IsPipe(conn) || deadline.IsPipe(network.UnwrapReader(conn)) { + return NewExtendedConn(conn) // pipe always have correctly deadline implement + } + if deadline.IsConn(conn) || deadline.IsConn(network.UnwrapReader(conn)) { + return NewExtendedConn(conn) // was a *deadline.Conn + } return deadline.NewConn(conn) } diff --git a/common/once/once_go120.go b/common/once/once_go120.go new file mode 100644 index 0000000000..51578a2da2 --- /dev/null +++ b/common/once/once_go120.go @@ -0,0 +1,26 @@ +//go:build !go1.22 + +package once + +import ( + "sync" + "sync/atomic" + "unsafe" +) + +type Once struct { + done uint32 + m sync.Mutex +} + +func Done(once *sync.Once) bool { + // atomic visit sync.Once.done + return atomic.LoadUint32((*uint32)(unsafe.Pointer(once))) == 1 +} + +func Reset(once *sync.Once) { + o := (*Once)(unsafe.Pointer(once)) + o.m.Lock() + defer o.m.Unlock() + atomic.StoreUint32(&o.done, 0) +} diff --git a/common/once/once_go122.go b/common/once/once_go122.go new file mode 100644 index 0000000000..5ff8461d6e --- /dev/null +++ b/common/once/once_go122.go @@ -0,0 +1,26 @@ +//go:build go1.22 + +package once + +import ( + "sync" + "sync/atomic" + "unsafe" +) + +type Once struct { + done atomic.Uint32 + m sync.Mutex +} + +func Done(once *sync.Once) bool { + // atomic visit sync.Once.done + return (*atomic.Uint32)(unsafe.Pointer(once)).Load() == 1 +} + +func Reset(once *sync.Once) { + o := (*Once)(unsafe.Pointer(once)) + o.m.Lock() + defer o.m.Unlock() + o.done.Store(0) +} diff --git a/common/utils/ranges.go b/common/utils/ranges.go index 810105ff73..c71f84c9d5 100644 --- a/common/utils/ranges.go +++ b/common/utils/ranges.go @@ -20,6 +20,8 @@ func newIntRanges[T constraints.Integer](expected string, parseFn func(string) ( return nil, nil } + // support: 200,302 or 200,204,401-429,501-503 + expected = strings.ReplaceAll(expected, ",", "/") list := strings.Split(expected, "/") if len(list) > 28 { return nil, fmt.Errorf("%w, too many ranges to use, maximum support 28 ranges", errIntRanges) @@ -47,9 +49,9 @@ func newIntRangesFromList[T constraints.Integer](list []string, parseFn func(str } switch statusLen { - case 1: + case 1: // Port range ranges = append(ranges, NewRange(T(start), T(start))) - case 2: + case 2: // Single port end, err := parseFn(strings.Trim(status[1], "[ ]")) if err != nil { return nil, errIntRanges @@ -108,7 +110,7 @@ func (ranges IntRanges[T]) Check(status T) bool { return false } -func (ranges IntRanges[T]) ToString() string { +func (ranges IntRanges[T]) String() string { if len(ranges) == 0 { return "*" } @@ -130,3 +132,17 @@ func (ranges IntRanges[T]) ToString() string { return strings.Join(terms, "/") } + +func (ranges IntRanges[T]) Range(f func(t T) bool) { + if len(ranges) == 0 { + return + } + + for _, r := range ranges { + for i := r.Start(); i <= r.End(); i++ { + if !f(i) { + return + } + } + } +} diff --git a/component/dialer/bind.go b/component/dialer/bind.go index 72df8c7210..9b6471a323 100644 --- a/component/dialer/bind.go +++ b/component/dialer/bind.go @@ -14,6 +14,7 @@ func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination if err != nil { return nil, err } + destination = destination.Unmap() var addr netip.Prefix switch network { @@ -23,7 +24,7 @@ func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination addr, err = ifaceObj.PickIPv6Addr(destination) default: if destination.IsValid() { - if destination.Is4() || destination.Is4In6() { + if destination.Is4() { addr, err = ifaceObj.PickIPv4Addr(destination) } else { addr, err = ifaceObj.PickIPv6Addr(destination) diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go index 070d98b816..12e7c960c5 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -7,12 +7,14 @@ import ( "net" "net/netip" "os" + "strconv" "strings" "sync" "time" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/constant/features" + "github.com/metacubex/mihomo/log" ) const ( @@ -24,6 +26,7 @@ type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port s var ( dialMux sync.Mutex + IP4PEnable bool actualSingleStackDialContext = serialSingleStackDialContext actualDualStackDialContext = serialDualStackDialContext tcpConcurrent = false @@ -128,7 +131,13 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po return dialContextHooked(ctx, network, destination, port) } - address := net.JoinHostPort(destination.String(), port) + var address string + if IP4PEnable { + NewDestination, NewPort := lookupIP4P(destination.String(), port) + address = net.JoinHostPort(NewDestination, NewPort) + } else { + address = net.JoinHostPort(destination.String(), port) + } netDialer := opt.netDialer switch netDialer.(type) { @@ -383,3 +392,21 @@ func NewDialer(options ...Option) Dialer { opt := applyOptions(options...) return Dialer{Opt: *opt} } + +func GetIP4PEnable(enableIP4PConvert bool) { + IP4PEnable = enableIP4PConvert +} + +// kanged from https://github.com/heiher/frp/blob/ip4p/client/ip4p.go + +func lookupIP4P(addr string, port string) (string, string) { + ip := net.ParseIP(addr) + if ip[0] == 0x20 && ip[1] == 0x01 && + ip[2] == 0x00 && ip[3] == 0x00 { + addr = net.IPv4(ip[12], ip[13], ip[14], ip[15]).String() + port = strconv.Itoa(int(ip[10])<<8 + int(ip[11])) + log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr, port)) + return addr, port + } + return addr, port +} diff --git a/component/dialer/tfo.go b/component/dialer/tfo.go index 950bdfe497..228e96898b 100644 --- a/component/dialer/tfo.go +++ b/component/dialer/tfo.go @@ -6,7 +6,7 @@ import ( "net" "time" - "github.com/sagernet/tfo-go" + "github.com/metacubex/tfo-go" ) type tfoConn struct { diff --git a/component/geodata/init.go b/component/geodata/init.go index 842efcc55d..834567a447 100644 --- a/component/geodata/init.go +++ b/component/geodata/init.go @@ -14,8 +14,11 @@ import ( "github.com/metacubex/mihomo/log" ) -var initGeoSite bool -var initGeoIP int +var ( + initGeoSite bool + initGeoIP int + initASN bool +) func InitGeoSite() error { if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { @@ -113,7 +116,7 @@ func InitGeoIP() error { } if initGeoIP != 2 { - if !mmdb.Verify() { + if !mmdb.Verify(C.Path.MMDB()) { log.Warnln("MMDB invalid, remove and download") if err := os.Remove(C.Path.MMDB()); err != nil { return fmt.Errorf("can't remove invalid MMDB: %s", err.Error()) @@ -126,3 +129,27 @@ func InitGeoIP() error { } return nil } + +func InitASN() error { + if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) { + log.Infoln("Can't find ASN.mmdb, start download") + if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { + return fmt.Errorf("can't download ASN.mmdb: %s", err.Error()) + } + log.Infoln("Download ASN.mmdb finish") + initASN = false + } + if !initASN { + if !mmdb.Verify(C.Path.ASN()) { + log.Warnln("ASN invalid, remove and download") + if err := os.Remove(C.Path.ASN()); err != nil { + return fmt.Errorf("can't remove invalid ASN: %s", err.Error()) + } + if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { + return fmt.Errorf("can't download ASN: %s", err.Error()) + } + } + initASN = true + } + return nil +} diff --git a/component/geodata/utils.go b/component/geodata/utils.go index a4002aeb3b..981d7eba4f 100644 --- a/component/geodata/utils.go +++ b/component/geodata/utils.go @@ -3,19 +3,37 @@ package geodata import ( "errors" "fmt" - "golang.org/x/sync/singleflight" "strings" + "golang.org/x/sync/singleflight" + "github.com/metacubex/mihomo/component/geodata/router" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" ) -var geoLoaderName = "memconservative" -var geoSiteMatcher = "succinct" +var ( + geoMode bool + AutoUpdate bool + UpdateInterval int + geoLoaderName = "memconservative" + geoSiteMatcher = "succinct" +) // geoLoaderName = "standard" +func GeodataMode() bool { + return geoMode +} + +func GeoAutoUpdate() bool { + return AutoUpdate +} + +func GeoUpdateInterval() int { + return UpdateInterval +} + func LoaderName() string { return geoLoaderName } @@ -24,6 +42,16 @@ func SiteMatcherName() string { return geoSiteMatcher } +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" { newLoader = "memconservative" diff --git a/component/iface/iface.go b/component/iface/iface.go index bf186165fc..1d0219dfac 100644 --- a/component/iface/iface.go +++ b/component/iface/iface.go @@ -4,7 +4,6 @@ import ( "errors" "net" "net/netip" - "strings" "time" "github.com/metacubex/mihomo/common/singledo" @@ -38,28 +37,26 @@ func ResolveInterface(name string) (*Interface, error) { if err != nil { continue } - // if not available device like Meta, dummy0, docker0, etc. - if (iface.Flags&net.FlagMulticast == 0) || (iface.Flags&net.FlagPointToPoint != 0) || (iface.Flags&net.FlagRunning == 0) { - continue - } ipNets := make([]netip.Prefix, 0, len(addrs)) for _, addr := range addrs { - ipNet := addr.(*net.IPNet) - ip, _ := netip.AddrFromSlice(ipNet.IP) - - //unavailable IPv6 Address - if ip.Is6() && strings.HasPrefix(ip.String(), "fe80") { - continue - } - - ones, bits := ipNet.Mask.Size() - if bits == 32 { + var pf netip.Prefix + switch ipNet := addr.(type) { + case *net.IPNet: + ip, _ := netip.AddrFromSlice(ipNet.IP) + ones, bits := ipNet.Mask.Size() + if bits == 32 { + ip = ip.Unmap() + } + pf = netip.PrefixFrom(ip, ones) + case *net.IPAddr: + ip, _ := netip.AddrFromSlice(ipNet.IP) ip = ip.Unmap() + pf = netip.PrefixFrom(ip, ip.BitLen()) + } + if pf.IsValid() { + ipNets = append(ipNets, pf) } - - pf := netip.PrefixFrom(ip, ones) - ipNets = append(ipNets, pf) } r[iface.Name] = &Interface{ diff --git a/component/mmdb/mmdb.go b/component/mmdb/mmdb.go index d411b2b42d..81156bc62d 100644 --- a/component/mmdb/mmdb.go +++ b/component/mmdb/mmdb.go @@ -8,6 +8,7 @@ import ( "sync" "time" + mihomoOnce "github.com/metacubex/mihomo/common/once" mihomoHttp "github.com/metacubex/mihomo/component/http" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" @@ -24,56 +25,58 @@ const ( ) var ( - reader Reader - once sync.Once + IPreader IPReader + ASNreader ASNReader + IPonce sync.Once + ASNonce sync.Once ) func LoadFromBytes(buffer []byte) { - once.Do(func() { + IPonce.Do(func() { mmdb, err := maxminddb.FromBytes(buffer) if err != nil { log.Fatalln("Can't load mmdb: %s", err.Error()) } - reader = Reader{Reader: mmdb} + IPreader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": - reader.databaseType = typeSing + IPreader.databaseType = typeSing case "Meta-geoip0": - reader.databaseType = typeMetaV0 + IPreader.databaseType = typeMetaV0 default: - reader.databaseType = typeMaxmind + IPreader.databaseType = typeMaxmind } }) } -func Verify() bool { - instance, err := maxminddb.Open(C.Path.MMDB()) +func Verify(path string) bool { + instance, err := maxminddb.Open(path) if err == nil { instance.Close() } return err == nil } -func Instance() Reader { - once.Do(func() { +func IPInstance() IPReader { + IPonce.Do(func() { mmdbPath := C.Path.MMDB() - log.Debugln("Load MMDB file: %s", mmdbPath) + log.Infoln("Load MMDB file: %s", mmdbPath) mmdb, err := maxminddb.Open(mmdbPath) if err != nil { log.Fatalln("Can't load MMDB: %s", err.Error()) } - reader = Reader{Reader: mmdb} + IPreader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": - reader.databaseType = typeSing + IPreader.databaseType = typeSing case "Meta-geoip0": - reader.databaseType = typeMetaV0 + IPreader.databaseType = typeMetaV0 default: - reader.databaseType = typeMaxmind + IPreader.databaseType = typeMaxmind } }) - return reader + return IPreader } func DownloadMMDB(path string) (err error) { @@ -94,3 +97,44 @@ func DownloadMMDB(path string) (err error) { return err } + +func ASNInstance() ASNReader { + ASNonce.Do(func() { + ASNPath := C.Path.ASN() + log.Infoln("Load ASN file: %s", ASNPath) + asn, err := maxminddb.Open(ASNPath) + if err != nil { + log.Fatalln("Can't load ASN: %s", err.Error()) + } + ASNreader = ASNReader{Reader: asn} + }) + + return ASNreader +} + +func DownloadASN(path string) (err error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, C.ASNUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) + if err != nil { + return + } + defer resp.Body.Close() + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + + return err +} + +func ReloadIP() { + mihomoOnce.Reset(&IPonce) +} + +func ReloadASN() { + mihomoOnce.Reset(&ASNonce) +} diff --git a/component/mmdb/patch_android.go b/component/mmdb/patch_android.go index a994b75e3f..147a332434 100644 --- a/component/mmdb/patch_android.go +++ b/component/mmdb/patch_android.go @@ -5,14 +5,14 @@ package mmdb import "github.com/oschwald/maxminddb-golang" func InstallOverride(override *maxminddb.Reader) { - newReader := Reader{Reader: override} + newReader := IPReader{Reader: override} switch override.Metadata.DatabaseType { case "sing-geoip": - reader.databaseType = typeSing + IPreader.databaseType = typeSing case "Meta-geoip0": - reader.databaseType = typeMetaV0 + IPreader.databaseType = typeMetaV0 default: - reader.databaseType = typeMaxmind + IPreader.databaseType = typeMaxmind } - reader = newReader + IPreader = newReader } diff --git a/component/mmdb/reader.go b/component/mmdb/reader.go index 4db53d4f7e..e76e993986 100644 --- a/component/mmdb/reader.go +++ b/component/mmdb/reader.go @@ -3,9 +3,9 @@ package mmdb import ( "fmt" "net" + "strings" "github.com/oschwald/maxminddb-golang" - "github.com/sagernet/sing/common" ) type geoip2Country struct { @@ -14,12 +14,21 @@ type geoip2Country struct { } `maxminddb:"country"` } -type Reader struct { +type IPReader struct { *maxminddb.Reader databaseType } -func (r Reader) LookupCode(ipAddress net.IP) []string { +type ASNReader struct { + *maxminddb.Reader +} + +type ASNResult struct { + AutonomousSystemNumber uint32 `maxminddb:"autonomous_system_number"` + AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` +} + +func (r IPReader) LookupCode(ipAddress net.IP) []string { switch r.databaseType { case typeMaxmind: var country geoip2Country @@ -27,7 +36,7 @@ func (r Reader) LookupCode(ipAddress net.IP) []string { if country.Country.IsoCode == "" { return []string{} } - return []string{country.Country.IsoCode} + return []string{strings.ToLower(country.Country.IsoCode)} case typeSing: var code string @@ -44,9 +53,11 @@ func (r Reader) LookupCode(ipAddress net.IP) []string { case string: return []string{record} case []any: // lookup returned type of slice is []any - return common.Map(record, func(it any) string { - return it.(string) - }) + result := make([]string, 0, len(record)) + for _, item := range record { + result = append(result, item.(string)) + } + return result } return []string{} @@ -54,3 +65,9 @@ func (r Reader) LookupCode(ipAddress net.IP) []string { panic(fmt.Sprint("unknown geoip database type:", r.databaseType)) } } + +func (r ASNReader) LookupASN(ip net.IP) ASNResult { + var result ASNResult + r.Lookup(ip, &result) + return result +} diff --git a/component/power/event.go b/component/power/event.go new file mode 100644 index 0000000000..f59c2ad43e --- /dev/null +++ b/component/power/event.go @@ -0,0 +1,22 @@ +package power + +type Type uint8 + +const ( + SUSPEND Type = iota + RESUME + RESUMEAUTOMATIC // Because the user is not present, most applications should do nothing. +) + +func (t Type) String() string { + switch t { + case SUSPEND: + return "SUSPEND" + case RESUME: + return "RESUME" + case RESUMEAUTOMATIC: + return "RESUMEAUTOMATIC" + default: + return "" + } +} diff --git a/component/power/event_other.go b/component/power/event_other.go new file mode 100644 index 0000000000..3a41d9e022 --- /dev/null +++ b/component/power/event_other.go @@ -0,0 +1,9 @@ +//go:build !windows + +package power + +import "errors" + +func NewEventListener(cb func(Type)) (func(), error) { + return nil, errors.New("not support on this platform") +} diff --git a/component/power/event_windows.go b/component/power/event_windows.go new file mode 100644 index 0000000000..1265569522 --- /dev/null +++ b/component/power/event_windows.go @@ -0,0 +1,74 @@ +package power + +// modify from https://github.com/golang/go/blob/b634f6fdcbebee23b7da709a243f3db217b64776/src/runtime/os_windows.go#L257 + +import ( + "runtime" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + libPowrProf = windows.NewLazySystemDLL("powrprof.dll") + powerRegisterSuspendResumeNotification = libPowrProf.NewProc("PowerRegisterSuspendResumeNotification") + powerUnregisterSuspendResumeNotification = libPowrProf.NewProc("PowerUnregisterSuspendResumeNotification") +) + +func NewEventListener(cb func(Type)) (func(), error) { + if err := powerRegisterSuspendResumeNotification.Find(); err != nil { + return nil, err // Running on Windows 7, where we don't need it anyway. + } + if err := powerUnregisterSuspendResumeNotification.Find(); err != nil { + return nil, err // Running on Windows 7, where we don't need it anyway. + } + + // Defines the type of event + const ( + PBT_APMSUSPEND uint32 = 4 + PBT_APMRESUMESUSPEND uint32 = 7 + PBT_APMRESUMEAUTOMATIC uint32 = 18 + ) + + const ( + _DEVICE_NOTIFY_CALLBACK = 2 + ) + type _DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS struct { + callback uintptr + context uintptr + } + + var fn interface{} = func(context uintptr, changeType uint32, setting uintptr) uintptr { + switch changeType { + case PBT_APMSUSPEND: + cb(SUSPEND) + case PBT_APMRESUMESUSPEND: + cb(RESUME) + case PBT_APMRESUMEAUTOMATIC: + cb(RESUMEAUTOMATIC) + } + return 0 + } + + params := _DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS{ + callback: windows.NewCallback(fn), + } + handle := uintptr(0) + + _, _, err := powerRegisterSuspendResumeNotification.Call( + _DEVICE_NOTIFY_CALLBACK, + uintptr(unsafe.Pointer(¶ms)), + uintptr(unsafe.Pointer(&handle)), + ) + if err != nil { + return nil, err + } + + return func() { + _, _, _ = powerUnregisterSuspendResumeNotification.Call( + uintptr(unsafe.Pointer(&handle)), + ) + runtime.KeepAlive(params) + runtime.KeepAlive(handle) + }, nil +} diff --git a/component/resolver/host.go b/component/resolver/host.go index 69c29a3c65..4a42962903 100644 --- a/component/resolver/host.go +++ b/component/resolver/host.go @@ -3,6 +3,8 @@ package resolver import ( "errors" "net/netip" + "os" + "strconv" "strings" _ "unsafe" @@ -11,6 +13,8 @@ import ( "github.com/zhangyunhao116/fastrand" ) +var DisableSystemHosts, _ = strconv.ParseBool(os.Getenv("DISABLE_SYSTEM_HOSTS")) + type Hosts struct { *trie.DomainTrie[HostValue] } @@ -47,7 +51,7 @@ func (h *Hosts) Search(domain string, isDomain bool) (*HostValue, bool) { return &hostValue, false } - if !isDomain { + if !isDomain && !DisableSystemHosts { addr, _ := lookupStaticHost(domain) if hostValue, err := NewHostValue(addr); err == nil { return &hostValue, true diff --git a/component/resolver/relay.go b/component/resolver/relay.go new file mode 100644 index 0000000000..27b25af1d9 --- /dev/null +++ b/component/resolver/relay.go @@ -0,0 +1,88 @@ +package resolver + +import ( + "context" + "encoding/binary" + "io" + "net" + "time" + + "github.com/metacubex/mihomo/common/pool" + + D "github.com/miekg/dns" +) + +const DefaultDnsReadTimeout = time.Second * 10 +const DefaultDnsRelayTimeout = time.Second * 5 + +const SafeDnsPacketSize = 2 * 1024 // safe size which is 1232 from https://dnsflagday.net/2020/, so 2048 is enough + +func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration) error { + buff := pool.Get(pool.UDPBufferSize) + defer func() { + _ = pool.Put(buff) + _ = conn.Close() + }() + for { + if readTimeout > 0 { + _ = conn.SetReadDeadline(time.Now().Add(readTimeout)) + } + + length := uint16(0) + if err := binary.Read(conn, binary.BigEndian, &length); err != nil { + break + } + + if int(length) > len(buff) { + break + } + + n, err := io.ReadFull(conn, buff[:length]) + if err != nil { + break + } + + err = func() error { + ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) + defer cancel() + inData := buff[:n] + msg, err := RelayDnsPacket(ctx, inData, buff) + if err != nil { + return err + } + + err = binary.Write(conn, binary.BigEndian, uint16(len(msg))) + if err != nil { + return err + } + + _, err = conn.Write(msg) + if err != nil { + return err + } + return nil + }() + if err != nil { + return err + } + } + return nil +} + +func RelayDnsPacket(ctx context.Context, payload []byte, target []byte) ([]byte, error) { + msg := &D.Msg{} + if err := msg.Unpack(payload); err != nil { + return nil, err + } + + r, err := ServeMsg(ctx, msg) + if err != nil { + m := new(D.Msg) + m.SetRcode(msg, D.RcodeServerFailure) + return m.PackBuffer(target) + } + + r.SetRcode(msg, r.Rcode) + r.Compress = true + return r.PackBuffer(target) +} diff --git a/component/slowdown/slowdown.go b/component/slowdown/slowdown.go index 3fc12191a3..eff4915f1d 100644 --- a/component/slowdown/slowdown.go +++ b/component/slowdown/slowdown.go @@ -12,8 +12,10 @@ type SlowDown struct { } func (s *SlowDown) Wait(ctx context.Context) (err error) { + timer := time.NewTimer(s.backoff.Duration()) + defer timer.Stop() select { - case <-time.After(s.backoff.Duration()): + case <-timer.C: case <-ctx.Done(): err = ctx.Err() } diff --git a/config/config.go b/config/config.go index b04015969f..ca179ed051 100644 --- a/config/config.go +++ b/config/config.go @@ -152,6 +152,7 @@ type IPTables struct { Enable bool `yaml:"enable" json:"enable"` InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"` Bypass []string `yaml:"bypass" json:"bypass"` + DnsRedirect bool `yaml:"dns-redirect" json:"dns-redirect"` } type Sniffer struct { @@ -168,6 +169,7 @@ type Experimental struct { Fingerprints []string `yaml:"fingerprints"` QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"` QUICGoDisableECN bool `yaml:"quic-go-disable-ecn"` + IP4PEnable bool `yaml:"dialer-ip4p-convert"` } // Config is mihomo config manager @@ -263,6 +265,7 @@ type RawTun struct { EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"` UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"` FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` + TableIndex int `yaml:"table-index" json:"table-index"` } type RawTuicServer struct { @@ -346,6 +349,7 @@ type RawConfig struct { type GeoXUrl struct { GeoIp string `yaml:"geoip" json:"geoip"` Mmdb string `yaml:"mmdb" json:"mmdb"` + ASN string `yaml:"asn" json:"asn"` GeoSite string `yaml:"geosite" json:"geosite"` } @@ -440,6 +444,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { Enable: false, InboundInterface: "lo", Bypass: []string{}, + DnsRedirect: true, }, NTP: RawNTP{ Enable: false, @@ -492,6 +497,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { }, GeoXUrl: GeoXUrl{ Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", + ASN: "https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb", GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", GeoSite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat", }, @@ -607,6 +613,9 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } func parseGeneral(cfg *RawConfig) (*General, error) { + geodata.SetGeodataMode(cfg.GeodataMode) + geodata.SetGeoAutoUpdate(cfg.GeoAutoUpdate) + geodata.SetGeoUpdateInterval(cfg.GeoUpdateInterval) geodata.SetLoader(cfg.GeodataLoader) geodata.SetSiteMatcher(cfg.GeositeMatcher) C.GeoAutoUpdate = cfg.GeoAutoUpdate @@ -614,6 +623,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) { C.GeoIpUrl = cfg.GeoXUrl.GeoIp C.GeoSiteUrl = cfg.GeoXUrl.GeoSite C.MmdbUrl = cfg.GeoXUrl.Mmdb + C.ASNUrl = cfg.GeoXUrl.ASN C.GeodataMode = cfg.GeodataMode C.UA = cfg.GlobalUA if cfg.KeepAliveInterval != 0 { @@ -1439,6 +1449,7 @@ func parseTun(rawTun RawTun, general *General) error { EndpointIndependentNat: rawTun.EndpointIndependentNat, UDPTimeout: rawTun.UDPTimeout, FileDescriptor: rawTun.FileDescriptor, + TableIndex: rawTun.TableIndex, } return nil diff --git a/config/update_geo.go b/config/update_geo.go index 2cde47a11d..43cac25c8d 100644 --- a/config/update_geo.go +++ b/config/update_geo.go @@ -6,6 +6,7 @@ import ( "github.com/metacubex/mihomo/component/geodata" _ "github.com/metacubex/mihomo/component/geodata/standard" + "github.com/metacubex/mihomo/component/mmdb" C "github.com/metacubex/mihomo/constant" "github.com/oschwald/maxminddb-golang" @@ -33,6 +34,7 @@ func UpdateGeoDatabases() error { } } else { + defer mmdb.ReloadIP() data, err := downloadForBytes(C.MmdbUrl) if err != nil { return fmt.Errorf("can't download MMDB database file: %w", err) @@ -43,11 +45,32 @@ func UpdateGeoDatabases() error { return fmt.Errorf("invalid MMDB database file: %s", err) } _ = instance.Close() + + mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file if err = saveFile(data, C.Path.MMDB()); err != nil { return fmt.Errorf("can't save MMDB database file: %w", err) } } + if C.ASNEnable { + defer mmdb.ReloadASN() + data, err := downloadForBytes(C.ASNUrl) + if err != nil { + return fmt.Errorf("can't download ASN database file: %w", err) + } + + instance, err := maxminddb.FromBytes(data) + if err != nil { + return fmt.Errorf("invalid ASN database file: %s", err) + } + _ = instance.Close() + + mmdb.ASNInstance().Reader.Close() + if err = saveFile(data, C.Path.ASN()); err != nil { + return fmt.Errorf("can't save ASN database file: %w", err) + } + } + data, err := downloadForBytes(C.GeoSiteUrl) if err != nil { return fmt.Errorf("can't download GeoSite database file: %w", err) diff --git a/constant/adapters.go b/constant/adapters.go index 83c8223a9c..cb213b3c34 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -21,6 +21,7 @@ const ( RejectDrop Compatible Pass + Dns Relay Selector @@ -40,6 +41,7 @@ const ( Hysteria2 WireGuard Tuic + Ssh ) const ( @@ -184,6 +186,8 @@ func (at AdapterType) String() string { return "Compatible" case Pass: return "Pass" + case Dns: + return "Dns" case Shadowsocks: return "Shadowsocks" case ShadowsocksR: @@ -219,7 +223,8 @@ func (at AdapterType) String() string { return "URLTest" case LoadBalance: return "LoadBalance" - + case Ssh: + return "Ssh" default: return "Unknown" } diff --git a/constant/geodata.go b/constant/geodata.go index e93d56b305..cd3f74e30c 100644 --- a/constant/geodata.go +++ b/constant/geodata.go @@ -1,10 +1,12 @@ package constant var ( + ASNEnable bool GeodataMode bool GeoAutoUpdate bool GeoUpdateInterval int GeoIpUrl string MmdbUrl string GeoSiteUrl string + ASNUrl string ) diff --git a/constant/metadata.go b/constant/metadata.go index 6df6ff433d..381e2dd44a 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -133,6 +133,8 @@ type Metadata struct { Type Type `json:"type"` SrcIP netip.Addr `json:"sourceIP"` DstIP netip.Addr `json:"destinationIP"` + DstGeoIP []string `json:"destinationGeoIP"` // can be nil if never queried, empty slice if got no result + DstIPASN string `json:"destinationIPASN"` SrcPort uint16 `json:"sourcePort,string"` // `,string` is used to compatible with old version json output DstPort uint16 `json:"destinationPort,string"` // `,string` is used to compatible with old version json output InIP netip.Addr `json:"inboundIP"` diff --git a/constant/path.go b/constant/path.go index a920fbbc62..77f7d0ef0a 100644 --- a/constant/path.go +++ b/constant/path.go @@ -15,6 +15,7 @@ const Name = "mihomo" var ( GeositeName = "GeoSite.dat" GeoipName = "GeoIP.dat" + ASNName = "ASN.mmdb" ) // Path is used to get the configuration path @@ -112,6 +113,25 @@ func (p *path) MMDB() string { return P.Join(p.homeDir, "geoip.metadb") } +func (p *path) ASN() string { + files, err := os.ReadDir(p.homeDir) + if err != nil { + return "" + } + for _, fi := range files { + if fi.IsDir() { + // 目录则直接跳过 + continue + } else { + if strings.EqualFold(fi.Name(), "ASN.mmdb") { + ASNName = fi.Name() + return P.Join(p.homeDir, fi.Name()) + } + } + } + return P.Join(p.homeDir, ASNName) +} + func (p *path) OldCache() string { return P.Join(p.homeDir, ".cache") } diff --git a/constant/provider/interface.go b/constant/provider/interface.go index 809db9c512..f2b6939e81 100644 --- a/constant/provider/interface.go +++ b/constant/provider/interface.go @@ -73,6 +73,7 @@ type ProxyProvider interface { HealthCheck() Version() uint32 RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) + HealthCheckURL() string } // RuleProvider interface diff --git a/constant/rule.go b/constant/rule.go index 66fc18bb7f..fcefaba6ca 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -5,9 +5,11 @@ const ( Domain RuleType = iota DomainSuffix DomainKeyword + DomainRegex GEOSITE GEOIP IPCIDR + IPASN SrcIPCIDR IPSuffix SrcIPSuffix @@ -40,12 +42,16 @@ func (rt RuleType) String() string { return "DomainSuffix" case DomainKeyword: return "DomainKeyword" + case DomainRegex: + return "DomainRegex" case GEOSITE: return "GeoSite" case GEOIP: return "GeoIP" case IPCIDR: return "IPCIDR" + case IPASN: + return "IPASN" case SrcIPCIDR: return "SrcIPCIDR" case IPSuffix: diff --git a/dns/client.go b/dns/client.go index 95f0f29b19..3b4efed105 100644 --- a/dns/client.go +++ b/dns/client.go @@ -12,6 +12,7 @@ import ( "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" "github.com/zhangyunhao116/fastrand" @@ -77,7 +78,9 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) options = append(options, dialer.WithInterface(c.iface)) } - conn, err := getDialHandler(c.r, c.proxyAdapter, c.proxyName, options...)(ctx, network, net.JoinHostPort(ip.String(), c.port)) + dialHandler := getDialHandler(c.r, c.proxyAdapter, c.proxyName, options...) + addr := net.JoinHostPort(ip.String(), c.port) + conn, err := dialHandler(ctx, network, addr) if err != nil { return nil, err } @@ -97,12 +100,31 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) conn = tls.Client(conn, ca.GetGlobalTLSConfig(c.Client.TLSConfig)) } - msg, _, err := c.Client.ExchangeWithConn(m, &D.Conn{ + dConn := &D.Conn{ Conn: conn, UDPSize: c.Client.UDPSize, TsigSecret: c.Client.TsigSecret, TsigProvider: c.Client.TsigProvider, - }) + } + + msg, _, err := c.Client.ExchangeWithConn(m, dConn) + + // Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)! + if msg != nil && msg.Truncated && c.Client.Net == "" { + tcpClient := *c.Client // copy a client + tcpClient.Net = "tcp" + network = "tcp" + log.Debugln("[DNS] Truncated reply from %s:%s for %s over UDP, retrying over TCP", c.host, c.port, m.Question[0].String()) + dConn.Conn, err = dialHandler(ctx, network, addr) + if err != nil { + ch <- result{msg, err} + return + } + defer func() { + _ = conn.Close() + }() + msg, _, err = tcpClient.ExchangeWithConn(m, dConn) + } ch <- result{msg, err} }() diff --git a/dns/doh.go b/dns/doh.go index 9e173c843f..ef4b653f5a 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -709,7 +709,8 @@ func (doh *dnsOverHTTPS) tlsDial(ctx context.Context, dialContext dialHandler, n err = conn.SetDeadline(time.Now().Add(dialTimeout)) if err != nil { // Must not happen in normal circumstances. - panic(fmt.Errorf("cannot set deadline: %w", err)) + log.Errorln("cannot set deadline: %v", err) + return nil, err } err = conn.Handshake() diff --git a/dns/filters.go b/dns/filters.go index d8633e8bb3..138f3429bb 100644 --- a/dns/filters.go +++ b/dns/filters.go @@ -24,7 +24,7 @@ var geoIPMatcher *router.GeoIPMatcher func (gf *geoipFilter) Match(ip netip.Addr) bool { if !C.GeodataMode { - codes := mmdb.Instance().LookupCode(ip.AsSlice()) + codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) for _, code := range codes { if !strings.EqualFold(code, gf.code) && !ip.IsPrivate() { return true diff --git a/dns/resolver.go b/dns/resolver.go index 8ea68ed700..08de69adff 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -324,7 +324,7 @@ func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err er func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (ips []netip.Addr, err error) { ip, err := netip.ParseAddr(host) if err == nil { - isIPv4 := ip.Is4() + isIPv4 := ip.Is4() || ip.Is4In6() if dnsType == D.TypeAAAA && !isIPv4 { return []netip.Addr{ip}, nil } else if dnsType == D.TypeA && isIPv4 { diff --git a/docs/config.yaml b/docs/config.yaml index eee6122be4..4d0e995e5d 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -674,6 +674,8 @@ proxies: # socks5 type: hysteria2 server: server.com port: 443 + # ports: 1000,2000-3000,5000 # port 不可省略 + # hop-interval: 15 # up和down均不写或为0则使用BBR流控 # up: "30 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps @@ -706,12 +708,10 @@ proxies: # socks5 # dialer-proxy: "ss1" # remote-dns-resolve: true # 强制dns远程解析,默认值为false # dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在remote-dns-resolve为true时生效 - # 如果peers不为空,该段落中的allowed-ips不可为空;前面段落的server,port,ip,ipv6,public-key,pre-shared-key均会被忽略,但private-key会被保留且只能在顶层指定 + # 如果peers不为空,该段落中的allowed-ips不可为空;前面段落的server,port,public-key,pre-shared-key均会被忽略,但private-key会被保留且只能在顶层指定 # peers: # - server: 162.159.192.1 # port: 2480 - # ip: 172.16.0.2 - # ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5 # public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= # # pre-shared-key: 31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM= # allowed-ips: ['0.0.0.0/0'] @@ -767,6 +767,18 @@ proxies: # socks5 # protocol-param: "#" # udp: true + - name: "ssh-out" + type: ssh + + server: 127.0.0.1 + port: 22 + username: root + password: password + privateKey: path + +# dns出站会将请求劫持到内部dns模块,所有请求均在内部处理 + - name: "dns-out" + type: dns proxy-groups: # 代理链,目前relay可以支持udp的只有vmess/vless/trojan/ss/ssr/tuic # wireguard目前不支持在relay中使用,请使用proxy中的dialer-proxy配置项 @@ -862,6 +874,8 @@ proxy-providers: # interface-name: tailscale0 # routing-mark: 233 # ip-version: ipv4-prefer + # additional-prefix: "[provider1]" + # additional-suffix: "test" test: type: file path: /test.yaml @@ -883,6 +897,8 @@ rule-providers: type: file rules: - RULE-SET,rule1,REJECT + - IP-ASN,1,PROXY + - DOMAIN-REGEX,^abc,DIRECT - DOMAIN-SUFFIX,baidu.com,DIRECT - DOMAIN-KEYWORD,google,ss1 - IP-CIDR,1.1.1.1/32,ss1 diff --git a/go.mod b/go.mod index 54a0a5dcc3..c70aca9784 100644 --- a/go.mod +++ b/go.mod @@ -8,51 +8,51 @@ require ( github.com/bahlo/generic-list-go v0.2.0 github.com/cilium/ebpf v0.12.3 github.com/coreos/go-iptables v0.7.0 - github.com/dlclark/regexp2 v1.10.0 - github.com/go-chi/chi/v5 v5.0.11 + github.com/dlclark/regexp2 v1.11.0 + github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.3 github.com/gobwas/ws v1.3.2 github.com/gofrs/uuid/v5 v5.0.0 - github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 - github.com/klauspost/cpuid/v2 v2.2.6 + github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8 + github.com/klauspost/cpuid/v2 v2.2.7 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/mdlayher/netlink v1.7.2 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 - github.com/metacubex/quic-go v0.41.1-0.20240120014142-a02f4a533d4a - github.com/metacubex/sing-quic v0.0.0-20240130040922-cbe613c88f20 + github.com/metacubex/quic-go v0.42.1-0.20240319071510-a251e5c66a5c + github.com/metacubex/sing-quic v0.0.0-20240310154810-47bca850fc01 github.com/metacubex/sing-shadowsocks v0.2.6 github.com/metacubex/sing-shadowsocks2 v0.2.0 - github.com/metacubex/sing-tun v0.2.1-0.20240130042529-1f983547e9d4 + github.com/metacubex/sing-tun v0.2.1-0.20240320004934-5d2b35447bfd github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f - github.com/metacubex/sing-wireguard v0.0.0-20231209125515-0594297f7232 - github.com/miekg/dns v1.1.57 + github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63 + github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 + github.com/miekg/dns v1.1.58 github.com/mroth/weightedrand/v2 v2.1.0 github.com/openacid/low v0.1.21 github.com/oschwald/maxminddb-golang v1.12.0 - github.com/puzpuzpuz/xsync/v3 v3.0.2 + github.com/puzpuzpuz/xsync/v3 v3.1.0 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 - github.com/sagernet/sing v0.3.0 + github.com/sagernet/sing v0.3.6 github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 github.com/sagernet/sing-shadowtls v0.1.4 - github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 github.com/sagernet/utls v1.5.4 github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e github.com/samber/lo v1.39.0 - github.com/shirou/gopsutil/v3 v3.23.12 + github.com/shirou/gopsutil/v3 v3.24.2 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/wk8/go-ordered-map/v2 v2.1.8 github.com/zhangyunhao116/fastrand v0.3.0 go.uber.org/automaxprocs v1.5.3 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.18.0 - golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e - golang.org/x/net v0.20.0 + golang.org/x/crypto v0.21.0 + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 + golang.org/x/net v0.22.0 golang.org/x/sync v0.6.0 - golang.org/x/sys v0.16.0 - google.golang.org/protobuf v1.32.0 + golang.org/x/sys v0.18.0 + google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v3 v3.0.1 lukechampine.com/blake3 v1.2.1 ) @@ -84,7 +84,7 @@ require ( github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mdlayher/socket v0.4.1 // indirect - github.com/metacubex/gvisor v0.0.0-20231209122014-3e43224c7bbc // indirect + github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect @@ -93,7 +93,6 @@ require ( github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect - github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect @@ -102,13 +101,13 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect - go.uber.org/mock v0.3.0 // indirect - golang.org/x/mod v0.14.0 // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/mod v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.16.0 // indirect + golang.org/x/tools v0.18.0 // indirect ) -replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240111014253-f1818b6a82b2 +replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240313064558-c197257f6542 diff --git a/go.sum b/go.sum index a7f6cd02a6..7422a0627a 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= @@ -44,8 +44,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= -github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= -github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= @@ -78,16 +78,16 @@ github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= -github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= +github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8 h1:V3plQrMHRWOB5zMm3yNqvBxDQVW1+/wHBSok5uPdmVs= +github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8/go.mod h1:izxuNQZeFrbx2nK2fAyN5iNUB34Fe9j0nK4PwLzAkKw= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -102,26 +102,28 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= -github.com/metacubex/gvisor v0.0.0-20231209122014-3e43224c7bbc h1:+yTZ6q2EeQCAJNpKNEu5j32Pm23ShD38ElIa635wTrk= -github.com/metacubex/gvisor v0.0.0-20231209122014-3e43224c7bbc/go.mod h1:rhBU9tD5ktoGPBtXUquhWuGJ4u+8ZZzBMi2cAdv9q8Y= -github.com/metacubex/quic-go v0.41.1-0.20240120014142-a02f4a533d4a h1:IMr75VdMnDUhkANZemUWqmOPLfwnemiIaCHRnGCdAsY= -github.com/metacubex/quic-go v0.41.1-0.20240120014142-a02f4a533d4a/go.mod h1:F/t8VnA47xoia8ABlNA4InkZjssvFJ5p6E6jKdbkgAs= -github.com/metacubex/sing v0.0.0-20240111014253-f1818b6a82b2 h1:upEO8dt9WDBavhgcgkXB3hRcwVNbkTbnd+xyzy6ZQZo= -github.com/metacubex/sing v0.0.0-20240111014253-f1818b6a82b2/go.mod h1:9pfuAH6mZfgnz/YjP6xu5sxx882rfyjpcrTdUpd6w3g= -github.com/metacubex/sing-quic v0.0.0-20240130040922-cbe613c88f20 h1:wt7ydRxm9Pvw+un6KD97tjLJHMrkzp83HyiGkoz6e7k= -github.com/metacubex/sing-quic v0.0.0-20240130040922-cbe613c88f20/go.mod h1:bdHqEysJclB9BzIa5jcKKSZ1qua+YEPjR8fOzzE3vZU= +github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc= +github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw= +github.com/metacubex/quic-go v0.42.1-0.20240319071510-a251e5c66a5c h1:AhaPKvVqF3N/jXFmlW51Cf1+KddslKAsZqcdgGhZjr0= +github.com/metacubex/quic-go v0.42.1-0.20240319071510-a251e5c66a5c/go.mod h1:iGx3Y1zynls/FjFgykLSqDcM81U0IKePRTXEz5g3iiQ= +github.com/metacubex/sing v0.0.0-20240313064558-c197257f6542 h1:e9nBnrJBv3HzZVeSzJN0G2SADjebd2ZLF1F5dmsjUTc= +github.com/metacubex/sing v0.0.0-20240313064558-c197257f6542/go.mod h1:+60H3Cm91RnL9dpVGWDPHt0zTQImO9Vfqt9a4rSambI= +github.com/metacubex/sing-quic v0.0.0-20240310154810-47bca850fc01 h1:5INHs85Gp1JZsdF7fQp1pXUjfJOX2dhwZjuUQWJVSt8= +github.com/metacubex/sing-quic v0.0.0-20240310154810-47bca850fc01/go.mod h1:WyY0zYxv+o+18R/Ece+QFontlgXoobKbNqbtYn2zjz8= github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ= github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg= github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A= github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8= -github.com/metacubex/sing-tun v0.2.1-0.20240130042529-1f983547e9d4 h1:qz256cI4oGBtLT0H3wQYgazLGYLQUEZqMkf0i8sGH5A= -github.com/metacubex/sing-tun v0.2.1-0.20240130042529-1f983547e9d4/go.mod h1:P+TjrGTG5AdQRaskP6NiI9gZmgnwR3o5ze9CkIQE+/s= +github.com/metacubex/sing-tun v0.2.1-0.20240320004934-5d2b35447bfd h1:NgLb6Lvr8ZxX0inWswVYjal2SUzsJJ54dFQNOluUJuE= +github.com/metacubex/sing-tun v0.2.1-0.20240320004934-5d2b35447bfd/go.mod h1:GfLZG/QgGpW9+BPjltzONrL5vVms86TWqmZ23J68ISc= github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ= github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= -github.com/metacubex/sing-wireguard v0.0.0-20231209125515-0594297f7232 h1:loWjR+k9dxqBSgruGyT5hE8UCRMmCEjxqZbryfY9no4= -github.com/metacubex/sing-wireguard v0.0.0-20231209125515-0594297f7232/go.mod h1:NGCrBZ+fUmp81yaA1kVskcNWBnwl5z4UHxz47A01zm8= -github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63 h1:AGyIB55UfQm/0ZH0HtQO9u3l//yjtHUpjeRjjPGfGRI= +github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo= +github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c= +github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= @@ -144,8 +146,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= -github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= +github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= @@ -161,18 +163,14 @@ github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnV github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= -github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 h1:z3SJQhVyU63FT26Wn/UByW6b7q8QKB0ZkPqsyqcz2PI= -github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6/go.mod h1:73xRZuxwkFk4aiLw28hG8W6o9cr2UPrGL9pdY2UTbvY= github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co= github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s= github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo= github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= -github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= -github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= -github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y= +github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= @@ -188,13 +186,15 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -208,32 +208,32 @@ github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695AP github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig= github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= -go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= -go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE= -golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= @@ -252,21 +252,22 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= -golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index 783da4d3d4..c23eb9f3ba 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -146,15 +146,18 @@ func GetGeneral() *config.General { AllowLan: listener.AllowLan(), BindAddress: listener.BindAddress(), }, - Controller: config.Controller{}, - Mode: tunnel.Mode(), - LogLevel: log.Level(), - IPv6: !resolver.DisableIPv6, - GeodataLoader: G.LoaderName(), - GeositeMatcher: G.SiteMatcherName(), - Interface: dialer.DefaultInterface.Load(), - Sniffing: tunnel.IsSniffing(), - TCPConcurrent: dialer.GetTcpConcurrent(), + Controller: config.Controller{}, + Mode: tunnel.Mode(), + LogLevel: log.Level(), + IPv6: !resolver.DisableIPv6, + GeodataMode: G.GeodataMode(), + GeoAutoUpdate: G.GeoAutoUpdate(), + GeoUpdateInterval: G.GeoUpdateInterval(), + GeodataLoader: G.LoaderName(), + GeositeMatcher: G.SiteMatcherName(), + Interface: dialer.DefaultInterface.Load(), + Sniffing: tunnel.IsSniffing(), + TCPConcurrent: dialer.GetTcpConcurrent(), } return general @@ -194,6 +197,7 @@ func updateExperimental(c *config.Config) { if c.Experimental.QUICGoDisableECN { _ = os.Setenv("QUIC_GO_DISABLE_ECN", strconv.FormatBool(true)) } + dialer.GetIP4PEnable(c.Experimental.IP4PEnable) } func updateNTP(c *config.NTP) { @@ -475,6 +479,9 @@ func updateIPTables(cfg *config.Config) { bypass = iptables.Bypass tProxyPort = cfg.General.TProxyPort dnsCfg = cfg.DNS + DnsRedirect = iptables.DnsRedirect + + dnsPort netip.AddrPort ) if tProxyPort == 0 { @@ -482,15 +489,17 @@ func updateIPTables(cfg *config.Config) { return } - if !dnsCfg.Enable { - err = fmt.Errorf("DNS server must be enable") - return - } + if DnsRedirect { + if !dnsCfg.Enable { + err = fmt.Errorf("DNS server must be enable") + return + } - dnsPort, err := netip.ParseAddrPort(dnsCfg.Listen) - if err != nil { - err = fmt.Errorf("DNS server must be correct") - return + dnsPort, err = netip.ParseAddrPort(dnsCfg.Listen) + if err != nil { + err = fmt.Errorf("DNS server must be correct") + return + } } if iptables.InboundInterface != "" { @@ -501,7 +510,7 @@ func updateIPTables(cfg *config.Config) { dialer.DefaultRoutingMark.Store(2158) } - err = tproxy.SetTProxyIPTables(inboundInterface, bypass, uint16(tProxyPort), dnsPort.Port()) + err = tproxy.SetTProxyIPTables(inboundInterface, bypass, uint16(tProxyPort), DnsRedirect, dnsPort.Port()) if err != nil { return } diff --git a/hub/route/configs.go b/hub/route/configs.go index ec0b464ce4..653e43519b 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -91,6 +91,7 @@ type tunSchema struct { EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` FileDescriptor *int `yaml:"file-descriptor" json:"file-descriptor"` + TableIndex *int `yaml:"table-index" json:"table-index"` } type tuicServerSchema struct { @@ -209,6 +210,9 @@ func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { if p.FileDescriptor != nil { def.FileDescriptor = *p.FileDescriptor } + if p.TableIndex != nil { + def.TableIndex = *p.TableIndex + } } return def } diff --git a/hub/updater/updater.go b/hub/updater/updater.go index 1967af42f9..02ff07ba26 100644 --- a/hub/updater/updater.go +++ b/hub/updater/updater.go @@ -137,6 +137,8 @@ func prepare(exePath string) (err error) { if runtime.GOOS == "windows" { updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible + ".exe" + } else if runtime.GOOS == "android" && runtime.GOARCH == "arm64" { + updateExeName = "mihomo-android-arm64-v8" } else { updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible } @@ -440,7 +442,11 @@ func updateDownloadURL() { middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, goarm, latestVersion) } else if runtime.GOARCH == "arm64" { //-linux-arm64-alpha-e552b54.gz - middle = fmt.Sprintf("-%s-%s-%s", runtime.GOOS, runtime.GOARCH, latestVersion) + if runtime.GOOS == "android" { + middle = fmt.Sprintf("-%s-%s-v8-%s", runtime.GOOS, runtime.GOARCH, latestVersion) + } else { + middle = fmt.Sprintf("-%s-%s-%s", runtime.GOOS, runtime.GOARCH, latestVersion) + } } else if isMIPS(runtime.GOARCH) && gomips != "" { middle = fmt.Sprintf("-%s-%s-%s-%s", runtime.GOOS, runtime.GOARCH, gomips, latestVersion) } else { diff --git a/listener/config/tun.go b/listener/config/tun.go index 1772c6f5b4..7467e4a6a7 100644 --- a/listener/config/tun.go +++ b/listener/config/tun.go @@ -49,4 +49,5 @@ type Tun struct { EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` + TableIndex int `yaml:"table-index" json:"table-index"` } diff --git a/listener/http/server.go b/listener/http/server.go index 3ff1679df1..8fc9da5962 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -36,7 +36,9 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener } func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) { + isDefault := false if len(additions) == 0 { + isDefault = true additions = []inbound.Addition{ inbound.WithInName("DEFAULT-HTTP"), inbound.WithSpecialRules(""), @@ -71,8 +73,8 @@ func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additi t.SetKeepAlive(false) } } - if len(additions) == 0 { // only apply on default listener - if inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) { + if isDefault { // only apply on default listener + if !inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) { _ = conn.Close() continue } diff --git a/listener/http/utils.go b/listener/http/utils.go index 63726d51ea..e67c0fde1b 100644 --- a/listener/http/utils.go +++ b/listener/http/utils.go @@ -5,6 +5,7 @@ import ( "errors" "net" "net/http" + "net/netip" "strings" ) @@ -48,6 +49,11 @@ func removeExtraHTTPHostPort(req *http.Request) { if pHost, port, err := net.SplitHostPort(host); err == nil && (port == "80" || port == "443") { host = pHost + if ip, err := netip.ParseAddr(pHost); err == nil && ip.Is6() { + // RFC 2617 Sec 3.2.2, for IPv6 literal + // addresses the Host header needs to follow the RFC 2732 grammar for "host" + host = "[" + host + "]" + } } req.Host = host diff --git a/listener/inbound/tun.go b/listener/inbound/tun.go index a1fdebfa4a..51747c4629 100644 --- a/listener/inbound/tun.go +++ b/listener/inbound/tun.go @@ -40,6 +40,7 @@ type TunOption struct { EndpointIndependentNat bool `inbound:"endpoint_independent_nat,omitempty"` UDPTimeout int64 `inbound:"udp_timeout,omitempty"` FileDescriptor int `inbound:"file-descriptor,omitempty"` + TableIndex int `inbound:"table-index,omitempty"` } func (o TunOption) Equal(config C.InboundConfig) bool { @@ -118,6 +119,7 @@ func NewTun(options *TunOption) (*Tun, error) { EndpointIndependentNat: options.EndpointIndependentNat, UDPTimeout: options.UDPTimeout, FileDescriptor: options.FileDescriptor, + TableIndex: options.TableIndex, }, }, nil } diff --git a/listener/listener.go b/listener/listener.go index ac60297196..e35061882d 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -823,7 +823,8 @@ func hasTunConfigChange(tunConf *LC.Tun) bool { LastTunConf.StrictRoute != tunConf.StrictRoute || LastTunConf.EndpointIndependentNat != tunConf.EndpointIndependentNat || LastTunConf.UDPTimeout != tunConf.UDPTimeout || - LastTunConf.FileDescriptor != tunConf.FileDescriptor { + LastTunConf.FileDescriptor != tunConf.FileDescriptor || + LastTunConf.TableIndex != tunConf.TableIndex { return true } diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go index 9461303944..367b7a3683 100644 --- a/listener/mixed/mixed.go +++ b/listener/mixed/mixed.go @@ -37,7 +37,9 @@ func (l *Listener) Close() error { } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { + isDefault := false if len(additions) == 0 { + isDefault = true additions = []inbound.Addition{ inbound.WithInName("DEFAULT-MIXED"), inbound.WithSpecialRules(""), @@ -62,8 +64,8 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener } continue } - if len(additions) == 0 { // only apply on default listener - if inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { + if isDefault { // only apply on default listener + if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { _ = c.Close() continue } diff --git a/listener/sing_tun/dns.go b/listener/sing_tun/dns.go index 056c9169d1..42926732f4 100644 --- a/listener/sing_tun/dns.go +++ b/listener/sing_tun/dns.go @@ -2,29 +2,21 @@ package sing_tun import ( "context" - "encoding/binary" - "io" "net" "net/netip" "sync" "time" - "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" - D "github.com/miekg/dns" - "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/network" ) -const DefaultDnsReadTimeout = time.Second * 10 -const DefaultDnsRelayTimeout = time.Second * 5 - type ListenerHandler struct { *sing.ListenerHandler DnsAdds []netip.AddrPort @@ -45,61 +37,11 @@ func (h *ListenerHandler) ShouldHijackDns(targetAddr netip.AddrPort) bool { func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { if h.ShouldHijackDns(metadata.Destination.AddrPort()) { log.Debugln("[DNS] hijack tcp:%s", metadata.Destination.String()) - buff := pool.Get(pool.UDPBufferSize) - defer func() { - _ = pool.Put(buff) - _ = conn.Close() - }() - for { - if conn.SetReadDeadline(time.Now().Add(DefaultDnsReadTimeout)) != nil { - break - } - - length := uint16(0) - if err := binary.Read(conn, binary.BigEndian, &length); err != nil { - break - } - - if int(length) > len(buff) { - break - } - - n, err := io.ReadFull(conn, buff[:length]) - if err != nil { - break - } - - err = func() error { - ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) - defer cancel() - inData := buff[:n] - msg, err := RelayDnsPacket(ctx, inData, buff) - if err != nil { - return err - } - - err = binary.Write(conn, binary.BigEndian, uint16(len(msg))) - if err != nil { - return err - } - - _, err = conn.Write(msg) - if err != nil { - return err - } - return nil - }() - if err != nil { - return err - } - } - return nil + return resolver.RelayDnsConn(ctx, conn, resolver.DefaultDnsReadTimeout) } return h.ListenerHandler.NewConnection(ctx, conn, metadata) } -const SafeDnsPacketSize = 2 * 1024 // safe size which is 1232 from https://dnsflagday.net/2020/, so 2048 is enough - func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error { if h.ShouldHijackDns(metadata.Destination.AddrPort()) { log.Debugln("[DNS] hijack udp:%s from %s", metadata.Destination.String(), metadata.Source.String()) @@ -114,7 +56,7 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. rwOptions := network.ReadWaitOptions{ FrontHeadroom: network.CalculateFrontHeadroom(conn), RearHeadroom: network.CalculateRearHeadroom(conn), - MTU: SafeDnsPacketSize, + MTU: resolver.SafeDnsPacketSize, } readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) if isReadWaiter { @@ -126,7 +68,7 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. dest M.Socksaddr err error ) - _ = conn.SetReadDeadline(time.Now().Add(DefaultDnsReadTimeout)) + _ = conn.SetReadDeadline(time.Now().Add(resolver.DefaultDnsReadTimeout)) readBuff = nil // clear last loop status, avoid repeat release if isReadWaiter { readBuff, dest, err = readWaiter.WaitReadPacket() @@ -147,15 +89,15 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. return err } go func() { - ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) + ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDnsRelayTimeout) defer cancel() inData := readBuff.Bytes() writeBuff := readBuff writeBuff.Resize(writeBuff.Start(), 0) - if len(writeBuff.FreeBytes()) < SafeDnsPacketSize { // only create a new buffer when space don't enough + if len(writeBuff.FreeBytes()) < resolver.SafeDnsPacketSize { // only create a new buffer when space don't enough writeBuff = rwOptions.NewPacketBuffer() } - msg, err := RelayDnsPacket(ctx, inData, writeBuff.FreeBytes()) + msg, err := resolver.RelayDnsPacket(ctx, inData, writeBuff.FreeBytes()) if writeBuff != readBuff { readBuff.Release() } @@ -182,21 +124,3 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. } return h.ListenerHandler.NewPacketConnection(ctx, conn, metadata) } - -func RelayDnsPacket(ctx context.Context, payload []byte, target []byte) ([]byte, error) { - msg := &D.Msg{} - if err := msg.Unpack(payload); err != nil { - return nil, err - } - - r, err := resolver.ServeMsg(ctx, msg) - if err != nil { - m := new(D.Msg) - m.SetRcode(msg, D.RcodeServerFailure) - return m.PackBuffer(target) - } - - r.SetRcode(msg, r.Rcode) - r.Compress = true - return r.PackBuffer(target) -} diff --git a/listener/sing_tun/server.go b/listener/sing_tun/server.go index cc26d37dcc..96ec1573f2 100644 --- a/listener/sing_tun/server.go +++ b/listener/sing_tun/server.go @@ -112,6 +112,10 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis } else { udpTimeout = int64(sing.UDPTimeout.Seconds()) } + tableIndex := options.TableIndex + if tableIndex == 0 { + tableIndex = 2022 + } includeUID := uidToRange(options.IncludeUID) if len(options.IncludeUIDRange) > 0 { var err error @@ -225,7 +229,7 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis ExcludePackage: options.ExcludePackage, FileDescriptor: options.FileDescriptor, InterfaceMonitor: defaultInterfaceMonitor, - TableIndex: 2022, + TableIndex: tableIndex, } err = l.buildAndroidRules(&tunOptions) diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go index 8016e95897..b6ea023a87 100644 --- a/listener/socks/tcp.go +++ b/listener/socks/tcp.go @@ -35,7 +35,9 @@ func (l *Listener) Close() error { } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { + isDefault := false if len(additions) == 0 { + isDefault = true additions = []inbound.Addition{ inbound.WithInName("DEFAULT-SOCKS"), inbound.WithSpecialRules(""), @@ -59,8 +61,8 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener } continue } - if len(additions) == 0 { // only apply on default listener - if inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { + if isDefault { // only apply on default listener + if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { _ = c.Close() continue } diff --git a/listener/tproxy/setsockopt_linux.go b/listener/tproxy/setsockopt_linux.go index b83b28a40b..9189f11529 100644 --- a/listener/tproxy/setsockopt_linux.go +++ b/listener/tproxy/setsockopt_linux.go @@ -36,13 +36,21 @@ func setsockopt(rc syscall.RawConn, addr string) error { } if err == nil { - err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVTOS, 1) - } - - if err == nil { - err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, syscall.IPV6_RECVTCLASS, 1) + _ = setDSCPsockopt(fd, isIPv6) } }) return err } + +func setDSCPsockopt(fd uintptr, isIPv6 bool) (err error) { + if err == nil { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVTOS, 1) + } + + if err == nil && isIPv6 { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, syscall.IPV6_RECVTCLASS, 1) + } + + return +} diff --git a/listener/tproxy/tproxy_iptables.go b/listener/tproxy/tproxy_iptables.go index 5ddd7b4c85..6c6e2cc81f 100644 --- a/listener/tproxy/tproxy_iptables.go +++ b/listener/tproxy/tproxy_iptables.go @@ -15,6 +15,7 @@ var ( dnsPort uint16 tProxyPort uint16 interfaceName string + DnsRedirect bool ) const ( @@ -22,7 +23,7 @@ const ( PROXY_ROUTE_TABLE = "0x2d0" ) -func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint16) error { +func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dnsredir bool, dport uint16) error { if _, err := cmd.ExecCmd("iptables -V"); err != nil { return fmt.Errorf("current operations system [%s] are not support iptables or command iptables does not exist", runtime.GOOS) } @@ -33,6 +34,7 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 interfaceName = ifname tProxyPort = tport + DnsRedirect = dnsredir dnsPort = dport // add route @@ -58,8 +60,10 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 execCmd("iptables -t mangle -N mihomo_prerouting") execCmd("iptables -t mangle -F mihomo_prerouting") execCmd("iptables -t mangle -A mihomo_prerouting -s 172.17.0.0/16 -j RETURN") - execCmd("iptables -t mangle -A mihomo_prerouting -p udp --dport 53 -j ACCEPT") - execCmd("iptables -t mangle -A mihomo_prerouting -p tcp --dport 53 -j ACCEPT") + if DnsRedirect { + execCmd("iptables -t mangle -A mihomo_prerouting -p udp --dport 53 -j ACCEPT") + execCmd("iptables -t mangle -A mihomo_prerouting -p tcp --dport 53 -j ACCEPT") + } execCmd("iptables -t mangle -A mihomo_prerouting -m addrtype --dst-type LOCAL -j RETURN") addLocalnetworkToChain("mihomo_prerouting", bypass) execCmd("iptables -t mangle -A mihomo_prerouting -p tcp -m socket -j mihomo_divert") @@ -68,8 +72,10 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_prerouting -p udp -j TPROXY --on-port %d --tproxy-mark %s/%s", tProxyPort, PROXY_FWMARK, PROXY_FWMARK)) execCmd("iptables -t mangle -A PREROUTING -j mihomo_prerouting") - execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) + if DnsRedirect { + execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) + execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) + } // set post routing if interfaceName != "lo" { @@ -80,8 +86,10 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 execCmd("iptables -t mangle -N mihomo_output") execCmd("iptables -t mangle -F mihomo_output") execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) - execCmd("iptables -t mangle -A mihomo_output -p udp -m multiport --dports 53,123,137 -j ACCEPT") - execCmd("iptables -t mangle -A mihomo_output -p tcp --dport 53 -j ACCEPT") + if DnsRedirect { + execCmd("iptables -t mangle -A mihomo_output -p udp -m multiport --dports 53,123,137 -j ACCEPT") + execCmd("iptables -t mangle -A mihomo_output -p tcp --dport 53 -j ACCEPT") + } execCmd("iptables -t mangle -A mihomo_output -m addrtype --dst-type LOCAL -j RETURN") execCmd("iptables -t mangle -A mihomo_output -m addrtype --dst-type BROADCAST -j RETURN") addLocalnetworkToChain("mihomo_output", bypass) @@ -90,20 +98,22 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 execCmd(fmt.Sprintf("iptables -t mangle -I OUTPUT -o %s -j mihomo_output", interfaceName)) // set dns output - execCmd("iptables -t nat -N mihomo_dns_output") - execCmd("iptables -t nat -F mihomo_dns_output") - execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) - execCmd("iptables -t nat -A mihomo_dns_output -s 172.17.0.0/16 -j RETURN") - execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p udp -j REDIRECT --to-ports %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p tcp -j REDIRECT --to-ports %d", dnsPort)) - execCmd("iptables -t nat -I OUTPUT -p tcp --dport 53 -j mihomo_dns_output") - execCmd("iptables -t nat -I OUTPUT -p udp --dport 53 -j mihomo_dns_output") + if DnsRedirect { + execCmd("iptables -t nat -N mihomo_dns_output") + execCmd("iptables -t nat -F mihomo_dns_output") + execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) + execCmd("iptables -t nat -A mihomo_dns_output -s 172.17.0.0/16 -j RETURN") + execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p udp -j REDIRECT --to-ports %d", dnsPort)) + execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p tcp -j REDIRECT --to-ports %d", dnsPort)) + execCmd("iptables -t nat -I OUTPUT -p tcp --dport 53 -j mihomo_dns_output") + execCmd("iptables -t nat -I OUTPUT -p udp --dport 53 -j mihomo_dns_output") + } return nil } func CleanupTProxyIPTables() { - if runtime.GOOS != "linux" || interfaceName == "" || tProxyPort == 0 || dnsPort == 0 { + if runtime.GOOS != "linux" || interfaceName == "" || tProxyPort == 0 { return } @@ -130,8 +140,10 @@ func CleanupTProxyIPTables() { } // clean PREROUTING - execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) + if DnsRedirect { + execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) + execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) + } execCmd("iptables -t mangle -D PREROUTING -j mihomo_prerouting") // clean POSTROUTING @@ -141,8 +153,10 @@ func CleanupTProxyIPTables() { // clean OUTPUT execCmd(fmt.Sprintf("iptables -t mangle -D OUTPUT -o %s -j mihomo_output", interfaceName)) - execCmd("iptables -t nat -D OUTPUT -p tcp --dport 53 -j mihomo_dns_output") - execCmd("iptables -t nat -D OUTPUT -p udp --dport 53 -j mihomo_dns_output") + if DnsRedirect { + execCmd("iptables -t nat -D OUTPUT -p tcp --dport 53 -j mihomo_dns_output") + execCmd("iptables -t nat -D OUTPUT -p udp --dport 53 -j mihomo_dns_output") + } // clean chain execCmd("iptables -t mangle -F mihomo_prerouting") @@ -151,9 +165,10 @@ func CleanupTProxyIPTables() { execCmd("iptables -t mangle -X mihomo_divert") execCmd("iptables -t mangle -F mihomo_output") execCmd("iptables -t mangle -X mihomo_output") - execCmd("iptables -t nat -F mihomo_dns_output") - execCmd("iptables -t nat -X mihomo_dns_output") - + if DnsRedirect { + execCmd("iptables -t nat -F mihomo_dns_output") + execCmd("iptables -t nat -X mihomo_dns_output") + } interfaceName = "" tProxyPort = 0 dnsPort = 0 diff --git a/listener/tproxy/udp_linux.go b/listener/tproxy/udp_linux.go index 02b5137939..c96d4cc735 100644 --- a/listener/tproxy/udp_linux.go +++ b/listener/tproxy/udp_linux.go @@ -104,7 +104,14 @@ func getOrigDst(oob []byte) (netip.AddrPort, error) { } // retrieve the destination address from the SCM. - sa, err := unix.ParseOrigDstAddr(&scms[1]) + var sa unix.Sockaddr + for i := range scms { + sa, err = unix.ParseOrigDstAddr(&scms[i]) + if err == nil { + break + } + } + if err != nil { return netip.AddrPort{}, fmt.Errorf("retrieve destination: %w", err) } @@ -123,12 +130,19 @@ func getOrigDst(oob []byte) (netip.AddrPort, error) { return rAddr, nil } -func getDSCP (oob []byte) (uint8, error) { +func getDSCP(oob []byte) (uint8, error) { scms, err := unix.ParseSocketControlMessage(oob) if err != nil { return 0, fmt.Errorf("parse control message: %w", err) } - dscp, err := parseDSCP(&scms[0]) + var dscp uint8 + for i := range scms { + dscp, err = parseDSCP(&scms[i]) + if err == nil { + break + } + } + if err != nil { return 0, fmt.Errorf("retrieve DSCP: %w", err) } diff --git a/rules/common/domain_regex.go b/rules/common/domain_regex.go new file mode 100644 index 0000000000..3d961542ad --- /dev/null +++ b/rules/common/domain_regex.go @@ -0,0 +1,44 @@ +package common + +import ( + "regexp" + + C "github.com/metacubex/mihomo/constant" +) + +type DomainRegex struct { + *Base + regex *regexp.Regexp + adapter string +} + +func (dr *DomainRegex) RuleType() C.RuleType { + return C.DomainRegex +} + +func (dr *DomainRegex) Match(metadata *C.Metadata) (bool, string) { + domain := metadata.RuleHost() + return dr.regex.MatchString(domain), dr.adapter +} + +func (dr *DomainRegex) Adapter() string { + return dr.adapter +} + +func (dr *DomainRegex) Payload() string { + return dr.regex.String() +} + +func NewDomainRegex(regex string, adapter string) (*DomainRegex, error) { + r, err := regexp.Compile(regex) + if err != nil { + return nil, err + } + return &DomainRegex{ + Base: &Base{}, + regex: r, + adapter: adapter, + }, nil +} + +//var _ C.Rule = (*DomainRegex)(nil) diff --git a/rules/common/geoip.go b/rules/common/geoip.go index ebca1d162d..223a79042d 100644 --- a/rules/common/geoip.go +++ b/rules/common/geoip.go @@ -21,6 +21,8 @@ type GEOIP struct { recodeSize int } +var _ C.Rule = (*GEOIP)(nil) + func (g *GEOIP) RuleType() C.RuleType { return C.GEOIP } @@ -31,7 +33,7 @@ func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) { return false, "" } - if strings.EqualFold(g.country, "LAN") { + if g.country == "lan" { return ip.IsPrivate() || ip.IsUnspecified() || ip.IsLoopback() || @@ -39,16 +41,31 @@ func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) { ip.IsLinkLocalUnicast() || resolver.IsFakeBroadcastIP(ip), g.adapter } + + for _, code := range metadata.DstGeoIP { + if g.country == code { + return true, g.adapter + } + } + if !C.GeodataMode { - codes := mmdb.Instance().LookupCode(ip.AsSlice()) - for _, code := range codes { - if strings.EqualFold(code, g.country) { + if metadata.DstGeoIP != nil { + return false, g.adapter + } + metadata.DstGeoIP = mmdb.IPInstance().LookupCode(ip.AsSlice()) + for _, code := range metadata.DstGeoIP { + if g.country == code { return true, g.adapter } } return false, g.adapter } - return g.geoIPMatcher.Match(ip), g.adapter + + match := g.geoIPMatcher.Match(ip) + if match { + metadata.DstGeoIP = append(metadata.DstGeoIP, g.country) + } + return match, g.adapter } func (g *GEOIP) Adapter() string { @@ -80,8 +97,9 @@ func NewGEOIP(country string, adapter string, noResolveIP bool) (*GEOIP, error) log.Errorln("can't initial GeoIP: %s", err) return nil, err } + country = strings.ToLower(country) - if !C.GeodataMode || strings.EqualFold(country, "LAN") { + if !C.GeodataMode || country == "lan" { geoip := &GEOIP{ Base: &Base{}, country: country, @@ -93,7 +111,7 @@ func NewGEOIP(country string, adapter string, noResolveIP bool) (*GEOIP, error) geoIPMatcher, size, err := geodata.LoadGeoIPMatcher(country) if err != nil { - return nil, fmt.Errorf("[GeoIP] %s", err.Error()) + return nil, fmt.Errorf("[GeoIP] %w", err) } log.Infoln("Start initial GeoIP rule %s => %s, records: %d", country, adapter, size) @@ -107,5 +125,3 @@ func NewGEOIP(country string, adapter string, noResolveIP bool) (*GEOIP, error) } return geoip, nil } - -//var _ C.Rule = (*GEOIP)(nil) diff --git a/rules/common/ipasn.go b/rules/common/ipasn.go new file mode 100644 index 0000000000..1fce8af482 --- /dev/null +++ b/rules/common/ipasn.go @@ -0,0 +1,67 @@ +package common + +import ( + "strconv" + + "github.com/metacubex/mihomo/component/geodata" + "github.com/metacubex/mihomo/component/mmdb" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" +) + +type ASN struct { + *Base + asn string + adapter string + noResolveIP bool +} + +func (a *ASN) Match(metadata *C.Metadata) (bool, string) { + ip := metadata.DstIP + if !ip.IsValid() { + return false, "" + } + + result := mmdb.ASNInstance().LookupASN(ip.AsSlice()) + + asnNumber := strconv.FormatUint(uint64(result.AutonomousSystemNumber), 10) + metadata.DstIPASN = asnNumber + " " + result.AutonomousSystemOrganization + + match := a.asn == asnNumber + return match, a.adapter +} + +func (a *ASN) RuleType() C.RuleType { + return C.IPASN +} + +func (a *ASN) Adapter() string { + return a.adapter +} + +func (a *ASN) Payload() string { + return a.asn +} + +func (a *ASN) ShouldResolveIP() bool { + return !a.noResolveIP +} + +func (a *ASN) GetASN() string { + return a.asn +} + +func NewIPASN(asn string, adapter string, noResolveIP bool) (*ASN, error) { + C.ASNEnable = true + if err := geodata.InitASN(); err != nil { + log.Errorln("can't initial ASN: %s", err) + return nil, err + } + + return &ASN{ + Base: &Base{}, + asn: asn, + adapter: adapter, + noResolveIP: noResolveIP, + }, nil +} diff --git a/rules/parser.go b/rules/parser.go index 7a79b18bf4..b69cc4fd9c 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -2,7 +2,7 @@ package rules import ( "fmt" - + C "github.com/metacubex/mihomo/constant" RC "github.com/metacubex/mihomo/rules/common" "github.com/metacubex/mihomo/rules/logic" @@ -17,6 +17,8 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] parsed = RC.NewDomainSuffix(payload, target) case "DOMAIN-KEYWORD": parsed = RC.NewDomainKeyword(payload, target) + case "DOMAIN-REGEX": + parsed, parseErr = RC.NewDomainRegex(payload, target) case "GEOSITE": parsed, parseErr = RC.NewGEOSITE(payload, target) case "GEOIP": @@ -25,6 +27,9 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] case "IP-CIDR", "IP-CIDR6": noResolve := RC.HasNoResolve(params) parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRNoResolve(noResolve)) + case "IP-ASN": + noResolve := RC.HasNoResolve(params) + parsed, parseErr = RC.NewIPASN(payload, target, noResolve) case "SRC-IP-CIDR": parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(true), RC.WithIPCIDRNoResolve(true)) case "IP-SUFFIX": diff --git a/transport/vmess/http.go b/transport/vmess/http.go index c77a7e9d00..b023fee41e 100644 --- a/transport/vmess/http.go +++ b/transport/vmess/http.go @@ -3,6 +3,7 @@ package vmess import ( "bufio" "bytes" + "errors" "fmt" "net" "net/http" @@ -54,13 +55,17 @@ func (hc *httpConn) Write(b []byte) (int, error) { return hc.Conn.Write(b) } + if len(hc.cfg.Path) == 0 { + return -1, errors.New("path is empty") + } + path := hc.cfg.Path[fastrand.Intn(len(hc.cfg.Path))] host := hc.cfg.Host if header := hc.cfg.Headers["Host"]; len(header) != 0 { host = header[fastrand.Intn(len(header))] } - u := fmt.Sprintf("http://%s%s", host, path) + u := fmt.Sprintf("http://%s%s", net.JoinHostPort(host, "80"), path) req, _ := http.NewRequest(utils.EmptyOr(hc.cfg.Method, http.MethodGet), u, bytes.NewBuffer(b)) for key, list := range hc.cfg.Headers { req.Header.Set(key, list[fastrand.Intn(len(list))])