Compare commits

..

47 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
dfb4ef2deb Initial plan 2025-12-09 15:50:26 +00:00
世界
9ec1549a23 Add naiveproxy outbound 2025-12-09 22:20:27 +08:00
世界
76d447d7d9 Add Windows WI-FI state support 2025-12-07 15:47:15 +08:00
世界
ab3caabde1 Add Linux WI-FI state support
Support monitoring WIFI state on Linux through:
- NetworkManager (D-Bus)
- IWD (D-Bus)
- wpa_supplicant (control socket)
- ConnMan (D-Bus)
2025-12-07 13:54:38 +08:00
世界
1338c1e1a9 documentation: Bump version 2025-12-07 10:09:13 +08:00
世界
822e140eed Add more tcp keep alive options
Also update default TCP keep-alive initial period from 10 minutes to 5 minutes.
2025-12-07 10:09:07 +08:00
世界
c726237522 Update quic-go to v0.57.1 2025-12-06 11:36:56 +08:00
世界
466925f636 Fix read credentials for ccm service 2025-12-06 11:36:56 +08:00
世界
517e1503e9 Add claude code multiplexer service 2025-12-06 11:36:56 +08:00
世界
b5f62c1b88 Fix compatibility with MPTCP 2025-12-06 11:36:55 +08:00
世界
6bdfa2d99e Use a more conservative strategy for resolving with systemd-resolved for local DNS server 2025-12-06 11:36:55 +08:00
世界
1662802db8 Fix missing mTLS support in client options 2025-12-06 11:36:55 +08:00
世界
c09a10074b Add curve preferences, pinned public key SHA256 and mTLS for TLS options 2025-12-06 11:36:55 +08:00
世界
269e4a40f7 Fix WireGuard input packet 2025-12-06 11:36:55 +08:00
世界
6a1640fe71 Update tfo-go to latest 2025-12-06 11:36:54 +08:00
世界
8cf6842a86 Remove compatibility codes 2025-12-06 11:36:54 +08:00
世界
2e6af5185c Do not use linkname by default to simplify debugging 2025-12-06 11:36:53 +08:00
世界
9c36b00526 documentation: Update chinese translations 2025-12-06 11:36:53 +08:00
世界
9d0510f384 Update quic-go to v0.55.0 2025-12-06 11:36:53 +08:00
世界
6a0ceb7839 Update WireGuard and Tailscale 2025-12-06 11:36:53 +08:00
世界
13ed7fce55 Fix preConnectionCopy 2025-12-06 11:36:53 +08:00
世界
788fd491d4 Fix ping domain 2025-12-06 11:36:52 +08:00
世界
c651b9b14c release: Fix linux build 2025-12-06 11:36:52 +08:00
世界
07a7530df0 Improve ktls rx error handling 2025-12-06 11:36:51 +08:00
世界
362edd2731 Improve compatibility for kTLS 2025-12-06 11:36:51 +08:00
世界
42e842a7da ktls: Add warning for inappropriate scenarios 2025-12-06 11:36:51 +08:00
世界
63121f18c0 Add support for kTLS
Reference: https://gitlab.com/go-extension/tls
2025-12-06 11:36:51 +08:00
世界
b13643bd42 Add proxy support for ICMP echo request 2025-12-06 11:36:50 +08:00
世界
9078f20860 Fix resolve using resolved 2025-12-06 11:36:50 +08:00
世界
a31ecd79c3 documentation: Update behavior of local DNS server on darwin 2025-12-06 11:36:50 +08:00
世界
4f4d18bfaf Remove use of ldflags -checklinkname=0 on darwin 2025-12-06 11:36:50 +08:00
世界
af2ea8f8b9 Fix legacy DNS config 2025-12-06 11:36:49 +08:00
世界
5452980523 Fix rule-set format 2025-12-06 11:36:49 +08:00
世界
1f421c04ac documentation: Remove outdated icons 2025-12-06 11:36:49 +08:00
世界
159610556d documentation: Improve local DNS server 2025-12-06 11:36:49 +08:00
世界
16de8b394f Stop using DHCP on iOS and tvOS
We do not have the `com.apple.developer.networking.multicast` entitlement and are unable to obtain it for non-technical reasons.
2025-12-06 11:36:49 +08:00
世界
52a0134af0 Improve local DNS server on darwin
We mistakenly believed that `libresolv`'s `search` function worked correctly in NetworkExtension, but it seems only `getaddrinfo` does.

This commit changes the behavior of the `local` DNS server in NetworkExtension to prefer DHCP, falling back to `getaddrinfo` if DHCP servers are unavailable.

It's worth noting that `prefer_go` does not disable DHCP since it respects Dial Fields, but `getaddrinfo` does the opposite. The new behavior only applies to NetworkExtension, not to all scenarios (primarily command-line binaries) as it did previously.

In addition, this commit also improves the DHCP DNS server to use the same robust query logic as `local`.
2025-12-06 11:36:49 +08:00
世界
d1bee96553 Use resolved in local DNS server if available 2025-12-06 11:36:48 +08:00
xchacha20-poly1305
0572e6c436 Fix rule set version 2025-12-06 11:36:48 +08:00
世界
7c1ad09c4c documentation: Add preferred_by route rule item 2025-12-06 11:36:48 +08:00
世界
89b8212ebb Add preferred_by route rule item 2025-12-06 11:36:48 +08:00
世界
fb765bc0da documentation: Add interface address rule items 2025-12-06 11:36:48 +08:00
世界
83cb784de4 Add interface address rule items 2025-12-06 11:36:47 +08:00
世界
73df4a7665 Fix ECH retry support 2025-12-06 11:36:47 +08:00
neletor
044057168c Add support for ech retry configs 2025-12-06 11:36:47 +08:00
Zephyruso
4407105f11 Add /dns/flush-clash meta api 2025-12-06 11:36:47 +08:00
世界
fdeea12514 Bump version 2025-12-06 11:36:30 +08:00
242 changed files with 4395 additions and 18431 deletions

View File

@@ -14,7 +14,6 @@
--depends kmod-inet-diag
--depends kmod-tun
--depends firewall4
--depends kmod-nft-queue
--before-remove release/config/openwrt.prerm

View File

@@ -1 +0,0 @@
abd78bb191a815236485ad929716845ffb41465a

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
VERSION="1.25.7"
VERSION="1.25.4"
mkdir -p $HOME/go
cd $HOME/go

11
.github/setup_musl_cross.sh vendored Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -xeuo pipefail
TARGET="$1"
# Download musl-cross toolchain from musl.cc
cd "$HOME"
wget -q "https://musl.cc/${TARGET}-cross.tgz"
mkdir -p musl-cross
tar -xf "${TARGET}-cross.tgz" -C musl-cross --strip-components=1
rm "${TARGET}-cross.tgz"

View File

@@ -1,13 +1,7 @@
#!/usr/bin/env bash
set -e -o pipefail
SCRIPT_DIR=$(dirname "$0")
PROJECTS=$SCRIPT_DIR/../..
git -C $PROJECTS/cronet-go fetch origin main
PROJECTS=$(dirname "$0")/../..
git -C $PROJECTS/cronet-go fetch origin go
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
go mod tidy
git -C $PROJECTS/cronet-go rev-parse origin/go > "$SCRIPT_DIR/CRONET_GO_VERSION"

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
SCRIPT_DIR=$(dirname "$0")
PROJECTS=$SCRIPT_DIR/../..
git -C $PROJECTS/cronet-go fetch origin dev
git -C $PROJECTS/cronet-go fetch origin go_dev
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
go mod tidy
git -C $PROJECTS/cronet-go rev-parse origin/dev > "$SCRIPT_DIR/CRONET_GO_VERSION"

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ^1.25.4
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -69,51 +69,34 @@ jobs:
strategy:
matrix:
include:
- { os: linux, arch: amd64, variant: purego, naive: true }
- { os: linux, arch: amd64, variant: glibc, naive: true }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: arm64, variant: purego, naive: true }
- { os: linux, arch: arm64, variant: glibc, naive: true }
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: "386", go386: sse2 }
- { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 }
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
- { os: linux, arch: arm, goarm: "7" }
- { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" }
- { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: mipsle, gomips: hardfloat, naive: true, variant: glibc }
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
- { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el }
- { os: linux, arch: riscv64, naive: true, variant: glibc }
- { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, naive: true, variant: glibc }
- { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
- { os: linux, arch: mipsle, gomips: hardfloat, openwrt: "mipsel_24kc_24kf" }
- { os: linux, arch: mipsle, gomips: softfloat }
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
- { os: linux, arch: mips64le, gomips: hardfloat }
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64 }
- { os: linux, arch: loong64 }
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64 }
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: "386" }
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: arm64 }
- { os: android, arch: arm64, ndk: "aarch64-linux-android23" }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi23" }
- { os: android, arch: amd64, ndk: "x86_64-linux-android23" }
- { os: android, arch: "386", ndk: "i686-linux-android23" }
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
- { os: android, arch: "386", ndk: "i686-linux-android21" }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
@@ -123,7 +106,7 @@ jobs:
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ^1.25.4
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
@@ -136,7 +119,7 @@ jobs:
with:
path: |
~/go/go_win7
key: go_win7_1255
key: go_win7_1254
- name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
run: |-
@@ -152,54 +135,6 @@ jobs:
with:
ndk-version: r28
local-cache: true
- name: Clone cronet-go
if: matrix.naive
run: |
set -xeuo pipefail
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
git init ~/cronet-go
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Regenerate Debian keyring
if: matrix.naive
run: |
set -xeuo pipefail
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: |
set -xeuo pipefail
cd ~/cronet-go
if [[ "${{ matrix.variant }}" == "musl" ]]; then
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
else
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} download-toolchain
fi
- name: Set Chromium toolchain environment
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
if [[ "${{ matrix.variant }}" == "musl" ]]; then
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
else
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} env >> $GITHUB_ENV
fi
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -207,79 +142,18 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.os }}" == "android" ]]; then
TAGS="${TAGS},with_naive_outbound"
fi
if [[ "${{ matrix.variant }}" == "purego" ]]; then
TAGS="${TAGS},with_purego"
elif [[ "${{ matrix.variant }}" == "musl" ]]; then
TAGS="${TAGS},with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build (purego)
if: matrix.variant == 'purego'
- name: Build
if: matrix.os != 'android'
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Extract libcronet.so
if: matrix.variant == 'purego' && matrix.naive
run: |
cd ~/cronet-go
CGO_ENABLED=0 go run -v ./cmd/build-naive extract-lib --target ${{ matrix.os }}/${{ matrix.arch }} -o $GITHUB_WORKSPACE/dist
- name: Build (glibc)
if: matrix.variant == 'glibc'
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (musl)
if: matrix.variant == 'musl'
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-variant)
if: matrix.os != 'android' && matrix.variant == ''
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
@@ -299,7 +173,7 @@ jobs:
export CXX="${CC}++"
mkdir -p dist
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
@@ -318,11 +192,6 @@ jobs:
elif [[ -n "${{ matrix.legacy_name }}" ]]; then
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
fi
if [[ "${{ matrix.variant }}" == "glibc" ]]; then
DIR_NAME="${DIR_NAME}-glibc"
elif [[ "${{ matrix.variant }}" == "musl" ]]; then
DIR_NAME="${DIR_NAME}-musl"
fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
@@ -405,18 +274,15 @@ jobs:
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
else
cp sing-box "${DIR_NAME}"
if [ -f libcronet.so ]; then
cp libcronet.so "${DIR_NAME}"
fi
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
fi
rm -r "${DIR_NAME}"
- name: Cleanup
run: rm -f dist/sing-box dist/libcronet.so
run: rm dist/sing-box
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }}${{ matrix.variant && format('-{0}', matrix.variant) }}
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }}
path: "dist"
build_darwin:
name: Build Darwin binaries
@@ -452,17 +318,14 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then
TAGS="${TAGS},with_naive_outbound"
fi
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_naive_outbound,badlinkname,tfogo_checklinkname0'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
@@ -492,18 +355,25 @@ jobs:
with:
name: binary-darwin_${{ matrix.arch }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }}
path: "dist"
build_windows:
name: Build Windows binaries
build_naive_linux:
name: Build Linux with naive outbound
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
runs-on: windows-latest
runs-on: ubuntu-latest
needs:
- calculate_version
strategy:
matrix:
include:
- { arch: amd64, naive: true }
- { arch: "386" }
- { arch: arm64, naive: true }
# Linux glibc (dynamic linking with Debian Bullseye sysroot)
- { arch: amd64, sysroot_arch: amd64, sysroot_sha: "36a164623d03f525e3dfb783a5e9b8a00e98e1ddd2b5cff4e449bd016dd27e50", cc_target: "x86_64-linux-gnu", suffix: "-naive" }
- { arch: arm64, sysroot_arch: arm64, sysroot_sha: "2f915d821eec27515c0c6d21b69898e23762908d8d7ccc1aa2a8f5f25e8b7e18", cc_target: "aarch64-linux-gnu", suffix: "-naive" }
- { arch: "386", sysroot_arch: i386, sysroot_sha: "63f0e5128b84f7b0421956a4a40affa472be8da0e58caf27e9acbc84072daee7", cc_target: "i686-linux-gnu", suffix: "-naive" }
- { arch: arm, goarm: "7", sysroot_arch: armhf, sysroot_sha: "47b3a0b161ca011b2b33d4fc1ef6ef269b8208a0b7e4c900700c345acdfd1814", cc_target: "arm-linux-gnueabihf", suffix: "-naive" }
# Linux musl (static linking)
- { arch: amd64, musl: true, cc_target: "x86_64-linux-musl", suffix: "-naive-musl" }
- { arch: arm64, musl: true, cc_target: "aarch64-linux-musl", suffix: "-naive-musl" }
- { arch: "386", musl: true, cc_target: "i686-linux-musl", suffix: "-naive-musl" }
- { arch: arm, goarm: "7", musl: true, cc_target: "arm-linux-musleabihf", suffix: "-naive-musl" }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
@@ -515,67 +385,97 @@ jobs:
go-version: ^1.25.4
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$env:GITHUB_ENV"
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
if: matrix.naive
- name: Download sysroot (glibc)
if: ${{ ! matrix.musl }}
run: |
set -xeuo pipefail
wget -q "https://commondatastorage.googleapis.com/chrome-linux-sysroot/${{ matrix.sysroot_sha }}" -O sysroot.tar.xz
mkdir -p /tmp/sysroot
tar -xf sysroot.tar.xz -C /tmp/sysroot
- name: Install cross compiler (glibc)
if: ${{ ! matrix.musl }}
run: |
set -xeuo pipefail
sudo apt-get update
sudo apt-get install -y clang lld
if [[ "${{ matrix.arch }}" == "arm64" ]]; then
sudo apt-get install -y libc6-dev-arm64-cross
elif [[ "${{ matrix.arch }}" == "386" ]]; then
sudo apt-get install -y libc6-dev-i386-cross
elif [[ "${{ matrix.arch }}" == "arm" ]]; then
sudo apt-get install -y libc6-dev-armhf-cross
fi
- name: Install musl cross compiler
if: matrix.musl
run: |
set -xeuo pipefail
.github/setup_musl_cross.sh "${{ matrix.cc_target }}"
echo "PATH=$HOME/musl-cross/bin:$PATH" >> $GITHUB_ENV
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_naive_outbound,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.musl }}" == "true" ]]; then
TAGS="${TAGS},with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build (glibc)
if: ${{ ! matrix.musl }}
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box.exe -tags "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0" `
-ldflags "-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0" `
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0 -linkmode=external -extldflags "-fuse-ld=lld --sysroot=/tmp/sysroot"' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: windows
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
CC: "clang --target=${{ matrix.cc_target }} --sysroot=/tmp/sysroot"
CXX: "clang++ --target=${{ matrix.cc_target }} --sysroot=/tmp/sysroot"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build
if: ${{ !matrix.naive }}
- name: Build (musl)
if: matrix.musl
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box.exe -tags "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0" `
-ldflags "-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0" `
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0 -linkmode=external -extldflags "-static"' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: windows
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
CC: "${{ matrix.cc_target }}-gcc"
CXX: "${{ matrix.cc_target }}-g++"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Extract libcronet.dll
if: matrix.naive
run: |
$CRONET_GO_VERSION = Get-Content .github/CRONET_GO_VERSION
$env:CGO_ENABLED = "0"
go run -v "github.com/sagernet/cronet-go/cmd/build-naive@$CRONET_GO_VERSION" extract-lib --target windows/${{ matrix.arch }} -o dist
- name: Set name
run: |-
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-linux-${{ matrix.arch }}"
if [[ -n "${{ matrix.goarm }}" ]]; then
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
fi
DIR_NAME="${DIR_NAME}${{ matrix.suffix }}"
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
- name: Archive
if: matrix.naive
run: |
$DIR_NAME = "sing-box-${{ needs.calculate_version.outputs.version }}-windows-${{ matrix.arch }}"
mkdir "dist/$DIR_NAME"
Copy-Item LICENSE "dist/$DIR_NAME"
Copy-Item "dist/sing-box.exe" "dist/$DIR_NAME"
Copy-Item "dist/libcronet.dll" "dist/$DIR_NAME"
Compress-Archive -Path "dist/$DIR_NAME" -DestinationPath "dist/$DIR_NAME.zip"
Remove-Item -Recurse "dist/$DIR_NAME"
- name: Archive
if: ${{ !matrix.naive }}
run: |
$DIR_NAME = "sing-box-${{ needs.calculate_version.outputs.version }}-windows-${{ matrix.arch }}"
mkdir "dist/$DIR_NAME"
Copy-Item LICENSE "dist/$DIR_NAME"
Copy-Item "dist/sing-box.exe" "dist/$DIR_NAME"
Compress-Archive -Path "dist/$DIR_NAME" -DestinationPath "dist/$DIR_NAME.zip"
Remove-Item -Recurse "dist/$DIR_NAME"
set -xeuo pipefail
cd dist
mkdir -p "${DIR_NAME}"
cp ../LICENSE "${DIR_NAME}"
cp sing-box "${DIR_NAME}"
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
rm -r "${DIR_NAME}"
- name: Cleanup
if: matrix.naive
run: Remove-Item dist/sing-box.exe, dist/libcronet.dll
- name: Cleanup
if: ${{ !matrix.naive }}
run: Remove-Item dist/sing-box.exe
run: rm dist/sing-box
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-windows_${{ matrix.arch }}
name: binary-linux_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.suffix }}
path: "dist"
build_android:
name: Build Android
@@ -592,7 +492,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ^1.25.4
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -640,9 +540,9 @@ jobs:
- name: Build
run: |-
mkdir clients/android/app/libs
cp *.aar clients/android/app/libs
cp libbox.aar clients/android/app/libs
cd clients/android
./gradlew :app:assembleOtherRelease :app:assembleOtherLegacyRelease
./gradlew :app:assemblePlayRelease :app:assembleOtherRelease
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
@@ -650,18 +550,8 @@ jobs:
- 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
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
{
"version_code": ${VERSION_CODE},
"version_name": "${VERSION_NAME}"
}
EOF
cat dist/SFA-version-metadata.json
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
@@ -682,7 +572,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ^1.25.4
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -723,7 +613,7 @@ jobs:
run: |-
go run -v ./cmd/internal/update_android_version --ci
mkdir clients/android/app/libs
cp *.aar clients/android/app/libs
cp libbox.aar clients/android/app/libs
cd clients/android
echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json
./gradlew :app:publishPlayReleaseBundle
@@ -735,7 +625,7 @@ jobs:
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'
if: false
needs:
- calculate_version
strategy:
@@ -781,7 +671,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ^1.25.4
- name: Set tag
if: matrix.if
run: |-
@@ -900,7 +790,7 @@ jobs:
--app-drop-link 0 0 \
--skip-jenkins \
SFM.dmg "${{ matrix.export_path }}/SFM.app"
xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password"
xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password"
cd "${{ matrix.archive }}"
zip -r SFM.dSYMs.zip dSYMs
popd
@@ -922,7 +812,7 @@ jobs:
- calculate_version
- build
- build_darwin
- build_windows
- build_naive_linux
- build_android
- build_apple
steps:

View File

@@ -1,10 +1,6 @@
name: Publish Docker Images
on:
#push:
# branches:
# - main-next
# - dev-next
release:
types:
- published
@@ -17,146 +13,8 @@ env:
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
jobs:
build_binary:
name: Build binary
build:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
include:
# Naive-enabled builds (musl)
- { arch: amd64, naive: true, docker_platform: "linux/amd64" }
- { arch: arm64, naive: true, docker_platform: "linux/arm64" }
- { arch: "386", naive: true, docker_platform: "linux/386" }
- { arch: arm, goarm: "7", naive: true, docker_platform: "linux/arm/v7" }
- { arch: mipsle, gomips: softfloat, naive: true, docker_platform: "linux/mipsle" }
- { arch: riscv64, naive: true, docker_platform: "linux/riscv64" }
- { arch: loong64, naive: true, docker_platform: "linux/loong64" }
# Non-naive builds
- { arch: arm, goarm: "6", docker_platform: "linux/arm/v6" }
- { arch: ppc64le, docker_platform: "linux/ppc64le" }
- { arch: s390x, docker_platform: "linux/s390x" }
steps:
- name: Get commit to build
id: ref
run: |-
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
ref="${{ github.ref_name }}"
else
ref="${{ github.event.inputs.tag }}"
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
- name: Clone cronet-go
if: matrix.naive
run: |
set -xeuo pipefail
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
git init ~/cronet-go
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Regenerate Debian keyring
if: matrix.naive
run: |
set -xeuo pipefail
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 }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium toolchain
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
- name: Set version
run: |
set -xeuo pipefail
VERSION=$(go run ./cmd/internal/read_tag)
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
- name: Set Chromium toolchain environment
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS="${TAGS},with_naive_outbound,with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build (naive)
if: matrix.naive
run: |
set -xeuo pipefail
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=${VERSION}\" -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
- name: Build (non-naive)
if: ${{ ! matrix.naive }}
run: |
set -xeuo pipefail
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=${VERSION}\" -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
- name: Prepare artifact
run: |
platform=${{ matrix.docker_platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
# Rename binary to include arch info for Dockerfile.binary
BINARY_NAME="sing-box-${{ matrix.arch }}"
if [[ -n "${{ matrix.goarm }}" ]]; then
BINARY_NAME="${BINARY_NAME}v${{ matrix.goarm }}"
fi
mv sing-box "${BINARY_NAME}"
echo "BINARY_NAME=${BINARY_NAME}" >> $GITHUB_ENV
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: binary-${{ env.PLATFORM_PAIR }}
path: ${{ env.BINARY_NAME }}
if-no-files-found: error
retention-days: 1
build_docker:
name: Build Docker image
runs-on: ubuntu-latest
needs:
- build_binary
strategy:
fail-fast: true
matrix:
@@ -166,11 +24,9 @@ jobs:
- linux/arm/v7
- linux/arm64
- linux/386
- linux/mipsle
- linux/ppc64le
- linux/riscv64
- linux/s390x
- linux/loong64
steps:
- name: Get commit to build
id: ref
@@ -191,16 +47,6 @@ jobs:
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Download binary
uses: actions/download-artifact@v5
with:
name: binary-${{ env.PLATFORM_PAIR }}
path: .
- name: Prepare binary
run: |
# Find and make the binary executable
chmod +x sing-box-*
ls -la sing-box-*
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
@@ -222,7 +68,8 @@ jobs:
with:
platforms: ${{ matrix.platform }}
context: .
file: Dockerfile.binary
build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
@@ -240,7 +87,7 @@ jobs:
merge:
runs-on: ubuntu-latest
needs:
- build_docker
- build
steps:
- name: Get commit to build
id: ref
@@ -274,7 +121,6 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
if: github.event_name != 'push'
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
@@ -282,7 +128,6 @@ jobs:
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
if: github.event_name != 'push'
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}

View File

@@ -32,7 +32,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: latest
version: v2.4.0
args: --timeout=30m
install-mode: binary
verify: false

View File

@@ -1,10 +1,6 @@
name: Build Linux Packages
on:
#push:
# branches:
# - main-next
# - dev-next
workflow_dispatch:
inputs:
version:
@@ -34,7 +30,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ^1.25.4
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -56,19 +52,17 @@ jobs:
strategy:
matrix:
include:
# Naive-enabled builds (musl)
- { os: linux, arch: amd64, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: arm64, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: "386", naive: true, debian: i386, rpm: i386 }
- { os: linux, arch: arm, goarm: "7", naive: true, debian: armhf, rpm: armv7hl, pacman: armv7hl }
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, debian: mipsel, rpm: mipsel }
- { os: linux, arch: riscv64, naive: true, debian: riscv64, rpm: riscv64 }
- { os: linux, arch: loong64, naive: true, debian: loongarch64, rpm: loongarch64 }
# Non-naive builds (unsupported architectures)
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: "386", debian: i386, rpm: i386 }
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
@@ -77,47 +71,13 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
- name: Clone cronet-go
if: matrix.naive
run: |
set -xeuo pipefail
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
git init ~/cronet-go
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Regenerate Debian keyring
if: matrix.naive
run: |
set -xeuo pipefail
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
go-version: ^1.25.4
- name: Setup Android NDK
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1
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 }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium toolchain
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
- name: Set Chromium toolchain environment
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
ndk-version: r28
local-cache: true
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -125,34 +85,14 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS="${TAGS},with_naive_outbound,with_musl"
fi
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,badlinkname,tfogo_checklinkname0'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build (naive)
if: matrix.naive
- name: Build
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-naive)
if: ${{ ! matrix.naive }}
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
@@ -245,6 +185,5 @@ jobs:
path: dist
merge-multiple: true
- name: Publish packages
if: github.event_name != 'push'
run: |-
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/

3
.gitignore vendored
View File

@@ -12,9 +12,6 @@
/*.jar
/*.aar
/*.xcframework/
/experimental/libbox/*.aar
/experimental/libbox/*.xcframework/
/experimental/libbox/*.nupkg
.DS_Store
/config.d/
/venv/

View File

@@ -13,9 +13,9 @@ RUN set -ex \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0" \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,badlinkname,tfogo_checklinkname0" \
-o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0" \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"

View File

@@ -1,8 +0,0 @@
FROM alpine
ARG TARGETARCH
ARG TARGETVARIANT
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
COPY sing-box-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"]

115
Makefile
View File

@@ -1,17 +1,15 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,badlinkname,tfogo_checklinkname0
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0"
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0"
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH)
SING_FFI ?= sing-ffi
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
.PHONY: test release docs build
@@ -39,11 +37,8 @@ fmt:
@gofmt -s -w .
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
fmt_docs:
go run ./cmd/internal/format_docs
fmt_install:
go install -v mvdan.cc/gofumpt@latest
go install -v mvdan.cc/gofumpt@v0.8.0
go install -v github.com/daixiang0/gci@latest
lint:
@@ -54,7 +49,7 @@ lint:
GOOS=freebsd golangci-lint run ./...
lint_install:
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
proto:
@go run ./cmd/internal/protogen
@@ -91,12 +86,12 @@ update_android_version:
go run ./cmd/internal/update_android_version
build_android:
cd ../sing-box-for-android && ./gradlew :app:clean :app:assembleOtherRelease :app:assembleOtherLegacyRelease && ./gradlew --stop
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :app:assembleOtherRelease && ./gradlew --stop
upload_android:
mkdir -p dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_android
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
rm -rf dist/release_android
@@ -111,7 +106,7 @@ build_ios:
cd ../sing-box-for-apple && \
rm -rf build/SFI.xcarchive && \
xcodebuild clean -scheme SFI && \
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates
upload_ios_app_store:
cd ../sing-box-for-apple && \
@@ -132,7 +127,7 @@ release_ios: build_ios upload_ios_app_store
build_macos:
cd ../sing-box-for-apple && \
rm -rf build/SFM.xcarchive && \
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates
upload_macos_app_store:
cd ../sing-box-for-apple && \
@@ -141,50 +136,54 @@ upload_macos_app_store:
release_macos: build_macos upload_macos_app_store
build_macos_standalone:
$(MAKE) -C ../sing-box-for-apple archive_macos_standalone
cd ../sing-box-for-apple && \
rm -rf build/SFM.System.xcarchive && \
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive -allowProvisioningUpdates
build_macos_dmg:
$(MAKE) -C ../sing-box-for-apple build_macos_dmg
build_macos_pkg:
$(MAKE) -C ../sing-box-for-apple build_macos_pkg
rm -rf dist/SFM
mkdir -p dist/SFM
cd ../sing-box-for-apple && \
rm -rf build/SFM.System && \
rm -rf build/SFM.dmg && \
xcodebuild -exportArchive \
-archivePath "build/SFM.System.xcarchive" \
-exportOptionsPlist SFM.System/Export.plist -allowProvisioningUpdates \
-exportPath "build/SFM.System" && \
create-dmg \
--volname "sing-box" \
--volicon "build/SFM.System/SFM.app/Contents/Resources/AppIcon.icns" \
--icon "SFM.app" 0 0 \
--hide-extension "SFM.app" \
--app-drop-link 0 0 \
--skip-jenkins \
"../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app"
notarize_macos_dmg:
$(MAKE) -C ../sing-box-for-apple notarize_macos_dmg
notarize_macos_pkg:
$(MAKE) -C ../sing-box-for-apple notarize_macos_pkg
xcrun notarytool submit "dist/SFM/SFM.dmg" --wait \
--keychain-profile "notarytool-password" \
--no-s3-acceleration
upload_macos_dmg:
mkdir -p dist/SFM
cp ../sing-box-for-apple/build/SFM-Apple.dmg "dist/SFM/SFM-${VERSION}-Apple.dmg"
cp ../sing-box-for-apple/build/SFM-Intel.dmg "dist/SFM/SFM-${VERSION}-Intel.dmg"
cp ../sing-box-for-apple/build/SFM-Universal.dmg "dist/SFM/SFM-${VERSION}-Universal.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.dmg"
upload_macos_pkg:
mkdir -p dist/SFM
cp ../sing-box-for-apple/build/SFM-Apple.pkg "dist/SFM/SFM-${VERSION}-Apple.pkg"
cp ../sing-box-for-apple/build/SFM-Intel.pkg "dist/SFM/SFM-${VERSION}-Intel.pkg"
cp ../sing-box-for-apple/build/SFM-Universal.pkg "dist/SFM/SFM-${VERSION}-Universal.pkg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.pkg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg"
cd dist/SFM && \
cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dmg"
upload_macos_dsyms:
mkdir -p dist/SFM
cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs
cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip"
pushd ../sing-box-for-apple/build/SFM.System.xcarchive && \
zip -r SFM.dSYMs.zip dSYMs && \
mv SFM.dSYMs.zip ../../../sing-box/dist/SFM && \
popd && \
cd dist/SFM && \
cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dSYMs.zip"
release_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms
release_macos_standalone: build_macos_standalone build_macos_dmg notarize_macos_dmg upload_macos_dmg upload_macos_dsyms
build_tvos:
cd ../sing-box-for-apple && \
rm -rf build/SFT.xcarchive && \
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates
upload_tvos_app_store:
cd ../sing-box-for-apple && \
@@ -208,12 +207,12 @@ update_apple_version:
update_macos_version:
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
release_apple_beta: update_apple_version release_ios release_macos release_tvos
publish_testflight:
go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS))
go run -v ./cmd/internal/app_store_connect publish_testflight
prepare_app_store:
go run -v ./cmd/internal/app_store_connect prepare_app_store
@@ -236,21 +235,22 @@ test_stdio:
lib_android:
go run ./cmd/internal/build_libbox -target android
lib_android_debug:
go run ./cmd/internal/build_libbox -target android -debug
lib_apple:
go run ./cmd/internal/build_libbox -target apple
lib_windows:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp
lib_ios:
go run ./cmd/internal/build_libbox -target apple -platform ios -debug
lib_android_new:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android
lib_apple_new:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
lib:
go run ./cmd/internal/build_libbox -target android
go run ./cmd/internal/build_libbox -target ios
lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.11
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.11
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.8
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.8
docs:
venv/bin/mkdocs serve
@@ -270,6 +270,3 @@ update:
git fetch
git reset FETCH_HEAD --hard
git clean -fdx
%:
@:

View File

@@ -27,6 +27,8 @@ type DNSClient interface {
Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
ClearCache()
}
@@ -68,7 +70,6 @@ type DNSTransport interface {
Type() string
Tag() string
Dependencies() []string
Reset()
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
@@ -12,7 +11,6 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
var _ adapter.EndpointManager = (*Manager)(nil)
@@ -48,14 +46,10 @@ func (m *Manager) Start(stage adapter.StartStage) error {
return nil
}
for _, endpoint := range m.endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(endpoint, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
@@ -72,15 +66,11 @@ func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, endpoint := range endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
monitor.Start("close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
err = E.Append(err, endpoint.Close(), func(err error) error {
return E.Cause(err, "close ", name)
return E.Cause(err, "close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
})
monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
@@ -129,15 +119,11 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(endpoint, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {

View File

@@ -4,10 +4,8 @@ import (
"bytes"
"context"
"encoding/binary"
"io"
"time"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/varbin"
)
@@ -16,7 +14,6 @@ type ClashServer interface {
ConnectionTracker
Mode() string
ModeList() []string
SetModeUpdateHook(hook *observable.Subscriber[struct{}])
HistoryStorage() URLTestHistoryStorage
}
@@ -26,7 +23,7 @@ type URLTestHistory struct {
}
type URLTestHistoryStorage interface {
SetHook(hook *observable.Subscriber[struct{}])
SetHook(hook chan<- struct{})
LoadURLTestHistory(tag string) *URLTestHistory
DeleteURLTestHistory(tag string)
StoreURLTestHistory(tag string, history *URLTestHistory)
@@ -69,11 +66,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
if err != nil {
return nil, err
}
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.Content)))
if err != nil {
return nil, err
}
_, err = buffer.Write(s.Content)
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
if err != nil {
return nil, err
}
@@ -81,11 +74,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
if err != nil {
return nil, err
}
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.LastEtag)))
if err != nil {
return nil, err
}
_, err = buffer.WriteString(s.LastEtag)
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
if err != nil {
return nil, err
}
@@ -99,12 +88,7 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
if err != nil {
return err
}
contentLength, err := binary.ReadUvarint(reader)
if err != nil {
return err
}
s.Content = make([]byte, contentLength)
_, err = io.ReadFull(reader, s.Content)
err = varbin.Read(reader, binary.BigEndian, &s.Content)
if err != nil {
return err
}
@@ -114,16 +98,10 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
return err
}
s.LastUpdated = time.Unix(lastUpdated, 0)
etagLength, err := binary.ReadUvarint(reader)
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
if err != nil {
return err
}
etagBytes := make([]byte, etagLength)
_, err = io.ReadFull(reader, etagBytes)
if err != nil {
return err
}
s.LastEtag = string(etagBytes)
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"net/netip"
"time"
"github.com/sagernet/sing-box/common/process"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
@@ -84,7 +85,7 @@ type InboundContext struct {
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
ProcessInfo *process.Info
QueryType uint16
FakeIP bool

View File

@@ -4,7 +4,6 @@ import (
"context"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
@@ -12,7 +11,6 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
var _ adapter.InboundManager = (*Manager)(nil)
@@ -47,14 +45,10 @@ func (m *Manager) Start(stage adapter.StartStage) error {
inbounds := m.inbounds
m.access.Unlock()
for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(inbound, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
@@ -71,15 +65,11 @@ func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
monitor.Start("close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
err = E.Append(err, inbound.Close(), func(err error) error {
return E.Cause(err, "close ", name)
return E.Cause(err, "close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
})
monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
@@ -131,15 +121,11 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(inbound, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
if existsInbound, loaded := m.inboundByTag[tag]; loaded {

View File

@@ -1,14 +1,6 @@
package adapter
import (
"reflect"
"strings"
"time"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
import E "github.com/sagernet/sing/common/exceptions"
type SimpleLifecycle interface {
Start() error
@@ -56,47 +48,22 @@ type LifecycleService interface {
Lifecycle
}
func getServiceName(service any) string {
if named, ok := service.(interface {
Type() string
Tag() string
}); ok {
tag := named.Tag()
if tag != "" {
return named.Type() + "[" + tag + "]"
}
return named.Type()
}
t := reflect.TypeOf(service)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
return strings.ToLower(t.Name())
}
func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
func Start(stage StartStage, services ...Lifecycle) error {
for _, service := range services {
name := getServiceName(service)
logger.Trace(stage, " ", name)
startTime := time.Now()
err := service.Start(stage)
if err != nil {
return err
}
logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
func StartNamed(stage StartStage, services []LifecycleService) error {
for _, service := range services {
logger.Trace(stage, " ", service.Name())
startTime := time.Now()
err := service.Start(stage)
if err != nil {
return E.Cause(err, stage.String(), " ", service.Name())
}
logger.Trace(stage, " ", service.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}

View File

@@ -6,7 +6,6 @@ import (
"os"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
@@ -14,7 +13,6 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger"
)
@@ -83,14 +81,10 @@ func (m *Manager) Start(stage adapter.StartStage) error {
outbounds := m.outbounds
m.access.Unlock()
for _, outbound := range outbounds {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(outbound, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
return nil
@@ -115,29 +109,22 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
}
started[outboundTag] = true
canContinue = true
name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]"
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
m.logger.Trace("start ", name)
startTime := time.Now()
monitor.Start("start ", name)
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start(adapter.StartStateStart)
monitor.Finish()
if err != nil {
return E.Cause(err, "start ", name)
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
}
m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} else if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
m.logger.Trace("start ", name)
startTime := time.Now()
monitor.Start("start ", name)
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start()
monitor.Finish()
if err != nil {
return E.Cause(err, "start ", name)
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
}
m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
if len(started) == len(outbounds) {
@@ -184,15 +171,11 @@ func (m *Manager) Close() error {
var err error
for _, outbound := range outbounds {
if closer, isCloser := outbound.(io.Closer); isCloser {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
monitor.Start("close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close ", name)
return E.Cause(err, "close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
})
monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
return nil
@@ -273,15 +256,11 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
return err
}
if m.started {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(outbound, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
m.access.Lock()

View File

@@ -1,70 +0,0 @@
package adapter
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger"
)
type PlatformInterface interface {
Initialize(networkManager NetworkManager) error
UsePlatformAutoDetectInterfaceControl() bool
AutoDetectInterfaceControl(fd int) error
UsePlatformInterface() bool
OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
UsePlatformDefaultInterfaceMonitor() bool
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
UsePlatformNetworkInterfaces() bool
NetworkInterfaces() ([]NetworkInterface, error)
UnderNetworkExtension() bool
NetworkExtensionIncludeAllNetworks() bool
ClearDNSCache()
RequestPermissionForWIFIState() error
ReadWIFIState() WIFIState
SystemCertificates() []string
UsePlatformConnectionOwnerFinder() bool
FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
UsePlatformWIFIMonitor() bool
UsePlatformNotification() bool
SendNotification(notification *Notification) error
}
type FindConnectionOwnerRequest struct {
IpProtocol int32
SourceAddress string
SourcePort int32
DestinationAddress string
DestinationPort int32
}
type ConnectionOwner struct {
ProcessID uint32
UserId int32
UserName string
ProcessPath string
AndroidPackageName string
}
type Notification struct {
Identifier string
TypeName string
TypeID int32
Title string
Subtitle string
Body string
OpenURL string
}
type SystemProxyStatus struct {
Available bool
Enabled bool
}

View File

@@ -21,11 +21,10 @@ import (
type Router interface {
Lifecycle
ConnectionRouter
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error)
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
ConnectionRouterEx
RuleSet(tag string) (RuleSet, bool)
Rules() []Rule
NeedFindProcess() bool
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
@@ -12,7 +11,6 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
var _ adapter.ServiceManager = (*Manager)(nil)
@@ -45,14 +43,10 @@ func (m *Manager) Start(stage adapter.StartStage) error {
services := m.services
m.access.Unlock()
for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
@@ -69,15 +63,11 @@ func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close ", name)
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
})
monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
@@ -126,15 +116,11 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
if existsService, loaded := m.serviceByTag[tag]; loaded {

56
box.go
View File

@@ -22,6 +22,7 @@ import (
"github.com/sagernet/sing-box/dns/transport/local"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct"
@@ -138,7 +139,7 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true
}
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer
if platformInterface != nil {
defaultLogWriter = io.Discard
@@ -443,15 +444,15 @@ func (s *Box) preStart() error {
if err != nil {
return E.Cause(err, "start logger")
}
err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil {
return err
}
@@ -463,27 +464,27 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService)
err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService)
err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService)
err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
if err != nil {
return err
}
@@ -497,42 +498,17 @@ func (s *Box) Close() error {
default:
close(s.done)
}
var err error
for _, closeItem := range []struct {
name string
service adapter.Lifecycle
}{
{"service", s.service},
{"endpoint", s.endpoint},
{"inbound", s.inbound},
{"outbound", s.outbound},
{"router", s.router},
{"connection", s.connection},
{"dns-router", s.dnsRouter},
{"dns-transport", s.dnsTransport},
{"network", s.network},
} {
s.logger.Trace("close ", closeItem.name)
startTime := time.Now()
err = E.Append(err, closeItem.service.Close(), func(err error) error {
return E.Cause(err, "close ", closeItem.name)
})
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
err := common.Close(
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
)
for _, lifecycleService := range s.internalService {
s.logger.Trace("close ", lifecycleService.Name())
startTime := time.Now()
err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name())
})
s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
s.logger.Trace("close logger")
startTime := time.Now()
err = E.Append(err, s.logFactory.Close(), func(err error) error {
return E.Cause(err, "close logger")
})
s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
return err
}
@@ -551,7 +527,3 @@ func (s *Box) Inbound() adapter.InboundManager {
func (s *Box) Outbound() adapter.OutboundManager {
return s.outbound
}
func (s *Box) LogFactory() log.Factory {
return s.logFactory
}

View File

@@ -100,32 +100,11 @@ findVersion:
}
func publishTestflight(ctx context.Context) error {
if len(os.Args) < 3 {
return E.New("platform required: ios, macos, or tvos")
}
var platform asc.Platform
switch os.Args[2] {
case "ios":
platform = asc.PlatformIOS
case "macos":
platform = asc.PlatformMACOS
case "tvos":
platform = asc.PlatformTVOS
default:
return E.New("unknown platform: ", os.Args[2])
}
tagVersion, err := build_shared.ReadTagVersion()
if err != nil {
return err
}
tag := tagVersion.VersionString()
releaseNotes := F.ToString("sing-box ", tagVersion.String())
if len(os.Args) >= 4 {
releaseNotes = strings.Join(os.Args[3:], " ")
}
client := createClient(20 * time.Minute)
log.Info(tag, " list build IDs")
@@ -136,76 +115,97 @@ func publishTestflight(ctx context.Context) error {
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
return it.ID
})
var platforms []asc.Platform
if len(os.Args) == 3 {
switch os.Args[2] {
case "ios":
platforms = []asc.Platform{asc.PlatformIOS}
case "macos":
platforms = []asc.Platform{asc.PlatformMACOS}
case "tvos":
platforms = []asc.Platform{asc.PlatformTVOS}
default:
return E.New("unknown platform: ", os.Args[2])
}
} else {
platforms = []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
}
}
waitingForProcess := false
log.Info(string(platform), " list builds")
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if err != nil {
return err
}
build := builds.Data[0]
log.Info(string(platform), " ", tag, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
if err != nil {
return err
}
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Fatal(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(releaseNotes))
for _, platform := range platforms {
log.Info(string(platform), " list builds")
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " publish")
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
log.Info("waiting for process")
time.Sleep(15 * time.Second)
continue
} else if err != nil {
return err
}
log.Info(string(platform), " ", tag, " list submissions")
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
FilterBuild: []string{build.ID},
})
if err != nil {
return err
}
if len(betaSubmissions.Data) == 0 {
log.Info(string(platform), " ", tag, " create submission")
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
build := builds.Data[0]
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
if err != nil {
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
log.Error(err)
break
return err
}
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Fatal(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
F.ToString("sing-box ", tagVersion.String()),
))
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " publish")
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
log.Info("waiting for process")
time.Sleep(15 * time.Second)
continue
} else if err != nil {
return err
}
log.Info(string(platform), " ", tag, " list submissions")
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
FilterBuild: []string{build.ID},
})
if err != nil {
return err
}
if len(betaSubmissions.Data) == 0 {
log.Info(string(platform), " ", tag, " create submission")
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
if err != nil {
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
log.Error(err)
break
}
return err
}
}
break
}
break
}
return nil
}

View File

@@ -5,7 +5,6 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
_ "github.com/sagernet/gomobile"
@@ -17,17 +16,17 @@ import (
)
var (
debugEnabled bool
target string
platform string
// withTailscale bool
debugEnabled bool
target string
platform string
withTailscale bool
)
func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform")
// flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
}
func main() {
@@ -47,8 +46,8 @@ var (
sharedFlags []string
debugFlags []string
sharedTags []string
darwinTags []string
// memcTags []string
macOSTags []string
memcTags []string
notMemcTags []string
debugTags []string
)
@@ -60,38 +59,19 @@ func init() {
if err != nil {
currentTag = "unknown"
}
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -checklinkname=0")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
// memcTags = append(memcTags, "with_tailscale")
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
macOSTags = append(macOSTags, "with_dhcp")
memcTags = append(memcTags, "with_tailscale")
notMemcTags = append(notMemcTags, "with_low_memory")
debugTags = append(debugTags, "debug")
}
type AndroidBuildConfig struct {
AndroidAPI int
OutputName string
Tags []string
}
func buildAndroid() {
build_shared.FindSDK()
func filterTags(tags []string, exclude ...string) []string {
excludeMap := make(map[string]bool)
for _, tag := range exclude {
excludeMap[tag] = true
}
var result []string
for _, tag := range tags {
if !excludeMap[tag] {
result = append(result, tag)
}
}
return result
}
func checkJavaVersion() {
var javaPath string
javaHome := os.Getenv("JAVA_HOME")
if javaHome == "" {
@@ -107,24 +87,21 @@ func checkJavaVersion() {
if !strings.Contains(javaVersion, "openjdk 17") {
log.Fatal("java version should be openjdk 17")
}
}
func getAndroidBindTarget() string {
var bindTarget string
if platform != "" {
return platform
bindTarget = platform
} else if debugEnabled {
return "android/arm64"
bindTarget = "android/arm64"
} else {
bindTarget = "android"
}
return "android"
}
func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
args := []string{
"bind",
"-v",
"-o", config.OutputName,
"-target", bindTarget,
"-androidapi", strconv.Itoa(config.AndroidAPI),
"-androidapi", "21",
"-javapkg=io.nekohasekai",
"-libname=box",
}
@@ -135,59 +112,34 @@ func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
args = append(args, debugFlags...)
}
args = append(args, "-tags", strings.Join(config.Tags, ","))
tags := append(sharedTags, memcTags...)
if debugEnabled {
tags = append(tags, debugTags...)
}
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
err := command.Run()
err = command.Run()
if err != nil {
log.Fatal(err)
}
const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.IsDir(copyPath) {
copyPath, _ = filepath.Abs(copyPath)
err = rw.CopyFile(config.OutputName, filepath.Join(copyPath, config.OutputName))
err = rw.CopyFile(name, filepath.Join(copyPath, name))
if err != nil {
log.Fatal(err)
}
log.Info("copied ", config.OutputName, " to ", copyPath)
log.Info("copied to ", copyPath)
}
}
func buildAndroid() {
build_shared.FindSDK()
checkJavaVersion()
bindTarget := getAndroidBindTarget()
// Build main variant (SDK 23)
mainTags := append([]string{}, sharedTags...)
// mainTags = append(mainTags, memcTags...)
if debugEnabled {
mainTags = append(mainTags, debugTags...)
}
buildAndroidVariant(AndroidBuildConfig{
AndroidAPI: 23,
OutputName: "libbox.aar",
Tags: mainTags,
}, bindTarget)
// Build legacy variant (SDK 21, no naive outbound)
legacyTags := filterTags(sharedTags, "with_naive_outbound")
// legacyTags = append(legacyTags, memcTags...)
if debugEnabled {
legacyTags = append(legacyTags, debugTags...)
}
buildAndroidVariant(AndroidBuildConfig{
AndroidAPI: 21,
OutputName: "libbox-legacy.aar",
Tags: legacyTags,
}, bindTarget)
}
func buildApple() {
var bindTarget string
if platform != "" {
@@ -195,7 +147,7 @@ func buildApple() {
} else if debugEnabled {
bindTarget = "ios"
} else {
bindTarget = "ios,iossimulator,tvos,tvossimulator,macos"
bindTarget = "ios,tvos,macos"
}
args := []string{
@@ -205,9 +157,11 @@ func buildApple() {
"-libname=box",
"-tags-not-macos=with_low_memory",
}
//if !withTailscale {
// args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
//}
if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ","))
} else {
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
}
if !debugEnabled {
args = append(args, sharedFlags...)
@@ -215,10 +169,10 @@ func buildApple() {
args = append(args, debugFlags...)
}
tags := append(sharedTags, darwinTags...)
//if withTailscale {
// tags = append(tags, memcTags...)
//}
tags := sharedTags
if withTailscale {
tags = append(tags, memcTags...)
}
if debugEnabled {
tags = append(tags, debugTags...)
}

View File

@@ -1,117 +0,0 @@
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing-box/log"
)
func main() {
err := filepath.Walk("docs", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".md") {
return nil
}
return processFile(path)
})
if err != nil {
log.Fatal(err)
}
}
func processFile(path string) error {
content, err := os.ReadFile(path)
if err != nil {
return err
}
lines := strings.Split(string(content), "\n")
modified := false
result := make([]string, 0, len(lines))
inQuoteBlock := false
materialLines := []int{} // indices of :material- lines in the block
for _, line := range lines {
// Check for quote block start
if strings.HasPrefix(line, "!!! quote \"") && strings.Contains(line, "sing-box") {
inQuoteBlock = true
materialLines = nil
result = append(result, line)
continue
}
// Inside a quote block
if inQuoteBlock {
trimmed := strings.TrimPrefix(line, " ")
isMaterialLine := strings.HasPrefix(trimmed, ":material-")
isEmpty := strings.TrimSpace(line) == ""
isIndented := strings.HasPrefix(line, " ")
if isMaterialLine {
materialLines = append(materialLines, len(result))
result = append(result, line)
continue
}
// Block ends when:
// - Empty line AFTER we've seen material lines, OR
// - Non-indented, non-empty line
blockEnds := (isEmpty && len(materialLines) > 0) || (!isEmpty && !isIndented)
if blockEnds {
// Process collected material lines
if len(materialLines) > 0 {
for j, idx := range materialLines {
isLast := j == len(materialLines)-1
resultLine := strings.TrimRight(result[idx], " ")
if !isLast {
// Add trailing two spaces for non-last lines
resultLine += " "
}
if result[idx] != resultLine {
modified = true
result[idx] = resultLine
}
}
}
inQuoteBlock = false
materialLines = nil
}
}
result = append(result, line)
}
// Handle case where file ends while still in a block
if inQuoteBlock && len(materialLines) > 0 {
for j, idx := range materialLines {
isLast := j == len(materialLines)-1
resultLine := strings.TrimRight(result[idx], " ")
if !isLast {
resultLine += " "
}
if result[idx] != resultLine {
modified = true
result[idx] = resultLine
}
}
}
if modified {
newContent := strings.Join(result, "\n")
if !bytes.Equal(content, []byte(newContent)) {
log.Info("formatted: ", path)
return os.WriteFile(path, []byte(newContent), 0o644)
}
}
return nil
}

View File

@@ -71,12 +71,12 @@ func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDLi
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
version := strings.Trim(projectContent[versionStart:versionEnd], "\"")
version := projectContent[versionStart:versionEnd]
if version == newVersion {
continue
}
updated = true
projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:]
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
}
return projectContent, updated
}

View File

@@ -17,10 +17,6 @@ func main() {
if err != nil {
log.Error(err)
}
err = updateChromeIncludedRootCAs()
if err != nil {
log.Error(err)
}
}
func updateMozillaIncludedRootCAs() error {
@@ -73,94 +69,3 @@ func init() {
generated.WriteString("}\n")
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
}
func fetchChinaFingerprints() (map[string]bool, error) {
response, err := http.Get("https://ccadb.my.salesforce-sites.com/ccadb/AllCertificateRecordsCSVFormatv4")
if err != nil {
return nil, err
}
defer response.Body.Close()
reader := csv.NewReader(response.Body)
header, err := reader.Read()
if err != nil {
return nil, err
}
countryIndex := slices.Index(header, "Country")
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
chinaFingerprints := make(map[string]bool)
for {
record, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
if record[countryIndex] == "China" {
chinaFingerprints[record[fingerprintIndex]] = true
}
}
return chinaFingerprints, nil
}
func updateChromeIncludedRootCAs() error {
chinaFingerprints, err := fetchChinaFingerprints()
if err != nil {
return err
}
response, err := http.Get("https://ccadb.my.salesforce-sites.com/ccadb/RootCACertificatesIncludedByRSReportCSV")
if err != nil {
return err
}
defer response.Body.Close()
reader := csv.NewReader(response.Body)
header, err := reader.Read()
if err != nil {
return err
}
subjectIndex := slices.Index(header, "Subject")
statusIndex := slices.Index(header, "Google Chrome Status")
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
generated := strings.Builder{}
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
package certificate
import "crypto/x509"
var chromeIncluded *x509.CertPool
func init() {
chromeIncluded = x509.NewCertPool()
`)
for {
record, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
return err
}
if record[statusIndex] != "Included" {
continue
}
if chinaFingerprints[record[fingerprintIndex]] {
continue
}
generated.WriteString("\n // ")
generated.WriteString(record[subjectIndex])
generated.WriteString("\n")
generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`")
cert := record[certIndex]
// Remove single quotes if present
if len(cert) > 0 && cert[0] == '\'' {
cert = cert[1 : len(cert)-1]
}
generated.WriteString(cert)
generated.WriteString("`))\n")
}
generated.WriteString("}\n")
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import (
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
@@ -35,7 +36,7 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
switch options.Store {
case C.CertificateStoreSystem, "":
systemPool = x509.NewCertPool()
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
platformInterface := service.FromContext[platform.Interface](ctx)
var systemValid bool
if platformInterface != nil {
for _, cert := range platformInterface.SystemCertificates() {
@@ -53,8 +54,6 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
}
case C.CertificateStoreMozilla:
systemPool = mozillaIncluded
case C.CertificateStoreChrome:
systemPool = chromeIncluded
case C.CertificateStoreNone:
systemPool = nil
default:

View File

@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/listener"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
@@ -48,7 +49,7 @@ type DefaultDialer struct {
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
networkManager := service.FromContext[adapter.NetworkManager](ctx)
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
platformInterface := service.FromContext[platform.Interface](ctx)
var (
dialer net.Dialer
@@ -137,12 +138,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath))
}
if options.BindAddressNoPort {
if !C.IsLinux {
return nil, E.New("`bind_address_no_port` is only supported on Linux")
}
dialer.Control = control.Append(dialer.Control, control.BindAddressNoPort())
}
if options.ConnectTimeout != 0 {
dialer.Timeout = time.Duration(options.ConnectTimeout)
} else {

View File

@@ -1,234 +0,0 @@
package geosite
import (
"bufio"
"bytes"
"encoding/binary"
"strings"
"testing"
"github.com/sagernet/sing/common/varbin"
"github.com/stretchr/testify/require"
)
// Old implementation using varbin reflection-based serialization
func oldWriteString(writer varbin.Writer, value string) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, value)
}
func oldWriteItem(writer varbin.Writer, item Item) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, item)
}
func oldReadString(reader varbin.Reader) (string, error) {
//nolint:staticcheck
return varbin.ReadValue[string](reader, binary.BigEndian)
}
func oldReadItem(reader varbin.Reader) (Item, error) {
//nolint:staticcheck
return varbin.ReadValue[Item](reader, binary.BigEndian)
}
func TestStringCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
}{
{"empty", ""},
{"single_char", "a"},
{"ascii", "example.com"},
{"utf8", "测试域名.中国"},
{"special_chars", "\x00\xff\n\t"},
{"127_bytes", strings.Repeat("x", 127)},
{"128_bytes", strings.Repeat("x", 128)},
{"16383_bytes", strings.Repeat("x", 16383)},
{"16384_bytes", strings.Repeat("x", 16384)},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWriteString(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = writeString(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> old read
readBack, err := oldReadString(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack2)
})
}
}
func TestItemCompat(t *testing.T) {
t.Parallel()
// Note: varbin.Write has a bug where struct values (not pointers) don't write their fields
// because field.CanSet() returns false for non-addressable values.
// The old geosite code passed Item values to varbin.Write, which silently wrote nothing.
// The new code correctly writes Type + Value using manual serialization.
// This test verifies the new serialization format and round-trip correctness.
cases := []struct {
name string
input Item
}{
{"domain_empty", Item{Type: RuleTypeDomain, Value: ""}},
{"domain_normal", Item{Type: RuleTypeDomain, Value: "example.com"}},
{"domain_suffix", Item{Type: RuleTypeDomainSuffix, Value: ".example.com"}},
{"domain_keyword", Item{Type: RuleTypeDomainKeyword, Value: "google"}},
{"domain_regex", Item{Type: RuleTypeDomainRegex, Value: `^.*\.example\.com$`}},
{"utf8_domain", Item{Type: RuleTypeDomain, Value: "测试.com"}},
{"long_domain", Item{Type: RuleTypeDomainSuffix, Value: strings.Repeat("a", 200) + ".com"}},
{"128_bytes_value", Item{Type: RuleTypeDomain, Value: strings.Repeat("x", 128)}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// New write
var newBuf bytes.Buffer
err := newBuf.WriteByte(byte(tc.input.Type))
require.NoError(t, err)
err = writeString(&newBuf, tc.input.Value)
require.NoError(t, err)
// Verify format: Type (1 byte) + Value (uvarint len + bytes)
require.True(t, len(newBuf.Bytes()) >= 1, "output too short")
require.Equal(t, byte(tc.input.Type), newBuf.Bytes()[0], "type byte mismatch")
// New write -> old read (varbin can read correctly when given addressable target)
readBack, err := oldReadItem(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack)
// New write -> new read
reader := bufio.NewReader(bytes.NewReader(newBuf.Bytes()))
typeByte, err := reader.ReadByte()
require.NoError(t, err)
value, err := readString(reader)
require.NoError(t, err)
require.Equal(t, tc.input, Item{Type: ItemType(typeByte), Value: value})
})
}
}
func TestGeositeWriteReadCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input map[string][]Item
}{
{
"empty_map",
map[string][]Item{},
},
{
"single_code_empty_items",
map[string][]Item{"test": {}},
},
{
"single_code_single_item",
map[string][]Item{"test": {{Type: RuleTypeDomain, Value: "a.com"}}},
},
{
"single_code_multi_items",
map[string][]Item{
"test": {
{Type: RuleTypeDomain, Value: "a.com"},
{Type: RuleTypeDomainSuffix, Value: ".b.com"},
{Type: RuleTypeDomainKeyword, Value: "keyword"},
{Type: RuleTypeDomainRegex, Value: `^.*$`},
},
},
},
{
"multi_code",
map[string][]Item{
"cn": {{Type: RuleTypeDomain, Value: "baidu.com"}, {Type: RuleTypeDomainSuffix, Value: ".cn"}},
"us": {{Type: RuleTypeDomain, Value: "google.com"}},
"jp": {{Type: RuleTypeDomainSuffix, Value: ".jp"}},
},
},
{
"utf8_values",
map[string][]Item{
"test": {
{Type: RuleTypeDomain, Value: "测试.中国"},
{Type: RuleTypeDomainSuffix, Value: ".テスト"},
},
},
},
{
"large_items",
generateLargeItems(1000),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Write using new implementation
var buf bytes.Buffer
err := Write(&buf, tc.input)
require.NoError(t, err)
// Read back and verify
reader, codes, err := NewReader(bytes.NewReader(buf.Bytes()))
require.NoError(t, err)
// Verify all codes exist
codeSet := make(map[string]bool)
for _, code := range codes {
codeSet[code] = true
}
for code := range tc.input {
require.True(t, codeSet[code], "missing code: %s", code)
}
// Verify items match
for code, expectedItems := range tc.input {
items, err := reader.Read(code)
require.NoError(t, err)
require.Equal(t, expectedItems, items, "items mismatch for code: %s", code)
}
})
}
}
func generateLargeItems(count int) map[string][]Item {
items := make([]Item, count)
for i := 0; i < count; i++ {
items[i] = Item{
Type: ItemType(i % 4),
Value: strings.Repeat("x", i%200) + ".com",
}
}
return map[string][]Item{"large": items}
}

View File

@@ -9,6 +9,7 @@ import (
"sync/atomic"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
)
type Reader struct {
@@ -77,7 +78,7 @@ func (r *Reader) readMetadata() error {
codeIndex uint64
codeLength uint64
)
code, err = readString(reader)
code, err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return err
}
@@ -111,16 +112,9 @@ func (r *Reader) Read(code string) ([]Item, error) {
}
r.bufferedReader.Reset(r.reader)
itemList := make([]Item, r.domainLength[code])
for i := range itemList {
typeByte, err := r.bufferedReader.ReadByte()
if err != nil {
return nil, err
}
itemList[i].Type = ItemType(typeByte)
itemList[i].Value, err = readString(r.bufferedReader)
if err != nil {
return nil, err
}
err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList)
if err != nil {
return nil, err
}
return itemList, nil
}
@@ -141,18 +135,3 @@ func (r *readCounter) Read(p []byte) (n int, err error) {
}
return
}
func readString(reader io.ByteReader) (string, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return "", err
}
bytes := make([]byte, length)
for i := range bytes {
bytes[i], err = reader.ReadByte()
if err != nil {
return "", err
}
}
return string(bytes), nil
}

View File

@@ -2,6 +2,7 @@ package geosite
import (
"bytes"
"encoding/binary"
"sort"
"github.com/sagernet/sing/common/varbin"
@@ -19,11 +20,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
for _, code := range keys {
index[code] = content.Len()
for _, item := range domains[code] {
err := content.WriteByte(byte(item.Type))
if err != nil {
return err
}
err = writeString(content, item.Value)
err := varbin.Write(content, binary.BigEndian, item)
if err != nil {
return err
}
@@ -41,7 +38,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
}
for _, code := range keys {
err = writeString(writer, code)
err = varbin.Write(writer, binary.BigEndian, code)
if err != nil {
return err
}
@@ -62,12 +59,3 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
return nil
}
func writeString(writer varbin.Writer, value string) error {
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
_, err = writer.Write([]byte(value))
return err
}

View File

@@ -5,7 +5,6 @@ import (
"net/netip"
"os/user"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
@@ -13,7 +12,7 @@ import (
)
type Searcher interface {
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error)
}
var ErrNotFound = E.New("process not found")
@@ -23,7 +22,15 @@ type Config struct {
PackageManager tun.PackageManager
}
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
type Info struct {
ProcessID uint32
ProcessPath string
PackageName string
User string
UserId int32
}
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
info, err := searcher.FindProcessInfo(ctx, network, source, destination)
if err != nil {
return nil, err
@@ -31,7 +38,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
if info.UserId != -1 {
osUser, _ := user.LookupId(F.ToString(info.UserId))
if osUser != nil {
info.UserName = osUser.Username
info.User = osUser.Username
}
}
return info, nil

View File

@@ -4,7 +4,6 @@ import (
"context"
"net/netip"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
)
@@ -18,22 +17,22 @@ func NewSearcher(config Config) (Searcher, error) {
return &androidSearcher{config.PackageManager}, nil
}
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
_, uid, err := resolveSocketByNetlink(network, source, destination)
if err != nil {
return nil, err
}
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
return &adapter.ConnectionOwner{
UserId: int32(uid),
AndroidPackageName: sharedPackage,
return &Info{
UserId: int32(uid),
PackageName: sharedPackage,
}, nil
}
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
return &adapter.ConnectionOwner{
UserId: int32(uid),
AndroidPackageName: packageName,
return &Info{
UserId: int32(uid),
PackageName: packageName,
}, nil
}
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
return &Info{UserId: int32(uid)}, nil
}

View File

@@ -10,7 +10,6 @@ import (
"syscall"
"unsafe"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix"
@@ -24,12 +23,12 @@ func NewSearcher(_ Config) (Searcher, error) {
return &darwinSearcher{}, nil
}
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
if err != nil {
return nil, err
}
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
return &Info{ProcessPath: processName, UserId: -1}, nil
}
var structSize = func() int {

View File

@@ -6,7 +6,6 @@ import (
"context"
"net/netip"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
)
@@ -20,7 +19,7 @@ func NewSearcher(config Config) (Searcher, error) {
return &linuxSearcher{config.Logger}, nil
}
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
inode, uid, err := resolveSocketByNetlink(network, source, destination)
if err != nil {
return nil, err
@@ -29,7 +28,7 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
if err != nil {
s.logger.DebugContext(ctx, "find process path: ", err)
}
return &adapter.ConnectionOwner{
return &Info{
UserId: int32(uid),
ProcessPath: processPath,
}, nil

View File

@@ -5,7 +5,6 @@ import (
"net/netip"
"syscall"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/winiphlpapi"
@@ -28,16 +27,16 @@ func initWin32API() error {
return winiphlpapi.LoadExtendedTable()
}
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
pid, err := winiphlpapi.FindPid(network, source)
if err != nil {
return nil, err
}
path, err := getProcessPath(pid)
if err != nil {
return &adapter.ConnectionOwner{ProcessID: pid, UserId: -1}, err
return &Info{ProcessID: pid, UserId: -1}, err
}
return &adapter.ConnectionOwner{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
}
func getProcessPath(pid uint32) (string, error) {

View File

@@ -1,5 +1,3 @@
//go:build linux
package settings
import (

View File

@@ -1,5 +1,3 @@
//go:build linux
package settings
import (

View File

@@ -1,5 +1,3 @@
//go:build linux
package settings
import (

View File

@@ -303,6 +303,8 @@ find:
metadata.Protocol = C.ProtocolQUIC
fingerprint, err := ja3.Compute(buffer.Bytes())
if err != nil {
metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium
metadata.SniffContext = fragments
return E.Cause1(ErrNeedMoreData, err)
}
@@ -332,7 +334,7 @@ find:
}
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
if isQUICGo(fingerprint) {
if maybeUQUIC(fingerprint) {
metadata.Client = C.ClientQUICGo
} else {
metadata.Client = C.ClientChromium

View File

@@ -1,29 +1,21 @@
package sniff
import (
"crypto/tls"
"github.com/sagernet/sing-box/common/ja3"
)
const (
// X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls
x25519Kyber768Draft00 uint16 = 0x11EC // 4588
// renegotiation_info extension used by Go crypto/tls
extensionRenegotiationInfo uint16 = 0xFF01 // 65281
)
// isQUICGo detects native quic-go by checking for Go crypto/tls specific features.
// Note: uQUIC with Chromium mimicry cannot be reliably distinguished from real Chromium
// since it uses the same TLS fingerprint, so it will be identified as Chromium.
func isQUICGo(fingerprint *ja3.ClientHello) bool {
for _, curve := range fingerprint.EllipticCurves {
if curve == x25519Kyber768Draft00 {
return true
}
}
for _, ext := range fingerprint.Extensions {
if ext == extensionRenegotiationInfo {
return true
}
}
return false
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
// The cronet without this behavior does not have version 115
var uQUICChrome115 = &ja3.ClientHello{
Version: tls.VersionTLS12,
CipherSuites: []uint16{4865, 4866, 4867},
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
EllipticCurves: []uint16{29, 23, 24},
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
}
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
return !uQUICChrome115.Equals(fingerprint, true)
}

View File

@@ -1,188 +0,0 @@
package sniff_test
import (
"context"
"crypto/tls"
"encoding/hex"
"errors"
"net"
"testing"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
"github.com/stretchr/testify/require"
)
func TestSniffQUICQuicGoFingerprint(t *testing.T) {
t.Parallel()
const testSNI = "test.example.com"
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer udpConn.Close()
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
packetsChan := make(chan [][]byte, 1)
go func() {
var packets [][]byte
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
for i := 0; i < 10; i++ {
buf := make([]byte, 2048)
n, _, err := udpConn.ReadFromUDP(buf)
if err != nil {
break
}
packets = append(packets, buf[:n])
}
packetsChan <- packets
}()
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer clientConn.Close()
tlsConfig := &tls.Config{
ServerName: testSNI,
InsecureSkipVerify: true,
NextProtos: []string{"h3"},
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
select {
case packets := <-packetsChan:
t.Logf("Captured %d packets", len(packets))
var metadata adapter.InboundContext
for i, pkt := range packets {
err := sniff.QUICClientHello(context.Background(), &metadata, pkt)
t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client)
if metadata.Domain != "" {
break
}
}
t.Logf("\n=== quic-go TLS Fingerprint Analysis ===")
t.Logf("Domain: %s", metadata.Domain)
t.Logf("Client: %s", metadata.Client)
t.Logf("Protocol: %s", metadata.Protocol)
// The client should be identified as quic-go, not chromium
// Current issue: it's being identified as chromium
if metadata.Client == "chromium" {
t.Log("WARNING: quic-go is being misidentified as chromium!")
}
case <-time.After(5 * time.Second):
t.Fatal("Timeout")
}
}
func TestSniffQUICInitialFromQuicGo(t *testing.T) {
t.Parallel()
const testSNI = "test.example.com"
// Create UDP listener to capture ALL initial packets
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer udpConn.Close()
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
// Channel to receive captured packets
packetsChan := make(chan [][]byte, 1)
// Start goroutine to capture packets
go func() {
var packets [][]byte
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
for i := 0; i < 5; i++ { // Capture up to 5 packets
buf := make([]byte, 2048)
n, _, err := udpConn.ReadFromUDP(buf)
if err != nil {
break
}
packets = append(packets, buf[:n])
}
packetsChan <- packets
}()
// Create QUIC client connection (will fail but we capture the initial packet)
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer clientConn.Close()
tlsConfig := &tls.Config{
ServerName: testSNI,
InsecureSkipVerify: true,
NextProtos: []string{"h3"},
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// This will fail (no server) but sends initial packet
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
// Wait for captured packets
select {
case packets := <-packetsChan:
t.Logf("Captured %d QUIC packets", len(packets))
for i, packet := range packets {
t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))])
}
// Test sniffer with first packet
if len(packets) > 0 {
var metadata adapter.InboundContext
err := sniff.QUICClientHello(context.Background(), &metadata, packets[0])
t.Logf("First packet sniff error: %v", err)
t.Logf("Protocol: %s", metadata.Protocol)
t.Logf("Domain: %s", metadata.Domain)
t.Logf("Client: %s", metadata.Client)
// If first packet needs more data, try with subsequent packets
// IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext
if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 {
t.Log("First packet needs more data, trying subsequent packets with shared context...")
for i := 1; i < len(packets); i++ {
// Reuse same metadata to accumulate fragments
err = sniff.QUICClientHello(context.Background(), &metadata, packets[i])
t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil)
if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) {
break
}
}
}
// Print hex dump for debugging
t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))]))
// Log final results
t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client)
// Verify SNI extraction
if metadata.Domain == "" {
t.Errorf("Failed to extract SNI, expected: %s", testSNI)
} else {
require.Equal(t, testSNI, metadata.Domain, "SNI should match")
}
// Check client identification - quic-go should be identified as quic-go, not chromium
t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client)
}
case <-time.After(5 * time.Second):
t.Fatal("Timeout waiting for QUIC packets")
}
}

View File

@@ -19,7 +19,7 @@ func TestSniffQUICChromeNew(t *testing.T) {
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Empty(t, metadata.Client)
require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
require.NoError(t, err)
@@ -39,7 +39,7 @@ func TestSniffQUICChromium(t *testing.T) {
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Empty(t, metadata.Client)
require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err)

View File

@@ -6,7 +6,6 @@ import (
"encoding/binary"
"io"
"net/netip"
"unsafe"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
@@ -506,24 +505,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
}
func readRuleItemString(reader varbin.Reader) ([]string, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
result := make([]string, length)
for i := range result {
strLen, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
buf := make([]byte, strLen)
_, err = io.ReadFull(reader, buf)
if err != nil {
return nil, err
}
result[i] = string(buf)
}
return result, nil
return varbin.ReadValue[[]string](reader, binary.BigEndian)
}
func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error {
@@ -531,34 +513,11 @@ func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) e
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
for _, s := range value {
_, err = varbin.WriteUvarint(writer, uint64(len(s)))
if err != nil {
return err
}
_, err = writer.Write([]byte(s))
if err != nil {
return err
}
}
return nil
return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
result := make([]E, length)
_, err = io.ReadFull(reader, *(*[]byte)(unsafe.Pointer(&result)))
if err != nil {
return nil, err
}
return result, nil
return varbin.ReadValue[[]E](reader, binary.BigEndian)
}
func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error {
@@ -566,25 +525,11 @@ func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
_, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value)))
return err
return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
result := make([]uint16, length)
err = binary.Read(reader, binary.BigEndian, result)
if err != nil {
return nil, err
}
return result, nil
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
}
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error {
@@ -592,11 +537,7 @@ func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) e
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
return binary.Write(writer, binary.BigEndian, value)
return varbin.Write(writer, binary.BigEndian, value)
}
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error {

View File

@@ -1,494 +0,0 @@
package srs
import (
"bufio"
"bytes"
"encoding/binary"
"net/netip"
"strings"
"testing"
"unsafe"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"github.com/stretchr/testify/require"
"go4.org/netipx"
)
// Old implementations using varbin reflection-based serialization
func oldWriteStringSlice(writer varbin.Writer, value []string) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, value)
}
func oldReadStringSlice(reader varbin.Reader) ([]string, error) {
//nolint:staticcheck
return varbin.ReadValue[[]string](reader, binary.BigEndian)
}
func oldWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, value)
}
func oldReadUint8Slice[E ~uint8](reader varbin.Reader) ([]E, error) {
//nolint:staticcheck
return varbin.ReadValue[[]E](reader, binary.BigEndian)
}
func oldWriteUint16Slice(writer varbin.Writer, value []uint16) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, value)
}
func oldReadUint16Slice(reader varbin.Reader) ([]uint16, error) {
//nolint:staticcheck
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
}
func oldWritePrefix(writer varbin.Writer, prefix netip.Prefix) error {
//nolint:staticcheck
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
if err != nil {
return err
}
return binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
}
type oldIPRangeData struct {
From []byte
To []byte
}
// Note: The old writeIPSet had a bug where varbin.Write(writer, binary.BigEndian, data)
// with a struct VALUE (not pointer) silently wrote nothing because field.CanSet() returned false.
// This caused IP range data to be missing from the output.
// The new implementation correctly writes all range data.
//
// The old readIPSet used varbin.Read with a pre-allocated slice, which worked because
// slice elements are addressable and CanSet() returns true for them.
//
// For compatibility testing, we verify:
// 1. New write produces correct output with range data
// 2. New read can parse the new format correctly
// 3. Round-trip works correctly
func oldReadIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
version, err := reader.ReadByte()
if err != nil {
return nil, err
}
if version != 1 {
return nil, err
}
var length uint64
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return nil, err
}
ranges := make([]oldIPRangeData, length)
//nolint:staticcheck
err = varbin.Read(reader, binary.BigEndian, &ranges)
if err != nil {
return nil, err
}
mySet := &myIPSet{
rr: make([]myIPRange, len(ranges)),
}
for i, rangeData := range ranges {
mySet.rr[i].from = M.AddrFromIP(rangeData.From)
mySet.rr[i].to = M.AddrFromIP(rangeData.To)
}
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
}
// New write functions (without itemType prefix for testing)
func newWriteStringSlice(writer varbin.Writer, value []string) error {
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
for _, s := range value {
_, err = varbin.WriteUvarint(writer, uint64(len(s)))
if err != nil {
return err
}
_, err = writer.Write([]byte(s))
if err != nil {
return err
}
}
return nil
}
func newWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error {
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
_, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value)))
return err
}
func newWriteUint16Slice(writer varbin.Writer, value []uint16) error {
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
return binary.Write(writer, binary.BigEndian, value)
}
func newWritePrefix(writer varbin.Writer, prefix netip.Prefix) error {
addrSlice := prefix.Addr().AsSlice()
_, err := varbin.WriteUvarint(writer, uint64(len(addrSlice)))
if err != nil {
return err
}
_, err = writer.Write(addrSlice)
if err != nil {
return err
}
return writer.WriteByte(uint8(prefix.Bits()))
}
// Tests
func TestStringSliceCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input []string
}{
{"nil", nil},
{"empty", []string{}},
{"single_empty", []string{""}},
{"single", []string{"test"}},
{"multi", []string{"a", "b", "c"}},
{"with_empty", []string{"a", "", "c"}},
{"utf8", []string{"测试", "テスト", "тест"}},
{"long_string", []string{strings.Repeat("x", 128)}},
{"many_elements", generateStrings(128)},
{"many_elements_256", generateStrings(256)},
{"127_byte_string", []string{strings.Repeat("x", 127)}},
{"128_byte_string", []string{strings.Repeat("x", 128)}},
{"mixed_lengths", []string{"a", strings.Repeat("b", 100), "", strings.Repeat("c", 200)}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWriteStringSlice(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = newWriteStringSlice(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> old read
readBack, err := oldReadStringSlice(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireStringSliceEqual(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readRuleItemString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
requireStringSliceEqual(t, tc.input, readBack2)
})
}
}
func TestUint8SliceCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input []uint8
}{
{"nil", nil},
{"empty", []uint8{}},
{"single_zero", []uint8{0}},
{"single_max", []uint8{255}},
{"multi", []uint8{0, 1, 127, 128, 255}},
{"boundary", []uint8{0x00, 0x7f, 0x80, 0xff}},
{"sequential", generateUint8Slice(256)},
{"127_elements", generateUint8Slice(127)},
{"128_elements", generateUint8Slice(128)},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWriteUint8Slice(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = newWriteUint8Slice(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> old read
readBack, err := oldReadUint8Slice[uint8](bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireUint8SliceEqual(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readRuleItemUint8[uint8](bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
requireUint8SliceEqual(t, tc.input, readBack2)
})
}
}
func TestUint16SliceCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input []uint16
}{
{"nil", nil},
{"empty", []uint16{}},
{"single_zero", []uint16{0}},
{"single_max", []uint16{65535}},
{"multi", []uint16{0, 255, 256, 32767, 32768, 65535}},
{"ports", []uint16{80, 443, 8080, 8443}},
{"127_elements", generateUint16Slice(127)},
{"128_elements", generateUint16Slice(128)},
{"256_elements", generateUint16Slice(256)},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWriteUint16Slice(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = newWriteUint16Slice(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> old read
readBack, err := oldReadUint16Slice(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireUint16SliceEqual(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readRuleItemUint16(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
requireUint16SliceEqual(t, tc.input, readBack2)
})
}
}
func TestPrefixCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input netip.Prefix
}{
{"ipv4_0", netip.MustParsePrefix("0.0.0.0/0")},
{"ipv4_8", netip.MustParsePrefix("10.0.0.0/8")},
{"ipv4_16", netip.MustParsePrefix("192.168.0.0/16")},
{"ipv4_24", netip.MustParsePrefix("192.168.1.0/24")},
{"ipv4_32", netip.MustParsePrefix("1.2.3.4/32")},
{"ipv6_0", netip.MustParsePrefix("::/0")},
{"ipv6_64", netip.MustParsePrefix("2001:db8::/64")},
{"ipv6_128", netip.MustParsePrefix("::1/128")},
{"ipv6_full", netip.MustParsePrefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")},
{"ipv4_private", netip.MustParsePrefix("172.16.0.0/12")},
{"ipv6_link_local", netip.MustParsePrefix("fe80::/10")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWritePrefix(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = newWritePrefix(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> new read (no old read for prefix)
readBack, err := readPrefix(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readPrefix(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack2)
})
}
}
func TestIPSetCompat(t *testing.T) {
t.Parallel()
// Note: The old writeIPSet was buggy (varbin.Write with struct values wrote nothing).
// This test verifies the new implementation writes correct data and round-trips correctly.
cases := []struct {
name string
input *netipx.IPSet
}{
{"single_ipv4", buildIPSet("1.2.3.4")},
{"ipv4_range", buildIPSet("192.168.0.0/16")},
{"multi_ipv4", buildIPSet("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")},
{"single_ipv6", buildIPSet("::1")},
{"ipv6_range", buildIPSet("2001:db8::/32")},
{"mixed", buildIPSet("10.0.0.0/8", "::1", "2001:db8::/32")},
{"large", buildLargeIPSet(100)},
{"adjacent_ranges", buildIPSet("192.168.0.0/24", "192.168.1.0/24", "192.168.2.0/24")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// New write
var newBuf bytes.Buffer
err := writeIPSet(&newBuf, tc.input)
require.NoError(t, err)
// Verify format starts with version byte (1) + uint64 count
require.True(t, len(newBuf.Bytes()) >= 9, "output too short")
require.Equal(t, byte(1), newBuf.Bytes()[0], "version byte mismatch")
// New write -> old read (varbin.Read with pre-allocated slice works correctly)
readBack, err := oldReadIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireIPSetEqual(t, tc.input, readBack)
// New write -> new read
readBack2, err := readIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireIPSetEqual(t, tc.input, readBack2)
})
}
}
// Helper functions
func generateStrings(count int) []string {
result := make([]string, count)
for i := range result {
result[i] = strings.Repeat("x", i%50)
}
return result
}
func generateUint8Slice(count int) []uint8 {
result := make([]uint8, count)
for i := range result {
result[i] = uint8(i % 256)
}
return result
}
func generateUint16Slice(count int) []uint16 {
result := make([]uint16, count)
for i := range result {
result[i] = uint16(i * 257)
}
return result
}
func buildIPSet(cidrs ...string) *netipx.IPSet {
var builder netipx.IPSetBuilder
for _, cidr := range cidrs {
prefix, err := netip.ParsePrefix(cidr)
if err != nil {
addr, err := netip.ParseAddr(cidr)
if err != nil {
panic(err)
}
builder.Add(addr)
} else {
builder.AddPrefix(prefix)
}
}
set, _ := builder.IPSet()
return set
}
func buildLargeIPSet(count int) *netipx.IPSet {
var builder netipx.IPSetBuilder
for i := 0; i < count; i++ {
prefix := netip.PrefixFrom(netip.AddrFrom4([4]byte{10, byte(i / 256), byte(i % 256), 0}), 24)
builder.AddPrefix(prefix)
}
set, _ := builder.IPSet()
return set
}
func requireStringSliceEqual(t *testing.T, expected, actual []string) {
t.Helper()
if len(expected) == 0 && len(actual) == 0 {
return
}
require.Equal(t, expected, actual)
}
func requireUint8SliceEqual(t *testing.T, expected, actual []uint8) {
t.Helper()
if len(expected) == 0 && len(actual) == 0 {
return
}
require.Equal(t, expected, actual)
}
func requireUint16SliceEqual(t *testing.T, expected, actual []uint16) {
t.Helper()
if len(expected) == 0 && len(actual) == 0 {
return
}
require.Equal(t, expected, actual)
}
func requireIPSetEqual(t *testing.T, expected, actual *netipx.IPSet) {
t.Helper()
expectedRanges := expected.Ranges()
actualRanges := actual.Ranges()
require.Equal(t, len(expectedRanges), len(actualRanges), "range count mismatch")
for i := range expectedRanges {
require.Equal(t, expectedRanges[i].From(), actualRanges[i].From(), "range[%d].from mismatch", i)
require.Equal(t, expectedRanges[i].To(), actualRanges[i].To(), "range[%d].to mismatch", i)
}
}

View File

@@ -2,7 +2,6 @@ package srs
import (
"encoding/binary"
"io"
"net/netip"
M "github.com/sagernet/sing/common/metadata"
@@ -10,16 +9,11 @@ import (
)
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
addrLen, err := binary.ReadUvarint(reader)
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
addrSlice := make([]byte, addrLen)
_, err = io.ReadFull(reader, addrSlice)
if err != nil {
return netip.Prefix{}, err
}
prefixBits, err := reader.ReadByte()
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
@@ -27,16 +21,11 @@ func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
}
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
addrSlice := prefix.Addr().AsSlice()
_, err := varbin.WriteUvarint(writer, uint64(len(addrSlice)))
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
if err != nil {
return err
}
_, err = writer.Write(addrSlice)
if err != nil {
return err
}
err = writer.WriteByte(uint8(prefix.Bits()))
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
if err != nil {
return err
}

View File

@@ -2,11 +2,11 @@ package srs
import (
"encoding/binary"
"io"
"net/netip"
"os"
"unsafe"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
@@ -22,6 +22,11 @@ type myIPRange struct {
to netip.Addr
}
type myIPRangeData struct {
From []byte
To []byte
}
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
version, err := reader.ReadByte()
if err != nil {
@@ -36,30 +41,17 @@ func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
if err != nil {
return nil, err
}
mySet := &myIPSet{
rr: make([]myIPRange, length),
ranges := make([]myIPRangeData, length)
err = varbin.Read(reader, binary.BigEndian, &ranges)
if err != nil {
return nil, err
}
for i := range mySet.rr {
fromLen, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
fromBytes := make([]byte, fromLen)
_, err = io.ReadFull(reader, fromBytes)
if err != nil {
return nil, err
}
toLen, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
toBytes := make([]byte, toLen)
_, err = io.ReadFull(reader, toBytes)
if err != nil {
return nil, err
}
mySet.rr[i].from = M.AddrFromIP(fromBytes)
mySet.rr[i].to = M.AddrFromIP(toBytes)
mySet := &myIPSet{
rr: make([]myIPRange, len(ranges)),
}
for i, rangeData := range ranges {
mySet.rr[i].from = M.AddrFromIP(rangeData.From)
mySet.rr[i].to = M.AddrFromIP(rangeData.To)
}
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
}
@@ -69,27 +61,18 @@ func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error {
if err != nil {
return err
}
mySet := (*myIPSet)(unsafe.Pointer(set))
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData {
return myIPRangeData{
From: rr.from.AsSlice(),
To: rr.to.AsSlice(),
}
})
err = binary.Write(writer, binary.BigEndian, uint64(len(dataList)))
if err != nil {
return err
}
for _, rr := range mySet.rr {
fromBytes := rr.from.AsSlice()
_, err = varbin.WriteUvarint(writer, uint64(len(fromBytes)))
if err != nil {
return err
}
_, err = writer.Write(fromBytes)
if err != nil {
return err
}
toBytes := rr.to.AsSlice()
_, err = varbin.WriteUvarint(writer, uint64(len(toBytes)))
if err != nil {
return err
}
_, err = writer.Write(toBytes)
for _, data := range dataList {
err = varbin.Write(writer, binary.BigEndian, data)
if err != nil {
return err
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/sagernet/sing/common/logger"
"github.com/caddyserver/certmagic"
"github.com/libdns/acmedns"
"github.com/libdns/alidns"
"github.com/libdns/cloudflare"
"github.com/mholt/acmez/v3/acme"
@@ -115,24 +114,13 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
switch dnsOptions.Provider {
case C.DNSProviderAliDNS:
solver.DNSProvider = &alidns.Provider{
CredentialInfo: alidns.CredentialInfo{
AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID,
AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
RegionID: dnsOptions.AliDNSOptions.RegionID,
SecurityToken: dnsOptions.AliDNSOptions.SecurityToken,
},
AccKeyID: dnsOptions.AliDNSOptions.AccessKeyID,
AccKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
RegionID: dnsOptions.AliDNSOptions.RegionID,
}
case C.DNSProviderCloudflare:
solver.DNSProvider = &cloudflare.Provider{
APIToken: dnsOptions.CloudflareOptions.APIToken,
ZoneToken: dnsOptions.CloudflareOptions.ZoneToken,
}
case C.DNSProviderACMEDNS:
solver.DNSProvider = &acmedns.Provider{
Username: dnsOptions.ACMEDNSOptions.Username,
Password: dnsOptions.ACMEDNSOptions.Password,
Subdomain: dnsOptions.ACMEDNSOptions.Subdomain,
ServerURL: dnsOptions.ACMEDNSOptions.ServerURL,
APIToken: dnsOptions.CloudflareOptions.APIToken,
}
default:
return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider)

View File

@@ -51,7 +51,6 @@ func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, op
return &ECHClientConfig{
ECHCapableConfig: clientConfig,
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
queryServerName: options.ECH.QueryServerName,
}, nil
}
}
@@ -109,11 +108,10 @@ func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
type ECHClientConfig struct {
ECHCapableConfig
access sync.Mutex
dnsRouter adapter.DNSRouter
queryServerName string
lastTTL time.Duration
lastUpdate time.Time
access sync.Mutex
dnsRouter adapter.DNSRouter
lastTTL time.Duration
lastUpdate time.Time
}
func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
@@ -132,17 +130,13 @@ func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn)
s.access.Lock()
defer s.access.Unlock()
if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Since(s.lastUpdate) > s.lastTTL {
queryServerName := s.queryServerName
if queryServerName == "" {
queryServerName = s.ServerName()
}
message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: mDNS.Fqdn(queryServerName),
Name: mDNS.Fqdn(s.ServerName()),
Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET,
},
@@ -181,12 +175,7 @@ func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn)
}
func (s *ECHClientConfig) Clone() Config {
return &ECHClientConfig{
ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig),
dnsRouter: s.dnsRouter,
queryServerName: s.queryServerName,
lastUpdate: s.lastUpdate,
}
return &ECHClientConfig{ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
}
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {

View File

@@ -14,7 +14,6 @@ import (
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/observable"
)
var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil)
@@ -22,7 +21,7 @@ var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil)
type HistoryStorage struct {
access sync.RWMutex
delayHistory map[string]*adapter.URLTestHistory
updateHook *observable.Subscriber[struct{}]
updateHook chan<- struct{}
}
func NewHistoryStorage() *HistoryStorage {
@@ -31,7 +30,7 @@ func NewHistoryStorage() *HistoryStorage {
}
}
func (s *HistoryStorage) SetHook(hook *observable.Subscriber[struct{}]) {
func (s *HistoryStorage) SetHook(hook chan<- struct{}) {
s.updateHook = hook
}
@@ -61,7 +60,10 @@ func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTes
func (s *HistoryStorage) notifyUpdated() {
updateHook := s.updateHook
if updateHook != nil {
updateHook.Emit(struct{}{})
select {
case updateHook <- struct{}{}:
default:
}
}
}

View File

@@ -3,6 +3,5 @@ package constant
const (
CertificateStoreSystem = "system"
CertificateStoreMozilla = "mozilla"
CertificateStoreChrome = "chrome"
CertificateStoreNone = "none"
)

View File

@@ -4,5 +4,5 @@ import "time"
const (
DHCPTTL = time.Hour
DHCPTimeout = 5 * time.Second
DHCPTimeout = time.Minute
)

View File

@@ -33,5 +33,4 @@ const (
const (
DNSProviderAliDNS = "alidns"
DNSProviderCloudflare = "cloudflare"
DNSProviderACMEDNS = "acmedns"
)

View File

@@ -29,7 +29,6 @@ const (
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
)
const (

View File

@@ -30,7 +30,6 @@ const (
RuleActionTypeRoute = "route"
RuleActionTypeRouteOptions = "route-options"
RuleActionTypeDirect = "direct"
RuleActionTypeBypass = "bypass"
RuleActionTypeReject = "reject"
RuleActionTypeHijackDNS = "hijack-dns"
RuleActionTypeSniff = "sniff"

View File

@@ -1,29 +0,0 @@
package daemon
import (
"sync"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing/common"
)
var _ deprecated.Manager = (*deprecatedManager)(nil)
type deprecatedManager struct {
access sync.Mutex
notes []deprecated.Note
}
func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) {
m.access.Lock()
defer m.access.Unlock()
m.notes = common.Uniq(append(m.notes, feature))
}
func (m *deprecatedManager) Get() []deprecated.Note {
m.access.Lock()
defer m.access.Unlock()
notes := m.notes
m.notes = nil
return notes
}

View File

@@ -1,135 +0,0 @@
package daemon
import (
"bytes"
"context"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
type Instance struct {
ctx context.Context
cancel context.CancelFunc
instance *box.Box
clashServer adapter.ClashServer
cacheFile adapter.CacheFile
pauseManager pause.Manager
urlTestHistoryStorage *urltest.HistoryStorage
}
func (s *StartedService) CheckConfig(configContent string) error {
options, err := parseConfig(s.ctx, configContent)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(s.ctx)
defer cancel()
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
})
if err == nil {
instance.Close()
}
return err
}
func (s *StartedService) FormatConfig(configContent string) (string, error) {
options, err := parseConfig(s.ctx, configContent)
if err != nil {
return "", err
}
var buffer bytes.Buffer
encoder := json.NewEncoder(&buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(options)
if err != nil {
return "", err
}
return buffer.String(), nil
}
type OverrideOptions struct {
AutoRedirect bool
IncludePackage []string
ExcludePackage []string
}
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
ctx := s.ctx
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
ctx, cancel := context.WithCancel(include.Context(ctx))
options, err := parseConfig(ctx, profileContent)
if err != nil {
cancel()
return nil, err
}
if overrideOptions != nil {
for _, inbound := range options.Inbounds {
if tunInboundOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN {
tunInboundOptions.AutoRedirect = overrideOptions.AutoRedirect
tunInboundOptions.IncludePackage = append(tunInboundOptions.IncludePackage, overrideOptions.IncludePackage...)
tunInboundOptions.ExcludePackage = append(tunInboundOptions.ExcludePackage, overrideOptions.ExcludePackage...)
break
}
}
}
urlTestHistoryStorage := urltest.NewHistoryStorage()
ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
i := &Instance{
ctx: ctx,
cancel: cancel,
urlTestHistoryStorage: urlTestHistoryStorage,
}
boxInstance, err := box.New(box.Options{
Context: ctx,
Options: options,
PlatformLogWriter: s,
})
if err != nil {
cancel()
return nil, err
}
i.instance = boxInstance
i.clashServer = service.FromContext[adapter.ClashServer](ctx)
i.pauseManager = service.FromContext[pause.Manager](ctx)
i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
log.SetStdLogger(boxInstance.LogFactory().Logger())
return i, nil
}
func (i *Instance) Start() error {
return i.instance.Start()
}
func (i *Instance) Close() error {
i.cancel()
i.urlTestHistoryStorage.Close()
return i.instance.Close()
}
func (i *Instance) Box() *box.Box {
return i.instance
}
func (i *Instance) PauseManager() pause.Manager {
return i.pauseManager
}
func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent))
if err != nil {
return option.Options{}, E.Cause(err, "decode config")
}
return options, nil
}

View File

@@ -1,9 +0,0 @@
package daemon
type PlatformHandler interface {
ServiceStop() error
ServiceReload() error
SystemProxyStatus() (*SystemProxyStatus, error)
SetSystemProxyEnabled(enabled bool) error
WriteDebugMessage(message string)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,217 +0,0 @@
syntax = "proto3";
package daemon;
option go_package = "github.com/sagernet/sing-box/daemon";
import "google/protobuf/empty.proto";
service StartedService {
rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc ReloadService(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {}
rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {}
rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {}
rpc ClearLogs(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc SubscribeStatus(SubscribeStatusRequest) returns(stream Status) {}
rpc SubscribeGroups(google.protobuf.Empty) returns(stream Groups) {}
rpc GetClashModeStatus(google.protobuf.Empty) returns(ClashModeStatus) {}
rpc SubscribeClashMode(google.protobuf.Empty) returns(stream ClashMode) {}
rpc SetClashMode(ClashMode) returns(google.protobuf.Empty) {}
rpc URLTest(URLTestRequest) returns(google.protobuf.Empty) {}
rpc SelectOutbound(SelectOutboundRequest) returns (google.protobuf.Empty) {}
rpc SetGroupExpand(SetGroupExpandRequest) returns (google.protobuf.Empty) {}
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
}
message ServiceStatus {
enum Type {
IDLE = 0;
STARTING = 1;
STARTED = 2;
STOPPING = 3;
FATAL = 4;
}
Type status = 1;
string errorMessage = 2;
}
message ReloadServiceRequest {
string newProfileContent = 1;
}
message SubscribeStatusRequest {
int64 interval = 1;
}
enum LogLevel {
PANIC = 0;
FATAL = 1;
ERROR = 2;
WARN = 3;
INFO = 4;
DEBUG = 5;
TRACE = 6;
}
message Log {
repeated Message messages = 1;
bool reset = 2;
message Message {
LogLevel level = 1;
string message = 2;
}
}
message DefaultLogLevel {
LogLevel level = 1;
}
message Status {
uint64 memory = 1;
int32 goroutines = 2;
int32 connectionsIn = 3;
int32 connectionsOut = 4;
bool trafficAvailable = 5;
int64 uplink = 6;
int64 downlink = 7;
int64 uplinkTotal = 8;
int64 downlinkTotal = 9;
}
message Groups {
repeated Group group = 1;
}
message Group {
string tag = 1;
string type = 2;
bool selectable = 3;
string selected = 4;
bool isExpand = 5;
repeated GroupItem items = 6;
}
message GroupItem {
string tag = 1;
string type = 2;
int64 urlTestTime = 3;
int32 urlTestDelay = 4;
}
message URLTestRequest {
string outboundTag = 1;
}
message SelectOutboundRequest {
string groupTag = 1;
string outboundTag = 2;
}
message SetGroupExpandRequest {
string groupTag = 1;
bool isExpand = 2;
}
message ClashMode {
string mode = 3;
}
message ClashModeStatus {
repeated string modeList = 1;
string currentMode = 2;
}
message SystemProxyStatus {
bool available = 1;
bool enabled = 2;
}
message SetSystemProxyEnabledRequest {
bool enabled = 1;
}
message SubscribeConnectionsRequest {
int64 interval = 1;
}
enum ConnectionEventType {
CONNECTION_EVENT_NEW = 0;
CONNECTION_EVENT_UPDATE = 1;
CONNECTION_EVENT_CLOSED = 2;
}
message ConnectionEvent {
ConnectionEventType type = 1;
string id = 2;
Connection connection = 3;
int64 uplinkDelta = 4;
int64 downlinkDelta = 5;
int64 closedAt = 6;
}
message ConnectionEvents {
repeated ConnectionEvent events = 1;
bool reset = 2;
}
message Connection {
string id = 1;
string inbound = 2;
string inboundType = 3;
int32 ipVersion = 4;
string network = 5;
string source = 6;
string destination = 7;
string domain = 8;
string protocol = 9;
string user = 10;
string fromOutbound = 11;
int64 createdAt = 12;
int64 closedAt = 13;
int64 uplink = 14;
int64 downlink = 15;
int64 uplinkTotal = 16;
int64 downlinkTotal = 17;
string rule = 18;
string outbound = 19;
string outboundType = 20;
repeated string chainList = 21;
ProcessInfo processInfo = 22;
}
message ProcessInfo {
uint32 processId = 1;
int32 userId = 2;
string userName = 3;
string processPath = 4;
string packageName = 5;
}
message CloseConnectionRequest {
string id = 1;
}
message DeprecatedWarnings {
repeated DeprecatedWarning warnings = 1;
}
message DeprecatedWarning {
string message = 1;
bool impending = 2;
string migrationLink = 3;
}
message StartedAt {
int64 startedAt = 1;
}

View File

@@ -1,916 +0,0 @@
package daemon
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
)
// StartedServiceClient is the client API for StartedService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type StartedServiceClient interface {
StopService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
ReloadService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error)
SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error)
GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error)
ClearLogs(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error)
SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error)
GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error)
SubscribeClashMode(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ClashMode], error)
SetClashMode(ctx context.Context, in *ClashMode, opts ...grpc.CallOption) (*emptypb.Empty, error)
URLTest(ctx context.Context, in *URLTestRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
SelectOutbound(ctx context.Context, in *SelectOutboundRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
}
type startedServiceClient struct {
cc grpc.ClientConnInterface
}
func NewStartedServiceClient(cc grpc.ClientConnInterface) StartedServiceClient {
return &startedServiceClient{cc}
}
func (c *startedServiceClient) StopService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_StopService_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) ReloadService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_ReloadService_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[0], StartedService_SubscribeServiceStatus_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, ServiceStatus]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeServiceStatusClient = grpc.ServerStreamingClient[ServiceStatus]
func (c *startedServiceClient) SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[1], StartedService_SubscribeLog_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, Log]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeLogClient = grpc.ServerStreamingClient[Log]
func (c *startedServiceClient) GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DefaultLogLevel)
err := c.cc.Invoke(ctx, StartedService_GetDefaultLogLevel_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) ClearLogs(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_ClearLogs_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[2], StartedService_SubscribeStatus_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[SubscribeStatusRequest, Status]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeStatusClient = grpc.ServerStreamingClient[Status]
func (c *startedServiceClient) SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[3], StartedService_SubscribeGroups_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, Groups]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeGroupsClient = grpc.ServerStreamingClient[Groups]
func (c *startedServiceClient) GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ClashModeStatus)
err := c.cc.Invoke(ctx, StartedService_GetClashModeStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeClashMode(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ClashMode], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[4], StartedService_SubscribeClashMode_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, ClashMode]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeClashModeClient = grpc.ServerStreamingClient[ClashMode]
func (c *startedServiceClient) SetClashMode(ctx context.Context, in *ClashMode, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_SetClashMode_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) URLTest(ctx context.Context, in *URLTestRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_URLTest_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SelectOutbound(ctx context.Context, in *SelectOutboundRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_SelectOutbound_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_SetGroupExpand_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SystemProxyStatus)
err := c.cc.Invoke(ctx, StartedService_GetSystemProxyStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_SetSystemProxyEnabled_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[SubscribeConnectionsRequest, ConnectionEvents]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeConnectionsClient = grpc.ServerStreamingClient[ConnectionEvents]
func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_CloseConnection_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_CloseAllConnections_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeprecatedWarnings)
err := c.cc.Invoke(ctx, StartedService_GetDeprecatedWarnings_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StartedAt)
err := c.cc.Invoke(ctx, StartedService_GetStartedAt_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// StartedServiceServer is the server API for StartedService service.
// All implementations must embed UnimplementedStartedServiceServer
// for forward compatibility.
type StartedServiceServer interface {
StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error
SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error
GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error)
ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error
SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error
GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error)
SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error
SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error)
URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error)
SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error)
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
mustEmbedUnimplementedStartedServiceServer()
}
// UnimplementedStartedServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedStartedServiceServer struct{}
func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method StopService not implemented")
}
func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ReloadService not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error {
return status.Error(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error {
return status.Error(codes.Unimplemented, "method SubscribeLog not implemented")
}
func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) {
return nil, status.Error(codes.Unimplemented, "method GetDefaultLogLevel not implemented")
}
func (UnimplementedStartedServiceServer) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ClearLogs not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error {
return status.Error(codes.Unimplemented, "method SubscribeStatus not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error {
return status.Error(codes.Unimplemented, "method SubscribeGroups not implemented")
}
func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) {
return nil, status.Error(codes.Unimplemented, "method GetClashModeStatus not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error {
return status.Error(codes.Unimplemented, "method SubscribeClashMode not implemented")
}
func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetClashMode not implemented")
}
func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method URLTest not implemented")
}
func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SelectOutbound not implemented")
}
func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetGroupExpand not implemented")
}
func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) {
return nil, status.Error(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
}
func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
}
func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CloseConnection not implemented")
}
func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CloseAllConnections not implemented")
}
func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) {
return nil, status.Error(codes.Unimplemented, "method GetDeprecatedWarnings not implemented")
}
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
}
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
// UnsafeStartedServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to StartedServiceServer will
// result in compilation errors.
type UnsafeStartedServiceServer interface {
mustEmbedUnimplementedStartedServiceServer()
}
func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) {
// If the following call panics, it indicates UnimplementedStartedServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&StartedService_ServiceDesc, srv)
}
func _StartedService_StopService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).StopService(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_StopService_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).StopService(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_ReloadService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).ReloadService(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_ReloadService_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).ReloadService(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeServiceStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeServiceStatus(m, &grpc.GenericServerStream[emptypb.Empty, ServiceStatus]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeServiceStatusServer = grpc.ServerStreamingServer[ServiceStatus]
func _StartedService_SubscribeLog_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeLog(m, &grpc.GenericServerStream[emptypb.Empty, Log]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeLogServer = grpc.ServerStreamingServer[Log]
func _StartedService_GetDefaultLogLevel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).GetDefaultLogLevel(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_GetDefaultLogLevel_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).GetDefaultLogLevel(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_ClearLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).ClearLogs(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_ClearLogs_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).ClearLogs(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeStatusRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeStatus(m, &grpc.GenericServerStream[SubscribeStatusRequest, Status]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeStatusServer = grpc.ServerStreamingServer[Status]
func _StartedService_SubscribeGroups_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeGroups(m, &grpc.GenericServerStream[emptypb.Empty, Groups]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeGroupsServer = grpc.ServerStreamingServer[Groups]
func _StartedService_GetClashModeStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).GetClashModeStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_GetClashModeStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).GetClashModeStatus(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeClashMode_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeClashMode(m, &grpc.GenericServerStream[emptypb.Empty, ClashMode]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeClashModeServer = grpc.ServerStreamingServer[ClashMode]
func _StartedService_SetClashMode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClashMode)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).SetClashMode(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_SetClashMode_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).SetClashMode(ctx, req.(*ClashMode))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_URLTest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(URLTestRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).URLTest(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_URLTest_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).URLTest(ctx, req.(*URLTestRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SelectOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SelectOutboundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).SelectOutbound(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_SelectOutbound_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).SelectOutbound(ctx, req.(*SelectOutboundRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SetGroupExpand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetGroupExpandRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).SetGroupExpand(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_SetGroupExpand_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).SetGroupExpand(ctx, req.(*SetGroupExpandRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_GetSystemProxyStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).GetSystemProxyStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_GetSystemProxyStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).GetSystemProxyStatus(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetSystemProxyEnabledRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).SetSystemProxyEnabled(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_SetSystemProxyEnabled_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).SetSystemProxyEnabled(ctx, req.(*SetSystemProxyEnabledRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeConnectionsRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, ConnectionEvents]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeConnectionsServer = grpc.ServerStreamingServer[ConnectionEvents]
func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CloseConnectionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).CloseConnection(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_CloseConnection_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).CloseConnection(ctx, req.(*CloseConnectionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_CloseAllConnections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).CloseAllConnections(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_CloseAllConnections_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).CloseAllConnections(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_GetDeprecatedWarnings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).GetDeprecatedWarnings(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_GetDeprecatedWarnings_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).GetDeprecatedWarnings(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).GetStartedAt(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_GetStartedAt_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).GetStartedAt(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var StartedService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "daemon.StartedService",
HandlerType: (*StartedServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "StopService",
Handler: _StartedService_StopService_Handler,
},
{
MethodName: "ReloadService",
Handler: _StartedService_ReloadService_Handler,
},
{
MethodName: "GetDefaultLogLevel",
Handler: _StartedService_GetDefaultLogLevel_Handler,
},
{
MethodName: "ClearLogs",
Handler: _StartedService_ClearLogs_Handler,
},
{
MethodName: "GetClashModeStatus",
Handler: _StartedService_GetClashModeStatus_Handler,
},
{
MethodName: "SetClashMode",
Handler: _StartedService_SetClashMode_Handler,
},
{
MethodName: "URLTest",
Handler: _StartedService_URLTest_Handler,
},
{
MethodName: "SelectOutbound",
Handler: _StartedService_SelectOutbound_Handler,
},
{
MethodName: "SetGroupExpand",
Handler: _StartedService_SetGroupExpand_Handler,
},
{
MethodName: "GetSystemProxyStatus",
Handler: _StartedService_GetSystemProxyStatus_Handler,
},
{
MethodName: "SetSystemProxyEnabled",
Handler: _StartedService_SetSystemProxyEnabled_Handler,
},
{
MethodName: "CloseConnection",
Handler: _StartedService_CloseConnection_Handler,
},
{
MethodName: "CloseAllConnections",
Handler: _StartedService_CloseAllConnections_Handler,
},
{
MethodName: "GetDeprecatedWarnings",
Handler: _StartedService_GetDeprecatedWarnings_Handler,
},
{
MethodName: "GetStartedAt",
Handler: _StartedService_GetStartedAt_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "SubscribeServiceStatus",
Handler: _StartedService_SubscribeServiceStatus_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeLog",
Handler: _StartedService_SubscribeLog_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeStatus",
Handler: _StartedService_SubscribeStatus_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeGroups",
Handler: _StartedService_SubscribeGroups_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeClashMode",
Handler: _StartedService_SubscribeClashMode_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeConnections",
Handler: _StartedService_SubscribeConnections_Handler,
ServerStreams: true,
},
},
Metadata: "daemon/started_service.proto",
}

View File

@@ -144,11 +144,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if c.cache != nil {
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
<-cond
} else {
defer func() {
c.cacheLock.Delete(question)
@@ -158,11 +154,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} else if c.transportCache != nil {
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
<-cond
} else {
defer func() {
c.transportCacheLock.Delete(question)
@@ -361,6 +353,68 @@ func (c *Client) ClearCache() {
}
}
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
if c.disableCache || c.independentCache {
return nil, false
}
if dns.IsFqdn(domain) {
domain = domain[:len(domain)-1]
}
dnsName := dns.Fqdn(domain)
if strategy == C.DomainStrategyIPv4Only {
addresses, err := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}, nil)
if err != ErrNotCached {
return addresses, true
}
} else if strategy == C.DomainStrategyIPv6Only {
addresses, err := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if err != ErrNotCached {
return addresses, true
}
} else {
response4, _ := c.loadResponse(dns.Question{
Name: dnsName,
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}, nil)
if response4 == nil {
return nil, false
}
response6, _ := c.loadResponse(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if response6 == nil {
return nil, false
}
return sortAddresses(MessageToAddresses(response4), MessageToAddresses(response6), strategy), true
}
return nil, false
}
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
if c.disableCache || c.independentCache || len(message.Question) != 1 {
return nil, false
}
question := message.Question[0]
response, ttl := c.loadResponse(question, nil)
if response == nil {
return nil, false
}
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, true
}
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
if strategy == C.DomainStrategyPreferIPv6 {
return append(response6, response4...)

View File

@@ -10,6 +10,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
R "github.com/sagernet/sing-box/route/rule"
@@ -37,7 +38,7 @@ type Router struct {
rules []adapter.DNSRule
defaultDomainStrategy C.DomainStrategy
dnsReverseMapping freelru.Cache[netip.Addr, string]
platformInterface adapter.PlatformInterface
platformInterface platform.Interface
}
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
@@ -213,94 +214,96 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
}
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
var (
response *mDNS.Msg
transport adapter.DNSTransport
err error
)
var metadata *adapter.InboundContext
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
response, cached := r.client.ExchangeCache(ctx, message)
if !cached {
var metadata *adapter.InboundContext
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
options.Strategy = r.defaultDomainStrategy
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else {
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else {
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
}
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
}
if responseCheck != nil && rejected {
continue
}
break
}
}
if err != nil {
@@ -324,6 +327,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
var (
responseAddrs []netip.Addr
cached bool
err error
)
printResult := func() {
@@ -343,6 +347,13 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err = E.Cause(err, "lookup ", domain)
}
}
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached {
if len(responseAddrs) == 0 {
return nil, E.New("lookup ", domain, ": empty result (cached)")
}
return responseAddrs, nil
}
r.logger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
@@ -377,11 +388,9 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
case *R.RuleActionReject:
return nil, &R.RejectedError{Cause: action.Error(ctx)}
case *R.RuleActionPredefined:
responseAddrs = nil
if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode)
} else {
err = nil
for _, answer := range action.Answer {
switch record := answer.(type) {
case *mDNS.A:
@@ -446,6 +455,6 @@ func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
func (r *Router) ResetNetwork() {
r.ClearCache()
for _, transport := range r.transport.Transports() {
transport.Reset()
transport.Close()
}
}

View File

@@ -1,145 +0,0 @@
package transport
import (
"context"
"os"
"sync"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
type TransportState int
const (
StateNew TransportState = iota
StateStarted
StateClosing
StateClosed
)
var (
ErrTransportClosed = os.ErrClosed
ErrConnectionReset = E.New("connection reset")
)
type BaseTransport struct {
dns.TransportAdapter
Logger logger.ContextLogger
mutex sync.Mutex
state TransportState
inFlight int32
queriesComplete chan struct{}
closeCtx context.Context
closeCancel context.CancelFunc
}
func NewBaseTransport(adapter dns.TransportAdapter, logger logger.ContextLogger) *BaseTransport {
ctx, cancel := context.WithCancel(context.Background())
return &BaseTransport{
TransportAdapter: adapter,
Logger: logger,
state: StateNew,
closeCtx: ctx,
closeCancel: cancel,
}
}
func (t *BaseTransport) State() TransportState {
t.mutex.Lock()
defer t.mutex.Unlock()
return t.state
}
func (t *BaseTransport) SetStarted() error {
t.mutex.Lock()
defer t.mutex.Unlock()
switch t.state {
case StateNew:
t.state = StateStarted
return nil
case StateStarted:
return nil
default:
return ErrTransportClosed
}
}
func (t *BaseTransport) BeginQuery() bool {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.state != StateStarted {
return false
}
t.inFlight++
return true
}
func (t *BaseTransport) EndQuery() {
t.mutex.Lock()
if t.inFlight > 0 {
t.inFlight--
}
if t.inFlight == 0 && t.queriesComplete != nil {
close(t.queriesComplete)
t.queriesComplete = nil
}
t.mutex.Unlock()
}
func (t *BaseTransport) CloseContext() context.Context {
return t.closeCtx
}
func (t *BaseTransport) Shutdown(ctx context.Context) error {
t.mutex.Lock()
if t.state >= StateClosing {
t.mutex.Unlock()
return nil
}
if t.state == StateNew {
t.state = StateClosed
t.mutex.Unlock()
t.closeCancel()
return nil
}
t.state = StateClosing
if t.inFlight == 0 {
t.state = StateClosed
t.mutex.Unlock()
t.closeCancel()
return nil
}
t.queriesComplete = make(chan struct{})
queriesComplete := t.queriesComplete
t.mutex.Unlock()
t.closeCancel()
select {
case <-queriesComplete:
t.mutex.Lock()
t.state = StateClosed
t.mutex.Unlock()
return nil
case <-ctx.Done():
t.mutex.Lock()
t.state = StateClosed
t.mutex.Unlock()
return ctx.Err()
}
}
func (t *BaseTransport) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
defer cancel()
return t.Shutdown(ctx)
}

View File

@@ -1,205 +0,0 @@
package transport
import (
"context"
"net"
"sync"
)
type ConnectorCallbacks[T any] struct {
IsClosed func(connection T) bool
Close func(connection T)
Reset func(connection T)
}
type Connector[T any] struct {
dial func(ctx context.Context) (T, error)
callbacks ConnectorCallbacks[T]
access sync.Mutex
connection T
hasConnection bool
connecting chan struct{}
closeCtx context.Context
closed bool
}
func NewConnector[T any](closeCtx context.Context, dial func(context.Context) (T, error), callbacks ConnectorCallbacks[T]) *Connector[T] {
return &Connector[T]{
dial: dial,
callbacks: callbacks,
closeCtx: closeCtx,
}
}
func NewSingleflightConnector(closeCtx context.Context, dial func(context.Context) (*Connection, error)) *Connector[*Connection] {
return NewConnector(closeCtx, dial, ConnectorCallbacks[*Connection]{
IsClosed: func(connection *Connection) bool {
return connection.IsClosed()
},
Close: func(connection *Connection) {
connection.CloseWithError(ErrTransportClosed)
},
Reset: func(connection *Connection) {
connection.CloseWithError(ErrConnectionReset)
},
})
}
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
var zero T
for {
c.access.Lock()
if c.closed {
c.access.Unlock()
return zero, ErrTransportClosed
}
if c.hasConnection && !c.callbacks.IsClosed(c.connection) {
connection := c.connection
c.access.Unlock()
return connection, nil
}
c.hasConnection = false
if c.connecting != nil {
connecting := c.connecting
c.access.Unlock()
select {
case <-connecting:
continue
case <-ctx.Done():
return zero, ctx.Err()
case <-c.closeCtx.Done():
return zero, ErrTransportClosed
}
}
c.connecting = make(chan struct{})
c.access.Unlock()
connection, err := c.dialWithCancellation(ctx)
c.access.Lock()
close(c.connecting)
c.connecting = nil
if err != nil {
c.access.Unlock()
return zero, err
}
if c.closed {
c.callbacks.Close(connection)
c.access.Unlock()
return zero, ErrTransportClosed
}
c.connection = connection
c.hasConnection = true
result := c.connection
c.access.Unlock()
return result, nil
}
}
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, error) {
dialCtx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
select {
case <-c.closeCtx.Done():
cancel()
case <-dialCtx.Done():
}
}()
return c.dial(dialCtx)
}
func (c *Connector[T]) Close() error {
c.access.Lock()
defer c.access.Unlock()
if c.closed {
return nil
}
c.closed = true
if c.hasConnection {
c.callbacks.Close(c.connection)
c.hasConnection = false
}
return nil
}
func (c *Connector[T]) Reset() {
c.access.Lock()
defer c.access.Unlock()
if c.hasConnection {
c.callbacks.Reset(c.connection)
c.hasConnection = false
}
}
type Connection struct {
net.Conn
closeOnce sync.Once
done chan struct{}
closeError error
}
func WrapConnection(conn net.Conn) *Connection {
return &Connection{
Conn: conn,
done: make(chan struct{}),
}
}
func (c *Connection) Done() <-chan struct{} {
return c.done
}
func (c *Connection) IsClosed() bool {
select {
case <-c.done:
return true
default:
return false
}
}
func (c *Connection) CloseError() error {
select {
case <-c.done:
if c.closeError != nil {
return c.closeError
}
return ErrTransportClosed
default:
return nil
}
}
func (c *Connection) Close() error {
return c.CloseWithError(ErrTransportClosed)
}
func (c *Connection) CloseWithError(err error) error {
var returnError error
c.closeOnce.Do(func() {
c.closeError = err
returnError = c.Conn.Close()
close(c.done)
})
return returnError
}

View File

@@ -49,7 +49,6 @@ type Transport struct {
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
transportLock sync.RWMutex
updatedAt time.Time
lastError error
servers []M.Socksaddr
search []string
ndots int
@@ -93,7 +92,7 @@ func (t *Transport) Start(stage adapter.StartStage) error {
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
}
go func() {
_, err := t.fetch()
_, err := t.Fetch()
if err != nil {
t.logger.Error(E.Cause(err, "fetch DNS servers"))
}
@@ -108,15 +107,8 @@ func (t *Transport) Close() error {
return nil
}
func (t *Transport) Reset() {
t.transportLock.Lock()
t.updatedAt = time.Time{}
t.servers = nil
t.transportLock.Unlock()
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
servers, err := t.fetch()
servers, err := t.Fetch()
if err != nil {
return nil, err
}
@@ -136,20 +128,11 @@ func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []
}
}
func (t *Transport) Fetch() []M.Socksaddr {
servers, _ := t.fetch()
return servers
}
func (t *Transport) fetch() ([]M.Socksaddr, error) {
func (t *Transport) Fetch() ([]M.Socksaddr, error) {
t.transportLock.RLock()
updatedAt := t.updatedAt
lastError := t.lastError
servers := t.servers
t.transportLock.RUnlock()
if lastError != nil {
return nil, lastError
}
if time.Since(updatedAt) < C.DHCPTTL {
return servers, nil
}
@@ -160,7 +143,7 @@ func (t *Transport) fetch() ([]M.Socksaddr, error) {
}
err := t.updateServers()
if err != nil {
return servers, err
return nil, err
}
return t.servers, nil
}
@@ -190,15 +173,12 @@ func (t *Transport) updateServers() error {
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
err = t.fetchServers0(fetchCtx, iface)
cancel()
t.updatedAt = time.Now()
if err != nil {
t.lastError = err
return err
} else if len(t.servers) == 0 {
t.lastError = E.New("dhcp: empty DNS servers response")
return t.lastError
return E.New("dhcp: empty DNS servers response")
} else {
t.lastError = nil
t.updatedAt = time.Now()
return nil
}
}
@@ -263,7 +243,6 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
defer buffer.Release()
for {
buffer.Reset()
_, _, err := buffer.ReadPacketFrom(packetConn)
if err != nil {
if errors.Is(err, io.ErrShortBuffer) {

View File

@@ -82,12 +82,8 @@ func (s *MemoryStorage) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr
}
func (s *MemoryStorage) FakeIPReset() error {
s.addressAccess.Lock()
s.domainAccess.Lock()
s.addressCache = make(map[netip.Addr]string)
s.domainCache4 = make(map[string]netip.Addr)
s.domainCache6 = make(map[string]netip.Addr)
s.domainAccess.Unlock()
s.addressAccess.Unlock()
return nil
}

View File

@@ -3,7 +3,6 @@ package fakeip
import (
"context"
"net/netip"
"sync"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
@@ -14,15 +13,13 @@ import (
var _ adapter.FakeIPStore = (*Store)(nil)
type Store struct {
ctx context.Context
logger logger.Logger
inet4Range netip.Prefix
inet6Range netip.Prefix
storage adapter.FakeIPStorage
addressAccess sync.Mutex
inet4Current netip.Addr
inet6Current netip.Addr
ctx context.Context
logger logger.Logger
inet4Range netip.Prefix
inet6Range netip.Prefix
storage adapter.FakeIPStorage
inet4Current netip.Addr
inet6Current netip.Addr
}
func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store {
@@ -68,30 +65,18 @@ func (s *Store) Close() error {
if s.storage == nil {
return nil
}
s.addressAccess.Lock()
metadata := &adapter.FakeIPMetadata{
return s.storage.FakeIPSaveMetadata(&adapter.FakeIPMetadata{
Inet4Range: s.inet4Range,
Inet6Range: s.inet6Range,
Inet4Current: s.inet4Current,
Inet6Current: s.inet6Current,
}
s.addressAccess.Unlock()
return s.storage.FakeIPSaveMetadata(metadata)
})
}
func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
if address, loaded := s.storage.FakeIPLoadDomain(domain, isIPv6); loaded {
return address, nil
}
s.addressAccess.Lock()
defer s.addressAccess.Unlock()
// Double-check after acquiring lock
if address, loaded := s.storage.FakeIPLoadDomain(domain, isIPv6); loaded {
return address, nil
}
var address netip.Addr
if !isIPv6 {
if !s.inet4Current.IsValid() {
@@ -114,10 +99,7 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
s.inet6Current = nextAddress
address = nextAddress
}
err := s.storage.FakeIPStore(address, domain)
if err != nil {
s.logger.Warn("save FakeIP cache: ", err)
}
s.storage.FakeIPStoreAsync(address, domain, s.logger)
s.storage.FakeIPSaveMetadataAsync(&adapter.FakeIPMetadata{
Inet4Range: s.inet4Range,
Inet6Range: s.inet6Range,

View File

@@ -59,9 +59,6 @@ func (t *Transport) Close() error {
return nil
}
func (t *Transport) Reset() {
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
domain := mDNS.CanonicalName(question.Name)

View File

@@ -145,13 +145,6 @@ func (t *HTTPSTransport) Close() error {
return nil
}
func (t *HTTPSTransport) Reset() {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
}
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
startAt := time.Now()
response, err := t.exchange(ctx, message)
@@ -189,10 +182,7 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
request.Header = t.headers.Clone()
request.Header.Set("Content-Type", MimeType)
request.Header.Set("Accept", MimeType)
t.transportAccess.Lock()
currentTransport := t.transport
t.transportAccess.Unlock()
response, err := currentTransport.RoundTrip(request)
response, err := t.transport.RoundTrip(request)
requestBuffer.Release()
if err != nil {
return nil, err
@@ -204,12 +194,12 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
var responseMessage mDNS.Msg
if response.ContentLength > 0 {
responseBuffer := buf.NewSize(int(response.ContentLength))
defer responseBuffer.Release()
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
if err != nil {
return nil, err
}
err = responseMessage.Unpack(responseBuffer.Bytes())
responseBuffer.Release()
} else {
rawMessage, err = io.ReadAll(response.Body)
if err != nil {

View File

@@ -76,9 +76,6 @@ func (t *Transport) Close() error {
return nil
}
func (t *Transport) Reset() {
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if t.resolved != nil {
resolverObject := t.resolved.Object()

View File

@@ -43,7 +43,7 @@ type Transport struct {
type dhcpTransport interface {
adapter.DNSTransport
Fetch() []M.Socksaddr
Fetch() ([]M.Socksaddr, error)
Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error)
}
@@ -74,12 +74,14 @@ func (t *Transport) Start(stage adapter.StartStage) error {
break
}
}
if t.fallback {
t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger)
if t.dhcpTransport != nil {
err := t.dhcpTransport.Start(stage)
if err != nil {
return err
if !C.IsIos {
if t.fallback {
t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger)
if t.dhcpTransport != nil {
err := t.dhcpTransport.Start(stage)
if err != nil {
return err
}
}
}
}
@@ -92,12 +94,6 @@ func (t *Transport) Close() error {
)
}
func (t *Transport) Reset() {
if t.dhcpTransport != nil {
t.dhcpTransport.Reset()
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
@@ -109,10 +105,12 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
if !t.fallback {
return t.exchange(ctx, message, question.Name)
}
if t.dhcpTransport != nil {
dhcpTransports := t.dhcpTransport.Fetch()
if len(dhcpTransports) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
if !C.IsIos {
if t.dhcpTransport != nil {
dhcpTransports, _ := t.dhcpTransport.Fetch()
if len(dhcpTransports) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
}
}
}
if t.preferGo {
@@ -136,5 +134,9 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
}
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.")
if C.IsIos {
return nil, E.New("only A and AAAA queries are supported on iOS and tvOS when using NetworkExtension.")
} else {
return nil, E.New("only A and AAAA queries are supported on macOS when using NetworkExtension and DHCP unavailable.")
}
}

View File

@@ -8,12 +8,10 @@ import (
"net/http"
"net/url"
"strconv"
"sync"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
@@ -25,7 +23,6 @@ import (
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
sHTTP "github.com/sagernet/sing/protocol/http"
@@ -40,14 +37,11 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
type HTTP3Transport struct {
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
serverAddr M.Socksaddr
tlsConfig *tls.STDConfig
transportAccess sync.Mutex
transport *http3.Transport
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
transport *http3.Transport
}
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
@@ -101,57 +95,33 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
t := &HTTP3Transport{
return &HTTP3Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
logger: logger,
dialer: transportDialer,
destination: &destinationURL,
headers: headers,
serverAddr: serverAddr,
tlsConfig: stdConfig,
}
t.transport = t.newTransport()
return t, nil
}
func (t *HTTP3Transport) newTransport() *http3.Transport {
return &http3.Transport{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if dialErr != nil {
return nil, dialErr
}
quicConn, dialErr := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg)
if dialErr != nil {
conn.Close()
return nil, dialErr
}
return quicConn, nil
transport: &http3.Transport{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
conn, dialErr := transportDialer.DialContext(ctx, N.NetworkUDP, serverAddr)
if dialErr != nil {
return nil, dialErr
}
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg)
},
TLSClientConfig: stdConfig,
},
TLSClientConfig: t.tlsConfig,
}
}, nil
}
func (t *HTTP3Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return dialer.InitializeDetour(t.dialer)
return nil
}
func (t *HTTP3Transport) Close() error {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
return t.transport.Close()
}
func (t *HTTP3Transport) Reset() {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
t.transport.Close()
t.transport = t.newTransport()
}
func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
exMessage := *message
exMessage.Id = 0
@@ -170,10 +140,7 @@ func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
request.Header = t.headers.Clone()
request.Header.Set("Content-Type", transport.MimeType)
request.Header.Set("Accept", transport.MimeType)
t.transportAccess.Lock()
currentTransport := t.transport
t.transportAccess.Unlock()
response, err := currentTransport.RoundTrip(request)
response, err := t.transport.RoundTrip(request)
requestBuffer.Release()
if err != nil {
return nil, err
@@ -185,12 +152,12 @@ func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
var responseMessage mDNS.Msg
if response.ContentLength > 0 {
responseBuffer := buf.NewSize(int(response.ContentLength))
defer responseBuffer.Release()
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
if err != nil {
return nil, err
}
err = responseMessage.Unpack(responseBuffer.Bytes())
responseBuffer.Release()
} else {
rawMessage, err = io.ReadAll(response.Body)
if err != nil {

View File

@@ -3,11 +3,10 @@ package quic
import (
"context"
"errors"
"os"
"sync"
"github.com/sagernet/quic-go"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
@@ -18,6 +17,7 @@ import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@@ -31,14 +31,14 @@ func RegisterTransport(registry *dns.TransportRegistry) {
}
type Transport struct {
*transport.BaseTransport
dns.TransportAdapter
ctx context.Context
logger logger.ContextLogger
dialer N.Dialer
serverAddr M.Socksaddr
tlsConfig tls.Config
connector *transport.Connector[*quic.Conn]
access sync.Mutex
connection *quic.Conn
}
func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
@@ -62,84 +62,38 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
t := &Transport{
BaseTransport: transport.NewBaseTransport(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
logger,
),
ctx: ctx,
dialer: transportDialer,
serverAddr: serverAddr,
tlsConfig: tlsConfig,
}
t.connector = transport.NewConnector(t.CloseContext(), t.dial, transport.ConnectorCallbacks[*quic.Conn]{
IsClosed: func(connection *quic.Conn) bool {
return common.Done(connection.Context())
},
Close: func(connection *quic.Conn) {
connection.CloseWithError(0, "")
},
Reset: func(connection *quic.Conn) {
connection.CloseWithError(0, "")
},
})
return t, nil
}
func (t *Transport) dial(ctx context.Context) (*quic.Conn, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
earlyConnection, err := sQUIC.DialEarly(
ctx,
bufio.NewUnbindPacketConn(conn),
t.serverAddr.UDPAddr(),
t.tlsConfig,
nil,
)
if err != nil {
conn.Close()
return nil, E.Cause(err, "establish QUIC connection")
}
return earlyConnection, nil
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
ctx: ctx,
logger: logger,
dialer: transportDialer,
serverAddr: serverAddr,
tlsConfig: tlsConfig,
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer)
return nil
}
func (t *Transport) Close() error {
return E.Errors(t.BaseTransport.Close(), t.connector.Close())
}
func (t *Transport) Reset() {
t.connector.Reset()
t.access.Lock()
defer t.access.Unlock()
connection := t.connection
if connection != nil {
connection.CloseWithError(0, "")
}
return nil
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, transport.ErrTransportClosed
}
defer t.EndQuery()
var (
conn *quic.Conn
err error
response *mDNS.Msg
)
for i := 0; i < 2; i++ {
conn, err = t.connector.Get(ctx)
conn, err = t.openConnection()
if err != nil {
return nil, err
}
@@ -149,38 +103,58 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
} else if !isQUICRetryError(err) {
return nil, err
} else {
t.connector.Reset()
conn.CloseWithError(quic.ApplicationErrorCode(0), "")
continue
}
}
return nil, err
}
func (t *Transport) openConnection() (*quic.Conn, error) {
connection := t.connection
if connection != nil && !common.Done(connection.Context()) {
return connection, nil
}
t.access.Lock()
defer t.access.Unlock()
connection = t.connection
if connection != nil && !common.Done(connection.Context()) {
return connection, nil
}
conn, err := t.dialer.DialContext(t.ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, err
}
earlyConnection, err := sQUIC.DialEarly(
t.ctx,
bufio.NewUnbindPacketConn(conn),
t.serverAddr.UDPAddr(),
t.tlsConfig,
nil,
)
if err != nil {
return nil, err
}
t.connection = earlyConnection
return earlyConnection, nil
}
func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn *quic.Conn) (*mDNS.Msg, error) {
stream, err := conn.OpenStreamSync(ctx)
if err != nil {
return nil, E.Cause(err, "open stream")
return nil, err
}
defer stream.CancelRead(0)
err = transport.WriteMessage(stream, 0, message)
if err != nil {
stream.Close()
return nil, E.Cause(err, "write request")
return nil, err
}
stream.Close()
response, err := transport.ReadMessage(stream)
if err != nil {
return nil, E.Cause(err, "read response")
}
return response, nil
return transport.ReadMessage(stream)
}
// https://github.com/AdguardTeam/dnsproxy/blob/fd1868577652c639cce3da00e12ca548f421baf1/upstream/upstream_quic.go#L394
func isQUICRetryError(err error) (ok bool) {
if errors.Is(err, os.ErrClosed) {
return true
}
var qAppErr *quic.ApplicationError
if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 {
return true

View File

@@ -62,24 +62,17 @@ func (t *TCPTransport) Close() error {
return nil
}
func (t *TCPTransport) Reset() {
}
func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial TCP connection")
return nil, err
}
defer conn.Close()
err = WriteMessage(conn, 0, message)
if err != nil {
return nil, E.Cause(err, "write request")
return nil, err
}
response, err := ReadMessage(conn)
if err != nil {
return nil, E.Cause(err, "read response")
}
return response, nil
return ReadMessage(conn)
}
func ReadMessage(reader io.Reader) (*mDNS.Msg, error) {

View File

@@ -3,7 +3,6 @@ package transport
import (
"context"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
@@ -29,8 +28,8 @@ func RegisterTLS(registry *dns.TransportRegistry) {
}
type TLSTransport struct {
*BaseTransport
dns.TransportAdapter
logger logger.ContextLogger
dialer tls.Dialer
serverAddr M.Socksaddr
tlsConfig tls.Config
@@ -66,10 +65,11 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
return &TLSTransport{
BaseTransport: NewBaseTransport(adapter, logger),
dialer: tls.NewDialer(dialer, tlsConfig),
serverAddr: serverAddr,
tlsConfig: tlsConfig,
TransportAdapter: adapter,
logger: logger,
dialer: tls.NewDialer(dialer, tlsConfig),
serverAddr: serverAddr,
tlsConfig: tlsConfig,
}
}
@@ -77,59 +77,37 @@ func (t *TLSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer)
}
func (t *TLSTransport) Close() error {
t.access.Lock()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close()
}
t.connections.Init()
t.access.Unlock()
return t.BaseTransport.Close()
}
func (t *TLSTransport) Reset() {
t.access.Lock()
defer t.access.Unlock()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close()
}
t.connections.Init()
return nil
}
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, ErrTransportClosed
}
defer t.EndQuery()
t.access.Lock()
conn := t.connections.PopFront()
t.access.Unlock()
if conn != nil {
response, err := t.exchange(ctx, message, conn)
response, err := t.exchange(message, conn)
if err == nil {
return response, nil
}
t.Logger.DebugContext(ctx, "discarded pooled connection: ", err)
}
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial TLS connection")
return nil, err
}
return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn})
return t.exchange(message, &tlsDNSConn{Conn: tlsConn})
}
func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
if deadline, ok := ctx.Deadline(); ok {
conn.SetDeadline(deadline)
}
func (t *TLSTransport) exchange(message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
conn.queryId++
err := WriteMessage(conn, conn.queryId, message)
if err != nil {
@@ -142,12 +120,6 @@ func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tl
return nil, E.Cause(err, "read response")
}
t.access.Lock()
if t.State() >= StateClosing {
t.access.Unlock()
conn.Close()
return response, nil
}
conn.SetDeadline(time.Time{})
t.connections.PushBack(conn)
t.access.Unlock()
return response, nil

View File

@@ -2,8 +2,9 @@ package transport
import (
"context"
"net"
"os"
"sync"
"sync/atomic"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
@@ -27,23 +28,15 @@ func RegisterUDP(registry *dns.TransportRegistry) {
}
type UDPTransport struct {
*BaseTransport
dialer N.Dialer
serverAddr M.Socksaddr
udpSize atomic.Int32
connector *Connector[*Connection]
callbackAccess sync.RWMutex
queryId uint16
callbacks map[uint16]*udpCallback
}
type udpCallback struct {
access sync.Mutex
response *mDNS.Msg
done chan struct{}
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
serverAddr M.Socksaddr
udpSize int
tcpTransport *TCPTransport
access sync.Mutex
conn *dnsConnection
done chan struct{}
}
func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) {
@@ -61,198 +54,180 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o
return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil
}
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
t := &UDPTransport{
BaseTransport: NewBaseTransport(adapter, logger),
dialer: dialerInstance,
serverAddr: serverAddr,
callbacks: make(map[uint16]*udpCallback),
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
return &UDPTransport{
TransportAdapter: adapter,
logger: logger,
dialer: dialer,
serverAddr: serverAddr,
udpSize: 2048,
tcpTransport: &TCPTransport{
dialer: dialer,
serverAddr: serverAddr,
},
done: make(chan struct{}),
}
t.udpSize.Store(2048)
t.connector = NewSingleflightConnector(t.CloseContext(), t.dial)
return t
}
func (t *UDPTransport) dial(ctx context.Context) (*Connection, error) {
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
conn := WrapConnection(rawConn)
go t.recvLoop(conn)
return conn, nil
}
func (t *UDPTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer)
}
func (t *UDPTransport) Close() error {
return E.Errors(t.BaseTransport.Close(), t.connector.Close())
}
func (t *UDPTransport) Reset() {
t.connector.Reset()
}
func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
start := t.queryId
for {
t.queryId++
if _, exists := t.callbacks[t.queryId]; !exists {
return t.queryId, nil
}
if t.queryId == start {
return 0, E.New("no available query ID")
}
}
t.access.Lock()
defer t.access.Unlock()
close(t.done)
t.done = make(chan struct{})
return nil
}
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, ErrTransportClosed
}
defer t.EndQuery()
response, err := t.exchange(ctx, message)
if err != nil {
return nil, err
}
if response.Truncated {
t.Logger.InfoContext(ctx, "response truncated, retrying with TCP")
return t.exchangeTCP(ctx, message)
}
return response, nil
}
func (t *UDPTransport) exchangeTCP(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial TCP connection")
}
defer conn.Close()
err = WriteMessage(conn, message.Id, message)
if err != nil {
return nil, E.Cause(err, "write request")
}
response, err := ReadMessage(conn)
if err != nil {
return nil, E.Cause(err, "read response")
t.logger.InfoContext(ctx, "response truncated, retrying with TCP")
return t.tcpTransport.Exchange(ctx, message)
}
return response, nil
}
func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
t.access.Lock()
if edns0Opt := message.IsEdns0(); edns0Opt != nil {
udpSize := int32(edns0Opt.UDPSize())
for {
current := t.udpSize.Load()
if udpSize <= current {
break
}
if t.udpSize.CompareAndSwap(current, udpSize) {
t.connector.Reset()
break
}
if udpSize := int(edns0Opt.UDPSize()); udpSize > t.udpSize {
t.udpSize = udpSize
close(t.done)
t.done = make(chan struct{})
}
}
conn, err := t.connector.Get(ctx)
t.access.Unlock()
conn, err := t.open(ctx)
if err != nil {
return nil, err
}
callback := &udpCallback{
done: make(chan struct{}),
}
t.callbackAccess.Lock()
queryId, err := t.nextAvailableQueryId()
if err != nil {
t.callbackAccess.Unlock()
return nil, err
}
t.callbacks[queryId] = callback
t.callbackAccess.Unlock()
defer func() {
t.callbackAccess.Lock()
delete(t.callbacks, queryId)
t.callbackAccess.Unlock()
}()
buffer := buf.NewSize(1 + message.Len())
defer buffer.Release()
exMessage := *message
exMessage.Compress = true
originalId := message.Id
exMessage.Id = queryId
messageId := message.Id
callback := &dnsCallback{
done: make(chan struct{}),
}
conn.access.Lock()
conn.queryId++
exMessage.Id = conn.queryId
conn.callbacks[exMessage.Id] = callback
conn.access.Unlock()
defer func() {
conn.access.Lock()
delete(conn.callbacks, exMessage.Id)
conn.access.Unlock()
}()
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
if err != nil {
return nil, err
}
_, err = conn.Write(rawMessage)
if err != nil {
conn.CloseWithError(err)
return nil, E.Cause(err, "write request")
conn.Close(err)
return nil, err
}
select {
case <-callback.done:
callback.response.Id = originalId
return callback.response, nil
case <-conn.Done():
return nil, conn.CloseError()
case <-t.CloseContext().Done():
return nil, ErrTransportClosed
callback.message.Id = messageId
return callback.message, nil
case <-conn.done:
return nil, conn.err
case <-t.done:
return nil, os.ErrClosed
case <-ctx.Done():
conn.Close(ctx.Err())
return nil, ctx.Err()
}
}
func (t *UDPTransport) recvLoop(conn *Connection) {
func (t *UDPTransport) open(ctx context.Context) (*dnsConnection, error) {
t.access.Lock()
defer t.access.Unlock()
if t.conn != nil {
select {
case <-t.conn.done:
default:
return t.conn, nil
}
}
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, err
}
dnsConn := &dnsConnection{
Conn: conn,
done: make(chan struct{}),
callbacks: make(map[uint16]*dnsCallback),
}
go t.recvLoop(dnsConn)
t.conn = dnsConn
return dnsConn, nil
}
func (t *UDPTransport) recvLoop(conn *dnsConnection) {
for {
buffer := buf.NewSize(int(t.udpSize.Load()))
buffer := buf.NewSize(t.udpSize)
_, err := buffer.ReadOnceFrom(conn)
if err != nil {
buffer.Release()
conn.CloseWithError(err)
conn.Close(err)
return
}
var message mDNS.Msg
err = message.Unpack(buffer.Bytes())
buffer.Release()
if err != nil {
t.Logger.Debug("discarded malformed UDP response: ", err)
continue
conn.Close(err)
return
}
t.callbackAccess.RLock()
callback, loaded := t.callbacks[message.Id]
t.callbackAccess.RUnlock()
conn.access.RLock()
callback, loaded := conn.callbacks[message.Id]
conn.access.RUnlock()
if !loaded {
continue
}
callback.access.Lock()
select {
case <-callback.done:
default:
callback.response = &message
callback.message = &message
close(callback.done)
}
callback.access.Unlock()
}
}
type dnsConnection struct {
net.Conn
access sync.RWMutex
done chan struct{}
closeOnce sync.Once
err error
queryId uint16
callbacks map[uint16]*dnsCallback
}
func (c *dnsConnection) Close(err error) {
c.closeOnce.Do(func() {
c.err = err
close(c.done)
})
c.Conn.Close()
}
type dnsCallback struct {
access sync.Mutex
message *mDNS.Msg
done chan struct{}
}

View File

@@ -2,452 +2,25 @@
icon: material/alert-decagram
---
#### 1.13.0-rc.5
* Add `mipsle`, `mips64le`, `riscv64` and `loong64` support for NaiveProxy outbound
Important changes since 1.12:
* Add NaiveProxy outbound **1**
* Add pre-match support for `auto_redirect` **2**
* Improve `auto_redirect` **3**
* Add Chrome Root Store certificate option **4**
* Add new options for ACME DNS-01 challenge providers **5**
* Add Wi-Fi state support for Linux and Windows **6**
* Add curve preferences, pinned public key SHA256, mTLS and ECH `query_server_name` for TLS options **7**
* Add kTLS support **8**
* Add ICMP echo (ping) proxy support **9**
* Add `interface_address`, `network_interface_address` and `default_interface_address` rule items **10**
* Add `preferred_by` route rule item **11**
* Improve `local` DNS server **12**
* Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for listen and dial fields **13**
* Add `bind_address_no_port` option for dial fields **14**
* Add system interface and relay server options for Tailscale endpoint **15**
* Add Claude Code Multiplexer service **16**
* Add OpenAI Codex Multiplexer service **17**
* Apple/Android: Refactor GUI
* Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs)
* Android: Add support for resisting VPN detection via Xposed
* Drop support for go1.23 **18**
* Drop support for Android 5.0 **19**
* Update uTLS to v1.8.2 **20**
* Update quic-go to v0.59.0
* Update gVisor to v20250811
* Update Tailscale to v1.92.4
**1**:
NaiveProxy outbound now supports QUIC, ECH, UDP over TCP, and configurable QUIC congestion control.
Only available on Apple platforms, Android, Windows and some Linux architectures.
Each Windows release includes `libcronet.dll`
ensure this file is in the same directory as `sing-box.exe` or in a directory listed in `PATH`.
See [NaiveProxy outbound](/configuration/outbound/naive/).
**2**:
`auto_redirect` now allows you to bypass sing-box for connections based on routing rules.
A new rule action `bypass` is introduced to support this feature. When matched during pre-match, the connection will bypass sing-box and connect directly.
This feature requires Linux with `auto_redirect` enabled.
See [Pre-match](/configuration/shared/pre-match/) and [Rule Action](/configuration/route/rule_action/#bypass).
**3**:
`auto_redirect` now rejects MPTCP connections by default to fix compatibility issues.
You can change it to bypass sing-box via the new `exclude_mptcp` option.
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
ensuring traffic is routed to the sing-box table when no route is found in system tables.
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
See [TUN](/configuration/inbound/tun/#exclude_mptcp).
**4**:
Adds `chrome` as a new certificate store option alongside `mozilla`.
Both stores filter out China-based CA certificates.
See [Certificate](/configuration/certificate/#store).
**5**:
See [DNS-01 Challenge](/configuration/shared/dns01_challenge/).
**6**:
sing-box can now monitor Wi-Fi state on Linux and Windows to enable routing rules based on `wifi_ssid` and `wifi_bssid`.
See [Wi-Fi State](/configuration/shared/wifi-state/).
**7**:
See [TLS](/configuration/shared/tls/).
**8**:
Adds `kernel_tx` and `kernel_rx` options for TLS inbound.
Enables kernel-level TLS offloading via `splice(2)` on Linux 5.1+ with TLS 1.3.
See [TLS](/configuration/shared/tls/).
**9**:
sing-box can now proxy ICMP echo (ping) requests.
A new `icmp` network type is available for route rules.
Supported from TUN, WireGuard and Tailscale inbounds to Direct, WireGuard and Tailscale outbounds.
The `reject` action can also reply to ICMP echo requests.
**10**:
New rule items for matching based on interface IP addresses, available in route rules, DNS rules and rule-sets.
**11**:
Matches outbounds' preferred routes.
For Tailscale: MagicDNS domains and peers' allowed IPs. For WireGuard: peers' allowed IPs.
**12**:
The `local` DNS server now uses platform-native resolution:
`getaddrinfo`/libresolv on Apple platforms, systemd-resolved DBus on Linux.
A new `prefer_go` option is available to opt out.
See [Local DNS](/configuration/dns/server/local/).
**13**:
The default TCP keep-alive initial period has been updated from 10 minutes to 5 minutes.
See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive).
**14**:
Adds the Linux socket option `IP_BIND_ADDRESS_NO_PORT` support when explicitly binding to a source address.
This allows reusing the same source port for multiple connections, improving scalability for high-concurrency proxy scenarios.
See [Dial Fields](/configuration/shared/dial/#bind_address_no_port).
**15**:
Tailscale endpoint can now create a system TUN interface to handle traffic directly.
New `relay_server_port` and `relay_server_static_endpoints` options for incoming relay connections.
See [Tailscale endpoint](/configuration/endpoint/tailscale/).
**16**:
CCM (Claude Code Multiplexer) service allows you to access your local Claude Code subscription remotely through custom tokens, eliminating the need for OAuth authentication on remote clients.
See [CCM](/configuration/service/ccm).
**17**:
See [OCM](/configuration/service/ocm).
**18**:
Due to maintenance difficulties, sing-box 1.13.0 requires at least Go 1.24 to compile.
**19**:
Due to maintenance difficulties, sing-box 1.13.0 will be the last version to support Android 5.0,
and only through a separate legacy build (with `-legacy-android-5` suffix).
For standalone binaries, the minimum Android version has been raised to Android 6.0,
since Termux requires Android 7.0 or later.
**20**:
This update fixes missing padding extension for Chrome 120+ fingerprints.
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
use NaiveProxy instead for TLS fingerprint resistance.
#### 1.12.22
* Fixes and improvements
#### 1.13.0-rc.3
* Fixes and improvements
#### 1.12.21
* Fixes and improvements
#### 1.13.0-rc.2
* Fixes and improvements
#### 1.12.20
* Fixes and improvements
#### 1.13.0-rc.1
* Fixes and improvements
#### 1.12.19
* Fixes and improvements
#### 1.13.0-beta.8
* Add fallback routing rule for `auto_redirect` **1**
* Fixes and improvements
**1**:
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
ensuring traffic is routed to the sing-box table when no route is found in system tables.
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
#### 1.12.18
* Add fallback routing rule for `auto_redirect` **1**
* Fixes and improvements
**1**:
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
ensuring traffic is routed to the sing-box table when no route is found in system tables.
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
#### 1.13.0-beta.6
* Update uTLS to v1.8.2 **1**
* Fixes and improvements
**1**:
This update fixes missing padding extension for Chrome 120+ fingerprints.
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
use NaiveProxy instead for TLS fingerprint resistance.
#### 1.12.17
* Update uTLS to v1.8.2 **1**
* Fixes and improvements
**1**:
This update fixes missing padding extension for Chrome 120+ fingerprints.
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
use NaiveProxy instead for TLS fingerprint resistance.
#### 1.13.0-beta.5
* Fixes and improvements
#### 1.12.16
* Fixes and improvements
#### 1.13.0-beta.4
* Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs)
* Android: Add support for resisting VPN detection via Xposed
* Update quic-go to v0.59.0
* Fixes and improvements
#### 1.13.0-beta.2
* Add `bind_address_no_port` option for dial fields **1**
* Fixes and improvements
**1**:
Adds the Linux socket option `IP_BIND_ADDRESS_NO_PORT` support when explicitly binding to a source address.
This allows reusing the same source port for multiple connections, improving scalability for high-concurrency proxy scenarios.
See [Dial Fields](/configuration/shared/dial/#bind_address_no_port).
#### 1.13.0-beta.1
* Add system interface support for Tailscale endpoint **1**
* Fixes and improvements
**1**:
Tailscale endpoint can now create a system TUN interface to handle traffic directly.
See [Tailscale endpoint](/configuration/endpoint/tailscale/#system_interface).
#### 1.12.15
* Fixes and improvements
#### 1.13.0-alpha.36
* Downgrade quic-go to v0.57.1
* Fixes and improvements
#### 1.13.0-alpha.35
* Add pre-match support for `auto_redirect` **1**
* Fixes and improvements
**1**:
`auto_redirect` now allows you to bypass sing-box for connections based on routing rules.
A new rule action `bypass` is introduced to support this feature. When matched during pre-match, the connection will bypass sing-box and connect directly.
This feature requires Linux with `auto_redirect` enabled.
See [Pre-match](/configuration/shared/pre-match/) and [Rule Action](/configuration/route/rule_action/#bypass).
#### 1.13.0-alpha.34
* Add Chrome Root Store certificate option **1**
* Add new options for ACME DNS-01 challenge providers **2**
* Add Wi-Fi state support for Linux and Windows **3**
* Update naiveproxy to 143.0.7499.109
* Update quic-go to v0.58.0
* Update tailscale to v1.92.4
* Drop support for go1.23 **4**
* Drop support for Android 5.0 **5**
**1**:
Adds `chrome` as a new certificate store option alongside `mozilla`.
Both stores filter out China-based CA certificates.
See [Certificate](/configuration/certificate/#store).
**2**:
See [DNS-01 Challenge](/configuration/shared/dns01_challenge/).
**3**:
sing-box can now monitor Wi-Fi state on Linux and Windows to enable routing rules based on `wifi_ssid` and `wifi_bssid`.
See [Wi-Fi State](/configuration/shared/wifi-state/).
**4**:
Due to maintenance difficulties, sing-box 1.13.0 requires at least Go 1.24 to compile.
**5**:
Due to maintenance difficulties, sing-box 1.13.0 will be the last version to support Android 5.0,
and only through a separate legacy build (with `-legacy-android-5` suffix).
For standalone binaries, the minimum Android version has been raised to Android 6.0,
since Termux requires Android 7.0 or later.
#### 1.12.14
* Fixes and improvements
#### 1.13.0-alpha.33
* Fixes and improvements
#### 1.13.0-alpha.32
* Remove `certificate_public_key_sha256` option for NaiveProxy outbound **1**
* Fixes and improvements
**1**:
Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis.
For this reason, and due to maintenance costs, there is no reason to continue supporting `certificate_public_key_sha256`, which was designed to simplify the use of self-signed certificates.
#### 1.13.0-alpha.31
* Add QUIC support for NaiveProxy outbound **1**
* Add QUIC congestion control option for NaiveProxy **2**
* Fixes and improvements
**1**:
NaiveProxy outbound now supports QUIC.
See [NaiveProxy outbound](/configuration/outbound/naive/#quic).
**2**:
NaiveProxy inbound and outbound now supports configurable QUIC congestion control algorithms, including BBR and BBRv2.
See [NaiveProxy inbound](/configuration/inbound/naive/#quic_congestion_control) and [NaiveProxy outbound](/configuration/outbound/naive/#quic_congestion_control).
#### 1.13.0-alpha.30
* Add ECH support for NaiveProxy outbound **1**
* Add `tls.ech.query_server_name` option **2**
* Fix NaiveProxy outbound on Windows **3**
* Add OpenAI Codex Multiplexer service **4**
* Fixes and improvements
**1**:
See [NaiveProxy outbound](/configuration/outbound/naive/#tls).
**2**:
See [TLS](/configuration/shared/tls/#query_server_name).
**3**:
Each Windows release now includes `libcronet.dll`.
Ensure this file is in the same directory as `sing-box.exe` or in a directory listed in `PATH`.
**4**:
See [OCM](/configuration/service/ocm).
#### 1.13.0-alpha.29
* Add UDP over TCP support for naiveproxy outbound **1**
* Fixes and improvements
**1**:
See [NaiveProxy outbound](/configuration/outbound/naive/#udp_over_tcp).
#### 1.13.0-alpha.28
* Add naiveproxy outbound **1**
* Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for dial fields **2**
* Update default TCP keep-alive initial period from 10 minutes to 5 minutes
* Update quic-go to v0.57.1
* Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for dial fields **1**
* Update default TCP keep-alive initial period from 10 minutes to 5 minutes
* Fixes and improvements
**1**:
Only available on Apple platforms, Android, Windows and some Linux architectures.
See [NaiveProxy outbound](/configuration/outbound/naive/).
**2**:
See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive).
* __Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
because system extensions require signatures to function, we have had to temporarily halt its release.__
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
#### 1.12.13
* Fix naive inbound
* Fixes and improvements
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:

View File

@@ -4,10 +4,6 @@ icon: material/new-box
!!! question "Since sing-box 1.12.0"
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [Chrome Root Store](#store)
# Certificate
### Structure
@@ -31,12 +27,11 @@ icon: material/new-box
The default X509 trusted CA certificate list.
| Type | Description |
|--------------------|----------------------------------------------------------------------------------------------------------------|
| `system` (default) | System trusted CA certificates |
| Type | Description |
|--------------------|---------------------------------------------------------------------------------------------------------------|
| `system` (default) | System trusted CA certificates |
| `mozilla` | [Mozilla Included List](https://wiki.mozilla.org/CA/Included_Certificates) with China CA certificates removed |
| `chrome` | [Chrome Root Store](https://g.co/chrome/root-policy) with China CA certificates removed |
| `none` | Empty list |
| `none` | Empty list |
#### certificate

View File

@@ -4,10 +4,6 @@ icon: material/new-box
!!! question "自 sing-box 1.12.0 起"
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [Chrome Root Store](#store)
# 证书
### 结构
@@ -31,12 +27,11 @@ icon: material/new-box
默认的 X509 受信任 CA 证书列表。
| 类型 | 描述 |
|-------------------|--------------------------------------------------------------------------------------------|
| `system`(默认) | 系统受信任的 CA 证书 |
| `mozilla` | [Mozilla 包含列表](https://wiki.mozilla.org/CA/Included_Certificates)(已移除中国 CA 证书) |
| `chrome` | [Chrome Root Store](https://g.co/chrome/root-policy)(已移除中国 CA 证书) |
| `none` | 空列表 |
| 类型 | 描述 |
|--------------------|--------------------------------------------------------------------------------------------|
| `system`(默认) | 系统受信任的 CA 证书 |
| `mozilla` | [Mozilla 包含列表](https://wiki.mozilla.org/CA/Included_Certificates)(已移除中国 CA 证书) |
| `none` | 空列表 |
#### certificate

View File

@@ -1,48 +1,48 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [type](#type)
# DNS Server
### 结构
```json
{
"dns": {
"servers": [
{
"type": "",
"tag": ""
}
]
}
}
```
#### type
DNS 服务器的类型。
| 类型 | 格式 |
|-----------------|---------------------------|
| empty (default) | [Legacy](./legacy/) |
| `local` | [Local](./local/) |
| `hosts` | [Hosts](./hosts/) |
| `tcp` | [TCP](./tcp/) |
| `udp` | [UDP](./udp/) |
| `tls` | [TLS](./tls/) |
| `quic` | [QUIC](./quic/) |
| `https` | [HTTPS](./https/) |
| `h3` | [HTTP/3](./http3/) |
| `dhcp` | [DHCP](./dhcp/) |
| `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) |
| `resolved` | [Resolved](./resolved/) |
#### tag
DNS 服务器的标签。
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [type](#type)
# DNS Server
### 结构
```json
{
"dns": {
"servers": [
{
"type": "",
"tag": ""
}
]
}
}
```
#### type
DNS 服务器的类型。
| 类型 | 格式 |
|-----------------|---------------------------|
| empty (default) | [Legacy](./legacy/) |
| `local` | [Local](./local/) |
| `hosts` | [Hosts](./hosts/) |
| `tcp` | [TCP](./tcp/) |
| `udp` | [UDP](./udp/) |
| `tls` | [TLS](./tls/) |
| `quic` | [QUIC](./quic/) |
| `https` | [HTTPS](./https/) |
| `h3` | [HTTP/3](./http3/) |
| `dhcp` | [DHCP](./dhcp/) |
| `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) |
| `resolved` | [Resolved](./resolved/) |
#### tag
DNS 服务器的标签。

View File

@@ -53,7 +53,7 @@ DNS 服务器的地址。
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto``dhcp://en0` |
| [FakeIP](/zh/configuration/dns/fakeip/) | `fakeip` |
| [FakeIP](/configuration/dns/fakeip/) | `fakeip` |
!!! warning ""

View File

@@ -4,7 +4,7 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [prefer_go](#prefer_go)
:material-plus: [prefer_go](#prefer_go)
!!! question "Since sing-box 1.12.0"

View File

@@ -2,14 +2,6 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [relay_server_port](#relay_server_port)
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
:material-plus: [system_interface](#system_interface)
:material-plus: [system_interface_name](#system_interface_name)
:material-plus: [system_interface_mtu](#system_interface_mtu)
!!! question "Since sing-box 1.12.0"
### Structure
@@ -28,13 +20,8 @@ icon: material/new-box
"exit_node_allow_lan_access": false,
"advertise_routes": [],
"advertise_exit_node": false,
"relay_server_port": 0,
"relay_server_static_endpoints": [],
"system_interface": false,
"system_interface_name": "",
"system_interface_mtu": 0,
"udp_timeout": "5m",
... // Dial Fields
}
```
@@ -102,36 +89,6 @@ Example: `["192.168.1.1/24"]`
Indicates whether the node should advertise itself as an exit node.
#### relay_server_port
!!! question "Since sing-box 1.13.0"
The port to listen on for incoming relay connections from other Tailscale nodes.
#### relay_server_static_endpoints
!!! question "Since sing-box 1.13.0"
Static endpoints to advertise for the relay server.
#### system_interface
!!! question "Since sing-box 1.13.0"
Create a system TUN interface for Tailscale.
#### system_interface_name
!!! question "Since sing-box 1.13.0"
Custom TUN interface name. By default, `tailscale` (or `utun` on macOS) will be used.
#### system_interface_mtu
!!! question "Since sing-box 1.13.0"
Override the TUN MTU. By default, Tailscale's own MTU is used.
#### udp_timeout
UDP NAT expiration time.

View File

@@ -2,14 +2,6 @@
icon: material/new-box
---
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [relay_server_port](#relay_server_port)
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
:material-plus: [system_interface](#system_interface)
:material-plus: [system_interface_name](#system_interface_name)
:material-plus: [system_interface_mtu](#system_interface_mtu)
!!! question "自 sing-box 1.12.0 起"
### 结构
@@ -28,11 +20,6 @@ icon: material/new-box
"exit_node_allow_lan_access": false,
"advertise_routes": [],
"advertise_exit_node": false,
"relay_server_port": 0,
"relay_server_static_endpoints": [],
"system_interface": false,
"system_interface_name": "",
"system_interface_mtu": 0,
"udp_timeout": "5m",
... // 拨号字段
@@ -101,36 +88,6 @@ icon: material/new-box
指示节点是否应将自己通告为出口节点。
#### relay_server_port
!!! question "自 sing-box 1.13.0 起"
监听来自其他 Tailscale 节点的中继连接的端口。
#### relay_server_static_endpoints
!!! question "自 sing-box 1.13.0 起"
为中继服务器通告的静态端点。
#### system_interface
!!! question "自 sing-box 1.13.0 起"
为 Tailscale 创建系统 TUN 接口。
#### system_interface_name
!!! question "自 sing-box 1.13.0 起"
自定义 TUN 接口名。默认使用 `tailscale`macOS 上为 `utun`)。
#### system_interface_mtu
!!! question "自 sing-box 1.13.0 起"
覆盖 TUN 的 MTU。默认使用 Tailscale 自己的 MTU。
#### udp_timeout
UDP NAT 过期时间。
@@ -143,4 +100,4 @@ UDP NAT 过期时间。
Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。

View File

@@ -3,7 +3,7 @@
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [store_rdrc](#store_rdrc)
:material-plus: [rdrc_timeout](#rdrc_timeout)
:material-plus: [rdrc_timeout](#rdrc_timeout)
### Structure

View File

@@ -3,7 +3,7 @@
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [store_rdrc](#store_rdrc)
:material-plus: [rdrc_timeout](#rdrc_timeout)
:material-plus: [rdrc_timeout](#rdrc_timeout)
### 结构

View File

@@ -1,25 +1,20 @@
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [quic_congestion_control](#quic_congestion_control)
### Structure
```json
{
"type": "naive",
"tag": "naive-in",
"network": "udp",
...
// Listen Fields
"type": "naive",
"tag": "naive-in",
"network": "udp",
"users": [
{
"username": "sekai",
"password": "password"
}
],
"quic_congestion_control": "",
"tls": {}
... // Listen Fields
"users": [
{
"username": "sekai",
"password": "password"
}
],
"tls": {}
}
```
@@ -41,23 +36,6 @@ Both if empty.
Naive users.
#### quic_congestion_control
!!! question "Since sing-box 1.13.0"
QUIC congestion control algorithm.
| Algorithm | Description |
|----------------|---------------------------------|
| `bbr` | BBR |
| `bbr_standard` | BBR (Standard version) |
| `bbr2` | BBRv2 |
| `bbr2_variant` | BBRv2 (An experimental variant) |
| `cubic` | CUBIC |
| `reno` | New Reno |
`bbr` is used by default (the default of QUICHE, used by Chromium which NaiveProxy is based on).
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).

View File

@@ -1,25 +1,20 @@
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [quic_congestion_control](#quic_congestion_control)
### 结构
```json
{
"type": "naive",
"tag": "naive-in",
"network": "udp",
"type": "naive",
"tag": "naive-in",
"network": "udp",
... // 监听字段
... // 监听字段
"users": [
{
"username": "sekai",
"password": "password"
}
],
"quic_congestion_control": "",
"tls": {}
"users": [
{
"username": "sekai",
"password": "password"
}
],
"tls": {}
}
```
@@ -41,23 +36,6 @@
Naive 用户。
#### quic_congestion_control
!!! question "Since sing-box 1.13.0"
QUIC 拥塞控制算法。
| 算法 | 描述 |
|----------------|--------------------|
| `bbr` | BBR |
| `bbr_standard` | BBR (标准版) |
| `bbr2` | BBRv2 |
| `bbr2_variant` | BBRv2 (一种试验变体) |
| `cubic` | CUBIC |
| `reno` | New Reno |
默认使用 `bbr`NaiveProxy 基于的 Chromium 使用的 QUICHE 的默认值)。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -9,7 +9,6 @@
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg==",
"managed": false,
"multiplex": {}
}
```
@@ -87,10 +86,6 @@ Both if empty.
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
| other methods | any string |
#### managed
Defaults to `false`. Enable this when the inbound is managed by the [SSM API](/configuration/service/ssm-api) for dynamic user.
#### multiplex
See [Multiplex](/configuration/shared/multiplex#inbound) for details.

View File

@@ -9,7 +9,6 @@
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg==",
"managed": false,
"multiplex": {}
}
```
@@ -49,9 +48,9 @@
}
```
### 监听字段
### Listen Fields
参阅 [监听字段](/zh/configuration/shared/listen/)
See [Listen Fields](/configuration/shared/listen/) for details.
### 字段
@@ -87,10 +86,6 @@
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
| other methods | 任意字符串 |
#### managed
默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。

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