diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index 424f08fc..ea51fa11 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -98d539ce67568fb911654e66a14cf4247ed833ec +617d38f41f935b46a68f550d9add2e38abb3f168 diff --git a/.github/setup_go_for_macos1013.sh b/.github/setup_go_for_macos1013.sh index 9e501ca9..ddcb3ea0 100755 --- a/.github/setup_go_for_macos1013.sh +++ b/.github/setup_go_for_macos1013.sh @@ -2,7 +2,7 @@ set -euo pipefail -VERSION="1.25.10" +VERSION="1.25.12" PATCH_COMMITS=( "afe69d3cec1c6dcf0f1797b20546795730850070" "1ed289b0cf87dc5aae9c6fe1aa5f200a83412938" diff --git a/.github/setup_go_for_windows7.sh b/.github/setup_go_for_windows7.sh index 01588bca..dfc2e0cb 100755 --- a/.github/setup_go_for_windows7.sh +++ b/.github/setup_go_for_windows7.sh @@ -2,14 +2,14 @@ set -euo pipefail -VERSION="1.25.10" +VERSION="1.25.12" PATCH_COMMITS=( - "466f6c7a29bc098b0d4c987b803c779222894a11" - "1bdabae205052afe1dadb2ad6f1ba612cdbc532a" - "a90777dcf692dd2168577853ba743b4338721b06" - "f6bddda4e8ff58a957462a1a09562924d5f3d05c" - "bed309eff415bcb3c77dd4bc3277b682b89a388d" - "34b899c2fb39b092db4fa67c4417e41dc046be4b" + "da4094da73b3b419e3f347594d805e2831f65667" + "824aa60e77f06dbae86c20a164c78df722eb7047" + "a3b6ba31c8cc67b6d899b978bba7b53e95afc46b" + "edfa8de63435a409a59f60731b66ab5940d6d3a4" + "284f9b24d6284984966a8431e30fdc2583938f96" + "9864798dee8dd47b55d1d5100d2f1b909a2a6e6c" ) CURL_ARGS=( -fL diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81c57086..641de573 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,12 +17,16 @@ on: - Binary - Android - Apple - - app-store + - Apple-release - iOS - macOS - tvOS - macOS-standalone - publish-android + test_message: + description: "TestFlight test message" + required: false + type: string push: branches: - stable @@ -47,7 +51,20 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.10 + go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- + - name: Cache Go tool builds + uses: actions/cache/restore@v4 + with: + path: ~/.cache/go-build + key: go-build-tools-linux-${{ hashFiles('go.sum') }} + restore-keys: go-build-tools-linux- - name: Check input version if: github.event_name == 'workflow_dispatch' run: |- @@ -64,10 +81,11 @@ jobs: build: name: Build binary if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary' - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 needs: - calculate_version strategy: + max-parallel: 13 matrix: include: - { os: linux, arch: amd64, variant: purego, naive: true } @@ -119,12 +137,19 @@ jobs: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Setup Go if: ${{ ! matrix.legacy_win7 }} uses: actions/setup-go@v5 with: - go-version: ~1.25.10 + go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- - name: Cache Go for Windows 7 if: matrix.legacy_win7 id: cache-go-for-windows7 @@ -132,7 +157,7 @@ jobs: with: path: | ~/go/go_win7 - key: go_win7_1258 + key: go_win7_12512 - name: Setup Go for Windows 7 if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true' env: @@ -149,7 +174,6 @@ jobs: uses: nttld/setup-ndk@v1 with: ndk-version: r28 - local-cache: true - name: Clone cronet-go if: matrix.naive run: | @@ -167,17 +191,6 @@ jobs: rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg cd ~/cronet-go GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh - - name: Cache Chromium toolchain - if: matrix.naive - id: cache-chromium-toolchain - uses: actions/cache@v4 - with: - path: | - ~/cronet-go/naiveproxy/src/third_party/llvm-build/ - ~/cronet-go/naiveproxy/src/gn/out/ - ~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/ - ~/cronet-go/naiveproxy/src/out/sysroot-build/ - key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }} - name: Download Chromium toolchain if: matrix.naive run: | @@ -335,7 +348,6 @@ jobs: run: | set -xeuo pipefail sudo gem install fpm - sudo apt-get update sudo apt-get install -y debsigs cp .fpm_systemd .fpm fpm -t deb \ @@ -343,8 +355,6 @@ jobs: -p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \ --architecture ${{ matrix.debian }} \ dist/sing-box=/usr/bin/sing-box - curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff' - sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff' rm -rf $HOME/.gnupg gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import < "$HOME/.rpmmacros" cp .fpm_systemd .fpm fpm -t rpm \ -v "$PKG_VERSION" \ @@ -374,7 +385,7 @@ jobs: run: |- set -xeuo pipefail sudo gem install fpm - sudo apt-get update + sudo apt-get update || true sudo apt-get install -y libarchive-tools cp .fpm_pacman .fpm fpm -t pacman \ @@ -447,10 +458,11 @@ jobs: path: "dist" build_darwin: name: Build Darwin binaries - if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary' + if: ${{ !failure() && !cancelled() && (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary') }} runs-on: macos-latest needs: - calculate_version + - build_apple_library strategy: matrix: include: @@ -461,12 +473,19 @@ jobs: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Setup Go if: ${{ ! matrix.legacy_osx }} uses: actions/setup-go@v5 with: go-version: ^1.25.3 + cache: false + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-macos-${{ hashFiles('go.sum') }} + restore-keys: go-mod-macos- - name: Cache Go for macOS 10.13 if: matrix.legacy_osx id: cache-go-for-macos1013 @@ -554,11 +573,18 @@ jobs: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ^1.25.4 + cache: false + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: go-mod-windows-${{ hashFiles('go.sum') }} + restore-keys: go-mod-windows- - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$env:GITHUB_ENV" @@ -634,31 +660,46 @@ jobs: with: name: binary-windows_${{ matrix.arch }} path: "dist" - build_android: - name: Build Android - if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable' + build_android_library: + name: Build Android library (${{ matrix.go_arch }}) + if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android' || inputs.build == 'publish-android') && github.ref != 'refs/heads/oldstable' runs-on: ubuntu-latest needs: - calculate_version + strategy: + fail-fast: false + matrix: + go_arch: + - "386" + - amd64 + - arm + - arm64 steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 - submodules: 'recursive' + fetch-depth: 1 - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.10 + go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 with: ndk-version: r28 - name: Setup OpenJDK - run: |- - sudo apt update && sudo apt install -y openjdk-17-jdk-headless - /usr/lib/jvm/java-17-openjdk-amd64/bin/java --version + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + with: + distribution: temurin + java-version: 17 - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" @@ -667,10 +708,82 @@ jobs: run: |- make lib_install export PATH="$PATH:$(go env GOPATH)/bin" - make lib_android + go run ./cmd/internal/build_libbox -target android -platform android/${{ matrix.go_arch }} + mkdir -p dist/${{ matrix.go_arch }} + mv libbox.aar libbox-legacy.aar dist/${{ matrix.go_arch }} env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + - name: Upload library + uses: actions/upload-artifact@v4 + with: + name: libbox-android-${{ matrix.go_arch }} + path: dist + compression-level: 0 + build_android: + name: Build Android (${{ matrix.variant }}) + if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable' + runs-on: ubuntu-latest + needs: + - calculate_version + - build_android_library + strategy: + fail-fast: false + matrix: + include: + - variant: other + task: :app:assembleOtherRelease + output: app/build/outputs/apk/other/release/*.apk + - variant: other-legacy + task: :app:assembleOtherLegacyRelease + output: app/build/outputs/apk/otherLegacy/release/*.apk + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + fetch-depth: 1 + - name: Checkout Android client + run: git submodule update --init clients/android + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- + - name: Cache Go tool builds + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: go-build-tools-linux-${{ hashFiles('go.sum') }} + restore-keys: go-build-tools-linux- + - name: Setup Android NDK + id: setup-ndk + uses: nttld/setup-ndk@v1 + with: + ndk-version: r28 + - name: Setup OpenJDK + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + with: + distribution: temurin + java-version: 17 + - name: Set tag + run: |- + git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" + git tag v${{ needs.calculate_version.outputs.version }} -f + - name: Download libraries + uses: actions/download-artifact@v4 + with: + pattern: libbox-android-* + path: libbox-inputs + merge-multiple: true + - name: Merge libraries + run: |- + go run ./cmd/internal/merge_aar -output libbox.aar libbox-inputs/*/libbox.aar + go run ./cmd/internal/merge_aar -output libbox-legacy.aar libbox-inputs/*/libbox-legacy.aar - name: Checkout main branch if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch' run: |- @@ -685,11 +798,10 @@ jobs: run: |- cd clients/android git submodule update --init --recursive -v - - name: Gradle cache - uses: actions/cache@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: - path: ~/.gradle - key: gradle-${{ hashFiles('**/*.gradle') }} + cache-provider: basic - name: Update version if: github.event_name == 'workflow_dispatch' run: |- @@ -700,20 +812,17 @@ jobs: go run -v ./cmd/internal/update_android_version --ci --nightly - name: Build run: |- - mkdir clients/android/app/libs + mkdir -p clients/android/app/libs cp *.aar clients/android/app/libs cd clients/android - ./gradlew :app:assembleOtherRelease :app:assembleOtherLegacyRelease + ./gradlew --build-cache --parallel ${{ matrix.task }} env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} - name: Prepare upload run: |- mkdir -p dist - #cp clients/android/app/build/outputs/apk/play/release/*.apk dist - cp clients/android/app/build/outputs/apk/other/release/*.apk dist - cp clients/android/app/build/outputs/apk/otherLegacy/release/*.apk dist + cp clients/android/${{ matrix.output }} dist VERSION_CODE=$(grep VERSION_CODE clients/android/version.properties | cut -d= -f2) VERSION_NAME=$(grep VERSION_NAME clients/android/version.properties | cut -d= -f2) cat > dist/SFA-version-metadata.json << EOF @@ -726,45 +835,63 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: binary-android-apks + name: binary-android-apks-${{ matrix.variant }} path: 'dist' publish_android: name: Publish Android - if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && github.ref != 'refs/heads/oldstable' + if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/testing') runs-on: ubuntu-latest needs: - calculate_version + - build_android_library steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 - submodules: 'recursive' + fetch-depth: 1 + - name: Checkout Android client + run: git submodule update --init clients/android - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.10 + go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- + - name: Cache Go tool builds + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: go-build-tools-linux-${{ hashFiles('go.sum') }} + restore-keys: go-build-tools-linux- - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 with: ndk-version: r28 - name: Setup OpenJDK - run: |- - sudo apt update && sudo apt install -y openjdk-17-jdk-headless - /usr/lib/jvm/java-17-openjdk-amd64/bin/java --version + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + with: + distribution: temurin + java-version: 17 - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f - - name: Build library + - name: Download libraries + uses: actions/download-artifact@v4 + with: + pattern: libbox-android-* + path: libbox-inputs + merge-multiple: true + - name: Merge libraries run: |- - make lib_install - export PATH="$PATH:$(go env GOPATH)/bin" - make lib_android - env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + go run ./cmd/internal/merge_aar -output libbox.aar libbox-inputs/*/libbox.aar + go run ./cmd/internal/merge_aar -output libbox-legacy.aar libbox-inputs/*/libbox-legacy.aar - name: Checkout main branch if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch' run: |- @@ -775,74 +902,156 @@ jobs: run: |- cd clients/android git checkout dev - - name: Gradle cache - uses: actions/cache@v4 + - name: Checkout submodule recursive + run: |- + cd clients/android + git submodule update --init --recursive -v + - name: Setup Gradle + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: - path: ~/.gradle - key: gradle-${{ hashFiles('**/*.gradle') }} - - name: Build + cache-provider: basic + - name: Prepare build run: |- go run -v ./cmd/internal/update_android_version --ci - mkdir clients/android/app/libs + mkdir -p clients/android/app/libs cp *.aar clients/android/app/libs cd clients/android echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json - ./gradlew :app:publishPlayReleaseBundle + chmod 600 service-account-credentials.json + jq -e ' + .type == "service_account" and + (.project_id | type == "string" and length > 0) and + (.private_key | type == "string" and length > 0) and + (.client_email | type == "string" and length > 0) and + (.token_uri | type == "string" and length > 0) + ' service-account-credentials.json > /dev/null + jq -r '.private_key' service-account-credentials.json | openssl pkey -check -noout env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }} - build_apple: - name: Build Apple clients - runs-on: macos-26 - if: false # github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone' + - name: Publish to Google Play + run: |- + cd clients/android + ./gradlew --build-cache --parallel :app:publishPlayReleaseBundle + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} + build_apple_library: + name: Build Apple library (${{ matrix.artifact }}) + runs-on: ${{ matrix.build && 'macos-26' || 'ubuntu-latest' }} + if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone' needs: - calculate_version strategy: + fail-fast: false matrix: include: - - name: iOS - if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }} - platform: ios - scheme: SFI - destination: 'generic/platform=iOS' - archive: build/SFI.xcarchive - upload: SFI/Upload.plist - - name: macOS - if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }} - platform: macos - scheme: SFM - destination: 'generic/platform=macOS' - archive: build/SFM.xcarchive - upload: SFI/Upload.plist - - name: tvOS - if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }} - platform: tvos - scheme: SFT - destination: 'generic/platform=tvOS' - archive: build/SFT.xcarchive - upload: SFI/Upload.plist - - name: macOS-standalone - if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }} - platform: macos - scheme: SFM.System - destination: 'generic/platform=macOS' - archive: build/SFM.System.xcarchive - export: SFM.System/Export.plist - export_path: build/SFM.System + - artifact: ios-arm64 + platform: ios/arm64 + build: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' }} + - artifact: tvos-arm64 + platform: tvos/arm64 + build: ${{ github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'tvOS') }} + - artifact: macos-arm64 + platform: macos/arm64 + build: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'macOS' || inputs.build == 'macOS-standalone' }} + - artifact: macos-amd64 + platform: macos/amd64 + build: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'macOS' || inputs.build == 'macOS-standalone' }} steps: + - name: Checkout + if: matrix.build + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + fetch-depth: 1 + - name: Setup Go + if: matrix.build + uses: actions/setup-go@v5 + with: + go-version: 1.25.12 + cache: false + - name: Cache Go modules + if: matrix.build + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: go-mod-macos-${{ hashFiles('go.sum') }} + restore-keys: go-mod-macos- + - name: Cache Go build + if: matrix.build + uses: actions/cache@v4 + with: + path: ~/Library/Caches/go-build + key: libbox-go-build-${{ matrix.artifact }}-1.25.12-${{ hashFiles('go.sum') }} + restore-keys: libbox-go-build-${{ matrix.artifact }}-1.25.12- + - name: Set tag + if: matrix.build + run: git tag v${{ needs.calculate_version.outputs.version }} -f + - name: Build library + if: matrix.build + run: |- + make lib_install + export PATH="$PATH:$(go env GOPATH)/bin" + go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }} + mkdir -p dist + tar -cf dist/Libbox-${{ matrix.artifact }}.tar Libbox.xcframework + - name: Upload library + if: matrix.build + uses: actions/upload-artifact@v4 + with: + name: libbox-apple-${{ matrix.artifact }} + path: dist + compression-level: 1 + build_apple: + name: Build Apple release clients + runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }} + if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone' + needs: + - calculate_version + - build_apple_library + strategy: + fail-fast: false + matrix: + include: + - name: macOS-standalone + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone' }} + type: standalone + platform: macos + variant: all + library_arch: '*' + scheme: '' + destination: '' + archive: '' + upload: '' + steps: &build_apple_steps - name: Checkout if: matrix.if uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 - submodules: 'recursive' + fetch-depth: 1 + - name: Checkout Apple client + if: matrix.if + run: git submodule update --init --recursive clients/apple - name: Setup Go if: matrix.if uses: actions/setup-go@v5 with: - go-version: ~1.25.10 + go-version: 1.25.12 + cache: false + - name: Cache Go modules + if: matrix.if + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-macos-${{ hashFiles('go.sum') }} + restore-keys: go-mod-macos- + - name: Cache Go tool builds + if: matrix.if + uses: actions/cache@v4 + with: + path: ~/Library/Caches/go-build + key: go-build-tools-macos-${{ hashFiles('go.sum') }} + restore-keys: go-build-tools-macos- - name: Set tag if: matrix.if run: |- @@ -859,52 +1068,139 @@ jobs: run: |- cd clients/apple git checkout dev - - name: Setup certificates + - name: Cache Swift packages if: matrix.if + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/SourcePackages + key: apple-spm-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} + restore-keys: apple-spm- + - name: Cache Xcode build data + if: matrix.if + uses: actions/cache@v4 + with: + path: ${{ matrix.type == 'standalone' && 'clients/apple/build/SFM.System-*.dd' || matrix.type == 'jailbreak' && 'clients/apple/build/jailbreak/DerivedData' || format('{0}/DerivedData', runner.temp) }} + key: apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ github.sha }} + restore-keys: | + apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}- + apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}- + - name: Setup App Store Connect key + if: matrix.if && matrix.type != 'jailbreak' && github.event_name == 'workflow_dispatch' run: |- - CERTIFICATE_PATH=$RUNNER_TEMP/Certificates.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/certificates.keychain-db - echo -n "$CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH + ASC_KEY_PATH=$RUNNER_TEMP/AuthKey.p8 + echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH + echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV" + echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV" + echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV" + env: + ASC_KEY: ${{ secrets.ASC_KEY }} + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }} + - name: Install Apple provisioning profiles + if: matrix.if && matrix.type != 'jailbreak' + run: |- + PROFILES_ZIP_PATH=$RUNNER_TEMP/AppleProvisioningProfiles.zip + printf '%s%s%s%s%s%s%s%s' \ + "$PROVISIONING_PROFILES_1" \ + "$PROVISIONING_PROFILES_2" \ + "$PROVISIONING_PROFILES_3" \ + "$PROVISIONING_PROFILES_4" \ + "$PROVISIONING_PROFILES_5" \ + "$PROVISIONING_PROFILES_6" \ + "$PROVISIONING_PROFILES_7" \ + "$PROVISIONING_PROFILES_8" \ + | base64 --decode -o $PROFILES_ZIP_PATH + PROFILES_PATH="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" + mkdir -p "$PROFILES_PATH" + unzip -q $PROFILES_ZIP_PATH -d "$PROFILES_PATH" + env: + PROVISIONING_PROFILES_1: ${{ secrets.APPLE_PROVISIONING_PROFILES_1 }} + PROVISIONING_PROFILES_2: ${{ secrets.APPLE_PROVISIONING_PROFILES_2 }} + PROVISIONING_PROFILES_3: ${{ secrets.APPLE_PROVISIONING_PROFILES_3 }} + PROVISIONING_PROFILES_4: ${{ secrets.APPLE_PROVISIONING_PROFILES_4 }} + PROVISIONING_PROFILES_5: ${{ secrets.APPLE_PROVISIONING_PROFILES_5 }} + PROVISIONING_PROFILES_6: ${{ secrets.APPLE_PROVISIONING_PROFILES_6 }} + PROVISIONING_PROFILES_7: ${{ secrets.APPLE_PROVISIONING_PROFILES_7 }} + PROVISIONING_PROFILES_8: ${{ secrets.APPLE_PROVISIONING_PROFILES_8 }} + - name: Setup Apple Development certificate + if: matrix.if && matrix.type != 'jailbreak' + run: |- + CERTIFICATE_PATH=$RUNNER_TEMP/AppleDevelopment.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/apple-development.keychain-db + echo -n "$APPLE_DEVELOPMENT_CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security set-keychain-settings -lut 21600 $KEYCHAIN_PATH security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security list-keychain -d user -s $KEYCHAIN_PATH - - PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip - echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH - - PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles" - mkdir -p "$PROFILES_PATH" - unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH" - - ASC_KEY_PATH=$RUNNER_TEMP/Key.p12 - echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH - + security list-keychains -d user -s $KEYCHAIN_PATH + security default-keychain -d user -s $KEYCHAIN_PATH + security find-identity -v -p codesigning $KEYCHAIN_PATH + echo "APPLE_DEVELOPMENT_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + env: + APPLE_DEVELOPMENT_CERTIFICATES_P12: ${{ secrets.APPLE_DEVELOPMENT_CERTIFICATES_P12 }} + P12_PASSWORD: ${{ secrets.APPLE_DEVELOPMENT_P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + - name: Setup Developer ID certificates + if: matrix.if && matrix.type == 'standalone' + run: |- + CERTIFICATE_PATH=$RUNNER_TEMP/DeveloperIDCertificates.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/developer-id.keychain-db + echo -n "$DEVELOPER_ID_CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security list-keychains -d user -s $KEYCHAIN_PATH $APPLE_DEVELOPMENT_KEYCHAIN_PATH + security default-keychain -d user -s $KEYCHAIN_PATH + security find-identity -v -p codesigning $KEYCHAIN_PATH + echo "SIGNING_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + env: + DEVELOPER_ID_CERTIFICATES_P12: ${{ secrets.DEVELOPER_ID_CERTIFICATES_P12 }} + P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + - name: Setup notarization credentials + if: matrix.if && matrix.type == 'standalone' && github.event_name == 'workflow_dispatch' + run: |- + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $SIGNING_KEYCHAIN_PATH xcrun notarytool store-credentials "notarytool-password" \ --key $ASC_KEY_PATH \ --key-id $ASC_KEY_ID \ - --issuer $ASC_KEY_ISSUER_ID - - echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV" - echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV" - echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV" + --issuer $ASC_KEY_ISSUER_ID \ + --keychain $SIGNING_KEYCHAIN_PATH env: - CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }} - P12_PASSWORD: ${{ secrets.P12_PASSWORD }} - KEYCHAIN_PASSWORD: ${{ secrets.P12_PASSWORD }} - PROVISIONING_PROFILES: ${{ secrets.PROVISIONING_PROFILES }} - ASC_KEY: ${{ secrets.ASC_KEY }} - ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} - ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }} - - name: Build library + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + - name: Install jailbreak packaging tools + if: matrix.if && matrix.type == 'jailbreak' + run: |- + brew install dpkg ldid + - name: Trust build plugins + if: matrix.if && matrix.type != 'jailbreak' + run: |- + defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES + - name: Download library + if: matrix.if + uses: actions/download-artifact@v4 + with: + pattern: ${{ matrix.type == 'standalone' && format('libbox-apple-macos-{0}', matrix.library_arch) || format('libbox-apple-{0}-*', matrix.platform) }} + path: libbox-inputs + merge-multiple: true + - name: Prepare library if: matrix.if run: |- - make lib_install - export PATH="$PATH:$(go env GOPATH)/bin" - go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }} - mv Libbox.xcframework clients/apple + mkdir -p libbox-slices + library_inputs=() + for library_archive in libbox-inputs/*.tar; do + slice_name=$(basename "$library_archive" .tar) + slice_path="libbox-slices/$slice_name" + mkdir -p "$slice_path" + tar -xf "$library_archive" -C "$slice_path" + library_inputs+=("$slice_path/Libbox.xcframework") + done + go run ./cmd/internal/merge_apple_xcframework \ + -output clients/apple/Libbox.xcframework \ + "${library_inputs[@]}" - name: Update macOS version if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch' run: |- @@ -915,82 +1211,151 @@ jobs: if: matrix.if && matrix.name != 'iOS' run: |- go run -v ./cmd/internal/update_apple_version --ci - - name: Build - if: matrix.if + - name: Archive App Store build + if: matrix.if && matrix.type == 'app-store' run: |- cd clients/apple xcodebuild archive \ + -clonedSourcePackagesDirPath "$RUNNER_TEMP/SourcePackages" \ + -derivedDataPath "$RUNNER_TEMP/DerivedData" \ -scheme "${{ matrix.scheme }}" \ -configuration Release \ -destination "${{ matrix.destination }}" \ - -archivePath "${{ matrix.archive }}" \ - -allowProvisioningUpdates \ - -authenticationKeyPath $ASC_KEY_PATH \ - -authenticationKeyID $ASC_KEY_ID \ - -authenticationKeyIssuerID $ASC_KEY_ISSUER_ID + -archivePath "${{ matrix.archive }}" + env: + DISABLE_SWIFTLINT: "1" - name: Upload to App Store Connect - if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' + if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch' run: |- go run -v ./cmd/internal/app_store_connect cancel_app_store ${{ matrix.platform }} cd clients/apple xcodebuild -exportArchive \ -archivePath "${{ matrix.archive }}" \ -exportOptionsPlist ${{ matrix.upload }} \ - -allowProvisioningUpdates \ -authenticationKeyPath $ASC_KEY_PATH \ -authenticationKeyID $ASC_KEY_ID \ -authenticationKeyIssuerID $ASC_KEY_ISSUER_ID - name: Publish to TestFlight - if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/testing' + if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/testing' run: |- - go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }} - - name: Build image - if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch' + arguments=(publish_testflight "${{ matrix.platform }}") + if [[ -n "$TEST_MESSAGE" ]]; then + arguments+=("$TEST_MESSAGE") + fi + go run -v ./cmd/internal/app_store_connect "${arguments[@]}" + env: + TEST_MESSAGE: ${{ inputs.test_message }} + - name: Build standalone packages + if: matrix.if && matrix.type == 'standalone' run: |- - pushd clients/apple - xcodebuild -exportArchive \ - -archivePath "${{ matrix.archive }}" \ - -exportOptionsPlist ${{ matrix.export }} \ - -exportPath "${{ matrix.export_path }}" - brew install create-dmg - create-dmg \ - --volname "sing-box" \ - --volicon "${{ matrix.export_path }}/SFM.app/Contents/Resources/AppIcon.icns" \ - --icon "SFM.app" 0 0 \ - --hide-extension "SFM.app" \ - --app-drop-link 0 0 \ - --skip-jenkins \ - SFM.dmg "${{ matrix.export_path }}/SFM.app" - xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password" - cd "${{ matrix.archive }}" + make -C clients/apple build_macos_pkg_${{ matrix.variant }} + env: + DISABLE_SWIFTLINT: "1" + XCODEBUILD_FLAGS: -clonedSourcePackagesDirPath ${{ runner.temp }}/SourcePackages + - name: Notarize standalone packages + if: matrix.if && matrix.type == 'standalone' && github.event_name == 'workflow_dispatch' + run: |- + make -C clients/apple notarize_macos_pkg_${{ matrix.variant }} + - name: Prepare standalone packages + if: matrix.if && matrix.type == 'standalone' + run: |- + mkdir -p dist + for label in Apple Intel Universal; do + cp "clients/apple/build/SFM-${label}.pkg" "dist/SFM-${VERSION}-${label}.pkg" + done + pushd clients/apple/build/SFM.System-universal.xcarchive zip -r SFM.dSYMs.zip dSYMs popd - + cp clients/apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM-${VERSION}.dSYMs.zip" + - name: Build jailbreak package + if: matrix.if && matrix.type == 'jailbreak' + run: |- + make -C clients/apple build_ios_deb mkdir -p dist - cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg" - cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip" - - name: Upload image - if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch' + cp "clients/apple/build/jailbreak/SFI-${VERSION}-iphoneos-arm64.deb" dist + env: + XCODEBUILD_CLONED_SOURCE_PACKAGES_DIR_PATH: ${{ runner.temp }}/SourcePackages + - name: Report cache directory sizes + if: matrix.if + run: |- + du -sh "$RUNNER_TEMP/SourcePackages"/* 2>/dev/null || true + du -sh "$RUNNER_TEMP/DerivedData" 2>/dev/null || true + du -sh clients/apple/build/jailbreak/DerivedData 2>/dev/null || true + du -sh clients/apple/build/SFM.System-*.dd 2>/dev/null || true + - name: Prune Swift package repositories + if: matrix.if + run: rm -rf "$RUNNER_TEMP/SourcePackages/repositories" + - name: Upload release artifact + if: matrix.if && (matrix.type == 'standalone' || matrix.type == 'jailbreak') uses: actions/upload-artifact@v4 with: - name: binary-macos-dmg + name: binary-apple-${{ matrix.type }}${{ matrix.variant && format('-{0}', matrix.variant) || '' }} path: 'dist' + build_apple_app_store: + name: Build Apple App Store clients + runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }} + if: github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS') + needs: + - calculate_version + - build_apple_library + strategy: + fail-fast: false + matrix: + include: + - name: iOS + if: ${{ inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'iOS' }} + type: app-store + platform: ios + scheme: SFI + destination: 'generic/platform=iOS' + archive: build/SFI.xcarchive + upload: SFI/Upload.plist + variant: '' + library_arch: '' + - name: macOS + if: ${{ inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS' }} + type: app-store + platform: macos + scheme: SFM + destination: 'generic/platform=macOS' + archive: build/SFM.xcarchive + upload: SFI/Upload.plist + variant: '' + library_arch: '' + - name: tvOS + if: ${{ inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'tvOS' }} + type: app-store + platform: tvos + scheme: SFT + destination: 'generic/platform=tvOS' + archive: build/SFT.xcarchive + upload: SFI/Upload.plist + variant: '' + library_arch: '' + steps: *build_apple_steps upload: name: Upload builds - if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')" + if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone')" runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write needs: - calculate_version - build - build_darwin - build_windows + - build_android_library - build_android + - build_apple_library - build_apple steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Cache ghr uses: actions/cache@v4 id: cache-ghr @@ -1013,8 +1378,13 @@ jobs: - name: Download builds uses: actions/download-artifact@v5 with: + pattern: binary-* path: dist merge-multiple: true + - name: Attest build provenance + uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + with: + subject-path: 'dist/**' - name: Upload builds if: ${{ env.PUBLISHED == 'false' }} run: |- @@ -1029,3 +1399,70 @@ jobs: ghr --replace -p 5 "v${VERSION}" dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + cache_gc: + name: Cache GC + if: always() + runs-on: ubuntu-latest + permissions: + actions: write + needs: + - build + - build_darwin + - build_windows + - build_android_library + - build_android + - build_apple_library + - build_apple + - build_apple_app_store + - upload + steps: + - name: Delete superseded caches + env: + GH_TOKEN: ${{ github.token }} + run: |- + set -euo pipefail + gh api "repos/$GITHUB_REPOSITORY/actions/caches?per_page=100" --paginate \ + --jq '.actions_caches[] | [.id, .ref, .created_at, .last_accessed_at, .size_in_bytes, .key] | @tsv' > "$RUNNER_TEMP/caches.tsv" + python3 - "$GITHUB_REPOSITORY" "$RUNNER_TEMP/caches.tsv" <<'EOF' + import re + import subprocess + import sys + + repository, list_path = sys.argv[1], sys.argv[2] + budget = 9 * 1024 * 1024 * 1024 + rows = [] + with open(list_path) as list_file: + for line in list_file: + cache_id, ref, created_at, accessed_at, size, key = line.rstrip("\n").split("\t") + family = key + while True: + stripped = re.sub(r"-[0-9a-f]{16,}$", "", family) + if stripped == family: + break + family = stripped + rows.append({ + "id": cache_id, "ref": ref, "created": created_at, + "accessed": accessed_at, "size": int(size), "key": key, + "family": family, + }) + newest = {} + for row in rows: + group = (row["ref"], row["family"]) + if group not in newest or row["created"] > newest[group]["created"]: + newest[group] = row + doomed = [row for row in rows if newest[(row["ref"], row["family"])] is not row] + kept = [row for row in rows if newest[(row["ref"], row["family"])] is row] + total = sum(row["size"] for row in kept) + for row in sorted(kept, key=lambda entry: entry["accessed"]): + if total <= budget: + break + doomed.append(row) + total -= row["size"] + for row in doomed: + print(f"delete {row['size'] >> 20}MB {row['key']} ({row['ref']})") + subprocess.run( + ["gh", "api", "-X", "DELETE", f"repos/{repository}/actions/caches/{row['id']}"], + check=False, + ) + print(f"kept {len(rows) - len(doomed)} caches, {total >> 20}MB total") + EOF diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0ef3fe96..518af08d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -55,7 +55,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.10 + go-version: 1.25.12 - name: Clone cronet-go if: matrix.naive run: | diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 6153c3be..be9b0bf8 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -29,7 +29,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.10 + go-version: 1.25.12 - name: Check input version if: github.event_name == 'workflow_dispatch' run: |- @@ -72,7 +72,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.10 + go-version: 1.25.12 - name: Clone cronet-go if: matrix.naive run: | @@ -194,6 +194,7 @@ jobs: run: |- set -xeuo pipefail sudo gem install fpm + echo '%_rpmformat 4' > "$HOME/.rpmmacros" cp .fpm_systemd .fpm fpm -t rpm \ --name "${NAME}" \ diff --git a/adapter/outbound.go b/adapter/outbound.go index 7816264f..6938f3ff 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -27,6 +27,11 @@ type OutboundWithPreferredRoutes interface { PreferredAddress(address netip.Addr) bool } +type OutboundWithMultiplex interface { + Outbound + MultiplexEnabled() bool +} + type DirectRouteOutbound interface { Outbound NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) diff --git a/cmd/internal/app_store_connect/main.go b/cmd/internal/app_store_connect/main.go index d415abd6..0a1ab61a 100644 --- a/cmd/internal/app_store_connect/main.go +++ b/cmd/internal/app_store_connect/main.go @@ -289,7 +289,7 @@ func prepareAppStore(ctx context.Context) error { return err } if len(builds.Data) == 0 { - log.Fatal(platform, " ", tag, " no build found") + log.Fatal(string(platform), " ", tag, " no build found") } buildID := common.Ptr(builds.Data[0].ID) if version.ID == "" { diff --git a/cmd/internal/merge_aar/main.go b/cmd/internal/merge_aar/main.go new file mode 100644 index 00000000..5c832c78 --- /dev/null +++ b/cmd/internal/merge_aar/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "archive/zip" + "crypto/sha256" + "flag" + "io" + "os" + "path/filepath" + "strings" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" +) + +var outputPath string + +func init() { + flag.StringVar(&outputPath, "output", "", "output AAR path") +} + +func main() { + flag.Parse() + err := merge() + if err != nil { + log.Fatal(err) + } +} + +func merge() error { + inputPaths := flag.Args() + if outputPath == "" { + return E.New("missing output path") + } + if len(inputPaths) == 0 { + return E.New("missing input AAR paths") + } + archiveReaders := make([]*zip.ReadCloser, 0, len(inputPaths)) + for _, inputPath := range inputPaths { + archiveReader, err := zip.OpenReader(inputPath) + if err != nil { + return E.Cause(err, "open input AAR: ", inputPath) + } + archiveReaders = append(archiveReaders, archiveReader) + } + defer func() { + for _, archiveReader := range archiveReaders { + archiveReader.Close() + } + }() + + referenceEntries := make(map[string][sha256.Size]byte) + selectedEntries := make([]*zip.File, 0) + selectedJNIEntries := make(map[string]bool) + for inputIndex, archiveReader := range archiveReaders { + seenEntries := make(map[string]bool) + for _, archiveFile := range archiveReader.File { + if strings.HasPrefix(archiveFile.Name, "jni/") { + if archiveFile.FileInfo().IsDir() { + continue + } + if selectedJNIEntries[archiveFile.Name] { + return E.New("duplicate AAR JNI entry: ", archiveFile.Name) + } + selectedJNIEntries[archiveFile.Name] = true + selectedEntries = append(selectedEntries, archiveFile) + continue + } + entryDigest, err := digestEntry(archiveFile) + if err != nil { + return E.Cause(err, "read AAR entry: ", archiveFile.Name) + } + if inputIndex == 0 { + referenceEntries[archiveFile.Name] = entryDigest + selectedEntries = append(selectedEntries, archiveFile) + } else { + referenceDigest, loaded := referenceEntries[archiveFile.Name] + if !loaded { + return E.New("unexpected AAR entry: ", archiveFile.Name) + } + if referenceDigest != entryDigest { + return E.New("AAR entry differs between architectures: ", archiveFile.Name) + } + } + seenEntries[archiveFile.Name] = true + } + if inputIndex > 0 { + for referenceName := range referenceEntries { + if !seenEntries[referenceName] { + return E.New("missing AAR entry: ", referenceName) + } + } + } + } + + absoluteOutputPath, err := filepath.Abs(outputPath) + if err != nil { + return E.Cause(err, "resolve output AAR path") + } + err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755) + if err != nil { + return E.Cause(err, "create output AAR directory") + } + temporaryFile, err := os.CreateTemp(filepath.Dir(absoluteOutputPath), ".merge-aar-*.aar") + if err != nil { + return E.Cause(err, "create temporary output AAR") + } + temporaryPath := temporaryFile.Name() + defer os.Remove(temporaryPath) + archiveWriter := zip.NewWriter(temporaryFile) + for _, archiveFile := range selectedEntries { + rawReader, openErr := archiveFile.OpenRaw() + if openErr != nil { + archiveWriter.Close() + temporaryFile.Close() + return E.Cause(openErr, "open raw AAR entry: ", archiveFile.Name) + } + header := archiveFile.FileHeader + rawWriter, createErr := archiveWriter.CreateRaw(&header) + if createErr != nil { + archiveWriter.Close() + temporaryFile.Close() + return E.Cause(createErr, "create output AAR entry: ", archiveFile.Name) + } + _, copyErr := io.Copy(rawWriter, rawReader) + if copyErr != nil { + archiveWriter.Close() + temporaryFile.Close() + return E.Cause(copyErr, "copy output AAR entry: ", archiveFile.Name) + } + } + err = archiveWriter.Close() + if err != nil { + temporaryFile.Close() + return E.Cause(err, "finalize output AAR") + } + err = temporaryFile.Close() + if err != nil { + return E.Cause(err, "close output AAR") + } + err = os.Rename(temporaryPath, absoluteOutputPath) + if err != nil { + return E.Cause(err, "replace output AAR") + } + return nil +} + +func digestEntry(archiveFile *zip.File) ([sha256.Size]byte, error) { + entryReader, err := archiveFile.Open() + if err != nil { + return [sha256.Size]byte{}, err + } + digest := sha256.New() + _, err = io.Copy(digest, entryReader) + closeErr := entryReader.Close() + if err != nil { + return [sha256.Size]byte{}, err + } + if closeErr != nil { + return [sha256.Size]byte{}, closeErr + } + var result [sha256.Size]byte + copy(result[:], digest.Sum(nil)) + return result, nil +} diff --git a/cmd/internal/merge_apple_xcframework/main.go b/cmd/internal/merge_apple_xcframework/main.go new file mode 100644 index 00000000..f8f98bec --- /dev/null +++ b/cmd/internal/merge_apple_xcframework/main.go @@ -0,0 +1,172 @@ +package main + +import ( + "flag" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "howett.net/plist" +) + +type xcFrameworkInfo struct { + AvailableLibraries []xcFrameworkLibrary `plist:"AvailableLibraries"` +} + +type xcFrameworkLibrary struct { + BinaryPath string `plist:"BinaryPath"` + LibraryIdentifier string `plist:"LibraryIdentifier"` + LibraryPath string `plist:"LibraryPath"` + SupportedArchitectures []string `plist:"SupportedArchitectures"` + SupportedPlatform string `plist:"SupportedPlatform"` + SupportedPlatformVariant string `plist:"SupportedPlatformVariant"` +} + +type frameworkSlice struct { + rootPath string + library xcFrameworkLibrary +} + +var outputPath string + +func init() { + flag.StringVar(&outputPath, "output", "", "output XCFramework path") +} + +func main() { + flag.Parse() + err := merge() + if err != nil { + log.Fatal(err) + } +} + +func merge() error { + inputPaths := flag.Args() + if outputPath == "" { + return E.New("missing output path") + } + if len(inputPaths) == 0 { + return E.New("missing input XCFramework paths") + } + frameworkGroups := make(map[string][]frameworkSlice) + for _, inputPath := range inputPaths { + infoFile, err := os.Open(filepath.Join(inputPath, "Info.plist")) + if err != nil { + return E.Cause(err, "open XCFramework metadata: ", inputPath) + } + var info xcFrameworkInfo + decoder := plist.NewDecoder(infoFile) + err = decoder.Decode(&info) + closeErr := infoFile.Close() + if err != nil { + return E.Cause(err, "decode XCFramework metadata: ", inputPath) + } + if closeErr != nil { + return E.Cause(closeErr, "close XCFramework metadata: ", inputPath) + } + for _, library := range info.AvailableLibraries { + groupName := library.SupportedPlatform + "|" + library.SupportedPlatformVariant + frameworkGroups[groupName] = append(frameworkGroups[groupName], frameworkSlice{ + rootPath: inputPath, + library: library, + }) + } + } + groupNames := make([]string, 0, len(frameworkGroups)) + for groupName := range frameworkGroups { + groupNames = append(groupNames, groupName) + } + sort.Strings(groupNames) + absoluteOutputPath, err := filepath.Abs(outputPath) + if err != nil { + return E.Cause(err, "resolve output XCFramework path") + } + err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755) + if err != nil { + return E.Cause(err, "create output XCFramework directory") + } + temporaryDirectory, err := os.MkdirTemp(filepath.Dir(absoluteOutputPath), ".merge-xcframework-*") + if err != nil { + return E.Cause(err, "create XCFramework merge directory") + } + defer os.RemoveAll(temporaryDirectory) + frameworkPaths := make([]string, 0, len(groupNames)) + for groupIndex, groupName := range groupNames { + frameworkSlices := frameworkGroups[groupName] + firstSlice := frameworkSlices[0] + firstFrameworkPath := filepath.Join(firstSlice.rootPath, firstSlice.library.LibraryIdentifier, firstSlice.library.LibraryPath) + if len(frameworkSlices) == 1 { + frameworkPaths = append(frameworkPaths, firstFrameworkPath) + continue + } + architectures := make(map[string]bool) + binaryPaths := make([]string, 0, len(frameworkSlices)) + for _, currentSlice := range frameworkSlices { + if currentSlice.library.LibraryPath != firstSlice.library.LibraryPath || currentSlice.library.BinaryPath != firstSlice.library.BinaryPath { + return E.New("incompatible XCFramework slices for platform: ", currentSlice.library.SupportedPlatform) + } + for _, architecture := range currentSlice.library.SupportedArchitectures { + if architectures[architecture] { + return E.New("duplicate XCFramework architecture: ", architecture) + } + architectures[architecture] = true + } + binaryPaths = append(binaryPaths, filepath.Join(currentSlice.rootPath, currentSlice.library.LibraryIdentifier, currentSlice.library.BinaryPath)) + } + mergedFrameworkPath := filepath.Join(temporaryDirectory, "framework-"+strconv.Itoa(groupIndex), filepath.Base(firstSlice.library.LibraryPath)) + copyCommand := exec.Command("ditto", firstFrameworkPath, mergedFrameworkPath) + copyCommand.Stdout = os.Stdout + copyCommand.Stderr = os.Stderr + err = copyCommand.Run() + if err != nil { + return E.Cause(err, "copy XCFramework slice") + } + binaryRelativePath, relativeErr := filepath.Rel(firstSlice.library.LibraryPath, firstSlice.library.BinaryPath) + if relativeErr != nil { + return E.Cause(relativeErr, "resolve XCFramework binary path") + } + if binaryRelativePath == "." || strings.HasPrefix(binaryRelativePath, ".."+string(filepath.Separator)) { + return E.New("invalid XCFramework binary path: ", firstSlice.library.BinaryPath) + } + mergedBinaryPath := filepath.Join(mergedFrameworkPath, binaryRelativePath) + temporaryBinaryPath := mergedBinaryPath + ".merged" + lipoArguments := append([]string{"lipo", "-create"}, binaryPaths...) + lipoArguments = append(lipoArguments, "-output", temporaryBinaryPath) + lipoCommand := exec.Command("xcrun", lipoArguments...) + lipoCommand.Stdout = os.Stdout + lipoCommand.Stderr = os.Stderr + err = lipoCommand.Run() + if err != nil { + return E.Cause(err, "merge XCFramework binaries") + } + err = os.Rename(temporaryBinaryPath, mergedBinaryPath) + if err != nil { + return E.Cause(err, "replace merged XCFramework binary") + } + frameworkPaths = append(frameworkPaths, mergedFrameworkPath) + } + err = os.RemoveAll(absoluteOutputPath) + if err != nil { + return E.Cause(err, "remove output XCFramework") + } + xcodebuildArguments := []string{"-create-xcframework"} + for _, frameworkPath := range frameworkPaths { + xcodebuildArguments = append(xcodebuildArguments, "-framework", frameworkPath) + } + xcodebuildArguments = append(xcodebuildArguments, "-output", absoluteOutputPath) + xcodebuildCommand := exec.Command("xcodebuild", xcodebuildArguments...) + xcodebuildCommand.Stdout = os.Stdout + xcodebuildCommand.Stderr = os.Stderr + err = xcodebuildCommand.Run() + if err != nil { + return E.Cause(err, "create XCFramework") + } + return nil +} diff --git a/cmd/internal/update_apple_version/main.go b/cmd/internal/update_apple_version/main.go index 1b2d0db5..e6be8442 100644 --- a/cmd/internal/update_apple_version/main.go +++ b/cmd/internal/update_apple_version/main.go @@ -106,6 +106,7 @@ func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent stri } func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string { + globalSettings := collectBuildSettings(objectsMap) var objectKeyList []string for objectKey, object := range objectsMap { buildSettings := object.(map[string]any)["buildSettings"] @@ -116,13 +117,51 @@ func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string { if bundleIDObject == nil { continue } - if common.Contains(bundleIDList, bundleIDObject.(string)) { + bundleID := expandBuildVariables(bundleIDObject.(string), globalSettings) + if common.Contains(bundleIDList, bundleID) { objectKeyList = append(objectKeyList, objectKey) } } return objectKeyList } +func collectBuildSettings(objectsMap map[string]any) map[string]string { + settings := make(map[string]string) + for _, object := range objectsMap { + buildSettings, loaded := object.(map[string]any)["buildSettings"].(map[string]any) + if !loaded { + continue + } + for key, value := range buildSettings { + valueString, isString := value.(string) + if !isString { + continue + } + settings[key] = valueString + } + } + return settings +} + +var buildVariableRegexp = regexp.MustCompile(`\$[({]([A-Za-z0-9_]+)[)}]`) + +func expandBuildVariables(value string, settings map[string]string) string { + for { + expanded := buildVariableRegexp.ReplaceAllStringFunc(value, func(match string) string { + name := buildVariableRegexp.FindStringSubmatch(match)[1] + replacement, loaded := settings[name] + if !loaded { + return match + } + return replacement + }) + if expanded == value { + return expanded + } + value = expanded + } +} + func findObjectKeyByDirectory(objectsMap map[string]any, directoryList []string) []string { var objectKeyList []string for objectKey, object := range objectsMap { diff --git a/common/dialer/default.go b/common/dialer/default.go index e4df1907..d1e8c338 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -36,6 +36,7 @@ type DefaultDialer struct { udpAddr4 string udpAddr6 string netns string + autoDetectBindFunc control.Func connectionManager adapter.ConnectionManager networkManager adapter.NetworkManager networkStrategy *C.NetworkStrategy @@ -60,6 +61,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial networkType []C.InterfaceType fallbackNetworkType []C.InterfaceType networkFallbackDelay time.Duration + autoDetectBindFunc control.Func ) if networkManager != nil { interfaceFinder = networkManager.InterfaceFinder() @@ -130,6 +132,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial bindFunc := networkManager.AutoDetectInterfaceFunc() dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) + autoDetectBindFunc = bindFunc } } if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 { @@ -224,6 +227,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial udpAddr4: udpAddr4, udpAddr6: udpAddr6, netns: options.NetNs, + autoDetectBindFunc: autoDetectBindFunc, connectionManager: connectionManager, networkManager: networkManager, networkStrategy: networkStrategy, @@ -328,12 +332,18 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if d.networkStrategy == nil { return d.trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) { + listenConfig := d.udpListener + if d.autoDetectBindFunc != nil && destination.Addr.IsValid() { + listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { + return d.autoDetectBindFunc(network, destination.String(), conn) + }) + } if destination.IsIPv6() { - return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6) + return listenConfig.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6) } else if destination.IsIPv4() && !destination.Addr.IsUnspecified() { - return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4) + return listenConfig.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4) } else { - return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4) + return listenConfig.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4) } })) } else { diff --git a/common/process/searcher_android.go b/common/process/searcher_android.go index 287c7219..e6347744 100644 --- a/common/process/searcher_android.go +++ b/common/process/searcher_android.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" ) var _ Searcher = (*androidSearcher)(nil) @@ -16,6 +17,9 @@ type androidSearcher struct { } func NewSearcher(config Config) (Searcher, error) { + if config.PackageManager == nil { + return nil, E.New("missing package manager") + } return &androidSearcher{config.PackageManager}, nil } diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 1611c83e..56b9e618 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -165,7 +165,7 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres if len(certificate) > 0 { certPool := x509.NewCertPool() if !certPool.AppendCertsFromPEM(certificate) { - return nil, E.New("failed to parse certificate:\n\n", certificate) + return nil, E.New("failed to parse certificate:\n\n", string(certificate)) } tlsConfig.RootCAs = certPool } diff --git a/common/tls/std_server.go b/common/tls/std_server.go index a96db65e..1c911397 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -171,7 +171,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error { for _, certPath := range c.clientCertificatePath { content, err := os.ReadFile(certPath) if err != nil { - c.logger.Error(E.Cause(err, "reload certificate from ", c.clientCertificatePath)) + c.logger.Error(E.Cause(err, "reload certificate from ", certPath)) continue } if !clientCertificateCA.AppendCertsFromPEM(content) { diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index 941192ba..1db3e493 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -218,7 +218,7 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre if len(certificate) > 0 { certPool := x509.NewCertPool() if !certPool.AppendCertsFromPEM(certificate) { - return nil, E.New("failed to parse certificate:\n\n", certificate) + return nil, E.New("failed to parse certificate:\n\n", string(certificate)) } tlsConfig.RootCAs = certPool } diff --git a/common/urltest/urltest.go b/common/urltest/urltest.go index 67b0d206..f1135ca1 100644 --- a/common/urltest/urltest.go +++ b/common/urltest/urltest.go @@ -11,6 +11,7 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" @@ -72,7 +73,18 @@ func (s *HistoryStorage) Close() error { return nil } -func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) { +func URLTest(ctx context.Context, link string, detour N.Dialer) (uint16, error) { + multiplexOutbound, isMultiplexOutbound := common.Cast[adapter.OutboundWithMultiplex](detour) + if isMultiplexOutbound && multiplexOutbound.MultiplexEnabled() { + _, err := urlTest(ctx, link, detour) + if err != nil { + return 0, err + } + } + return urlTest(ctx, link, detour) +} + +func urlTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) { if link == "" { link = "https://www.gstatic.com/generate_204" } diff --git a/dns/client.go b/dns/client.go index aa1aeecf..a193c89a 100644 --- a/dns/client.go +++ b/dns/client.go @@ -106,6 +106,18 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) { return 0, false } +func stripDNSPadding(response *dns.Msg) { + for _, record := range response.Extra { + opt, isOpt := record.(*dns.OPT) + if !isOpt { + continue + } + opt.Option = common.Filter(opt.Option, func(it dns.EDNS0) bool { + return it.Option() != dns.EDNS0PADDING + }) + } +} + func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) { transportStack := transportStackFromContext(ctx) if containsTransport(transportStack, transport.Tag()) { @@ -198,6 +210,8 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m } else { return nil, err } + } else { + stripDNSPadding(response) } /*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA { validResponse := response diff --git a/dns/transport/dhcp/dhcp.go b/dns/transport/dhcp/dhcp.go index c0988766..04bd79b7 100644 --- a/dns/transport/dhcp/dhcp.go +++ b/dns/transport/dhcp/dhcp.go @@ -111,6 +111,7 @@ func (t *Transport) Close() error { func (t *Transport) Reset() { t.transportLock.Lock() t.updatedAt = time.Time{} + t.lastError = nil t.servers = nil t.transportLock.Unlock() } @@ -295,9 +296,14 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4.DHCPv4) error { searchList := dhcpPacket.DomainSearch() if searchList != nil && len(searchList.Labels) > 0 { - t.search = searchList.Labels + t.search = common.Filter(common.Map(searchList.Labels, mDNS.Fqdn), func(it string) bool { + return it != "." + }) } else if dhcpPacket.DomainName() != "" { - t.search = []string{dhcpPacket.DomainName()} + domainName := mDNS.Fqdn(dhcpPacket.DomainName()) + if domainName != "." { + t.search = []string{domainName} + } } serverAddrs := common.Map(dhcpPacket.DNS(), func(it net.IP) M.Socksaddr { return M.SocksaddrFrom(M.AddrFromIP(it), 53) diff --git a/dns/transport/https.go b/dns/transport/https.go index b508e6ea..206e6fb8 100644 --- a/dns/transport/https.go +++ b/dns/transport/https.go @@ -120,13 +120,16 @@ func NewHTTPSRaw( serverAddr M.Socksaddr, tlsConfig tls.Config, ) *HTTPSTransport { + if tlsConfig != nil { + dialer = tls.NewDialer(dialer, tlsConfig) + } return &HTTPSTransport{ TransportAdapter: adapter, logger: logger, dialer: dialer, destination: destination, headers: headers, - transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr), + transport: NewHTTPSTransportWrapper(dialer, serverAddr, destination), } } diff --git a/dns/transport/https_transport.go b/dns/transport/https_transport.go index 84cfa17c..c823718a 100644 --- a/dns/transport/https_transport.go +++ b/dns/transport/https_transport.go @@ -5,11 +5,13 @@ import ( "errors" "net" "net/http" + "net/url" "sync/atomic" "github.com/sagernet/sing-box/common/tls" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" "golang.org/x/net/http2" ) @@ -22,27 +24,36 @@ type HTTPSTransportWrapper struct { fallback *atomic.Bool } -func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper { +func NewHTTPSTransportWrapper(dialer N.Dialer, serverAddr M.Socksaddr, destination *url.URL) *HTTPSTransportWrapper { var fallback atomic.Bool + if destination.Scheme == "http" { + // plain HTTP DoH used by Tailscale + fallback.Store(true) + } return &HTTPSTransportWrapper{ http2Transport: &http2.Transport{ DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) { - tlsConn, err := dialer.DialTLSContext(ctx, serverAddr) + resultConn, err := dialer.DialContext(ctx, N.NetworkTCP, serverAddr) if err != nil { return nil, err } - state := tlsConn.ConnectionState() - if state.NegotiatedProtocol == http2.NextProtoTLS { - return tlsConn, nil + if tlsConn, isTLSConn := resultConn.(tls.Conn); isTLSConn { + state := tlsConn.ConnectionState() + if state.NegotiatedProtocol != http2.NextProtoTLS { + tlsConn.Close() + fallback.Store(true) + return nil, errFallback + } } - tlsConn.Close() - fallback.Store(true) - return nil, errFallback + return resultConn, nil }, }, httpTransport: &http.Transport{ + DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, N.NetworkTCP, serverAddr) + }, DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - return dialer.DialTLSContext(ctx, serverAddr) + return dialer.DialContext(ctx, N.NetworkTCP, serverAddr) }, }, fallback: &fallback, @@ -52,16 +63,15 @@ func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPST func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) { if h.fallback.Load() { return h.httpTransport.RoundTrip(request) - } else { - response, err := h.http2Transport.RoundTrip(request) - if err != nil { - if errors.Is(err, errFallback) { - return h.httpTransport.RoundTrip(request) - } - return nil, err - } - return response, nil } + response, err := h.http2Transport.RoundTrip(request) + if err != nil { + if errors.Is(err, errFallback) { + return h.httpTransport.RoundTrip(request) + } + return nil, err + } + return response, nil } func (h *HTTPSTransportWrapper) CloseIdleConnections() { diff --git a/dns/transport/quic/http3.go b/dns/transport/quic/http3.go index c3a5ca81..0a93e515 100644 --- a/dns/transport/quic/http3.go +++ b/dns/transport/quic/http3.go @@ -126,6 +126,12 @@ func (t *HTTP3Transport) newTransport() *http3.Transport { conn.Close() return nil, dialErr } + // quic-go does not take ownership of the packet conn passed to + // DialEarly: when the connection ends it only stops reading. + go func() { + <-quicConn.Context().Done() + conn.Close() + }() return quicConn, nil }, TLSClientConfig: t.tlsConfig, diff --git a/dns/transport/quic/quic.go b/dns/transport/quic/quic.go index 3bb93e41..8d45bd82 100644 --- a/dns/transport/quic/quic.go +++ b/dns/transport/quic/quic.go @@ -4,6 +4,7 @@ import ( "context" "errors" "os" + "time" "github.com/sagernet/quic-go" "github.com/sagernet/sing-box/adapter" @@ -117,6 +118,12 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, rawConn.Close() return nil, E.Cause(err, "establish QUIC connection") } + // quic-go does not take ownership of the packet conn passed to + // DialEarly: when the connection ends it only stops reading. + go func() { + <-earlyConnection.Context().Done() + rawConn.Close() + }() return earlyConnection, nil }) if err != nil { @@ -144,6 +151,11 @@ func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn *quic. return nil, E.Cause(err, "open stream") } defer stream.CancelRead(0) + stopWatch := context.AfterFunc(ctx, func() { + stream.CancelRead(0) + _ = stream.SetWriteDeadline(time.Now()) + }) + defer stopWatch() err = transport.WriteMessage(stream, 0, message) if err != nil { stream.Close() diff --git a/docs/changelog.md b/docs/changelog.md index 99c22b1c..51c11620 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,23 @@ icon: material/alert-decagram --- +#### 1.13.16 + +* Remove client metadata from AnyTLS requests by default **1** +* Fixes and improvements + +**1**: + +We found that the AnyTLS client implementation uploads metadata that is +**not used by the open-source server**, and there are reports of vendors using +it to profile and discriminate against users. We now leave it empty by default +and allow you to customize it, see +[AnyTLS client metadata](/manual/misc/anytls-client-metadata/). + +#### 1.13.15 + +* Fixes and improvements + #### 1.13.14 * Fixes and improvements diff --git a/docs/configuration/outbound/anytls.md b/docs/configuration/outbound/anytls.md index 4b3e0371..af324891 100644 --- a/docs/configuration/outbound/anytls.md +++ b/docs/configuration/outbound/anytls.md @@ -17,6 +17,7 @@ icon: material/new-box "idle_session_check_interval": "30s", "idle_session_timeout": "30s", "min_idle_session": 5, + "client_metadata": "", "tls": {}, ... // Dial Fields @@ -55,6 +56,12 @@ In the check, close sessions that have been idle for longer than this. Default: In the check, at least the first `n` idle sessions are kept open. Default value: `n`=0 +#### client_metadata + +!!! question "Since sing-box 1.13.16" + +Check [AnyTLS client metadata](/manual/misc/anytls-client-metadata/). + #### tls ==Required== diff --git a/docs/configuration/outbound/anytls.zh.md b/docs/configuration/outbound/anytls.zh.md index c1f8999e..c3f7abaa 100644 --- a/docs/configuration/outbound/anytls.zh.md +++ b/docs/configuration/outbound/anytls.zh.md @@ -17,6 +17,7 @@ icon: material/new-box "idle_session_check_interval": "30s", "idle_session_timeout": "30s", "min_idle_session": 5, + "client_metadata": "", "tls": {}, ... // 拨号字段 @@ -55,6 +56,12 @@ AnyTLS 密码。 在检查中,至少前 `n` 个空闲会话保持打开状态。默认值:`n`=0 +#### client_metadata + +!!! question "自 sing-box 1.13.16 起" + +参阅 [AnyTLS 客户端元数据](/zh/manual/misc/anytls-client-metadata/)。 + #### tls ==必填== diff --git a/docs/manual/misc/anytls-client-metadata.md b/docs/manual/misc/anytls-client-metadata.md new file mode 100644 index 00000000..f1a80e00 --- /dev/null +++ b/docs/manual/misc/anytls-client-metadata.md @@ -0,0 +1,67 @@ +--- +icon: material/incognito +--- + +# AnyTLS client metadata + +The AnyTLS protocol has a design flaw: its settings frame requires the client +to send its software name and version to the server, and the protocol +specification requires that clients not disguise this information. + +This field serves no protocol purpose — AnyTLS already has a separate version +field for compatibility negotiation, and the open-source server implementation +does not use client metadata. However, the field allows vendors to collect and track client types, and for +platform-specific clients, potentially infer private information such as the operating system type and version range — +something that should not, and is not expected by users to, appear in an +anti-censorship protocol. We have received reports that +commercial proxy providers use this information to identify and block +connections from the official library provided by AnyTLS for sing-box +integration, reportedly because abusive users connect to their servers with +sing-box or with clients using the same official library. This indicates that +client metadata is being collected and used for discrimination in practice. + +The protocol specification states that "disguising it has no value." We +disagree: the situation is analogous to browsers implementing TLS ECH GREASE — +without it, privacy-protecting clients can be fingerprinted and treated +differently. + +## Status + +### 2025-02-20 + +We merged the +[pull request adding this protocol](https://github.com/SagerNet/sing-box/pull/2615). +Since the metadata was fixed at `sing-anytls/` in the +implementation provided for our use, and we did not carefully review the +protocol specification and other implementations, we wrongly believed that it +was not private information. + +### 2025-04-05 + +The protocol document +[added](https://github.com/anytls/anytls-go/commit/8812aae7ab29dd88bb89067b9ca676e2e7e29171) +the requirement that third-party implementations fill in the real software +name and version, claiming that "disguising it has no value". + +### 2026-07-18 + +A [pull request submitted to sing-box](https://github.com/SagerNet/sing-box/pull/4311) +was found to additionally upload the `sing-box` name and the actual version; +the change was subsequently reverted and was never released. + +### 2026-08-03 + +sing-box 1.13.16 and 1.14.0-beta.5 have been released; the client metadata in +AnyTLS requests is now empty by default. For compatibility, the +[client_metadata](/configuration/outbound/anytls/#client_metadata) outbound +option allows users to set a custom value. + +Since the open-source server implementation does not use this information and +it has no legitimate use, this is not considered a breaking change. + +## Recommendations + +We recommend that the AnyTLS protocol remove the client metadata, or replace +it with an option that is not sent by default and can be customized by the +user; and that other client implementations also take action, to jointly stop +statistics collection and discrimination based on client metadata. diff --git a/docs/manual/misc/anytls-client-metadata.zh.md b/docs/manual/misc/anytls-client-metadata.zh.md new file mode 100644 index 00000000..2251339c --- /dev/null +++ b/docs/manual/misc/anytls-client-metadata.zh.md @@ -0,0 +1,35 @@ +--- +icon: material/incognito +--- + +# AnyTLS 客户端元数据 + +AnyTLS 协议具有设计缺陷:其 settings 帧要求客户端向服务器发送软件名称和版本,且协议规范要求客户端不得伪装此信息。 + +此字段不承担协议功能——AnyTLS 已有独立的版本字段用于兼容性协商,且开源服务端实现不使用客户端元数据。然而,此字段使得供应商可以收集并统计客户端类型,对于某些平台特定的客户端,还可能推断出操作系统类型与版本范围等隐私信息,而这不应该,也不是被用户预期的,在一个反审查协议中出现。我们收到报告,有商业代理提供商利用此信息识别和阻止来自 sing-box 使用的、由 AnyTLS 提供的用于 sing-box 集成的官方代码库的连接,据传原因是恶意用户使用 sing-box 或使用相同官方代码库的客户端连接到服务器,这表明客户端元数据在实践中已被用于收集和区别对待。 + +协议规范称「伪装它没有任何意义」。我们不同意:这类似于浏览器实现 TLS ECH GREASE——如果没有这一机制,保护隐私的客户端会被识别并受到差别对待。 + +## 状态 + +### 2025-02-20 + +我们合并了[添加此协议的 PR](https://github.com/SagerNet/sing-box/pull/2615)。由于在供我们使用的实现中,metadata 被固定在 `sing-anytls/`,且我们没有仔细审查协议规范和其他实现,我们错误地认为这不是隐私信息。 + +### 2025-04-05 + +协议文档[加入](https://github.com/anytls/anytls-go/commit/8812aae7ab29dd88bb89067b9ca676e2e7e29171)了要求第三方实现填写真实软件名称与版本号的条款,并声称「伪装它没有任何意义」。 + +### 2026-07-18 + +[向 sing-box 提出的 PR](https://github.com/SagerNet/sing-box/pull/4311) 被发现额外上传了 `sing-box` 和实际版本的字符串,随后此更改被回退,没有发布。 + +### 2026-08-03 + +发布了 sing-box 1.13.16 和 1.14.0-beta.5,现在 AnyTLS 请求中的客户端元数据默认为空。出于兼容性考虑,[client_metadata](/zh/configuration/outbound/anytls/#client_metadata) 出站选项允许用户自定义此值。 + +由于开源服务端实现不使用此信息,且它没有合理用途,这不被视为破坏性更改。 + +## 建议 + +我们建议 AnyTLS 协议移除客户端元数据,或将其替换为非默认提供、且用户可以自定义的选项;并建议其他客户端实现也采取行动,共同阻止基于客户端元数据的统计和区别对待。 diff --git a/experimental/deprecated/stderr.go b/experimental/deprecated/stderr.go index 0dfb9354..a999baea 100644 --- a/experimental/deprecated/stderr.go +++ b/experimental/deprecated/stderr.go @@ -3,11 +3,13 @@ package deprecated import ( "os" "strconv" + "sync" "github.com/sagernet/sing/common/logger" ) type stderrManager struct { + access sync.Mutex logger logger.Logger reported map[string]bool } @@ -20,6 +22,8 @@ func NewStderrManager(logger logger.Logger) Manager { } func (f *stderrManager) ReportDeprecated(feature Note) { + f.access.Lock() + defer f.access.Unlock() if f.reported[feature.Name] { return } diff --git a/experimental/libbox/dns.go b/experimental/libbox/dns.go index b7b3b0f6..75472188 100644 --- a/experimental/libbox/dns.go +++ b/experimental/libbox/dns.go @@ -13,7 +13,6 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" - "github.com/sagernet/sing/common/task" mDNS "github.com/miekg/dns" ) @@ -58,24 +57,23 @@ func (p *platformTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m if err != nil { return nil, err } - var responseMessage *mDNS.Msg - var group task.Group - group.Append0(func(ctx context.Context) error { - err = p.iif.Exchange(response, messageBytes) + done := make(chan error, 1) + go func() { + exchangeErr := p.iif.Exchange(response, messageBytes) + if exchangeErr == nil { + exchangeErr = response.error + } + done <- exchangeErr + }() + select { + case err = <-done: if err != nil { - return err + return nil, err } - if response.error != nil { - return response.error - } - responseMessage = &response.message - return nil - }) - err = group.Run(ctx) - if err != nil { - return nil, err + return &response.message, nil + case <-ctx.Done(): + return nil, ctx.Err() } - return responseMessage, nil } else { question := message.Question[0] var network string @@ -87,24 +85,23 @@ func (p *platformTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m default: return nil, E.New("only IP queries are supported by current version of Android") } - var responseAddrs []netip.Addr - var group task.Group - group.Append0(func(ctx context.Context) error { - err := p.iif.Lookup(response, network, question.Name) + done := make(chan error, 1) + go func() { + lookupErr := p.iif.Lookup(response, network, question.Name) + if lookupErr == nil { + lookupErr = response.error + } + done <- lookupErr + }() + select { + case err := <-done: if err != nil { - return err + return nil, err } - if response.error != nil { - return response.error - } - responseAddrs = response.addresses - return nil - }) - err := group.Run(ctx) - if err != nil { - return nil, err + return dns.FixedResponse(message.Id, question, response.addresses, C.DefaultDNSTTL), nil + case <-ctx.Done(): + return nil, ctx.Err() } - return dns.FixedResponse(message.Id, question, responseAddrs, C.DefaultDNSTTL), nil } } diff --git a/go.mod b/go.mod index 17dd64e1..2f57576b 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,11 @@ require ( github.com/Diniboy1123/connect-ip-go v0.0.0-20260409225322-8d7bb0a858a2 github.com/anthropics/anthropic-sdk-go v1.26.0 github.com/anytls/sing-anytls v0.0.11 - github.com/caddyserver/certmagic v0.25.2 + github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6 github.com/coder/websocket v1.8.14 github.com/cretz/bine v0.2.0 github.com/database64128/tfo-go/v2 v2.3.2 + github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa github.com/enfein/mieru/v3 v3.33.0 github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/render v1.0.3 @@ -36,22 +37,22 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260620140045-05ab0dc17597 - github.com/sagernet/cronet-go/all v0.0.0-20260620140045-05ab0dc17597 + github.com/sagernet/cronet-go v0.0.0-20260712143338-d22f2ea3630e + github.com/sagernet/cronet-go/all v0.0.0-20260712143338-d22f2ea3630e github.com/sagernet/fswatch v0.1.2 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.11 + github.com/sagernet/sing v0.8.12-0.20260726145744-ef2df370afca github.com/sagernet/sing-mux v0.3.5 - github.com/sagernet/sing-quic v0.6.1 + github.com/sagernet/sing-quic v0.6.4-0.20260803041914-d83826c306d7 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 - github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.11 + github.com/sagernet/sing-shadowtls v0.2.1 + github.com/sagernet/sing-tun v0.8.12-0.20260727151122-3a09076491df github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 - github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.8 github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.0.2 @@ -119,7 +120,6 @@ require ( github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/database64128/netx-go v0.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect github.com/dolonet/mtg-multi v1.8.0 github.com/ebitengine/purego v0.10.1 // indirect @@ -162,35 +162,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260712142643-1e5048bd5587 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260712142643-1e5048bd5587 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-mod.2 // indirect github.com/spf13/pflag v1.0.10 // indirect @@ -239,4 +239,4 @@ replace github.com/Diniboy1123/connect-ip-go => github.com/shtorm-7/connect-ip-g replace github.com/shtorm-7/go-cache/v2 => github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.2.0 -replace github.com/sagernet/sing => github.com/shtorm-7/sing v0.8.10-extended-1.2.0 +replace github.com/sagernet/sing => github.com/shtorm-7/sing v0.8.12-extended-1.2.0 diff --git a/go.sum b/go.sum index a6edf2bd..0db41c77 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= -github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= +github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6 h1:LYSB6VgWzKtNrcxElw3c97BP40Oc7bizKxA9K1Vi/5k= +github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -287,68 +287,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260620140045-05ab0dc17597 h1:QkwE/ZFnShDuPF+ExmAyZlQaMwFNgkYZMekrabiStfg= -github.com/sagernet/cronet-go v0.0.0-20260620140045-05ab0dc17597/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= -github.com/sagernet/cronet-go/all v0.0.0-20260620140045-05ab0dc17597 h1:cLALmGKP9eOS8622gWQIiVbZlOfH29PGNsoxbEloIdk= -github.com/sagernet/cronet-go/all v0.0.0-20260620140045-05ab0dc17597/go.mod h1:zVHZ5tgDTwbNvUGffAgLmouYs4in0grEzhSdaggoZOw= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260620135226-def9ff0fb992 h1:J9l8PP4vb79Wm5zKaMO6LNZ/AiP1FvyAWJBlkKHrRBU= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260620135226-def9ff0fb992 h1:wXDjUNeKuihv85Kg51FomkiEH7xGsDgRcfRLiyZxacQ= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260620135226-def9ff0fb992 h1:IF04nGyY3Q6Nbk9XJwTX1mckwhf12iIx4RhZ4TLOZIU= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260620135226-def9ff0fb992/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:dlnG1E42xx8ms2fyZigYwsYJ1Gqoj2QT8WeGlpOAWK8= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260620135226-def9ff0fb992 h1:aYAQQN3jZP89MmT1ZzpJz52jAsXx1WApmv5Qidc+ez4= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:n81+aLphvjLpL2M6lI+BC8Ldw4S/FPA3CFDXTWL4g3I= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260620135226-def9ff0fb992 h1:mOseesF+CxgPG2U1a2Yh2fUUMdOaPxSuk4eIL6g7EU8= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260620135226-def9ff0fb992/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:JK9kP72IxAoyVQTnh9gGkh8S9RjP4FBkj95WjrLPyKs= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 h1:3EtgLRsUpmeRaOBynTARxVC8tDegykigutmpAow2ayc= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260620135226-def9ff0fb992/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260620135226-def9ff0fb992 h1:Vr3I2sC9E/1FurpZwJXAL29C7jJROyN3JfulQWjhKuA= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260620135226-def9ff0fb992 h1:flxwC8loz0C4LQ/tLK7LvNMKx4iqaXayPggzzCxCevI= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260620135226-def9ff0fb992 h1:G4vwPmOVR/jXDngIUC9owbEtMKXLZl/BgUHhEWWl8ec= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260620135226-def9ff0fb992 h1:zHIxR2FlJOW5GRmgwyA2Gjgx7potOCtlOmdl8k4mwJI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260620135226-def9ff0fb992 h1:U1OrR5zP+lkOPqrDpZsn8sPK1XBWZ84isXaFBZLsOfs= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260620135226-def9ff0fb992/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:F59ptY4AdtKEg73OWaL+lTb5yoxp5b/gTuDbvA6xMyg= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260620135226-def9ff0fb992 h1:Y5axK4sCWXH+2OCpKYPI8nX3OSBuRb/6yC+5xBi9/uo= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260620135226-def9ff0fb992 h1:QCBwCdv9y+RJj7p0b9Db2p9fFt1wtnawD0sn9oV3vRs= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260620135226-def9ff0fb992 h1:6gXFFaAMiGCPZdfUs64qzj9cl7EleVs9HsNfRi8jWWw= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260620135226-def9ff0fb992 h1:VGoIX2u4CWVg9kiyjQdIpFMaFUNCW3yz0pyrEKI5X0o= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260620135226-def9ff0fb992 h1:SjPuqtUNCzIDaEm7iY86JZ7L+ixTmai4i2DIC++eUvw= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260620135226-def9ff0fb992/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260620135226-def9ff0fb992 h1:ao2FrDzTYhu2MYsMri9nzqIdnAL7ooUWQN6/FFr+Lbk= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260620135226-def9ff0fb992/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260620135226-def9ff0fb992 h1:5gPMu6EUlX6gqCRTOJrJk1FMILO6ugtnopQF1c2R/lY= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260620135226-def9ff0fb992 h1:UaLOQKbjnLrO943Sm+ff/jm+NmemRuJXiImmtqShd8s= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260620135226-def9ff0fb992 h1:wxlsDfxDowVk99Ay2hzfuIXPpXH1lGQxSL/2sX/D7jw= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260620135226-def9ff0fb992/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260620135226-def9ff0fb992 h1:pgRpWh2JPE73mtedovPPu3gmmAqHz3Rfz79QVxciu2o= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260620135226-def9ff0fb992/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:9jtXyxukTS2ZdhMy3u1hg4StkZpgP48BOfgYhXf835w= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 h1:gKgD1LJZbZzacaaqBQX/YKy4dyomhn8xtfmDKayVLW4= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260620135226-def9ff0fb992/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260620135226-def9ff0fb992 h1:Lv8gtKP4QRn6Yjv3h5L215aGQBgwCyAE5YqejmN9Bqc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260620135226-def9ff0fb992 h1:2wsRAcqJHOTlj6zGc3oQyxAZHDnGwyB/pTdAOUtLgHY= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260620135226-def9ff0fb992/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260712143338-d22f2ea3630e h1:Y5mhsZrYuZ9jraIqg7hg1fw4zoVreae81fWvmozbCsQ= +github.com/sagernet/cronet-go v0.0.0-20260712143338-d22f2ea3630e/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= +github.com/sagernet/cronet-go/all v0.0.0-20260712143338-d22f2ea3630e h1:rdNlS1dRSi7jQe/ingFf7QmV9ZCUZMbxAfHnBrdVW7g= +github.com/sagernet/cronet-go/all v0.0.0-20260712143338-d22f2ea3630e/go.mod h1:WNl4xfTNuR+f7SObmuBtrk0p4MhlmvuuiWYoty3U52E= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260712142643-1e5048bd5587 h1:ENmDXbGH92/jsMwhjIxK2a0URkA8ILC3npjqmTGj0Yc= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260712142643-1e5048bd5587 h1:5xn/EZOO5LriSEih91thvTuR+gxb59jNxNQiB/KQPEc= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260712142643-1e5048bd5587 h1:2/UN0LvWnAM0Sc9B/zE2qWylGrX2hQXhknyQyTlvz4k= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260712142643-1e5048bd5587/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:SDULc9o0HkneJD38H1G+HRLby69zfdtyQk5qaKtn/tU= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260712142643-1e5048bd5587 h1:hfcM9YccWN4O2LENHN16Jgm4g1/1PRouV0RmvvrS1f0= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:arY9CL3C7lwJfG2Cdz2ZLvHLzT1wnzCVyAomNER6CrY= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260712142643-1e5048bd5587 h1:HrtCf6KPJsW9KJ4L4T8HMW1vx+3xefLRLObpjZEtzF4= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260712142643-1e5048bd5587/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:Inzzp4hyvcC0lawhndaK8iwEN0vGLDJLCThbaKZpksg= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260712142643-1e5048bd5587 h1:qN6vqr9nFnZqjZXhLez+7tMcbm3FMfhaJRWNMlK3SLI= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260712142643-1e5048bd5587/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260712142643-1e5048bd5587 h1:Zwzpw6555p3rWw844JxNZ+5iRqli6ZBOEtMH3qq1c7s= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260712142643-1e5048bd5587 h1:CwsGHfo1HwvEhA2Wnh3O8WxazN3d4Un44LXhiuIvA/w= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260712142643-1e5048bd5587 h1:Pn6vsVFOJkj9q/XKBOZosfuDLB6luNMLkqq6YLUPXEk= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260712142643-1e5048bd5587 h1:vgRWcEr2jlgICj28XwDMXPQcy8mTob1ZcO+tPdiZjes= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260712142643-1e5048bd5587 h1:uiO62HvSAdRJR3d1Jc4duxWig3kkdwHBSQ3TmUFqw48= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260712142643-1e5048bd5587/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:mXioRdq9h2YlIr9XGM51kXgIkToywwVfnGmJiaz/uzM= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260712142643-1e5048bd5587 h1:oOlfOL+sq0KNZRsk06+5KgvtKw2TEXl2EZnLakTx+WM= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260712142643-1e5048bd5587 h1:UDCa0lYiSUXD4wbxA/G5pGPSXhJaesCQCh5IEiZKb3M= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260712142643-1e5048bd5587 h1:Yg2Ut7mPs0GK4W6p9LDh8RrDOnhTe4YpvG/WeJyUqMo= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260712142643-1e5048bd5587 h1:NtagC/YHvucD0Azh86aVghOk4z5f7oOAAy25Uw4knXQ= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260712142643-1e5048bd5587 h1:INzfLHBKjgJxoUDBeCaiB9hYqjhn5yfudkMz2phYypA= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260712142643-1e5048bd5587/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260712142643-1e5048bd5587 h1:IX5NCEV9nojHdjpOkxKVN4L5FUuA5nwawXlSTdFXjCE= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260712142643-1e5048bd5587/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260712142643-1e5048bd5587 h1:pzgA94sR7kvrP+H86/to2oN3efYORQmh0Q3b6AyfRJQ= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260712142643-1e5048bd5587 h1:nkS6jhF90E24PW37Xemhs6+hUM0l2iRcQcZFhPJL8R8= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260712142643-1e5048bd5587 h1:EbydHlp6vWdqhb8e8zBPVJv/F3HPob+FgO6rsSdNSr0= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260712142643-1e5048bd5587/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260712142643-1e5048bd5587 h1:sSfVTswgqQZJqh9wTP0Acvvo5/qYARAoHegRYwZ+gyU= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260712142643-1e5048bd5587/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:lHqqbALbKdJdq/1rcI4yyg3zvibHDw7wmhDYVbjy498= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260712142643-1e5048bd5587 h1:23GyWjb58Nk9a7WXgRfFUiTrMD9kvYSRHx73AaBthYc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260712142643-1e5048bd5587/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260712142643-1e5048bd5587 h1:64EMjgVuZMD4TX5b7oWUfWRVp8aVl3hg4aWrriQKOWo= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260712142643-1e5048bd5587 h1:x8cvgMQUs0EgVwt3iT/isRQ9KImvXvPobLKO5bHTN4o= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260712142643-1e5048bd5587/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk= github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= @@ -361,16 +361,16 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= -github.com/sagernet/sing-quic v0.6.1/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= +github.com/sagernet/sing-quic v0.6.4-0.20260803041914-d83826c306d7 h1:D46kmyvKMNVFvL3KXdq3T4vy8r29xCMmSqNuQHJBybE= +github.com/sagernet/sing-quic v0.6.4-0.20260803041914-d83826c306d7/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= -github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= -github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.11 h1:BFu4+8LNl2JiTQtto5f+5AbkH90qgdoZEAqUbGiEXCg= -github.com/sagernet/sing-tun v0.8.11/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkGCKNXhbaM= +github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= +github.com/sagernet/sing-tun v0.8.12-0.20260727151122-3a09076491df h1:IVG68QmUeO9xlvrBMB/0u/prBot+jIiEjCsNterk7qs= +github.com/sagernet/sing-tun v0.8.12-0.20260727151122-3a09076491df/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= @@ -383,8 +383,8 @@ github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.2.0 h1:aOd9Vy2LGSwgMM+4805AgLB github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.2.0/go.mod h1:Ek4yz5OK6stwhLKgLsRRYDI+FA+ZWvRJiWLjsi/vMM4= github.com/shtorm-7/mtg-multi v1.11.0-extended-1.0.0 h1:iBLll4ZZG8ULQcHWs6gGslZWtBN72Zo1zjySzMVHF7g= github.com/shtorm-7/mtg-multi v1.11.0-extended-1.0.0/go.mod h1:3rvdhwdPABkwKBdvgMt3VwMn9uSq8hpoHRezZ5jRJU0= -github.com/shtorm-7/sing v0.8.10-extended-1.2.0 h1:5yw9j0+P2QkRWvxBvb71wvNdpAlHmmpBv4hj2gqvass= -github.com/shtorm-7/sing v0.8.10-extended-1.2.0/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= +github.com/shtorm-7/sing v0.8.12-extended-1.2.0 h1:/CYLFBi+Xrj0R6hFvYoqmDfcLe3WB6McZq5t9UvVhrc= +github.com/shtorm-7/sing v0.8.12-extended-1.2.0/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0 h1:a5OoXr3e2ACbM6vDIaaGL44IdHQ6wPjcSoU13vfC0Sw= github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/shtorm-7/sing-vmess v0.2.8-extended-1.0.0 h1:OjvqZOhYSi9eIJGYC0iPDPqvgo/asujvhAGkFzbZ5/Y= diff --git a/mkdocs.yml b/mkdocs.yml index e2959266..e47ec04e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,6 +75,7 @@ nav: - Hysteria 2: manual/proxy-protocol/hysteria2.md - Misc: - TunnelVision: manual/misc/tunnelvision.md + - AnyTLS client metadata: manual/misc/anytls-client-metadata.md - Configuration: - configuration/index.md - Log: diff --git a/option/anytls.go b/option/anytls.go index 0f785263..24efbfc6 100644 --- a/option/anytls.go +++ b/option/anytls.go @@ -22,4 +22,5 @@ type AnyTLSOutboundOptions struct { IdleSessionCheckInterval badoption.Duration `json:"idle_session_check_interval,omitempty"` IdleSessionTimeout badoption.Duration `json:"idle_session_timeout,omitempty"` MinIdleSession int `json:"min_idle_session,omitempty"` + ClientMetadata string `json:"client_metadata,omitempty"` } diff --git a/option/tailscale.go b/option/tailscale.go index 68a14369..cdb29706 100644 --- a/option/tailscale.go +++ b/option/tailscale.go @@ -90,7 +90,7 @@ type DERPMeshOptions struct { } type _DERPSTUNListenOptions struct { - Enabled bool + Enabled bool `json:"enabled,omitempty"` ListenOptions } diff --git a/protocol/anytls/client_metadata.go b/protocol/anytls/client_metadata.go new file mode 100644 index 00000000..09e21195 --- /dev/null +++ b/protocol/anytls/client_metadata.go @@ -0,0 +1,64 @@ +package anytls + +import ( + "encoding/binary" + "net" + "reflect" + "strings" + "sync" + "unsafe" + + "github.com/sagernet/sing/common" + + anytls "github.com/anytls/sing-anytls" + "github.com/anytls/sing-anytls/session" +) + +const ( + commandSettings = 4 + frameHeaderSize = 7 +) + +var ( + clientSessionField, _ = reflect.TypeFor[anytls.Client]().FieldByName("sessionClient") + streamSessionField, _ = reflect.TypeFor[session.Stream]().FieldByName("sess") + sessionConnLockField, _ = reflect.TypeFor[session.Session]().FieldByName("connLock") + sessionBufferField, _ = reflect.TypeFor[session.Session]().FieldByName("buffer") +) + +func sessionClientOf(client *anytls.Client) *session.Client { + return *(**session.Client)(unsafe.Add(unsafe.Pointer(client), clientSessionField.Offset)) +} + +func (h *Outbound) rewriteClientMetadata(conn net.Conn) { + sess := *(**session.Session)(unsafe.Add(unsafe.Pointer(conn.(*session.Stream)), streamSessionField.Offset)) + connLock := (*sync.Mutex)(unsafe.Add(unsafe.Pointer(sess), sessionConnLockField.Offset)) + bufferPointer := (*[]byte)(unsafe.Add(unsafe.Pointer(sess), sessionBufferField.Offset)) + connLock.Lock() + defer connLock.Unlock() + buffer := *bufferPointer + offset := 0 + for offset+frameHeaderSize <= len(buffer) { + dataLength := int(binary.BigEndian.Uint16(buffer[offset+5 : offset+7])) + frameEnd := offset + frameHeaderSize + dataLength + if frameEnd > len(buffer) { + return + } + if buffer[offset] == commandSettings { + data := []byte(strings.Join(common.Map(strings.Split(string(buffer[offset+frameHeaderSize:frameEnd]), "\n"), func(line string) string { + if strings.HasPrefix(line, "client=") { + return "client=" + h.clientMetadata + } + return line + }), "\n")) + newBuffer := make([]byte, 0, offset+frameHeaderSize+len(data)+len(buffer)-frameEnd) + newBuffer = append(newBuffer, buffer[:offset+5]...) + newBuffer = binary.BigEndian.AppendUint16(newBuffer, uint16(len(data))) + newBuffer = append(newBuffer, data...) + newBuffer = append(newBuffer, buffer[frameEnd:]...) + *bufferPointer = newBuffer + return + } + offset = frameEnd + } +} diff --git a/protocol/anytls/outbound.go b/protocol/anytls/outbound.go index 46ea2cc3..7f03c8f6 100644 --- a/protocol/anytls/outbound.go +++ b/protocol/anytls/outbound.go @@ -19,20 +19,25 @@ import ( "github.com/sagernet/sing/common/uot" anytls "github.com/anytls/sing-anytls" + "github.com/anytls/sing-anytls/session" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.AnyTLSOutboundOptions](registry, C.TypeAnyTLS, NewOutbound) } +var _ adapter.OutboundWithMultiplex = (*Outbound)(nil) + type Outbound struct { outbound.Adapter - dialer tls.Dialer - server M.Socksaddr - tlsConfig tls.Config - client *anytls.Client - uotClient *uot.Client - logger log.ContextLogger + dialer tls.Dialer + server M.Socksaddr + tlsConfig tls.Config + clientMetadata string + client *anytls.Client + sessionClient *session.Client + uotClient *uot.Client + logger log.ContextLogger } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSOutboundOptions) (adapter.Outbound, error) { @@ -81,14 +86,30 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } outbound.client = client + outbound.clientMetadata = options.ClientMetadata + outbound.sessionClient = sessionClientOf(client) outbound.uotClient = &uot.Client{ - Dialer: anytlsDialer(client.CreateProxy), + Dialer: (anytlsDialer)(outbound.createProxy), Version: uot.Version, } return outbound, nil } +func (h *Outbound) createProxy(ctx context.Context, destination M.Socksaddr) (net.Conn, error) { + conn, err := h.sessionClient.CreateStream(ctx) + if err != nil { + return nil, err + } + h.rewriteClientMetadata(conn) + err = M.SocksaddrSerializer.WriteAddrPort(conn, destination) + if err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + type anytlsDialer func(ctx context.Context, destination M.Socksaddr) (net.Conn, error) func (d anytlsDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { @@ -103,6 +124,10 @@ func (h *Outbound) dialOut(ctx context.Context) (net.Conn, error) { return h.dialer.DialTLSContext(ctx, h.server) } +func (h *Outbound) MultiplexEnabled() bool { + return true +} + func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() @@ -110,7 +135,7 @@ func (h *Outbound) DialContext(ctx context.Context, network string, destination switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) - return h.client.CreateProxy(ctx, destination) + return h.createProxy(ctx, destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) return h.uotClient.DialContext(ctx, network, destination) diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index b4e7eba0..e483339a 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -108,7 +108,10 @@ func (h *Outbound) fetchMyAddresses() { func (h *Outbound) isMyLoopbackAddress(addresses ...netip.Addr) bool { for _, prefix := range h.myAddresses.Load() { for _, address := range addresses { - if prefix.Addr() != address && prefix.Contains(address) { + if !C.IsDarwin && prefix.Addr() == address { + continue + } + if prefix.Contains(address) { return true } } diff --git a/protocol/naive/inbound.go b/protocol/naive/inbound.go index 6b8f7f6d..f4de3db7 100644 --- a/protocol/naive/inbound.go +++ b/protocol/naive/inbound.go @@ -225,7 +225,7 @@ func (n *Inbound) newConnection(ctx context.Context, waitForClose bool, conn net } else { done := make(chan struct{}) wrapper := v2rayhttp.NewHTTP2Wrapper(conn) - n.router.RouteConnectionEx(ctx, conn, metadata, N.OnceClose(func(it error) { + n.router.RouteConnectionEx(ctx, wrapper, metadata, N.OnceClose(func(it error) { close(done) })) <-done diff --git a/protocol/shadowsocks/outbound.go b/protocol/shadowsocks/outbound.go index 9b9d9252..ebf21f6a 100644 --- a/protocol/shadowsocks/outbound.go +++ b/protocol/shadowsocks/outbound.go @@ -26,6 +26,8 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.ShadowsocksOutboundOptions](registry, C.TypeShadowsocks, NewOutbound) } +var _ adapter.OutboundWithMultiplex = (*Outbound)(nil) + type Outbound struct { outbound.Adapter logger logger.ContextLogger @@ -124,6 +126,10 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n } } +func (h *Outbound) MultiplexEnabled() bool { + return h.multiplexDialer != nil +} + func (h *Outbound) InterfaceUpdated() { if h.multiplexDialer != nil { h.multiplexDialer.Reset() diff --git a/protocol/ssh/outbound.go b/protocol/ssh/outbound.go index b2f96807..b76c79ad 100644 --- a/protocol/ssh/outbound.go +++ b/protocol/ssh/outbound.go @@ -103,7 +103,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL for _, hostKey := range options.HostKey { key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey)) if err != nil { - return nil, E.New("parse host key ", key) + return nil, E.Cause(err, "parse host key: ", hostKey) } outbound.hostKey = append(outbound.hostKey, key) } diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index fd6263e0..f54a5214 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -161,8 +161,9 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n func (t *DNSTransport) createResolver(directDialer func() N.Dialer, resolver *dnstype.Resolver) (adapter.DNSTransport, error) { serverURL, parseURLErr := url.Parse(resolver.Addr) + isHTTPScheme := parseURLErr == nil && (serverURL.Scheme == "http" || serverURL.Scheme == "https") var myDialer N.Dialer - if parseURLErr == nil && serverURL.Scheme == "http" { + if isHTTPScheme && serverURL.Scheme == "http" { myDialer = t.endpoint } else { myDialer = directDialer() @@ -170,36 +171,39 @@ func (t *DNSTransport) createResolver(directDialer func() N.Dialer, resolver *dn if len(resolver.BootstrapResolution) > 0 { bootstrapTransport := transport.NewUDPRaw(t.logger, t.TransportAdapter, myDialer, M.SocksaddrFrom(resolver.BootstrapResolution[0], 53)) myDialer = dialer.NewResolveDialer(t.ctx, myDialer, false, "", adapter.DNSQueryOptions{Transport: bootstrapTransport}, 0) - } - if serverAddr := M.ParseSocksaddr(resolver.Addr); serverAddr.IsValid() { - if serverAddr.Port == 0 { - serverAddr.Port = 53 - } - return transport.NewUDPRaw(t.logger, t.TransportAdapter, myDialer, serverAddr), nil - } else if parseURLErr != nil { - return nil, E.Cause(parseURLErr, "parse resolver address") } else { + myDialer = dialer.NewResolveDialer(t.ctx, myDialer, false, "", t.endpoint.queryOptions, 0) + } + if isHTTPScheme { + serverAddr := M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port()) switch serverURL.Scheme { case "https": - serverAddr = M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port()) if serverAddr.Port == 0 { serverAddr.Port = 443 } tlsConfig := common.Must1(tls.NewClient(t.ctx, t.logger, serverAddr.AddrString(), option.OutboundTLSOptions{ - ALPN: []string{http2.NextProtoTLS, "http/1.1"}, + Enabled: true, + ALPN: []string{http2.NextProtoTLS, "http/1.1"}, })) return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, tlsConfig), nil case "http": - serverAddr = M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port()) if serverAddr.Port == 0 { serverAddr.Port = 80 } return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, nil), nil - // case "tls": - default: - return nil, E.New("unknown resolver scheme: ", serverURL.Scheme) } } + serverAddr := M.ParseSocksaddr(resolver.Addr) + if !serverAddr.IsValid() { + if parseURLErr != nil { + return nil, E.Cause(parseURLErr, "parse resolver address") + } + return nil, E.New("invalid resolver address: ", resolver.Addr) + } + if serverAddr.Port == 0 { + serverAddr.Port = 53 + } + return transport.NewUDPRaw(t.logger, t.TransportAdapter, myDialer, serverAddr), nil } func buildRoutePrefixes(routeConfig *router.Config) []netip.Prefix { @@ -279,7 +283,7 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M } } for domainSuffix, transports := range routes { - if strings.HasSuffix(question.Name, domainSuffix) { + if mDNS.IsSubDomain(domainSuffix, question.Name) { if len(transports) == 0 { return &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index f0ac76ac..e8dd298a 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -9,7 +9,6 @@ import ( "net" "net/http" "net/netip" - "net/url" "os" "path/filepath" "reflect" @@ -71,7 +70,7 @@ var ( ) func init() { - version.SetVersion(tailscaleroot.VersionDotTxt + " (sing-box " + C.Version + ")") + version.SetVersion(strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-0-(sing-box " + C.Version + ")") } func RegisterEndpoint(registry *endpoint.Registry) { @@ -83,6 +82,7 @@ type Endpoint struct { ctx context.Context router adapter.Router logger logger.ContextLogger + queryOptions adapter.DNSQueryOptions dnsRouter adapter.DNSRouter network adapter.NetworkManager platformInterface adapter.PlatformInterface @@ -93,6 +93,7 @@ type Endpoint struct { onReconfigHook wgengine.ReconfigListener cfg *wgcfg.Config + routerCfg *router.Config dnsCfg *tsDNS.Config routeDomains common.TypedValue[map[string]bool] routePrefixes atomic.Pointer[netipx.IPSet] @@ -188,27 +189,17 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL } else { udpTimeout = C.UDPTimeout } - var remoteIsDomain bool - if options.ControlURL != "" { - controlURL, err := url.Parse(options.ControlURL) - if err != nil { - return nil, E.Cause(err, "parse control URL") - } - remoteIsDomain = M.ParseSocksaddr(controlURL.Hostname()).IsDomain() - } else { - // controlplane.tailscale.com - remoteIsDomain = true - } outboundDialer, err := dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, - RemoteIsDomain: remoteIsDomain, + RemoteIsDomain: true, ResolverOnDetour: true, NewDialer: true, }) if err != nil { return nil, err } + dialerQueryOptions := outboundDialer.(dialer.ResolveDialer).QueryOptions() dnsRouter := service.FromContext[adapter.DNSRouter](ctx) server := &tsnet.Server{ Dir: stateDirectory, @@ -225,7 +216,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL AdvertiseTags: options.AdvertiseTags, Dialer: &endpointDialer{Dialer: outboundDialer, logger: logger}, LookupHook: func(ctx context.Context, host string) ([]netip.Addr, error) { - return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions()) + return dnsRouter.Lookup(ctx, host, dialerQueryOptions) }, DNS: &dnsConfigurtor{}, HTTPClient: &http.Client{ @@ -242,10 +233,11 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL }, } return &Endpoint{ - Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil), + Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions), ctx: ctx, router: router, logger: logger, + queryOptions: dialerQueryOptions, dnsRouter: dnsRouter, network: service.FromContext[adapter.NetworkManager](ctx), platformInterface: service.FromContext[adapter.PlatformInterface](ctx), @@ -704,12 +696,17 @@ func (t *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M. metadata.Inbound = t.Tag() metadata.InboundType = t.Type() metadata.Source = source - addr4, addr6 := t.server.TailscaleIPs() - switch destination.Addr { - case addr4: - destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) - case addr6: - destination.Addr = netip.IPv6Loopback() + destinationAddress := tsaddr.UnmapVia(destination.Addr) + if destinationAddress != destination.Addr { + destination.Addr = destinationAddress + } else { + addr4, addr6 := t.server.TailscaleIPs() + switch destination.Addr { + case addr4: + destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + case addr6: + destination.Addr = netip.IPv6Loopback() + } } metadata.Destination = destination t.logger.InfoContext(ctx, "inbound connection from ", source) @@ -722,16 +719,22 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata.Inbound = t.Tag() metadata.InboundType = t.Type() metadata.Source = source - addr4, addr6 := t.server.TailscaleIPs() - switch destination.Addr { - case addr4: - metadata.OriginDestination = destination - destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) - conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination) - case addr6: - metadata.OriginDestination = destination - destination.Addr = netip.IPv6Loopback() - conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination) + originDestination := destination + destinationAddress := tsaddr.UnmapVia(destination.Addr) + if destinationAddress != destination.Addr { + destination.Addr = destinationAddress + } else { + addr4, addr6 := t.server.TailscaleIPs() + switch destination.Addr { + case addr4: + destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + case addr6: + destination.Addr = netip.IPv6Loopback() + } + } + if destination != originDestination { + metadata.OriginDestination = originDestination + conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), originDestination, destination) } metadata.Destination = destination t.logger.InfoContext(ctx, "inbound packet connection from ", source) @@ -797,7 +800,9 @@ func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCf if cfg == nil || dnsCfg == nil { return } - if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) { + if t.cfg != nil && reflect.DeepEqual(t.cfg, cfg) && + t.routerCfg != nil && reflect.DeepEqual(t.routerCfg, routerCfg) && + t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg) { return } var inet4Address, inet6Address netip.Addr @@ -810,6 +815,7 @@ func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCf } t.icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) t.cfg = cfg + t.routerCfg = routerCfg t.dnsCfg = dnsCfg routeDomains := make(map[string]bool) diff --git a/protocol/tailscale/tun_device_windows.go b/protocol/tailscale/tun_device_windows.go index 8c9e87ce..37f813cb 100644 --- a/protocol/tailscale/tun_device_windows.go +++ b/protocol/tailscale/tun_device_windows.go @@ -11,6 +11,9 @@ import ( singTun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/logger" wgTun "github.com/sagernet/wireguard-go/tun" + + "github.com/dblohm7/wingoes/com" + "golang.org/x/sys/windows/svc" ) type tunDeviceAdapter struct { @@ -21,7 +24,23 @@ type tunDeviceAdapter struct { closeOnce sync.Once } -func newTunDeviceAdapter(tun singTun.Tun, mtu int, _ logger.ContextLogger) (wgTun.Device, error) { +var comRuntimeOnce sync.Once + +func newTunDeviceAdapter(tun singTun.Tun, mtu int, contextLogger logger.ContextLogger) (wgTun.Device, error) { + // wgengine/router/osrouter/ifconfig_windows.go setPrivateNetwork assumes COM + // is initialized process-wide, which upstream performs only in tailscaled's + // main package; library consumers must do it themselves. + comRuntimeOnce.Do(func() { + processType := com.ConsoleApp + isService, serviceErr := svc.IsWindowsService() + if serviceErr == nil && isService { + processType = com.Service + } + err := com.StartRuntime(processType) + if err != nil { + contextLogger.Warn("initialize COM runtime: ", err) + } + }) winTun, ok := tun.(singTun.WinTun) if !ok { return nil, errors.New("not a windows tun device") diff --git a/protocol/trojan/outbound.go b/protocol/trojan/outbound.go index c8853fea..6a07755c 100644 --- a/protocol/trojan/outbound.go +++ b/protocol/trojan/outbound.go @@ -26,6 +26,8 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.TrojanOutboundOptions](registry, C.TypeTrojan, NewOutbound) } +var _ adapter.OutboundWithMultiplex = (*Outbound)(nil) + type Outbound struct { outbound.Adapter logger logger.ContextLogger @@ -107,6 +109,10 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n } } +func (h *Outbound) MultiplexEnabled() bool { + return h.multiplexDialer != nil +} + func (h *Outbound) InterfaceUpdated() { if h.transport != nil { h.transport.Close() diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 249c4556..edc14158 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -198,6 +198,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, InterfaceMonitor: networkManager.InterfaceMonitor(), + Logger: logger, EXP_MultiPendingPackets: multiPendingPackets, }, udpTimeout: udpTimeout, diff --git a/protocol/vless/outbound.go b/protocol/vless/outbound.go index 3cbcdb9a..f1e19552 100644 --- a/protocol/vless/outbound.go +++ b/protocol/vless/outbound.go @@ -33,6 +33,8 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.VLESSOutboundOptions](registry, C.TypeVLESS, NewOutbound) } +var _ adapter.OutboundWithMultiplex = (*Outbound)(nil) + type Outbound struct { outbound.Adapter logger logger.ContextLogger @@ -152,6 +154,10 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n } } +func (h *Outbound) MultiplexEnabled() bool { + return h.multiplexDialer != nil +} + func (h *Outbound) InterfaceUpdated() { if h.transport != nil { h.transport.Close() diff --git a/protocol/vmess/outbound.go b/protocol/vmess/outbound.go index cba0e7f9..999d1856 100644 --- a/protocol/vmess/outbound.go +++ b/protocol/vmess/outbound.go @@ -27,6 +27,8 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.VMessOutboundOptions](registry, C.TypeVMess, NewOutbound) } +var _ adapter.OutboundWithMultiplex = (*Outbound)(nil) + type Outbound struct { outbound.Adapter logger logger.ContextLogger @@ -105,6 +107,10 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return outbound, nil } +func (h *Outbound) MultiplexEnabled() bool { + return h.multiplexDialer != nil +} + func (h *Outbound) InterfaceUpdated() { if h.transport != nil { h.transport.Close() diff --git a/route/rule/rule_set_local.go b/route/rule/rule_set_local.go index ed873d70..ee19cc45 100644 --- a/route/rule/rule_set_local.go +++ b/route/rule/rule_set_local.go @@ -114,6 +114,7 @@ func (s *LocalRuleSet) reloadFile(path string) error { if err != nil { return err } + defer setFile.Close() ruleSet, err = srs.Read(setFile, false) if err != nil { return err diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index d626f6b1..f28ba94f 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -98,10 +98,11 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter. if savedSet := s.cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { err := s.loadBytes(savedSet.Content) if err != nil { - return E.Cause(err, "restore cached rule-set") + s.logger.Warn(E.Cause(err, "restore cached rule-set, will refetch")) + } else { + s.lastUpdated = savedSet.LastUpdated + s.lastEtag = savedSet.LastEtag } - s.lastUpdated = savedSet.LastUpdated - s.lastEtag = savedSet.LastEtag } } if s.lastUpdated.IsZero() { diff --git a/service/resolved/service.go b/service/resolved/service.go index eaedc09d..0b9b66fc 100644 --- a/service/resolved/service.go +++ b/service/resolved/service.go @@ -204,10 +204,12 @@ func (i *Service) onNetworkUpdate() { } func (conf *TransportLink) nameList(ndots int, name string) []string { - search := common.Map(common.Filter(conf.domain, func(it LinkDomain) bool { + search := common.Filter(common.Map(common.Filter(conf.domain, func(it LinkDomain) bool { return !it.RoutingOnly }), func(it LinkDomain) string { - return it.Domain + return mDNS.Fqdn(it.Domain) + }), func(it string) bool { + return it != "." }) l := len(name) diff --git a/service/resolved/transport.go b/service/resolved/transport.go index bdc35551..f4beafd8 100644 --- a/service/resolved/transport.go +++ b/service/resolved/transport.go @@ -6,7 +6,6 @@ import ( "context" "net/netip" "os" - "strings" "sync" "sync/atomic" "time" @@ -200,7 +199,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, if domain.Domain == "." && domain.RoutingOnly && !t.acceptDefaultResolvers { continue } - if strings.HasSuffix(question.Name, domain.Domain) { + if mDNS.IsSubDomain(domain.Domain, question.Name) { selectedLink = link } } diff --git a/transport/v2rayhttpupgrade/client.go b/transport/v2rayhttpupgrade/client.go index f282d3f6..27ef8758 100644 --- a/transport/v2rayhttpupgrade/client.go +++ b/transport/v2rayhttpupgrade/client.go @@ -87,22 +87,27 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { request.Header.Set("Upgrade", "websocket") err = request.Write(conn) if err != nil { + conn.Close() return nil, err } bufReader := std_bufio.NewReader(conn) response, err := http.ReadResponse(bufReader, request) if err != nil { + conn.Close() return nil, err } if response.StatusCode != 101 || !strings.EqualFold(response.Header.Get("Connection"), "upgrade") || !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") { + conn.Close() + response.Body.Close() return nil, E.New("v2ray-http-upgrade: unexpected status: ", response.Status) } if bufReader.Buffered() > 0 { buffer := buf.NewSize(bufReader.Buffered()) _, err = buffer.ReadFullFrom(bufReader, buffer.Len()) if err != nil { + conn.Close() return nil, err } conn = bufio.NewCachedConn(conn, buffer) diff --git a/transport/v2rayquic/client.go b/transport/v2rayquic/client.go index 3e0d8b81..c4e0b102 100644 --- a/transport/v2rayquic/client.go +++ b/transport/v2rayquic/client.go @@ -78,6 +78,12 @@ func (c *Client) offerNew() (*quic.Conn, error) { packetConn.Close() return nil, err } + // quic-go does not take ownership of the packet conn passed to Dial: + // when the connection ends it only stops reading. + go func() { + <-quicConn.Context().Done() + packetConn.Close() + }() c.conn.Store(quicConn) c.rawConn = udpConn return quicConn, nil diff --git a/transport/v2rayquic/stream.go b/transport/v2rayquic/stream.go index aad62afb..411c57a4 100644 --- a/transport/v2rayquic/stream.go +++ b/transport/v2rayquic/stream.go @@ -2,6 +2,7 @@ package v2rayquic import ( "net" + "time" "github.com/sagernet/quic-go" qtls "github.com/sagernet/sing-quic" @@ -37,5 +38,8 @@ func (s *StreamWrapper) Upstream() any { func (s *StreamWrapper) Close() error { s.CancelRead(0) s.Stream.Close() + // quic-go's Stream.Close does not unblock a Write blocked on flow control, + // but a past write deadline does; buffered data and the FIN are unaffected. + s.Stream.SetWriteDeadline(time.Now()) return nil } diff --git a/transport/v2raywebsocket/client.go b/transport/v2raywebsocket/client.go index e5630109..a071e8d1 100644 --- a/transport/v2raywebsocket/client.go +++ b/transport/v2raywebsocket/client.go @@ -93,12 +93,14 @@ func (c *Client) dialContext(ctx context.Context, requestURL *url.URL, headers h reader, _, err := ws.Dialer{Header: ws.HandshakeHeaderHTTP(headers), Protocols: protocols}.Upgrade(deadlineConn, requestURL) deadlineConn.SetDeadline(time.Time{}) if err != nil { + conn.Close() return nil, err } if reader != nil { buffer := buf.NewSize(reader.Buffered()) _, err = buffer.ReadFullFrom(reader, buffer.Len()) if err != nil { + conn.Close() return nil, err } conn = bufio.NewCachedConn(conn, buffer) diff --git a/transport/v2raywebsocket/conn.go b/transport/v2raywebsocket/conn.go index cb788fc9..4939e737 100644 --- a/transport/v2raywebsocket/conn.go +++ b/transport/v2raywebsocket/conn.go @@ -187,6 +187,7 @@ func (c *EarlyWebsocketConn) writeRequest(content []byte) error { if len(lateData) > 0 { _, err = conn.Write(lateData) if err != nil { + conn.Close() return err } }