Compare commits

..

39 Commits

Author SHA1 Message Date
世界
766972ce52 Add Linux WI-FI state support 2025-10-07 23:07:08 +08:00
世界
f1e48a1cad A bit of refactor 2025-10-07 21:20:57 +08:00
世界
ba496ae300 documentation: Bump version 2025-10-07 14:03:15 +08:00
世界
4488148322 Add curve preferences, pinned public key SHA256 and mTLS for TLS options 2025-10-07 14:02:00 +08:00
世界
f086454f81 Fix WireGuard input packet 2025-10-07 14:02:00 +08:00
世界
f9b6a068ee Update tfo-go to latest 2025-10-07 14:02:00 +08:00
世界
0ad2a441d9 Remove compatibility codes 2025-10-07 14:02:00 +08:00
世界
c714b59c87 Do not use linkname by default to simplify debugging 2025-10-07 14:01:59 +08:00
世界
bd6b125707 documentation: Update chinese translations 2025-10-07 14:01:59 +08:00
世界
d1109cee90 Update quic-go to v0.54.0 2025-10-07 14:01:58 +08:00
世界
b48002b4db Update WireGuard and Tailscale 2025-10-07 14:01:58 +08:00
世界
67cedfd927 Fix preConnectionCopy 2025-10-07 14:01:58 +08:00
世界
853b576d12 Fix ping domain 2025-10-07 14:01:57 +08:00
世界
05cd7f6192 release: Fix linux build 2025-10-07 14:01:57 +08:00
世界
e4cf55a86d Improve ktls rx error handling 2025-10-07 14:01:56 +08:00
世界
2bf605fad4 Improve compatibility for kTLS 2025-10-07 14:01:56 +08:00
世界
b913899d43 ktls: Add warning for inappropriate scenarios 2025-10-07 14:01:56 +08:00
世界
e1a22c0cc2 Add support for kTLS
Reference: https://gitlab.com/go-extension/tls
2025-10-07 14:01:56 +08:00
世界
d890c8e8d7 Add proxy support for ICMP echo request 2025-10-07 14:01:33 +08:00
世界
1b4ffff67c Fix resolve using resolved 2025-10-07 14:01:33 +08:00
世界
6d012c04cd documentation: Update behavior of local DNS server on darwin 2025-10-07 14:01:33 +08:00
世界
c1309b63c9 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-10-07 14:01:33 +08:00
世界
6674b252bf Remove use of ldflags -checklinkname=0 on darwin 2025-10-07 14:01:33 +08:00
世界
8c7b2e4ac3 Fix 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-10-07 14:01:32 +08:00
世界
f400c927c4 Fix legacy DNS config 2025-10-07 14:01:32 +08:00
世界
587d330b58 Fix rule-set format 2025-10-07 14:01:32 +08:00
世界
719a28920a documentation: Remove outdated icons 2025-10-07 14:01:32 +08:00
世界
2e0d344a3d documentation: Improve local DNS server 2025-10-07 14:01:32 +08:00
世界
387084b7c7 Use libresolv in local DNS server on darwin 2025-10-07 14:01:31 +08:00
世界
e15c02ee33 Use resolved in local DNS server if available 2025-10-07 14:01:31 +08:00
xchacha20-poly1305
2648b80c6b Fix rule set version 2025-10-07 14:01:30 +08:00
世界
391f7a73d2 documentation: Add preferred_by route rule item 2025-10-07 14:01:30 +08:00
世界
c1565e2bb0 Add preferred_by route rule item 2025-10-07 14:01:30 +08:00
世界
605d8b41f9 documentation: Add interface address rule items 2025-10-07 14:01:30 +08:00
世界
03d8243d41 Add interface address rule items 2025-10-07 14:01:30 +08:00
世界
30f1beb071 Fix ECH retry support 2025-10-07 14:01:12 +08:00
neletor
3841d9cb5a Add support for ech retry configs 2025-10-07 14:01:07 +08:00
Zephyruso
dbb7345ec9 Add /dns/flush-clash meta api 2025-10-07 14:01:00 +08:00
世界
88f000412b Improve HTTPS DNS transport 2025-10-07 14:00:44 +08:00
245 changed files with 4435 additions and 17295 deletions

View File

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

View File

@@ -1 +0,0 @@
abd78bb191a815236485ad929716845ffb41465a

View File

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

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 main
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 - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.7 go-version: ^1.25.1
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -69,66 +69,53 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- { os: linux, arch: amd64, variant: purego, naive: true } - { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: amd64, variant: glibc, naive: true } - { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
- { 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: "386", go386: softfloat, openwrt: "i386_pentium-mmx" } - { 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: "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: "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: 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: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
- { os: linux, arch: mipsle, gomips: softfloat } - { 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: 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: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x } - { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le } - { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64 } - { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64 } - { 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: 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: "386", legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: arm64 }
- { os: android, arch: arm64, ndk: "aarch64-linux-android23" } - { os: darwin, arch: amd64 }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi23" } - { os: darwin, arch: arm64 }
- { os: android, arch: amd64, ndk: "x86_64-linux-android23" } - { os: darwin, arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
- { 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: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }} if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.7 go-version: ^1.25.1
- name: Setup Go 1.24 - name: Setup Go 1.24
if: matrix.legacy_go124 if: matrix.legacy_go124
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.24.10 go-version: ~1.24.6
- name: Cache Go for Windows 7 - name: Cache Go for Windows 7
if: matrix.legacy_win7 if: matrix.legacy_win7
id: cache-go-for-windows7 id: cache-go-for-windows7
@@ -136,7 +123,7 @@ jobs:
with: with:
path: | path: |
~/go/go_win7 ~/go/go_win7
key: go_win7_1255 key: go_win7_1251
- name: Setup Go for Windows 7 - name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true' if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
run: |- run: |-
@@ -152,54 +139,6 @@ jobs:
with: with:
ndk-version: r28 ndk-version: r28
local-cache: true 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 - name: Set tag
run: |- run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -207,79 +146,15 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail 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' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.naive }}" == "true" ]]; 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}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build (purego) - name: Build
if: matrix.variant == 'purego' if: matrix.os != 'android'
run: | run: |
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ 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"
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' \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "0" CGO_ENABLED: "0"
@@ -299,7 +174,7 @@ jobs:
export CXX="${CC}++" export CXX="${CC}++"
mkdir -p dist mkdir -p dist
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ 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 ./cmd/sing-box
env: env:
CGO_ENABLED: "1" CGO_ENABLED: "1"
@@ -318,11 +193,6 @@ jobs:
elif [[ -n "${{ matrix.legacy_name }}" ]]; then elif [[ -n "${{ matrix.legacy_name }}" ]]; then
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}" DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
fi 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}" echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}" PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}" PKG_VERSION="${PKG_VERSION//-/\~}"
@@ -405,177 +275,15 @@ jobs:
zip -r "${DIR_NAME}.zip" "${DIR_NAME}" zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
else else
cp sing-box "${DIR_NAME}" cp sing-box "${DIR_NAME}"
if [ -f libcronet.so ]; then
cp libcronet.so "${DIR_NAME}"
fi
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}" tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
fi fi
rm -r "${DIR_NAME}" rm -r "${DIR_NAME}"
- name: Cleanup
run: rm -f dist/sing-box dist/libcronet.so
- 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) }}
path: "dist"
build_darwin:
name: Build Darwin binaries
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
runs-on: macos-latest
needs:
- calculate_version
strategy:
matrix:
include:
- { arch: amd64 }
- { arch: arm64 }
- { arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! matrix.legacy_go124 }}
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.6
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: 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
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' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: darwin
GOARCH: ${{ matrix.arch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set name
run: |-
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-darwin-${{ matrix.arch }}"
if [[ -n "${{ matrix.legacy_name }}" ]]; then
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
- name: Archive
run: |
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 - name: Cleanup
run: rm dist/sing-box run: rm dist/sing-box
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: binary-darwin_${{ matrix.arch }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }} 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_windows:
name: Build Windows binaries
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
runs-on: windows-latest
needs:
- calculate_version
strategy:
matrix:
include:
- { arch: amd64, naive: true }
- { arch: "386" }
- { arch: arm64, naive: true }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
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 tag v${{ needs.calculate_version.outputs.version }} -f
- name: Build
if: matrix.naive
run: |
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" `
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: windows
GOARCH: ${{ matrix.arch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build
if: ${{ !matrix.naive }}
run: |
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" `
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: windows
GOARCH: ${{ matrix.arch }}
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: 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"
- 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
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-windows_${{ matrix.arch }}
path: "dist" path: "dist"
build_android: build_android:
name: Build Android name: Build Android
@@ -592,7 +300,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.7 go-version: ^1.25.1
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -640,9 +348,9 @@ jobs:
- name: Build - name: Build
run: |- run: |-
mkdir clients/android/app/libs mkdir clients/android/app/libs
cp *.aar clients/android/app/libs cp libbox.aar clients/android/app/libs
cd clients/android cd clients/android
./gradlew :app:assembleOtherRelease :app:assembleOtherLegacyRelease ./gradlew :app:assemblePlayRelease :app:assembleOtherRelease
env: env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
@@ -650,18 +358,8 @@ jobs:
- name: Prepare upload - name: Prepare upload
run: |- run: |-
mkdir -p dist mkdir -p dist
#cp clients/android/app/build/outputs/apk/play/release/*.apk 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/other/release/*-universal.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
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -682,7 +380,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.7 go-version: ^1.25.1
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -723,7 +421,7 @@ jobs:
run: |- run: |-
go run -v ./cmd/internal/update_android_version --ci go run -v ./cmd/internal/update_android_version --ci
mkdir clients/android/app/libs mkdir clients/android/app/libs
cp *.aar clients/android/app/libs cp libbox.aar clients/android/app/libs
cd clients/android cd clients/android
echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json
./gradlew :app:publishPlayReleaseBundle ./gradlew :app:publishPlayReleaseBundle
@@ -735,7 +433,7 @@ jobs:
build_apple: build_apple:
name: Build Apple clients name: Build Apple clients
runs-on: macos-26 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: needs:
- calculate_version - calculate_version
strategy: strategy:
@@ -781,7 +479,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.7 go-version: ^1.25.1
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-
@@ -900,7 +598,7 @@ jobs:
--app-drop-link 0 0 \ --app-drop-link 0 0 \
--skip-jenkins \ --skip-jenkins \
SFM.dmg "${{ matrix.export_path }}/SFM.app" 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 }}" cd "${{ matrix.archive }}"
zip -r SFM.dSYMs.zip dSYMs zip -r SFM.dSYMs.zip dSYMs
popd popd
@@ -921,8 +619,6 @@ jobs:
needs: needs:
- calculate_version - calculate_version
- build - build
- build_darwin
- build_windows
- build_android - build_android
- build_apple - build_apple
steps: steps:

View File

@@ -1,10 +1,6 @@
name: Publish Docker Images name: Publish Docker Images
on: on:
#push:
# branches:
# - main-next
# - dev-next
release: release:
types: types:
- published - published
@@ -17,146 +13,8 @@ env:
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
jobs: jobs:
build_binary: build:
name: Build binary
runs-on: ubuntu-latest 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: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
@@ -166,11 +24,9 @@ jobs:
- linux/arm/v7 - linux/arm/v7
- linux/arm64 - linux/arm64
- linux/386 - linux/386
- linux/mipsle
- linux/ppc64le - linux/ppc64le
- linux/riscv64 - linux/riscv64
- linux/s390x - linux/s390x
- linux/loong64
steps: steps:
- name: Get commit to build - name: Get commit to build
id: ref id: ref
@@ -191,16 +47,6 @@ jobs:
run: | run: |
platform=${{ matrix.platform }} platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV 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 - name: Setup QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx - name: Setup Docker Buildx
@@ -222,7 +68,8 @@ jobs:
with: with:
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
context: . context: .
file: Dockerfile.binary build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest - name: Export digest
@@ -240,7 +87,7 @@ jobs:
merge: merge:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build_docker - build
steps: steps:
- name: Get commit to build - name: Get commit to build
id: ref id: ref
@@ -274,7 +121,6 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push - name: Create manifest list and push
if: github.event_name != 'push'
working-directory: /tmp/digests working-directory: /tmp/digests
run: | run: |
docker buildx imagetools create \ docker buildx imagetools create \
@@ -282,7 +128,6 @@ jobs:
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \ -t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image - name: Inspect image
if: github.event_name != 'push'
run: | run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }} docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }} docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}

View File

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

View File

@@ -1,10 +1,6 @@
name: Build Linux Packages name: Build Linux Packages
on: on:
#push:
# branches:
# - main-next
# - dev-next
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version: version:
@@ -34,7 +30,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.7 go-version: ^1.25.1
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -56,19 +52,17 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
# Naive-enabled builds (musl) - { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: amd64, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64 } - { os: linux, arch: "386", debian: i386, rpm: i386 }
- { 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: arm, goarm: "6", debian: armel, rpm: armv6hl } - { 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: mips64le, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x } - { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le } - { 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: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
@@ -77,47 +71,13 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.7 go-version: ^1.25.1
- name: Clone cronet-go - name: Setup Android NDK
if: matrix.naive if: matrix.os == 'android'
run: | uses: nttld/setup-ndk@v1
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: with:
path: | ndk-version: r28
~/cronet-go/naiveproxy/src/third_party/llvm-build/ local-cache: true
~/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
- name: Set tag - name: Set tag
run: |- run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" 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 - name: Set build tags
run: | run: |
set -xeuo pipefail 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' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS="${TAGS},with_naive_outbound,with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build (naive) - name: Build
if: matrix.naive
run: | run: |
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ 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"
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' \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "0" CGO_ENABLED: "0"
@@ -245,6 +185,5 @@ jobs:
path: dist path: dist
merge-multiple: true merge-multiple: true
- name: Publish packages - name: Publish packages
if: github.event_name != 'push'
run: |- run: |-
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/ 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 /*.jar
/*.aar /*.aar
/*.xcframework/ /*.xcframework/
/experimental/libbox/*.aar
/experimental/libbox/*.xcframework/
/experimental/libbox/*.nupkg
.DS_Store .DS_Store
/config.d/ /config.d/
/venv/ /venv/

103
.goreleaser.fury.yaml Normal file
View File

@@ -0,0 +1,103 @@
project_name: sing-box
builds:
- id: main
main: ./cmd/sing-box
flags:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
targets:
- linux_386
- linux_amd64_v1
- linux_arm64
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips64le
mod_timestamp: '{{ .CommitTimestamp }}'
snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}"
nfpms:
- &template
id: package
package_name: sing-box
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds:
- main
homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform.
license: GPLv3 or later
formats:
- deb
- rpm
priority: extra
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: "config|noreplace"
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
deb:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
fields:
Bugs: https://github.com/SagerNet/sing-box/issues
rpm:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
conflicts:
- sing-box-beta
- id: package_beta
<<: *template
package_name: sing-box-beta
file_name_template: '{{ .ProjectName }}-beta_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
formats:
- deb
- rpm
conflicts:
- sing-box
release:
disable: true
furies:
- account: sagernet
ids:
- package
disable: "{{ not (not .Prerelease) }}"
- account: sagernet
ids:
- package_beta
disable: "{{ not .Prerelease }}"

213
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,213 @@
version: 2
project_name: sing-box
builds:
- &template
id: main
main: ./cmd/sing-box
flags:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
- linux_arm64
- linux_arm_6
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips64le
- windows_amd64_v1
- windows_386
- windows_arm64
- darwin_amd64_v1
- darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}'
- id: legacy
<<: *template
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go_legacy
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
targets:
- windows_amd64_v1
- windows_386
- id: android
<<: *template
env:
- CGO_ENABLED=1
- GOTOOLCHAIN=local
overrides:
- goos: android
goarch: arm
goarm: 7
env:
- CC=armv7a-linux-androideabi21-clang
- CXX=armv7a-linux-androideabi21-clang++
- goos: android
goarch: arm64
env:
- CC=aarch64-linux-android21-clang
- CXX=aarch64-linux-android21-clang++
- goos: android
goarch: 386
env:
- CC=i686-linux-android21-clang
- CXX=i686-linux-android21-clang++
- goos: android
goarch: amd64
goamd64: v1
env:
- CC=x86_64-linux-android21-clang
- CXX=x86_64-linux-android21-clang++
targets:
- android_arm_7
- android_arm64
- android_386
- android_amd64
archives:
- &template
id: archive
builds:
- main
- android
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
wrap_in_directory: true
files:
- LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: archive-legacy
<<: *template
builds:
- legacy
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
nfpms:
- id: package
package_name: sing-box
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds:
- main
homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform.
license: GPLv3 or later
formats:
- deb
- rpm
- archlinux
# - apk
# - ipk
priority: extra
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: "config|noreplace"
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
deb:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
fields:
Bugs: https://github.com/SagerNet/sing-box/issues
rpm:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
overrides:
apk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/sing-box.initd
dst: /etc/init.d/sing-box
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
ipk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/openwrt.init
dst: /etc/init.d/sing-box
- src: release/config/openwrt.conf
dst: /etc/config/sing-box
source:
enabled: false
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
prefix_template: '{{ .ProjectName }}-{{ .Version }}/'
checksum:
disable: true
name_template: '{{ .ProjectName }}-{{ .Version }}.checksum'
signs:
- artifacts: checksum
release:
github:
owner: SagerNet
name: sing-box
draft: true
prerelease: auto
mode: replace
ids:
- archive
- package
skip_upload: true
partial:
by: target

View File

@@ -13,13 +13,15 @@ RUN set -ex \
&& export COMMIT=$(git rev-parse --short HEAD) \ && export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \ && export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \ && 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,badlinkname,tfogo_checklinkname0" \
-o /go/bin/sing-box \ -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 ./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine AS dist FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \ RUN set -ex \
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables && apk upgrade \
&& apk add bash tzdata ca-certificates nftables \
&& rm -rf /var/cache/apk/*
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"] ENTRYPOINT ["sing-box"]

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 NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) 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,badlinkname,tfogo_checklinkname0
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) 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) 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_PARAMS = $(PARAMS) -tags "$(TAGS)"
MAIN = ./cmd/sing-box MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH) PREFIX ?= $(shell go env GOPATH)
SING_FFI ?= sing-ffi
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
.PHONY: test release docs build .PHONY: test release docs build
@@ -39,11 +37,8 @@ fmt:
@gofmt -s -w . @gofmt -s -w .
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" . @gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
fmt_docs:
go run ./cmd/internal/format_docs
fmt_install: 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 go install -v github.com/daixiang0/gci@latest
lint: lint:
@@ -54,7 +49,7 @@ lint:
GOOS=freebsd golangci-lint run ./... GOOS=freebsd golangci-lint run ./...
lint_install: 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: proto:
@go run ./cmd/internal/protogen @go run ./cmd/internal/protogen
@@ -91,12 +86,12 @@ update_android_version:
go run ./cmd/internal/update_android_version go run ./cmd/internal/update_android_version
build_android: 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: upload_android:
mkdir -p dist/release_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/play/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/other/release/*-universal.apk dist/release_android
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
rm -rf dist/release_android rm -rf dist/release_android
@@ -111,7 +106,7 @@ build_ios:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFI.xcarchive && \ rm -rf build/SFI.xcarchive && \
xcodebuild clean -scheme SFI && \ 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: upload_ios_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
@@ -132,7 +127,7 @@ release_ios: build_ios upload_ios_app_store
build_macos: build_macos:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFM.xcarchive && \ 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: upload_macos_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
@@ -141,50 +136,54 @@ upload_macos_app_store:
release_macos: build_macos upload_macos_app_store release_macos: build_macos upload_macos_app_store
build_macos_standalone: 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: build_macos_dmg:
$(MAKE) -C ../sing-box-for-apple build_macos_dmg rm -rf dist/SFM
mkdir -p dist/SFM
build_macos_pkg: cd ../sing-box-for-apple && \
$(MAKE) -C ../sing-box-for-apple build_macos_pkg 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: notarize_macos_dmg:
$(MAKE) -C ../sing-box-for-apple notarize_macos_dmg xcrun notarytool submit "dist/SFM/SFM.dmg" --wait \
--keychain-profile "notarytool-password" \
notarize_macos_pkg: --no-s3-acceleration
$(MAKE) -C ../sing-box-for-apple notarize_macos_pkg
upload_macos_dmg: upload_macos_dmg:
mkdir -p dist/SFM cd dist/SFM && \
cp ../sing-box-for-apple/build/SFM-Apple.dmg "dist/SFM/SFM-${VERSION}-Apple.dmg" cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \
cp ../sing-box-for-apple/build/SFM-Intel.dmg "dist/SFM/SFM-${VERSION}-Intel.dmg" ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.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"
upload_macos_dsyms: upload_macos_dsyms:
mkdir -p dist/SFM pushd ../sing-box-for-apple/build/SFM.System.xcarchive && \
cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs 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" mv SFM.dSYMs.zip ../../../sing-box/dist/SFM && \
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip" 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: build_tvos:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFT.xcarchive && \ 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: upload_tvos_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
@@ -208,12 +207,12 @@ update_apple_version:
update_macos_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 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 release_apple_beta: update_apple_version release_ios release_macos release_tvos
publish_testflight: 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: prepare_app_store:
go run -v ./cmd/internal/app_store_connect prepare_app_store go run -v ./cmd/internal/app_store_connect prepare_app_store
@@ -236,21 +235,22 @@ test_stdio:
lib_android: lib_android:
go run ./cmd/internal/build_libbox -target android go run ./cmd/internal/build_libbox -target android
lib_android_debug:
go run ./cmd/internal/build_libbox -target android -debug
lib_apple: lib_apple:
go run ./cmd/internal/build_libbox -target apple go run ./cmd/internal/build_libbox -target apple
lib_windows: lib_ios:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp go run ./cmd/internal/build_libbox -target apple -platform ios -debug
lib_android_new: lib:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android go run ./cmd/internal/build_libbox -target android
go run ./cmd/internal/build_libbox -target ios
lib_apple_new:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
lib_install: lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@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.11 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.8
docs: docs:
venv/bin/mkdocs serve venv/bin/mkdocs serve
@@ -270,6 +270,3 @@ update:
git fetch git fetch
git reset FETCH_HEAD --hard git reset FETCH_HEAD --hard
git clean -fdx git clean -fdx
%:
@:

View File

@@ -1,11 +1,3 @@
> Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents
<a href="https://go.warp.dev/sing-box">
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
</a>
---
# sing-box # sing-box
The universal proxy platform. The universal proxy platform.

View File

@@ -27,6 +27,8 @@ type DNSClient interface {
Start() Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) 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) 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() ClearCache()
} }
@@ -68,7 +70,6 @@ type DNSTransport interface {
Type() string Type() string
Tag() string Tag() string
Dependencies() []string Dependencies() []string
Reset()
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,11 +21,10 @@ import (
type Router interface { type Router interface {
Lifecycle Lifecycle
ConnectionRouter 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 ConnectionRouterEx
RuleSet(tag string) (RuleSet, bool) RuleSet(tag string) (RuleSet, bool)
Rules() []Rule Rules() []Rule
NeedFindProcess() bool
AppendTracker(tracker ConnectionTracker) AppendTracker(tracker ConnectionTracker)
ResetNetwork() ResetNetwork()
} }

View File

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

View File

@@ -73,7 +73,7 @@ func NewUpstreamContextHandlerEx(
} }
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, myMetadata := ExtendContext(ctx) myMetadata := ContextFrom(ctx)
if source.IsValid() { if source.IsValid() {
myMetadata.Source = source myMetadata.Source = source
} }
@@ -84,7 +84,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
} }
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, myMetadata := ExtendContext(ctx) myMetadata := ContextFrom(ctx)
if source.IsValid() { if source.IsValid() {
myMetadata.Source = source myMetadata.Source = source
} }
@@ -146,7 +146,7 @@ type routeContextHandlerWrapperEx struct {
} }
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, metadata := ExtendContext(ctx) metadata := ContextFrom(ctx)
if source.IsValid() { if source.IsValid() {
metadata.Source = source metadata.Source = source
} }
@@ -157,7 +157,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
} }
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, metadata := ExtendContext(ctx) metadata := ContextFrom(ctx)
if source.IsValid() { if source.IsValid() {
metadata.Source = source metadata.Source = source
} }

49
box.go
View File

@@ -443,15 +443,15 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return E.Cause(err, "start logger") 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 { if err != nil {
return err 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 { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@@ -463,27 +463,27 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService) err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err 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 { if err != nil {
return err return err
} }
err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService) err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService) err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
if err != nil { if err != nil {
return err return err
} }
@@ -497,42 +497,17 @@ func (s *Box) Close() error {
default: default:
close(s.done) close(s.done)
} }
var err error err := common.Close(
for _, closeItem := range []struct { s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
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)")
}
for _, lifecycleService := range s.internalService { for _, lifecycleService := range s.internalService {
s.logger.Trace("close ", lifecycleService.Name())
startTime := time.Now()
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name()) 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 { err = E.Append(err, s.logFactory.Close(), func(err error) error {
return E.Cause(err, "close logger") return E.Cause(err, "close logger")
}) })
s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
return err return err
} }

View File

@@ -100,32 +100,11 @@ findVersion:
} }
func publishTestflight(ctx context.Context) error { 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() tagVersion, err := build_shared.ReadTagVersion()
if err != nil { if err != nil {
return err return err
} }
tag := tagVersion.VersionString() 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) client := createClient(20 * time.Minute)
log.Info(tag, " list build IDs") 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 { buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
return it.ID 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 waitingForProcess := false
log.Info(string(platform), " list builds") for _, platform := range platforms {
for { log.Info(string(platform), " list builds")
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{ for {
FilterApp: []string{appID}, builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterPreReleaseVersionPlatform: []string{string(platform)}, 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))
if err != nil { if err != nil {
return err return err
} }
} build := builds.Data[0]
log.Info(string(platform), " ", tag, " publish") if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID}) log.Info(string(platform), " ", tag, " waiting for process")
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) { time.Sleep(15 * time.Second)
log.Info("waiting for process") continue
time.Sleep(15 * time.Second) }
continue if *build.Attributes.ProcessingState != "VALID" {
} else if err != nil { waitingForProcess = true
return err log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
} time.Sleep(15 * time.Second)
log.Info(string(platform), " ", tag, " list submissions") continue
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{ }
FilterBuild: []string{build.ID}, log.Info(string(platform), " ", tag, " list localizations")
}) localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
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 err != nil {
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") { return err
log.Error(err) }
break 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 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 return nil
} }

View File

@@ -5,7 +5,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
_ "github.com/sagernet/gomobile" _ "github.com/sagernet/gomobile"
@@ -17,17 +16,17 @@ import (
) )
var ( var (
debugEnabled bool debugEnabled bool
target string target string
platform string platform string
// withTailscale bool withTailscale bool
) )
func init() { func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug") flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform") flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify 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() { func main() {
@@ -47,8 +46,8 @@ var (
sharedFlags []string sharedFlags []string
debugFlags []string debugFlags []string
sharedTags []string sharedTags []string
darwinTags []string macOSTags []string
// memcTags []string memcTags []string
notMemcTags []string notMemcTags []string
debugTags []string debugTags []string
) )
@@ -60,38 +59,19 @@ func init() {
if err != nil { if err != nil {
currentTag = "unknown" 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") 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+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -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") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace") macOSTags = append(macOSTags, "with_dhcp")
// memcTags = append(memcTags, "with_tailscale") 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")
notMemcTags = append(notMemcTags, "with_low_memory") notMemcTags = append(notMemcTags, "with_low_memory")
debugTags = append(debugTags, "debug") debugTags = append(debugTags, "debug")
} }
type AndroidBuildConfig struct { func buildAndroid() {
AndroidAPI int build_shared.FindSDK()
OutputName string
Tags []string
}
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 var javaPath string
javaHome := os.Getenv("JAVA_HOME") javaHome := os.Getenv("JAVA_HOME")
if javaHome == "" { if javaHome == "" {
@@ -107,24 +87,21 @@ func checkJavaVersion() {
if !strings.Contains(javaVersion, "openjdk 17") { if !strings.Contains(javaVersion, "openjdk 17") {
log.Fatal("java version should be openjdk 17") log.Fatal("java version should be openjdk 17")
} }
}
func getAndroidBindTarget() string { var bindTarget string
if platform != "" { if platform != "" {
return platform bindTarget = platform
} else if debugEnabled { } else if debugEnabled {
return "android/arm64" bindTarget = "android/arm64"
} else {
bindTarget = "android"
} }
return "android"
}
func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
args := []string{ args := []string{
"bind", "bind",
"-v", "-v",
"-o", config.OutputName,
"-target", bindTarget, "-target", bindTarget,
"-androidapi", strconv.Itoa(config.AndroidAPI), "-androidapi", "21",
"-javapkg=io.nekohasekai", "-javapkg=io.nekohasekai",
"-libname=box", "-libname=box",
} }
@@ -135,59 +112,34 @@ func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
args = append(args, debugFlags...) 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") args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
command.Stdout = os.Stdout command.Stdout = os.Stdout
command.Stderr = os.Stderr command.Stderr = os.Stderr
err := command.Run() err = command.Run()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.IsDir(copyPath) { if rw.IsDir(copyPath) {
copyPath, _ = filepath.Abs(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 { if err != nil {
log.Fatal(err) 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() { func buildApple() {
var bindTarget string var bindTarget string
if platform != "" { if platform != "" {
@@ -195,7 +147,7 @@ func buildApple() {
} else if debugEnabled { } else if debugEnabled {
bindTarget = "ios" bindTarget = "ios"
} else { } else {
bindTarget = "ios,iossimulator,tvos,tvossimulator,macos" bindTarget = "ios,tvos,macos"
} }
args := []string{ args := []string{
@@ -205,9 +157,11 @@ func buildApple() {
"-libname=box", "-libname=box",
"-tags-not-macos=with_low_memory", "-tags-not-macos=with_low_memory",
} }
//if !withTailscale { if !withTailscale {
// args = append(args, "-tags-macos="+strings.Join(memcTags, ",")) args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ","))
//} } else {
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
}
if !debugEnabled { if !debugEnabled {
args = append(args, sharedFlags...) args = append(args, sharedFlags...)
@@ -215,10 +169,10 @@ func buildApple() {
args = append(args, debugFlags...) args = append(args, debugFlags...)
} }
tags := append(sharedTags, darwinTags...) tags := sharedTags
//if withTailscale { if withTailscale {
// tags = append(tags, memcTags...) tags = append(tags, memcTags...)
//} }
if debugEnabled { if debugEnabled {
tags = append(tags, debugTags...) 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:], "}") indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20 versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";") versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
version := strings.Trim(projectContent[versionStart:versionEnd], "\"") version := projectContent[versionStart:versionEnd]
if version == newVersion { if version == newVersion {
continue continue
} }
updated = true updated = true
projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:] projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
} }
return projectContent, updated return projectContent, updated
} }

View File

@@ -17,10 +17,6 @@ func main() {
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
err = updateChromeIncludedRootCAs()
if err != nil {
log.Error(err)
}
} }
func updateMozillaIncludedRootCAs() error { func updateMozillaIncludedRootCAs() error {
@@ -73,94 +69,3 @@ func init() {
generated.WriteString("}\n") generated.WriteString("}\n")
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644) 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

@@ -53,8 +53,6 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
} }
case C.CertificateStoreMozilla: case C.CertificateStoreMozilla:
systemPool = mozillaIncluded systemPool = mozillaIncluded
case C.CertificateStoreChrome:
systemPool = chromeIncluded
case C.CertificateStoreNone: case C.CertificateStoreNone:
systemPool = nil systemPool = nil
default: default:

View File

@@ -137,29 +137,14 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath)) dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
listener.Control = control.Append(listener.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 { if options.ConnectTimeout != 0 {
dialer.Timeout = time.Duration(options.ConnectTimeout) dialer.Timeout = time.Duration(options.ConnectTimeout)
} else { } else {
dialer.Timeout = C.TCPConnectTimeout dialer.Timeout = C.TCPConnectTimeout
} }
if !options.DisableTCPKeepAlive { // TODO: Add an option to customize the keep alive period
keepIdle := time.Duration(options.TCPKeepAlive) dialer.KeepAlive = C.TCPKeepAliveInitial
if keepIdle == 0 { dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
keepIdle = C.TCPKeepAliveInitial
}
keepInterval := time.Duration(options.TCPKeepAliveInterval)
if keepInterval == 0 {
keepInterval = C.TCPKeepAliveInterval
}
dialer.KeepAlive = keepIdle
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(keepIdle, keepInterval))
}
var udpFragment bool var udpFragment bool
if options.UDPFragment != nil { if options.UDPFragment != nil {
udpFragment = *options.UDPFragment udpFragment = *options.UDPFragment

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" "sync/atomic"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
) )
type Reader struct { type Reader struct {
@@ -77,7 +78,7 @@ func (r *Reader) readMetadata() error {
codeIndex uint64 codeIndex uint64
codeLength uint64 codeLength uint64
) )
code, err = readString(reader) code, err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }
@@ -111,16 +112,9 @@ func (r *Reader) Read(code string) ([]Item, error) {
} }
r.bufferedReader.Reset(r.reader) r.bufferedReader.Reset(r.reader)
itemList := make([]Item, r.domainLength[code]) itemList := make([]Item, r.domainLength[code])
for i := range itemList { err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList)
typeByte, err := r.bufferedReader.ReadByte() if err != nil {
if err != nil { return nil, err
return nil, err
}
itemList[i].Type = ItemType(typeByte)
itemList[i].Value, err = readString(r.bufferedReader)
if err != nil {
return nil, err
}
} }
return itemList, nil return itemList, nil
} }
@@ -141,18 +135,3 @@ func (r *readCounter) Read(p []byte) (n int, err error) {
} }
return 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 ( import (
"bytes" "bytes"
"encoding/binary"
"sort" "sort"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/varbin"
@@ -19,11 +20,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
for _, code := range keys { for _, code := range keys {
index[code] = content.Len() index[code] = content.Len()
for _, item := range domains[code] { for _, item := range domains[code] {
err := content.WriteByte(byte(item.Type)) err := varbin.Write(content, binary.BigEndian, item)
if err != nil {
return err
}
err = writeString(content, item.Value)
if err != nil { if err != nil {
return err return err
} }
@@ -41,7 +38,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
} }
for _, code := range keys { for _, code := range keys {
err = writeString(writer, code) err = varbin.Write(writer, binary.BigEndian, code)
if err != nil { if err != nil {
return err return err
} }
@@ -62,12 +59,3 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
return nil 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

@@ -37,7 +37,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
if l.listenOptions.ReuseAddr { if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
} }
if !l.listenOptions.DisableTCPKeepAlive { if l.listenOptions.TCPKeepAlive >= 0 {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive) keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 { if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial keepIdle = C.TCPKeepAliveInitial

View File

@@ -1,5 +1,3 @@
//go:build linux
package settings package settings
import ( import (
@@ -136,9 +134,7 @@ func (m *connmanMonitor) monitorSignals(ctx context.Context, signalChan chan *db
if !ok { if !ok {
return return
} }
// godbus Signal.Name uses "interface.member" format (e.g. "net.connman.Service.PropertyChanged"), if signal.Name == "PropertyChanged" {
// not just the member name. This differs from the D-Bus signal member in the match rule.
if signal.Name == "net.connman.Service.PropertyChanged" {
state := m.ReadWIFIState() state := m.ReadWIFIState()
if state != lastState { if state != lastState {
lastState = state lastState = state
@@ -158,10 +154,6 @@ func (m *connmanMonitor) Close() error {
close(m.signalChan) close(m.signalChan)
} }
if m.conn != nil { if m.conn != nil {
m.conn.RemoveMatchSignal(
dbus.WithMatchInterface("net.connman.Service"),
dbus.WithMatchSender("net.connman"),
)
return m.conn.Close() return m.conn.Close()
} }
return nil return nil

View File

@@ -1,5 +1,3 @@
//go:build linux
package settings package settings
import ( import (
@@ -180,10 +178,6 @@ func (m *iwdMonitor) Close() error {
close(m.signalChan) close(m.signalChan)
} }
if m.conn != nil { if m.conn != nil {
m.conn.RemoveMatchSignal(
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
dbus.WithMatchSender("net.connman.iwd"),
)
return m.conn.Close() return m.conn.Close()
} }
return nil return nil

View File

@@ -1,5 +1,3 @@
//go:build linux
package settings package settings
import ( import (
@@ -42,59 +40,57 @@ func (m *networkManagerMonitor) ReadWIFIState() adapter.WIFIState {
nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
var activeConnectionPaths []dbus.ObjectPath var primaryConnectionPath dbus.ObjectPath
err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "ActiveConnections").Store(&activeConnectionPaths) err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "PrimaryConnection").Store(&primaryConnectionPath)
if err != nil || len(activeConnectionPaths) == 0 { if err != nil || primaryConnectionPath == "/" {
return adapter.WIFIState{} return adapter.WIFIState{}
} }
for _, connectionPath := range activeConnectionPaths { connObj := m.conn.Object("org.freedesktop.NetworkManager", primaryConnectionPath)
connObj := m.conn.Object("org.freedesktop.NetworkManager", connectionPath)
var devicePaths []dbus.ObjectPath var devicePaths []dbus.ObjectPath
err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths) err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths)
if err != nil || len(devicePaths) == 0 { if err != nil || len(devicePaths) == 0 {
return adapter.WIFIState{}
}
for _, devicePath := range devicePaths {
deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath)
var deviceType uint32
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType)
if err != nil || deviceType != 2 {
continue continue
} }
for _, devicePath := range devicePaths { var accessPointPath dbus.ObjectPath
deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath) err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath)
if err != nil || accessPointPath == "/" {
continue
}
var deviceType uint32 apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath)
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType)
if err != nil || deviceType != 2 {
continue
}
var accessPointPath dbus.ObjectPath var ssidBytes []byte
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath) err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes)
if err != nil || accessPointPath == "/" { if err != nil {
continue continue
} }
apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath) var hwAddress string
err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress)
if err != nil {
continue
}
var ssidBytes []byte ssid := strings.TrimSpace(string(ssidBytes))
err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes) if ssid == "" {
if err != nil { continue
continue }
}
var hwAddress string return adapter.WIFIState{
err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress) SSID: ssid,
if err != nil { BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")),
continue
}
ssid := strings.TrimSpace(string(ssidBytes))
if ssid == "" {
continue
}
return adapter.WIFIState{
SSID: ssid,
BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")),
}
} }
} }
@@ -155,10 +151,6 @@ func (m *networkManagerMonitor) Close() error {
close(m.signalChan) close(m.signalChan)
} }
if m.conn != nil { if m.conn != nil {
m.conn.RemoveMatchSignal(
dbus.WithMatchSender("org.freedesktop.NetworkManager"),
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
)
return m.conn.Close() return m.conn.Close()
} }
return nil return nil

View File

@@ -8,21 +8,15 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
) )
var wpaSocketCounter atomic.Uint64
type wpaSupplicantMonitor struct { type wpaSupplicantMonitor struct {
socketPath string socketPath string
callback func(adapter.WIFIState) callback func(adapter.WIFIState)
cancel context.CancelFunc cancel context.CancelFunc
monitorConn *net.UnixConn
connMutex sync.Mutex
} }
func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
@@ -37,8 +31,7 @@ func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, err
continue continue
} }
socketPath := filepath.Join(socketDir, entry.Name()) socketPath := filepath.Join(socketDir, entry.Name())
id := wpaSocketCounter.Add(1) localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"}
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"} remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"}
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
if err != nil { if err != nil {
@@ -52,8 +45,7 @@ func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, err
} }
func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState { func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState {
id := wpaSocketCounter.Add(1) localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"}
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
if err != nil { if err != nil {
@@ -93,11 +85,8 @@ func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState {
} }
} }
// sendCommand sends a command to wpa_supplicant and returns the response.
// Commands are sent without trailing newlines per the wpa_supplicant control
// interface protocol - the official wpa_ctrl.c sends raw command strings.
func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) { func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) {
_, err := conn.Write([]byte(command)) _, err := conn.Write([]byte(command + "\n"))
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -132,8 +121,6 @@ func (m *wpaSupplicantMonitor) Start() error {
func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) { func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) {
var consecutiveErrors int var consecutiveErrors int
var debounceTimer *time.Timer
var debounceMutex sync.Mutex
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"} localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
@@ -143,14 +130,7 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
} }
defer conn.Close() defer conn.Close()
m.connMutex.Lock() _, err = conn.Write([]byte("ATTACH\n"))
m.monitorConn = conn
m.connMutex.Unlock()
// ATTACH/DETACH commands use os_strcmp() for exact matching in wpa_supplicant,
// so they must be sent without trailing newlines.
// See: https://w1.fi/cgit/hostap/tree/wpa_supplicant/ctrl_iface_unix.c
_, err = conn.Write([]byte("ATTACH"))
if err != nil { if err != nil {
return return
} }
@@ -164,12 +144,6 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
debounceMutex.Lock()
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceMutex.Unlock()
conn.Write([]byte("DETACH"))
return return
default: default:
} }
@@ -177,14 +151,6 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
conn.SetReadDeadline(time.Now().Add(30 * time.Second)) conn.SetReadDeadline(time.Now().Add(30 * time.Second))
n, err := conn.Read(buf) n, err := conn.Read(buf)
if err != nil { if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
select {
case <-ctx.Done():
return
default:
}
consecutiveErrors++ consecutiveErrors++
if consecutiveErrors > 10 { if consecutiveErrors > 10 {
return return
@@ -196,18 +162,11 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
msg := string(buf[:n]) msg := string(buf[:n])
if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") { if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") {
debounceMutex.Lock() state := m.ReadWIFIState()
if debounceTimer != nil { if state != lastState {
debounceTimer.Stop() lastState = state
m.callback(state)
} }
debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
state := m.ReadWIFIState()
if state != lastState {
lastState = state
m.callback(state)
}
})
debounceMutex.Unlock()
} }
} }
} }
@@ -216,10 +175,5 @@ func (m *wpaSupplicantMonitor) Close() error {
if m.cancel != nil { if m.cancel != nil {
m.cancel() m.cancel()
} }
m.connMutex.Lock()
if m.monitorConn != nil {
m.monitorConn.Close()
}
m.connMutex.Unlock()
return nil return nil
} }

View File

@@ -1,4 +1,4 @@
//go:build !linux && !windows //go:build !linux
package settings package settings

View File

@@ -1,144 +0,0 @@
//go:build windows
package settings
import (
"context"
"fmt"
"strings"
"sync"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/winwlanapi"
"golang.org/x/sys/windows"
)
type windowsWIFIMonitor struct {
handle windows.Handle
callback func(adapter.WIFIState)
cancel context.CancelFunc
lastState adapter.WIFIState
mutex sync.Mutex
}
func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
handle, err := winwlanapi.OpenHandle()
if err != nil {
return nil, err
}
interfaces, err := winwlanapi.EnumInterfaces(handle)
if err != nil {
winwlanapi.CloseHandle(handle)
return nil, err
}
if len(interfaces) == 0 {
winwlanapi.CloseHandle(handle)
return nil, fmt.Errorf("no wireless interfaces found")
}
return &windowsWIFIMonitor{
handle: handle,
callback: callback,
}, nil
}
func (m *windowsWIFIMonitor) ReadWIFIState() adapter.WIFIState {
interfaces, err := winwlanapi.EnumInterfaces(m.handle)
if err != nil || len(interfaces) == 0 {
return adapter.WIFIState{}
}
for _, iface := range interfaces {
if iface.InterfaceState != winwlanapi.InterfaceStateConnected {
continue
}
guid := iface.InterfaceGUID
attrs, err := winwlanapi.QueryCurrentConnection(m.handle, &guid)
if err != nil {
continue
}
ssidLength := attrs.AssociationAttributes.SSID.Length
if ssidLength == 0 || ssidLength > winwlanapi.Dot11SSIDMaxLength {
continue
}
ssid := string(attrs.AssociationAttributes.SSID.SSID[:ssidLength])
bssid := formatBSSID(attrs.AssociationAttributes.BSSID)
return adapter.WIFIState{
SSID: strings.TrimSpace(ssid),
BSSID: bssid,
}
}
return adapter.WIFIState{}
}
func formatBSSID(mac winwlanapi.Dot11MacAddress) string {
return fmt.Sprintf("%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
}
func (m *windowsWIFIMonitor) Start() error {
if m.callback == nil {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
m.lastState = m.ReadWIFIState()
callbackFunc := func(data *winwlanapi.NotificationData, callbackContext uintptr) uintptr {
if data.NotificationSource != winwlanapi.NotificationSourceACM {
return 0
}
switch data.NotificationCode {
case winwlanapi.NotificationACMConnectionComplete,
winwlanapi.NotificationACMDisconnected:
m.checkAndNotify()
}
return 0
}
callbackPointer := syscall.NewCallback(callbackFunc)
err := winwlanapi.RegisterNotification(m.handle, winwlanapi.NotificationSourceACM, callbackPointer, 0)
if err != nil {
cancel()
return err
}
go func() {
<-ctx.Done()
}()
m.callback(m.lastState)
return nil
}
func (m *windowsWIFIMonitor) checkAndNotify() {
m.mutex.Lock()
defer m.mutex.Unlock()
state := m.ReadWIFIState()
if state != m.lastState {
m.lastState = state
if m.callback != nil {
m.callback(state)
}
}
}
func (m *windowsWIFIMonitor) Close() error {
if m.cancel != nil {
m.cancel()
}
winwlanapi.UnregisterNotification(m.handle)
return winwlanapi.CloseHandle(m.handle)
}

View File

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

View File

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

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 var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC) 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) require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894") pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
require.NoError(t, err) require.NoError(t, err)
@@ -39,7 +39,7 @@ func TestSniffQUICChromium(t *testing.T) {
var metadata adapter.InboundContext var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC) 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) require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28") pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err) require.NoError(t, err)

View File

@@ -6,7 +6,6 @@ import (
"encoding/binary" "encoding/binary"
"io" "io"
"net/netip" "net/netip"
"unsafe"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "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) { func readRuleItemString(reader varbin.Reader) ([]string, error) {
length, err := binary.ReadUvarint(reader) return varbin.ReadValue[[]string](reader, binary.BigEndian)
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
} }
func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error { 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 { if err != nil {
return err return err
} }
_, err = varbin.WriteUvarint(writer, uint64(len(value))) return varbin.Write(writer, binary.BigEndian, 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 readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) { func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) {
length, err := binary.ReadUvarint(reader) return varbin.ReadValue[[]E](reader, binary.BigEndian)
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
} }
func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error { 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 { if err != nil {
return err return err
} }
_, err = varbin.WriteUvarint(writer, uint64(len(value))) return varbin.Write(writer, binary.BigEndian, value)
if err != nil {
return err
}
_, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value)))
return err
} }
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) { func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
length, err := binary.ReadUvarint(reader) return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
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
} }
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error { 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 { if err != nil {
return err return err
} }
_, err = varbin.WriteUvarint(writer, uint64(len(value))) return varbin.Write(writer, binary.BigEndian, value)
if err != nil {
return err
}
return binary.Write(writer, binary.BigEndian, value)
} }
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error { 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 ( import (
"encoding/binary" "encoding/binary"
"io"
"net/netip" "net/netip"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
@@ -10,16 +9,11 @@ import (
) )
func readPrefix(reader varbin.Reader) (netip.Prefix, error) { func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
addrLen, err := binary.ReadUvarint(reader) addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
if err != nil { if err != nil {
return netip.Prefix{}, err return netip.Prefix{}, err
} }
addrSlice := make([]byte, addrLen) prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
_, err = io.ReadFull(reader, addrSlice)
if err != nil {
return netip.Prefix{}, err
}
prefixBits, err := reader.ReadByte()
if err != nil { if err != nil {
return netip.Prefix{}, err 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 { func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
addrSlice := prefix.Addr().AsSlice() err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
_, err := varbin.WriteUvarint(writer, uint64(len(addrSlice)))
if err != nil { if err != nil {
return err return err
} }
_, err = writer.Write(addrSlice) err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
if err != nil {
return err
}
err = writer.WriteByte(uint8(prefix.Bits()))
if err != nil { if err != nil {
return err return err
} }

View File

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

View File

@@ -14,7 +14,6 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/libdns/acmedns"
"github.com/libdns/alidns" "github.com/libdns/alidns"
"github.com/libdns/cloudflare" "github.com/libdns/cloudflare"
"github.com/mholt/acmez/v3/acme" "github.com/mholt/acmez/v3/acme"
@@ -115,24 +114,13 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
switch dnsOptions.Provider { switch dnsOptions.Provider {
case C.DNSProviderAliDNS: case C.DNSProviderAliDNS:
solver.DNSProvider = &alidns.Provider{ solver.DNSProvider = &alidns.Provider{
CredentialInfo: alidns.CredentialInfo{ AccKeyID: dnsOptions.AliDNSOptions.AccessKeyID,
AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID, AccKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret, RegionID: dnsOptions.AliDNSOptions.RegionID,
RegionID: dnsOptions.AliDNSOptions.RegionID,
SecurityToken: dnsOptions.AliDNSOptions.SecurityToken,
},
} }
case C.DNSProviderCloudflare: case C.DNSProviderCloudflare:
solver.DNSProvider = &cloudflare.Provider{ solver.DNSProvider = &cloudflare.Provider{
APIToken: dnsOptions.CloudflareOptions.APIToken, 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,
} }
default: default:
return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider) 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{ return &ECHClientConfig{
ECHCapableConfig: clientConfig, ECHCapableConfig: clientConfig,
dnsRouter: service.FromContext[adapter.DNSRouter](ctx), dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
queryServerName: options.ECH.QueryServerName,
}, nil }, nil
} }
} }
@@ -109,11 +108,10 @@ func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
type ECHClientConfig struct { type ECHClientConfig struct {
ECHCapableConfig ECHCapableConfig
access sync.Mutex access sync.Mutex
dnsRouter adapter.DNSRouter dnsRouter adapter.DNSRouter
queryServerName string lastTTL time.Duration
lastTTL time.Duration lastUpdate time.Time
lastUpdate time.Time
} }
func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { 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() s.access.Lock()
defer s.access.Unlock() defer s.access.Unlock()
if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Since(s.lastUpdate) > s.lastTTL { 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{ message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
RecursionDesired: true, RecursionDesired: true,
}, },
Question: []mDNS.Question{ Question: []mDNS.Question{
{ {
Name: mDNS.Fqdn(queryServerName), Name: mDNS.Fqdn(s.ServerName()),
Qtype: mDNS.TypeHTTPS, Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET, Qclass: mDNS.ClassINET,
}, },
@@ -181,12 +175,7 @@ func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn)
} }
func (s *ECHClientConfig) Clone() Config { func (s *ECHClientConfig) Clone() Config {
return &ECHClientConfig{ return &ECHClientConfig{ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig),
dnsRouter: s.dnsRouter,
queryServerName: s.queryServerName,
lastUpdate: s.lastUpdate,
}
} }
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {

View File

@@ -169,35 +169,6 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
} }
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
} }
var clientCertificate []byte
if len(options.ClientCertificate) > 0 {
clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n"))
} else if options.ClientCertificatePath != "" {
content, err := os.ReadFile(options.ClientCertificatePath)
if err != nil {
return nil, E.Cause(err, "read client certificate")
}
clientCertificate = content
}
var clientKey []byte
if len(options.ClientKey) > 0 {
clientKey = []byte(strings.Join(options.ClientKey, "\n"))
} else if options.ClientKeyPath != "" {
content, err := os.ReadFile(options.ClientKeyPath)
if err != nil {
return nil, E.Cause(err, "read client key")
}
clientKey = content
}
if len(clientCertificate) > 0 && len(clientKey) > 0 {
keyPair, err := tls.X509KeyPair(clientCertificate, clientKey)
if err != nil {
return nil, E.Cause(err, "parse client x509 key pair")
}
tlsConfig.Certificates = []tls.Certificate{keyPair}
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
return nil, E.New("client certificate and client key must be provided together")
}
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled { if options.ECH != nil && options.ECH.Enabled {
var err error var err error

View File

@@ -222,35 +222,6 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
} }
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
} }
var clientCertificate []byte
if len(options.ClientCertificate) > 0 {
clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n"))
} else if options.ClientCertificatePath != "" {
content, err := os.ReadFile(options.ClientCertificatePath)
if err != nil {
return nil, E.Cause(err, "read client certificate")
}
clientCertificate = content
}
var clientKey []byte
if len(options.ClientKey) > 0 {
clientKey = []byte(strings.Join(options.ClientKey, "\n"))
} else if options.ClientKeyPath != "" {
content, err := os.ReadFile(options.ClientKeyPath)
if err != nil {
return nil, E.Cause(err, "read client key")
}
clientKey = content
}
if len(clientCertificate) > 0 && len(clientKey) > 0 {
keyPair, err := utls.X509KeyPair(clientCertificate, clientKey)
if err != nil {
return nil, E.Cause(err, "parse client x509 key pair")
}
tlsConfig.Certificates = []utls.Certificate{keyPair}
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
return nil, E.New("client certificate and client key must be provided together")
}
id, err := uTLSClientHelloID(options.UTLS.Fingerprint) id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
if err != nil { if err != nil {
return nil, err return nil, err

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,8 +28,6 @@ const (
TypeDERP = "derp" TypeDERP = "derp"
TypeResolved = "resolved" TypeResolved = "resolved"
TypeSSMAPI = "ssm-api" TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
) )
const ( const (

View File

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

View File

@@ -3,7 +3,7 @@ package constant
import "time" import "time"
const ( const (
TCPKeepAliveInitial = 5 * time.Minute TCPKeepAliveInitial = 10 * time.Minute
TCPKeepAliveInterval = 75 * time.Second TCPKeepAliveInterval = 75 * time.Second
TCPConnectTimeout = 5 * time.Second TCPConnectTimeout = 5 * time.Second
TCPTimeout = 15 * time.Second TCPTimeout = 15 * time.Second

702
daemon/helper.pb.go Normal file
View File

@@ -0,0 +1,702 @@
package daemon
import (
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type SubscribeHelperRequestRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
AcceptGetWIFIStateRequests bool `protobuf:"varint,1,opt,name=acceptGetWIFIStateRequests,proto3" json:"acceptGetWIFIStateRequests,omitempty"`
AcceptFindConnectionOwnerRequests bool `protobuf:"varint,2,opt,name=acceptFindConnectionOwnerRequests,proto3" json:"acceptFindConnectionOwnerRequests,omitempty"`
AcceptSendNotificationRequests bool `protobuf:"varint,3,opt,name=acceptSendNotificationRequests,proto3" json:"acceptSendNotificationRequests,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SubscribeHelperRequestRequest) Reset() {
*x = SubscribeHelperRequestRequest{}
mi := &file_daemon_helper_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SubscribeHelperRequestRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SubscribeHelperRequestRequest) ProtoMessage() {}
func (x *SubscribeHelperRequestRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SubscribeHelperRequestRequest.ProtoReflect.Descriptor instead.
func (*SubscribeHelperRequestRequest) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{0}
}
func (x *SubscribeHelperRequestRequest) GetAcceptGetWIFIStateRequests() bool {
if x != nil {
return x.AcceptGetWIFIStateRequests
}
return false
}
func (x *SubscribeHelperRequestRequest) GetAcceptFindConnectionOwnerRequests() bool {
if x != nil {
return x.AcceptFindConnectionOwnerRequests
}
return false
}
func (x *SubscribeHelperRequestRequest) GetAcceptSendNotificationRequests() bool {
if x != nil {
return x.AcceptSendNotificationRequests
}
return false
}
type HelperRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
// Types that are valid to be assigned to Request:
//
// *HelperRequest_GetWIFIState
// *HelperRequest_FindConnectionOwner
// *HelperRequest_SendNotification
Request isHelperRequest_Request `protobuf_oneof:"request"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HelperRequest) Reset() {
*x = HelperRequest{}
mi := &file_daemon_helper_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HelperRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelperRequest) ProtoMessage() {}
func (x *HelperRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelperRequest.ProtoReflect.Descriptor instead.
func (*HelperRequest) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{1}
}
func (x *HelperRequest) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *HelperRequest) GetRequest() isHelperRequest_Request {
if x != nil {
return x.Request
}
return nil
}
func (x *HelperRequest) GetGetWIFIState() *emptypb.Empty {
if x != nil {
if x, ok := x.Request.(*HelperRequest_GetWIFIState); ok {
return x.GetWIFIState
}
}
return nil
}
func (x *HelperRequest) GetFindConnectionOwner() *FindConnectionOwnerRequest {
if x != nil {
if x, ok := x.Request.(*HelperRequest_FindConnectionOwner); ok {
return x.FindConnectionOwner
}
}
return nil
}
func (x *HelperRequest) GetSendNotification() *Notification {
if x != nil {
if x, ok := x.Request.(*HelperRequest_SendNotification); ok {
return x.SendNotification
}
}
return nil
}
type isHelperRequest_Request interface {
isHelperRequest_Request()
}
type HelperRequest_GetWIFIState struct {
GetWIFIState *emptypb.Empty `protobuf:"bytes,2,opt,name=getWIFIState,proto3,oneof"`
}
type HelperRequest_FindConnectionOwner struct {
FindConnectionOwner *FindConnectionOwnerRequest `protobuf:"bytes,3,opt,name=findConnectionOwner,proto3,oneof"`
}
type HelperRequest_SendNotification struct {
SendNotification *Notification `protobuf:"bytes,4,opt,name=sendNotification,proto3,oneof"`
}
func (*HelperRequest_GetWIFIState) isHelperRequest_Request() {}
func (*HelperRequest_FindConnectionOwner) isHelperRequest_Request() {}
func (*HelperRequest_SendNotification) isHelperRequest_Request() {}
type FindConnectionOwnerRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
IpProtocol int32 `protobuf:"varint,1,opt,name=ipProtocol,proto3" json:"ipProtocol,omitempty"`
SourceAddress string `protobuf:"bytes,2,opt,name=sourceAddress,proto3" json:"sourceAddress,omitempty"`
SourcePort int32 `protobuf:"varint,3,opt,name=sourcePort,proto3" json:"sourcePort,omitempty"`
DestinationAddress string `protobuf:"bytes,4,opt,name=destinationAddress,proto3" json:"destinationAddress,omitempty"`
DestinationPort int32 `protobuf:"varint,5,opt,name=destinationPort,proto3" json:"destinationPort,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FindConnectionOwnerRequest) Reset() {
*x = FindConnectionOwnerRequest{}
mi := &file_daemon_helper_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FindConnectionOwnerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FindConnectionOwnerRequest) ProtoMessage() {}
func (x *FindConnectionOwnerRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FindConnectionOwnerRequest.ProtoReflect.Descriptor instead.
func (*FindConnectionOwnerRequest) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{2}
}
func (x *FindConnectionOwnerRequest) GetIpProtocol() int32 {
if x != nil {
return x.IpProtocol
}
return 0
}
func (x *FindConnectionOwnerRequest) GetSourceAddress() string {
if x != nil {
return x.SourceAddress
}
return ""
}
func (x *FindConnectionOwnerRequest) GetSourcePort() int32 {
if x != nil {
return x.SourcePort
}
return 0
}
func (x *FindConnectionOwnerRequest) GetDestinationAddress() string {
if x != nil {
return x.DestinationAddress
}
return ""
}
func (x *FindConnectionOwnerRequest) GetDestinationPort() int32 {
if x != nil {
return x.DestinationPort
}
return 0
}
type Notification struct {
state protoimpl.MessageState `protogen:"open.v1"`
Identifier string `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
TypeName string `protobuf:"bytes,2,opt,name=typeName,proto3" json:"typeName,omitempty"`
TypeId int32 `protobuf:"varint,3,opt,name=typeId,proto3" json:"typeId,omitempty"`
Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"`
Subtitle string `protobuf:"bytes,5,opt,name=subtitle,proto3" json:"subtitle,omitempty"`
Body string `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
OpenURL string `protobuf:"bytes,7,opt,name=openURL,proto3" json:"openURL,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Notification) Reset() {
*x = Notification{}
mi := &file_daemon_helper_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Notification) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Notification) ProtoMessage() {}
func (x *Notification) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Notification.ProtoReflect.Descriptor instead.
func (*Notification) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{3}
}
func (x *Notification) GetIdentifier() string {
if x != nil {
return x.Identifier
}
return ""
}
func (x *Notification) GetTypeName() string {
if x != nil {
return x.TypeName
}
return ""
}
func (x *Notification) GetTypeId() int32 {
if x != nil {
return x.TypeId
}
return 0
}
func (x *Notification) GetTitle() string {
if x != nil {
return x.Title
}
return ""
}
func (x *Notification) GetSubtitle() string {
if x != nil {
return x.Subtitle
}
return ""
}
func (x *Notification) GetBody() string {
if x != nil {
return x.Body
}
return ""
}
func (x *Notification) GetOpenURL() string {
if x != nil {
return x.OpenURL
}
return ""
}
type HelperResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
// Types that are valid to be assigned to Response:
//
// *HelperResponse_WifiState
// *HelperResponse_Error
// *HelperResponse_ConnectionOwner
Response isHelperResponse_Response `protobuf_oneof:"response"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HelperResponse) Reset() {
*x = HelperResponse{}
mi := &file_daemon_helper_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HelperResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelperResponse) ProtoMessage() {}
func (x *HelperResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelperResponse.ProtoReflect.Descriptor instead.
func (*HelperResponse) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{4}
}
func (x *HelperResponse) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *HelperResponse) GetResponse() isHelperResponse_Response {
if x != nil {
return x.Response
}
return nil
}
func (x *HelperResponse) GetWifiState() *WIFIState {
if x != nil {
if x, ok := x.Response.(*HelperResponse_WifiState); ok {
return x.WifiState
}
}
return nil
}
func (x *HelperResponse) GetError() string {
if x != nil {
if x, ok := x.Response.(*HelperResponse_Error); ok {
return x.Error
}
}
return ""
}
func (x *HelperResponse) GetConnectionOwner() *ConnectionOwner {
if x != nil {
if x, ok := x.Response.(*HelperResponse_ConnectionOwner); ok {
return x.ConnectionOwner
}
}
return nil
}
type isHelperResponse_Response interface {
isHelperResponse_Response()
}
type HelperResponse_WifiState struct {
WifiState *WIFIState `protobuf:"bytes,2,opt,name=wifiState,proto3,oneof"`
}
type HelperResponse_Error struct {
Error string `protobuf:"bytes,3,opt,name=error,proto3,oneof"`
}
type HelperResponse_ConnectionOwner struct {
ConnectionOwner *ConnectionOwner `protobuf:"bytes,4,opt,name=connectionOwner,proto3,oneof"`
}
func (*HelperResponse_WifiState) isHelperResponse_Response() {}
func (*HelperResponse_Error) isHelperResponse_Response() {}
func (*HelperResponse_ConnectionOwner) isHelperResponse_Response() {}
type ConnectionOwner struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId int32 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"`
UserName string `protobuf:"bytes,2,opt,name=userName,proto3" json:"userName,omitempty"`
ProcessPath string `protobuf:"bytes,3,opt,name=processPath,proto3" json:"processPath,omitempty"`
AndroidPackageName string `protobuf:"bytes,4,opt,name=androidPackageName,proto3" json:"androidPackageName,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConnectionOwner) Reset() {
*x = ConnectionOwner{}
mi := &file_daemon_helper_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConnectionOwner) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConnectionOwner) ProtoMessage() {}
func (x *ConnectionOwner) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ConnectionOwner.ProtoReflect.Descriptor instead.
func (*ConnectionOwner) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{5}
}
func (x *ConnectionOwner) GetUserId() int32 {
if x != nil {
return x.UserId
}
return 0
}
func (x *ConnectionOwner) GetUserName() string {
if x != nil {
return x.UserName
}
return ""
}
func (x *ConnectionOwner) GetProcessPath() string {
if x != nil {
return x.ProcessPath
}
return ""
}
func (x *ConnectionOwner) GetAndroidPackageName() string {
if x != nil {
return x.AndroidPackageName
}
return ""
}
type WIFIState struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ssid string `protobuf:"bytes,1,opt,name=ssid,proto3" json:"ssid,omitempty"`
Bssid string `protobuf:"bytes,2,opt,name=bssid,proto3" json:"bssid,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *WIFIState) Reset() {
*x = WIFIState{}
mi := &file_daemon_helper_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *WIFIState) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*WIFIState) ProtoMessage() {}
func (x *WIFIState) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use WIFIState.ProtoReflect.Descriptor instead.
func (*WIFIState) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{6}
}
func (x *WIFIState) GetSsid() string {
if x != nil {
return x.Ssid
}
return ""
}
func (x *WIFIState) GetBssid() string {
if x != nil {
return x.Bssid
}
return ""
}
var File_daemon_helper_proto protoreflect.FileDescriptor
const file_daemon_helper_proto_rawDesc = "" +
"\n" +
"\x13daemon/helper.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xf5\x01\n" +
"\x1dSubscribeHelperRequestRequest\x12>\n" +
"\x1aacceptGetWIFIStateRequests\x18\x01 \x01(\bR\x1aacceptGetWIFIStateRequests\x12L\n" +
"!acceptFindConnectionOwnerRequests\x18\x02 \x01(\bR!acceptFindConnectionOwnerRequests\x12F\n" +
"\x1eacceptSendNotificationRequests\x18\x03 \x01(\bR\x1eacceptSendNotificationRequests\"\x84\x02\n" +
"\rHelperRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12<\n" +
"\fgetWIFIState\x18\x02 \x01(\v2\x16.google.protobuf.EmptyH\x00R\fgetWIFIState\x12V\n" +
"\x13findConnectionOwner\x18\x03 \x01(\v2\".daemon.FindConnectionOwnerRequestH\x00R\x13findConnectionOwner\x12B\n" +
"\x10sendNotification\x18\x04 \x01(\v2\x14.daemon.NotificationH\x00R\x10sendNotificationB\t\n" +
"\arequest\"\xdc\x01\n" +
"\x1aFindConnectionOwnerRequest\x12\x1e\n" +
"\n" +
"ipProtocol\x18\x01 \x01(\x05R\n" +
"ipProtocol\x12$\n" +
"\rsourceAddress\x18\x02 \x01(\tR\rsourceAddress\x12\x1e\n" +
"\n" +
"sourcePort\x18\x03 \x01(\x05R\n" +
"sourcePort\x12.\n" +
"\x12destinationAddress\x18\x04 \x01(\tR\x12destinationAddress\x12(\n" +
"\x0fdestinationPort\x18\x05 \x01(\x05R\x0fdestinationPort\"\xc2\x01\n" +
"\fNotification\x12\x1e\n" +
"\n" +
"identifier\x18\x01 \x01(\tR\n" +
"identifier\x12\x1a\n" +
"\btypeName\x18\x02 \x01(\tR\btypeName\x12\x16\n" +
"\x06typeId\x18\x03 \x01(\x05R\x06typeId\x12\x14\n" +
"\x05title\x18\x04 \x01(\tR\x05title\x12\x1a\n" +
"\bsubtitle\x18\x05 \x01(\tR\bsubtitle\x12\x12\n" +
"\x04body\x18\x06 \x01(\tR\x04body\x12\x18\n" +
"\aopenURL\x18\a \x01(\tR\aopenURL\"\xbc\x01\n" +
"\x0eHelperResponse\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x121\n" +
"\twifiState\x18\x02 \x01(\v2\x11.daemon.WIFIStateH\x00R\twifiState\x12\x16\n" +
"\x05error\x18\x03 \x01(\tH\x00R\x05error\x12C\n" +
"\x0fconnectionOwner\x18\x04 \x01(\v2\x17.daemon.ConnectionOwnerH\x00R\x0fconnectionOwnerB\n" +
"\n" +
"\bresponse\"\x97\x01\n" +
"\x0fConnectionOwner\x12\x16\n" +
"\x06userId\x18\x01 \x01(\x05R\x06userId\x12\x1a\n" +
"\buserName\x18\x02 \x01(\tR\buserName\x12 \n" +
"\vprocessPath\x18\x03 \x01(\tR\vprocessPath\x12.\n" +
"\x12androidPackageName\x18\x04 \x01(\tR\x12androidPackageName\"5\n" +
"\tWIFIState\x12\x12\n" +
"\x04ssid\x18\x01 \x01(\tR\x04ssid\x12\x14\n" +
"\x05bssid\x18\x02 \x01(\tR\x05bssidB%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
var (
file_daemon_helper_proto_rawDescOnce sync.Once
file_daemon_helper_proto_rawDescData []byte
)
func file_daemon_helper_proto_rawDescGZIP() []byte {
file_daemon_helper_proto_rawDescOnce.Do(func() {
file_daemon_helper_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_daemon_helper_proto_rawDesc), len(file_daemon_helper_proto_rawDesc)))
})
return file_daemon_helper_proto_rawDescData
}
var (
file_daemon_helper_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
file_daemon_helper_proto_goTypes = []any{
(*SubscribeHelperRequestRequest)(nil), // 0: daemon.SubscribeHelperRequestRequest
(*HelperRequest)(nil), // 1: daemon.HelperRequest
(*FindConnectionOwnerRequest)(nil), // 2: daemon.FindConnectionOwnerRequest
(*Notification)(nil), // 3: daemon.Notification
(*HelperResponse)(nil), // 4: daemon.HelperResponse
(*ConnectionOwner)(nil), // 5: daemon.ConnectionOwner
(*WIFIState)(nil), // 6: daemon.WIFIState
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
}
)
var file_daemon_helper_proto_depIdxs = []int32{
7, // 0: daemon.HelperRequest.getWIFIState:type_name -> google.protobuf.Empty
2, // 1: daemon.HelperRequest.findConnectionOwner:type_name -> daemon.FindConnectionOwnerRequest
3, // 2: daemon.HelperRequest.sendNotification:type_name -> daemon.Notification
6, // 3: daemon.HelperResponse.wifiState:type_name -> daemon.WIFIState
5, // 4: daemon.HelperResponse.connectionOwner:type_name -> daemon.ConnectionOwner
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_daemon_helper_proto_init() }
func file_daemon_helper_proto_init() {
if File_daemon_helper_proto != nil {
return
}
file_daemon_helper_proto_msgTypes[1].OneofWrappers = []any{
(*HelperRequest_GetWIFIState)(nil),
(*HelperRequest_FindConnectionOwner)(nil),
(*HelperRequest_SendNotification)(nil),
}
file_daemon_helper_proto_msgTypes[4].OneofWrappers = []any{
(*HelperResponse_WifiState)(nil),
(*HelperResponse_Error)(nil),
(*HelperResponse_ConnectionOwner)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_helper_proto_rawDesc), len(file_daemon_helper_proto_rawDesc)),
NumEnums: 0,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_daemon_helper_proto_goTypes,
DependencyIndexes: file_daemon_helper_proto_depIdxs,
MessageInfos: file_daemon_helper_proto_msgTypes,
}.Build()
File_daemon_helper_proto = out.File
file_daemon_helper_proto_goTypes = nil
file_daemon_helper_proto_depIdxs = nil
}

61
daemon/helper.proto Normal file
View File

@@ -0,0 +1,61 @@
syntax = "proto3";
package daemon;
option go_package = "github.com/sagernet/sing-box/daemon";
import "google/protobuf/empty.proto";
message SubscribeHelperRequestRequest {
bool acceptGetWIFIStateRequests = 1;
bool acceptFindConnectionOwnerRequests = 2;
bool acceptSendNotificationRequests = 3;
}
message HelperRequest {
int64 id = 1;
oneof request {
google.protobuf.Empty getWIFIState = 2;
FindConnectionOwnerRequest findConnectionOwner = 3;
Notification sendNotification = 4;
}
}
message FindConnectionOwnerRequest {
int32 ipProtocol = 1;
string sourceAddress = 2;
int32 sourcePort = 3;
string destinationAddress = 4;
int32 destinationPort = 5;
}
message Notification {
string identifier = 1;
string typeName = 2;
int32 typeId = 3;
string title = 4;
string subtitle = 5;
string body = 6;
string openURL = 7;
}
message HelperResponse {
int64 id = 1;
oneof response {
WIFIState wifiState = 2;
string error = 3;
ConnectionOwner connectionOwner = 4;
}
}
message ConnectionOwner {
int32 userId = 1;
string userName = 2;
string processPath = 3;
string androidPackageName = 4;
}
message WIFIState {
string ssid = 1;
string bssid = 2;
}

View File

@@ -7,13 +7,15 @@ import (
"github.com/sagernet/sing-box" "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/sing/service/pause" "github.com/sagernet/sing/service/pause"
) )
@@ -27,12 +29,23 @@ type Instance struct {
urlTestHistoryStorage *urltest.HistoryStorage urlTestHistoryStorage *urltest.HistoryStorage
} }
func (s *StartedService) baseContext() context.Context {
dnsRegistry := include.DNSTransportRegistry()
if s.platform != nil && s.platform.UsePlatformLocalDNSTransport() {
dns.RegisterTransport[option.LocalDNSServerOptions](dnsRegistry, C.DNSTypeLocal, s.platform.LocalDNSTransport())
}
ctx := box.Context(s.ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry())
ctx = filemanager.WithDefault(ctx, s.workingDirectory, s.tempDirectory, s.userID, s.groupID)
return ctx
}
func (s *StartedService) CheckConfig(configContent string) error { func (s *StartedService) CheckConfig(configContent string) error {
options, err := parseConfig(s.ctx, configContent) ctx := s.baseContext()
options, err := parseConfig(ctx, configContent)
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithCancel(s.ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
instance, err := box.New(box.Options{ instance, err := box.New(box.Options{
Context: ctx, Context: ctx,
@@ -45,7 +58,7 @@ func (s *StartedService) CheckConfig(configContent string) error {
} }
func (s *StartedService) FormatConfig(configContent string) (string, error) { func (s *StartedService) FormatConfig(configContent string) (string, error) {
options, err := parseConfig(s.ctx, configContent) options, err := parseConfig(s.baseContext(), configContent)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -66,7 +79,7 @@ type OverrideOptions struct {
} }
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) { func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
ctx := s.ctx ctx := s.baseContext()
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
ctx, cancel := context.WithCancel(include.Context(ctx)) ctx, cancel := context.WithCancel(include.Context(ctx))
options, err := parseConfig(ctx, profileContent) options, err := parseConfig(ctx, profileContent)
@@ -104,7 +117,6 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
i.clashServer = service.FromContext[adapter.ClashServer](ctx) i.clashServer = service.FromContext[adapter.ClashServer](ctx)
i.pauseManager = service.FromContext[pause.Manager](ctx) i.pauseManager = service.FromContext[pause.Manager](ctx)
i.cacheFile = service.FromContext[adapter.CacheFile](ctx) i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
log.SetStdLogger(boxInstance.LogFactory().Logger())
return i, nil return i, nil
} }

View File

@@ -1,5 +1,11 @@
package daemon package daemon
import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/option"
)
type PlatformHandler interface { type PlatformHandler interface {
ServiceStop() error ServiceStop() error
ServiceReload() error ServiceReload() error
@@ -7,3 +13,10 @@ type PlatformHandler interface {
SetSystemProxyEnabled(enabled bool) error SetSystemProxyEnabled(enabled bool) error
WriteDebugMessage(message string) WriteDebugMessage(message string)
} }
type PlatformInterface interface {
adapter.PlatformInterface
UsePlatformLocalDNSTransport() bool
LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions]
}

View File

@@ -31,16 +31,16 @@ import (
var _ StartedServiceServer = (*StartedService)(nil) var _ StartedServiceServer = (*StartedService)(nil)
type StartedService struct { type StartedService struct {
ctx context.Context ctx context.Context
// platform adapter.PlatformInterface platform PlatformInterface
handler PlatformHandler platformHandler PlatformHandler
debug bool debug bool
logMaxLines int logMaxLines int
// workingDirectory string workingDirectory string
// tempDirectory string tempDirectory string
// userID int userID int
// groupID int groupID int
// systemProxyEnabled bool systemProxyEnabled bool
serviceAccess sync.RWMutex serviceAccess sync.RWMutex
serviceStatus *ServiceStatus serviceStatus *ServiceStatus
serviceStatusSubscriber *observable.Subscriber[*ServiceStatus] serviceStatusSubscriber *observable.Subscriber[*ServiceStatus]
@@ -50,55 +50,49 @@ type StartedService struct {
logSubscriber *observable.Subscriber[*log.Entry] logSubscriber *observable.Subscriber[*log.Entry]
logObserver *observable.Observer[*log.Entry] logObserver *observable.Observer[*log.Entry]
instance *Instance instance *Instance
startedAt time.Time
urlTestSubscriber *observable.Subscriber[struct{}] urlTestSubscriber *observable.Subscriber[struct{}]
urlTestObserver *observable.Observer[struct{}] urlTestObserver *observable.Observer[struct{}]
urlTestHistoryStorage *urltest.HistoryStorage urlTestHistoryStorage *urltest.HistoryStorage
clashModeSubscriber *observable.Subscriber[struct{}] clashModeSubscriber *observable.Subscriber[struct{}]
clashModeObserver *observable.Observer[struct{}] clashModeObserver *observable.Observer[struct{}]
connectionEventSubscriber *observable.Subscriber[trafficontrol.ConnectionEvent]
connectionEventObserver *observable.Observer[trafficontrol.ConnectionEvent]
} }
type ServiceOptions struct { type ServiceOptions struct {
Context context.Context Context context.Context
// Platform adapter.PlatformInterface Platform PlatformInterface
Handler PlatformHandler PlatformHandler PlatformHandler
Debug bool Debug bool
LogMaxLines int LogMaxLines int
// WorkingDirectory string WorkingDirectory string
// TempDirectory string TempDirectory string
// UserID int UserID int
// GroupID int GroupID int
// SystemProxyEnabled bool SystemProxyEnabled bool
} }
func NewStartedService(options ServiceOptions) *StartedService { func NewStartedService(options ServiceOptions) *StartedService {
s := &StartedService{ s := &StartedService{
ctx: options.Context, ctx: options.Context,
// platform: options.Platform, platform: options.Platform,
handler: options.Handler, platformHandler: options.PlatformHandler,
debug: options.Debug, debug: options.Debug,
logMaxLines: options.LogMaxLines, logMaxLines: options.LogMaxLines,
// workingDirectory: options.WorkingDirectory, workingDirectory: options.WorkingDirectory,
// tempDirectory: options.TempDirectory, tempDirectory: options.TempDirectory,
// userID: options.UserID, userID: options.UserID,
// groupID: options.GroupID, groupID: options.GroupID,
// systemProxyEnabled: options.SystemProxyEnabled, systemProxyEnabled: options.SystemProxyEnabled,
serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE}, serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE},
serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4), serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4),
logSubscriber: observable.NewSubscriber[*log.Entry](128), logSubscriber: observable.NewSubscriber[*log.Entry](128),
urlTestSubscriber: observable.NewSubscriber[struct{}](1), urlTestSubscriber: observable.NewSubscriber[struct{}](1),
urlTestHistoryStorage: urltest.NewHistoryStorage(), urlTestHistoryStorage: urltest.NewHistoryStorage(),
clashModeSubscriber: observable.NewSubscriber[struct{}](1), clashModeSubscriber: observable.NewSubscriber[struct{}](1),
connectionEventSubscriber: observable.NewSubscriber[trafficontrol.ConnectionEvent](256),
} }
s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2) s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2)
s.logObserver = observable.NewObserver(s.logSubscriber, 64) s.logObserver = observable.NewObserver(s.logSubscriber, 64)
s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1) s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1)
s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1) s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1)
s.connectionEventObserver = observable.NewObserver(s.connectionEventSubscriber, 64)
return s return s
} }
@@ -123,46 +117,6 @@ func (s *StartedService) updateStatusError(err error) error {
return err return err
} }
func (s *StartedService) waitForStarted(ctx context.Context) error {
s.serviceAccess.RLock()
currentStatus := s.serviceStatus.Status
s.serviceAccess.RUnlock()
switch currentStatus {
case ServiceStatus_STARTED:
return nil
case ServiceStatus_STARTING:
default:
return os.ErrInvalid
}
subscription, done, err := s.serviceStatusObserver.Subscribe()
if err != nil {
return err
}
defer s.serviceStatusObserver.UnSubscribe(subscription)
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-s.ctx.Done():
return s.ctx.Err()
case status := <-subscription:
switch status.Status {
case ServiceStatus_STARTED:
return nil
case ServiceStatus_FATAL:
return E.New(status.ErrorMessage)
case ServiceStatus_IDLE, ServiceStatus_STOPPING:
return os.ErrInvalid
}
case <-done:
return os.ErrClosed
}
}
}
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error { func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
s.serviceAccess.Lock() s.serviceAccess.Lock()
switch s.serviceStatus.Status { switch s.serviceStatus.Status {
@@ -171,13 +125,6 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
return os.ErrInvalid return os.ErrInvalid
} }
oldInstance := s.instance
if oldInstance != nil {
s.updateStatus(ServiceStatus_STOPPING)
s.serviceAccess.Unlock()
_ = oldInstance.Close()
s.serviceAccess.Lock()
}
s.updateStatus(ServiceStatus_STARTING) s.updateStatus(ServiceStatus_STARTING)
s.resetLogs() s.resetLogs()
instance, err := s.newInstance(profileContent, options) instance, err := s.newInstance(profileContent, options)
@@ -185,11 +132,6 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
return s.updateStatusError(err) return s.updateStatusError(err)
} }
s.instance = instance s.instance = instance
instance.urlTestHistoryStorage.SetHook(s.urlTestSubscriber)
if instance.clashServer != nil {
instance.clashServer.SetModeUpdateHook(s.clashModeSubscriber)
instance.clashServer.(*clashapi.Server).TrafficManager().SetEventHook(s.connectionEventSubscriber)
}
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
err = instance.Start() err = instance.Start()
s.serviceAccess.Lock() s.serviceAccess.Lock()
@@ -200,7 +142,6 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
if err != nil { if err != nil {
return s.updateStatusError(err) return s.updateStatusError(err)
} }
s.startedAt = time.Now()
s.updateStatus(ServiceStatus_STARTED) s.updateStatus(ServiceStatus_STARTED)
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
runtime.GC() runtime.GC()
@@ -223,7 +164,6 @@ func (s *StartedService) CloseService() error {
} }
} }
s.instance = nil s.instance = nil
s.startedAt = time.Time{}
s.updateStatus(ServiceStatus_IDLE) s.updateStatus(ServiceStatus_IDLE)
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
runtime.GC() runtime.GC()
@@ -233,11 +173,12 @@ func (s *StartedService) CloseService() error {
func (s *StartedService) SetError(err error) { func (s *StartedService) SetError(err error) {
s.serviceAccess.Lock() s.serviceAccess.Lock()
s.updateStatusError(err) s.updateStatusError(err)
s.serviceAccess.Unlock()
s.WriteMessage(log.LevelError, err.Error()) s.WriteMessage(log.LevelError, err.Error())
} }
func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
err := s.handler.ServiceStop() err := s.platformHandler.ServiceStop()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -245,7 +186,7 @@ func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty)
} }
func (s *StartedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { func (s *StartedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
err := s.handler.ServiceReload() err := s.platformHandler.ServiceReload()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -286,8 +227,8 @@ func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerSt
for element := s.logLines.Front(); element != nil; element = element.Next() { for element := s.logLines.Front(); element != nil; element = element.Next() {
savedLines = append(savedLines, element.Value) savedLines = append(savedLines, element.Value)
} }
subscription, done, err := s.logObserver.Subscribe()
s.logAccess.Unlock() s.logAccess.Unlock()
subscription, done, err := s.logObserver.Subscribe()
if err != nil { if err != nil {
return err return err
} }
@@ -311,33 +252,30 @@ func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerSt
case <-server.Context().Done(): case <-server.Context().Done():
return server.Context().Err() return server.Context().Err()
case message := <-subscription: case message := <-subscription:
var rawMessage Log
if message == nil { if message == nil {
rawMessage.Reset_ = true err = server.Send(&Log{Reset_: true})
} else { if err != nil {
rawMessage.Messages = append(rawMessage.Messages, &Log_Message{ return err
Level: LogLevel(message.Level), }
Message: message.Message, continue
})
} }
messages := []*Log_Message{{
Level: LogLevel(message.Level),
Message: message.Message,
}}
fetch: fetch:
for { for {
select { select {
case message = <-subscription: case message = <-subscription:
if message == nil { messages = append(messages, &Log_Message{
rawMessage.Messages = nil Level: LogLevel(message.Level),
rawMessage.Reset_ = true Message: message.Message,
} else { })
rawMessage.Messages = append(rawMessage.Messages, &Log_Message{
Level: LogLevel(message.Level),
Message: message.Message,
})
}
default: default:
break fetch break fetch
} }
} }
err = server.Send(&rawMessage) err = server.Send(&Log{Messages: messages})
if err != nil { if err != nil {
return err return err
} }
@@ -360,11 +298,6 @@ func (s *StartedService) GetDefaultLogLevel(ctx context.Context, empty *emptypb.
return &DefaultLogLevel{Level: LogLevel(logLevel)}, nil return &DefaultLogLevel{Level: LogLevel(logLevel)}, nil
} }
func (s *StartedService) ClearLogs(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
s.resetLogs()
return &emptypb.Empty{}, nil
}
func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server grpc.ServerStreamingServer[Status]) error { func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server grpc.ServerStreamingServer[Status]) error {
interval := time.Duration(request.Interval) interval := time.Duration(request.Interval)
if interval <= 0 { if interval <= 0 {
@@ -402,9 +335,7 @@ func (s *StartedService) readStatus() *Status {
status.Memory = memory.Inuse() status.Memory = memory.Inuse()
status.Goroutines = int32(runtime.NumGoroutine()) status.Goroutines = int32(runtime.NumGoroutine())
status.ConnectionsOut = int32(conntrack.Count()) status.ConnectionsOut = int32(conntrack.Count())
s.serviceAccess.RLock()
nowService := s.instance nowService := s.instance
s.serviceAccess.RUnlock()
if nowService != nil { if nowService != nil {
if clashServer := nowService.clashServer; clashServer != nil { if clashServer := nowService.clashServer; clashServer != nil {
status.TrafficAvailable = true status.TrafficAvailable = true
@@ -417,10 +348,6 @@ func (s *StartedService) readStatus() *Status {
} }
func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.ServerStreamingServer[Groups]) error { func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.ServerStreamingServer[Groups]) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
subscription, done, err := s.urlTestObserver.Subscribe() subscription, done, err := s.urlTestObserver.Subscribe()
if err != nil { if err != nil {
return err return err
@@ -428,16 +355,18 @@ func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.Serve
defer s.urlTestObserver.UnSubscribe(subscription) defer s.urlTestObserver.UnSubscribe(subscription)
for { for {
s.serviceAccess.RLock() s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED { switch s.serviceStatus.Status {
case ServiceStatus_STARTING, ServiceStatus_STARTED:
groups := s.readGroups()
s.serviceAccess.RUnlock()
err = server.Send(groups)
if err != nil {
return err
}
default:
s.serviceAccess.RUnlock() s.serviceAccess.RUnlock()
return os.ErrInvalid return os.ErrInvalid
} }
groups := s.readGroups()
s.serviceAccess.RUnlock()
err = server.Send(groups)
if err != nil {
return err
}
select { select {
case <-subscription: case <-subscription:
case <-s.ctx.Done(): case <-s.ctx.Done():
@@ -514,27 +443,12 @@ func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.
} }
func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.ServerStreamingServer[ClashMode]) error { func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.ServerStreamingServer[ClashMode]) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
subscription, done, err := s.clashModeObserver.Subscribe() subscription, done, err := s.clashModeObserver.Subscribe()
if err != nil { if err != nil {
return err return err
} }
defer s.clashModeObserver.UnSubscribe(subscription) defer s.clashModeObserver.UnSubscribe(subscription)
for { for {
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return os.ErrInvalid
}
message := &ClashMode{Mode: s.instance.clashServer.Mode()}
s.serviceAccess.RUnlock()
err = server.Send(message)
if err != nil {
return err
}
select { select {
case <-subscription: case <-subscription:
case <-s.ctx.Done(): case <-s.ctx.Done():
@@ -544,6 +458,16 @@ func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.Se
case <-done: case <-done:
return nil return nil
} }
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
return nil
}
message := &ClashMode{Mode: s.instance.clashServer.Mode()}
s.serviceAccess.RUnlock()
err = server.Send(message)
if err != nil {
return err
}
} }
} }
@@ -580,7 +504,12 @@ func (s *StartedService) URLTest(ctx context.Context, request *URLTestRequest) (
if isURLTest { if isURLTest {
go urlTest.CheckOutbounds() go urlTest.CheckOutbounds()
} else { } else {
historyStorage := boxService.urlTestHistoryStorage var historyStorage adapter.URLTestHistoryStorage
if s.instance.clashServer != nil {
historyStorage = s.instance.clashServer.HistoryStorage()
} else {
return nil, E.New("Clash API is required for URLTest on non-URLTest group")
}
outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
itOutbound, _ := boxService.instance.Outbound().Outbound(it) itOutbound, _ := boxService.instance.Outbound().Outbound(it)
@@ -637,7 +566,6 @@ func (s *StartedService) SelectOutbound(ctx context.Context, request *SelectOutb
if !selector.SelectOutbound(request.OutboundTag) { if !selector.SelectOutbound(request.OutboundTag) {
return nil, E.New("outbound not found in selector: ", request.OutboundTag) return nil, E.New("outbound not found in selector: ", request.OutboundTag)
} }
s.urlTestObserver.Emit(struct{}{})
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
} }
@@ -661,290 +589,91 @@ func (s *StartedService) SetGroupExpand(ctx context.Context, request *SetGroupEx
} }
func (s *StartedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) { func (s *StartedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) {
return s.handler.SystemProxyStatus() return s.platformHandler.SystemProxyStatus()
} }
func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) { func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
err := s.handler.SetSystemProxyEnabled(request.Enabled) err := s.platformHandler.SetSystemProxyEnabled(request.Enabled)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nil, err return nil, err
} }
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error { func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[Connections]) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock() s.serviceAccess.RLock()
switch s.serviceStatus.Status {
case ServiceStatus_STARTING, ServiceStatus_STARTED:
default:
s.serviceAccess.RUnlock()
return os.ErrInvalid
}
boxService := s.instance boxService := s.instance
s.serviceAccess.RUnlock() s.serviceAccess.RUnlock()
ticker := time.NewTicker(time.Duration(request.Interval))
if boxService.clashServer == nil {
return E.New("clash server not available")
}
trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
subscription, done, err := s.connectionEventObserver.Subscribe()
if err != nil {
return err
}
defer s.connectionEventObserver.UnSubscribe(subscription)
connectionSnapshots := make(map[uuid.UUID]connectionSnapshot)
initialEvents := s.buildInitialConnectionState(trafficManager, connectionSnapshots)
err = server.Send(&ConnectionEvents{
Events: initialEvents,
Reset_: true,
})
if err != nil {
return err
}
interval := time.Duration(request.Interval)
if interval <= 0 {
interval = time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
var (
connections = make(map[uuid.UUID]*Connection)
outConnections []*Connection
)
for { for {
outConnections = outConnections[:0]
for _, connection := range trafficManager.Connections() {
outConnections = append(outConnections, newConnection(connections, connection, false))
}
for _, connection := range trafficManager.ClosedConnections() {
outConnections = append(outConnections, newConnection(connections, connection, true))
}
err := server.Send(&Connections{Connections: outConnections})
if err != nil {
return err
}
select { select {
case <-s.ctx.Done(): case <-s.ctx.Done():
return s.ctx.Err() return s.ctx.Err()
case <-server.Context().Done(): case <-server.Context().Done():
return server.Context().Err() return server.Context().Err()
case <-done:
return nil
case event := <-subscription:
var pendingEvents []*ConnectionEvent
if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil {
pendingEvents = append(pendingEvents, protoEvent)
}
drain:
for {
select {
case event = <-subscription:
if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil {
pendingEvents = append(pendingEvents, protoEvent)
}
default:
break drain
}
}
if len(pendingEvents) > 0 {
err = server.Send(&ConnectionEvents{Events: pendingEvents})
if err != nil {
return err
}
}
case <-ticker.C: case <-ticker.C:
protoEvents := s.buildTrafficUpdates(trafficManager, connectionSnapshots)
if len(protoEvents) == 0 {
continue
}
err = server.Send(&ConnectionEvents{Events: protoEvents})
if err != nil {
return err
}
} }
} }
} }
type connectionSnapshot struct { func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) *Connection {
uplink int64 if oldConnection, loaded := connections[metadata.ID]; loaded {
downlink int64 if isClosed {
hadTraffic bool if oldConnection.ClosedAt == 0 {
} oldConnection.Uplink = 0
oldConnection.Downlink = 0
func (s *StartedService) buildInitialConnectionState(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent { oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
var events []*ConnectionEvent
for _, metadata := range manager.Connections() {
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
Id: metadata.ID.String(),
Connection: buildConnectionProto(metadata),
})
snapshots[metadata.ID] = connectionSnapshot{
uplink: metadata.Upload.Load(),
downlink: metadata.Download.Load(),
}
}
for _, metadata := range manager.ClosedConnections() {
conn := buildConnectionProto(metadata)
conn.ClosedAt = metadata.ClosedAt.UnixMilli()
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
Id: metadata.ID.String(),
Connection: conn,
})
}
return events
}
func (s *StartedService) applyConnectionEvent(event trafficontrol.ConnectionEvent, snapshots map[uuid.UUID]connectionSnapshot) *ConnectionEvent {
switch event.Type {
case trafficontrol.ConnectionEventNew:
if _, exists := snapshots[event.ID]; exists {
return nil
}
snapshots[event.ID] = connectionSnapshot{
uplink: event.Metadata.Upload.Load(),
downlink: event.Metadata.Download.Load(),
}
return &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
Id: event.ID.String(),
Connection: buildConnectionProto(event.Metadata),
}
case trafficontrol.ConnectionEventClosed:
delete(snapshots, event.ID)
protoEvent := &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_CLOSED,
Id: event.ID.String(),
}
closedAt := event.ClosedAt
if closedAt.IsZero() && !event.Metadata.ClosedAt.IsZero() {
closedAt = event.Metadata.ClosedAt
}
if closedAt.IsZero() {
closedAt = time.Now()
}
protoEvent.ClosedAt = closedAt.UnixMilli()
if event.Metadata.ID != uuid.Nil {
conn := buildConnectionProto(event.Metadata)
conn.ClosedAt = protoEvent.ClosedAt
protoEvent.Connection = conn
}
return protoEvent
default:
return nil
}
}
func (s *StartedService) buildTrafficUpdates(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent {
activeConnections := manager.Connections()
activeIndex := make(map[uuid.UUID]*trafficontrol.TrackerMetadata, len(activeConnections))
var events []*ConnectionEvent
for _, metadata := range activeConnections {
activeIndex[metadata.ID] = metadata
currentUpload := metadata.Upload.Load()
currentDownload := metadata.Download.Load()
snapshot, exists := snapshots[metadata.ID]
if !exists {
snapshots[metadata.ID] = connectionSnapshot{
uplink: currentUpload,
downlink: currentDownload,
} }
events = append(events, &ConnectionEvent{ return oldConnection
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
Id: metadata.ID.String(),
Connection: buildConnectionProto(metadata),
})
continue
}
uplinkDelta := currentUpload - snapshot.uplink
downlinkDelta := currentDownload - snapshot.downlink
if uplinkDelta < 0 || downlinkDelta < 0 {
if snapshot.hadTraffic {
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_UPDATE,
Id: metadata.ID.String(),
UplinkDelta: 0,
DownlinkDelta: 0,
})
}
snapshot.uplink = currentUpload
snapshot.downlink = currentDownload
snapshot.hadTraffic = false
snapshots[metadata.ID] = snapshot
continue
}
if uplinkDelta > 0 || downlinkDelta > 0 {
snapshot.uplink = currentUpload
snapshot.downlink = currentDownload
snapshot.hadTraffic = true
snapshots[metadata.ID] = snapshot
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_UPDATE,
Id: metadata.ID.String(),
UplinkDelta: uplinkDelta,
DownlinkDelta: downlinkDelta,
})
continue
}
if snapshot.hadTraffic {
snapshot.uplink = currentUpload
snapshot.downlink = currentDownload
snapshot.hadTraffic = false
snapshots[metadata.ID] = snapshot
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_UPDATE,
Id: metadata.ID.String(),
UplinkDelta: 0,
DownlinkDelta: 0,
})
} }
lastUplink := oldConnection.UplinkTotal
lastDownlink := oldConnection.DownlinkTotal
uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load()
oldConnection.Uplink = uplinkTotal - lastUplink
oldConnection.Downlink = downlinkTotal - lastDownlink
oldConnection.UplinkTotal = uplinkTotal
oldConnection.DownlinkTotal = downlinkTotal
return oldConnection
} }
var closedIndex map[uuid.UUID]*trafficontrol.TrackerMetadata
for id := range snapshots {
if _, exists := activeIndex[id]; exists {
continue
}
if closedIndex == nil {
closedIndex = make(map[uuid.UUID]*trafficontrol.TrackerMetadata)
for _, metadata := range manager.ClosedConnections() {
closedIndex[metadata.ID] = metadata
}
}
closedAt := time.Now()
var conn *Connection
if metadata, ok := closedIndex[id]; ok {
if !metadata.ClosedAt.IsZero() {
closedAt = metadata.ClosedAt
}
conn = buildConnectionProto(metadata)
conn.ClosedAt = closedAt.UnixMilli()
}
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_CLOSED,
Id: id.String(),
ClosedAt: closedAt.UnixMilli(),
Connection: conn,
})
delete(snapshots, id)
}
return events
}
func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
var rule string var rule string
if metadata.Rule != nil { if metadata.Rule != nil {
rule = metadata.Rule.String() rule = metadata.Rule.String()
} }
uplinkTotal := metadata.Upload.Load() uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load() downlinkTotal := metadata.Download.Load()
var processInfo *ProcessInfo uplink := uplinkTotal
if metadata.Metadata.ProcessInfo != nil { downlink := downlinkTotal
processInfo = &ProcessInfo{ var closedAt int64
ProcessId: metadata.Metadata.ProcessInfo.ProcessID, if !metadata.ClosedAt.IsZero() {
UserId: metadata.Metadata.ProcessInfo.UserId, closedAt = metadata.ClosedAt.UnixMilli()
UserName: metadata.Metadata.ProcessInfo.UserName, uplink = 0
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath, downlink = 0
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
}
} }
return &Connection{ connection := &Connection{
Id: metadata.ID.String(), Id: metadata.ID.String(),
Inbound: metadata.Metadata.Inbound, Inbound: metadata.Metadata.Inbound,
InboundType: metadata.Metadata.InboundType, InboundType: metadata.Metadata.InboundType,
@@ -957,14 +686,18 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
User: metadata.Metadata.User, User: metadata.Metadata.User,
FromOutbound: metadata.Metadata.Outbound, FromOutbound: metadata.Metadata.Outbound,
CreatedAt: metadata.CreatedAt.UnixMilli(), CreatedAt: metadata.CreatedAt.UnixMilli(),
ClosedAt: closedAt,
Uplink: uplink,
Downlink: downlink,
UplinkTotal: uplinkTotal, UplinkTotal: uplinkTotal,
DownlinkTotal: downlinkTotal, DownlinkTotal: downlinkTotal,
Rule: rule, Rule: rule,
Outbound: metadata.Outbound, Outbound: metadata.Outbound,
OutboundType: metadata.OutboundType, OutboundType: metadata.OutboundType,
ChainList: metadata.Chain, ChainList: metadata.Chain,
ProcessInfo: processInfo,
} }
connections[metadata.ID] = connection
return connection
} }
func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConnectionRequest) (*emptypb.Empty, error) { func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConnectionRequest) (*emptypb.Empty, error) {
@@ -1009,10 +742,12 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty
}, nil }, nil
} }
func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty) (*StartedAt, error) { func (s *StartedService) SubscribeHelperEvents(empty *emptypb.Empty, server grpc.ServerStreamingServer[HelperRequest]) error {
s.serviceAccess.RLock() return os.ErrInvalid
defer s.serviceAccess.RUnlock() }
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
func (s *StartedService) SendHelperResponse(ctx context.Context, response *HelperResponse) (*emptypb.Empty, error) {
return nil, os.ErrInvalid
} }
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() { func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
@@ -1020,15 +755,15 @@ func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
func (s *StartedService) WriteMessage(level log.Level, message string) { func (s *StartedService) WriteMessage(level log.Level, message string) {
item := &log.Entry{Level: level, Message: message} item := &log.Entry{Level: level, Message: message}
s.logSubscriber.Emit(item)
s.logAccess.Lock() s.logAccess.Lock()
s.logLines.PushBack(item) s.logLines.PushBack(item)
if s.logLines.Len() > s.logMaxLines { if s.logLines.Len() > s.logMaxLines {
s.logLines.Remove(s.logLines.Front()) s.logLines.Remove(s.logLines.Front())
} }
s.logAccess.Unlock() s.logAccess.Unlock()
s.logSubscriber.Emit(item)
if s.debug { if s.debug {
s.handler.WriteDebugMessage(message) s.platformHandler.WriteDebugMessage(message)
} }
} }

View File

@@ -78,55 +78,104 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{0} return file_daemon_started_service_proto_rawDescGZIP(), []int{0}
} }
type ConnectionEventType int32 type ConnectionFilter int32
const ( const (
ConnectionEventType_CONNECTION_EVENT_NEW ConnectionEventType = 0 ConnectionFilter_ALL ConnectionFilter = 0
ConnectionEventType_CONNECTION_EVENT_UPDATE ConnectionEventType = 1 ConnectionFilter_ACTIVE ConnectionFilter = 1
ConnectionEventType_CONNECTION_EVENT_CLOSED ConnectionEventType = 2 ConnectionFilter_CLOSED ConnectionFilter = 2
) )
// Enum value maps for ConnectionEventType. // Enum value maps for ConnectionFilter.
var ( var (
ConnectionEventType_name = map[int32]string{ ConnectionFilter_name = map[int32]string{
0: "CONNECTION_EVENT_NEW", 0: "ALL",
1: "CONNECTION_EVENT_UPDATE", 1: "ACTIVE",
2: "CONNECTION_EVENT_CLOSED", 2: "CLOSED",
} }
ConnectionEventType_value = map[string]int32{ ConnectionFilter_value = map[string]int32{
"CONNECTION_EVENT_NEW": 0, "ALL": 0,
"CONNECTION_EVENT_UPDATE": 1, "ACTIVE": 1,
"CONNECTION_EVENT_CLOSED": 2, "CLOSED": 2,
} }
) )
func (x ConnectionEventType) Enum() *ConnectionEventType { func (x ConnectionFilter) Enum() *ConnectionFilter {
p := new(ConnectionEventType) p := new(ConnectionFilter)
*p = x *p = x
return p return p
} }
func (x ConnectionEventType) String() string { func (x ConnectionFilter) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
} }
func (ConnectionEventType) Descriptor() protoreflect.EnumDescriptor { func (ConnectionFilter) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[1].Descriptor() return file_daemon_started_service_proto_enumTypes[1].Descriptor()
} }
func (ConnectionEventType) Type() protoreflect.EnumType { func (ConnectionFilter) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[1] return &file_daemon_started_service_proto_enumTypes[1]
} }
func (x ConnectionEventType) Number() protoreflect.EnumNumber { func (x ConnectionFilter) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x) return protoreflect.EnumNumber(x)
} }
// Deprecated: Use ConnectionEventType.Descriptor instead. // Deprecated: Use ConnectionFilter.Descriptor instead.
func (ConnectionEventType) EnumDescriptor() ([]byte, []int) { func (ConnectionFilter) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{1} return file_daemon_started_service_proto_rawDescGZIP(), []int{1}
} }
type ConnectionSortBy int32
const (
ConnectionSortBy_DATE ConnectionSortBy = 0
ConnectionSortBy_TRAFFIC ConnectionSortBy = 1
ConnectionSortBy_TOTAL_TRAFFIC ConnectionSortBy = 2
)
// Enum value maps for ConnectionSortBy.
var (
ConnectionSortBy_name = map[int32]string{
0: "DATE",
1: "TRAFFIC",
2: "TOTAL_TRAFFIC",
}
ConnectionSortBy_value = map[string]int32{
"DATE": 0,
"TRAFFIC": 1,
"TOTAL_TRAFFIC": 2,
}
)
func (x ConnectionSortBy) Enum() *ConnectionSortBy {
p := new(ConnectionSortBy)
*p = x
return p
}
func (x ConnectionSortBy) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ConnectionSortBy) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[2].Descriptor()
}
func (ConnectionSortBy) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[2]
}
func (x ConnectionSortBy) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ConnectionSortBy.Descriptor instead.
func (ConnectionSortBy) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{2}
}
type ServiceStatus_Type int32 type ServiceStatus_Type int32
const ( const (
@@ -166,11 +215,11 @@ func (x ServiceStatus_Type) String() string {
} }
func (ServiceStatus_Type) Descriptor() protoreflect.EnumDescriptor { func (ServiceStatus_Type) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[2].Descriptor() return file_daemon_started_service_proto_enumTypes[3].Descriptor()
} }
func (ServiceStatus_Type) Type() protoreflect.EnumType { func (ServiceStatus_Type) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[2] return &file_daemon_started_service_proto_enumTypes[3]
} }
func (x ServiceStatus_Type) Number() protoreflect.EnumNumber { func (x ServiceStatus_Type) Number() protoreflect.EnumNumber {
@@ -1065,6 +1114,8 @@ func (x *SetSystemProxyEnabledRequest) GetEnabled() bool {
type SubscribeConnectionsRequest struct { type SubscribeConnectionsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"` Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"`
Filter ConnectionFilter `protobuf:"varint,2,opt,name=filter,proto3,enum=daemon.ConnectionFilter" json:"filter,omitempty"`
SortBy ConnectionSortBy `protobuf:"varint,3,opt,name=sortBy,proto3,enum=daemon.ConnectionSortBy" json:"sortBy,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -1106,32 +1157,41 @@ func (x *SubscribeConnectionsRequest) GetInterval() int64 {
return 0 return 0
} }
type ConnectionEvent struct { func (x *SubscribeConnectionsRequest) GetFilter() ConnectionFilter {
if x != nil {
return x.Filter
}
return ConnectionFilter_ALL
}
func (x *SubscribeConnectionsRequest) GetSortBy() ConnectionSortBy {
if x != nil {
return x.SortBy
}
return ConnectionSortBy_DATE
}
type Connections struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Type ConnectionEventType `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.ConnectionEventType" json:"type,omitempty"` Connections []*Connection `protobuf:"bytes,1,rep,name=connections,proto3" json:"connections,omitempty"`
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
Connection *Connection `protobuf:"bytes,3,opt,name=connection,proto3" json:"connection,omitempty"`
UplinkDelta int64 `protobuf:"varint,4,opt,name=uplinkDelta,proto3" json:"uplinkDelta,omitempty"`
DownlinkDelta int64 `protobuf:"varint,5,opt,name=downlinkDelta,proto3" json:"downlinkDelta,omitempty"`
ClosedAt int64 `protobuf:"varint,6,opt,name=closedAt,proto3" json:"closedAt,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *ConnectionEvent) Reset() { func (x *Connections) Reset() {
*x = ConnectionEvent{} *x = Connections{}
mi := &file_daemon_started_service_proto_msgTypes[17] mi := &file_daemon_started_service_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
func (x *ConnectionEvent) String() string { func (x *Connections) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*ConnectionEvent) ProtoMessage() {} func (*Connections) ProtoMessage() {}
func (x *ConnectionEvent) ProtoReflect() protoreflect.Message { func (x *Connections) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[17] mi := &file_daemon_started_service_proto_msgTypes[17]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -1143,105 +1203,18 @@ func (x *ConnectionEvent) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use ConnectionEvent.ProtoReflect.Descriptor instead. // Deprecated: Use Connections.ProtoReflect.Descriptor instead.
func (*ConnectionEvent) Descriptor() ([]byte, []int) { func (*Connections) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{17} return file_daemon_started_service_proto_rawDescGZIP(), []int{17}
} }
func (x *ConnectionEvent) GetType() ConnectionEventType { func (x *Connections) GetConnections() []*Connection {
if x != nil { if x != nil {
return x.Type return x.Connections
}
return ConnectionEventType_CONNECTION_EVENT_NEW
}
func (x *ConnectionEvent) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *ConnectionEvent) GetConnection() *Connection {
if x != nil {
return x.Connection
} }
return nil return nil
} }
func (x *ConnectionEvent) GetUplinkDelta() int64 {
if x != nil {
return x.UplinkDelta
}
return 0
}
func (x *ConnectionEvent) GetDownlinkDelta() int64 {
if x != nil {
return x.DownlinkDelta
}
return 0
}
func (x *ConnectionEvent) GetClosedAt() int64 {
if x != nil {
return x.ClosedAt
}
return 0
}
type ConnectionEvents struct {
state protoimpl.MessageState `protogen:"open.v1"`
Events []*ConnectionEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"`
Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConnectionEvents) Reset() {
*x = ConnectionEvents{}
mi := &file_daemon_started_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConnectionEvents) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConnectionEvents) ProtoMessage() {}
func (x *ConnectionEvents) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ConnectionEvents.ProtoReflect.Descriptor instead.
func (*ConnectionEvents) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
}
func (x *ConnectionEvents) GetEvents() []*ConnectionEvent {
if x != nil {
return x.Events
}
return nil
}
func (x *ConnectionEvents) GetReset_() bool {
if x != nil {
return x.Reset_
}
return false
}
type Connection struct { type Connection struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -1265,14 +1238,13 @@ type Connection struct {
Outbound string `protobuf:"bytes,19,opt,name=outbound,proto3" json:"outbound,omitempty"` Outbound string `protobuf:"bytes,19,opt,name=outbound,proto3" json:"outbound,omitempty"`
OutboundType string `protobuf:"bytes,20,opt,name=outboundType,proto3" json:"outboundType,omitempty"` OutboundType string `protobuf:"bytes,20,opt,name=outboundType,proto3" json:"outboundType,omitempty"`
ChainList []string `protobuf:"bytes,21,rep,name=chainList,proto3" json:"chainList,omitempty"` ChainList []string `protobuf:"bytes,21,rep,name=chainList,proto3" json:"chainList,omitempty"`
ProcessInfo *ProcessInfo `protobuf:"bytes,22,opt,name=processInfo,proto3" json:"processInfo,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Connection) Reset() { func (x *Connection) Reset() {
*x = Connection{} *x = Connection{}
mi := &file_daemon_started_service_proto_msgTypes[19] mi := &file_daemon_started_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1284,7 +1256,7 @@ func (x *Connection) String() string {
func (*Connection) ProtoMessage() {} func (*Connection) ProtoMessage() {}
func (x *Connection) ProtoReflect() protoreflect.Message { func (x *Connection) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[19] mi := &file_daemon_started_service_proto_msgTypes[18]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1297,7 +1269,7 @@ func (x *Connection) ProtoReflect() protoreflect.Message {
// Deprecated: Use Connection.ProtoReflect.Descriptor instead. // Deprecated: Use Connection.ProtoReflect.Descriptor instead.
func (*Connection) Descriptor() ([]byte, []int) { func (*Connection) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{19} return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
} }
func (x *Connection) GetId() string { func (x *Connection) GetId() string {
@@ -1447,89 +1419,6 @@ func (x *Connection) GetChainList() []string {
return nil return nil
} }
func (x *Connection) GetProcessInfo() *ProcessInfo {
if x != nil {
return x.ProcessInfo
}
return nil
}
type ProcessInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
ProcessId uint32 `protobuf:"varint,1,opt,name=processId,proto3" json:"processId,omitempty"`
UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"`
UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"`
ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"`
PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProcessInfo) Reset() {
*x = ProcessInfo{}
mi := &file_daemon_started_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProcessInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProcessInfo) ProtoMessage() {}
func (x *ProcessInfo) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProcessInfo.ProtoReflect.Descriptor instead.
func (*ProcessInfo) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
}
func (x *ProcessInfo) GetProcessId() uint32 {
if x != nil {
return x.ProcessId
}
return 0
}
func (x *ProcessInfo) GetUserId() int32 {
if x != nil {
return x.UserId
}
return 0
}
func (x *ProcessInfo) GetUserName() string {
if x != nil {
return x.UserName
}
return ""
}
func (x *ProcessInfo) GetProcessPath() string {
if x != nil {
return x.ProcessPath
}
return ""
}
func (x *ProcessInfo) GetPackageName() string {
if x != nil {
return x.PackageName
}
return ""
}
type CloseConnectionRequest struct { type CloseConnectionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -1539,7 +1428,7 @@ type CloseConnectionRequest struct {
func (x *CloseConnectionRequest) Reset() { func (x *CloseConnectionRequest) Reset() {
*x = CloseConnectionRequest{} *x = CloseConnectionRequest{}
mi := &file_daemon_started_service_proto_msgTypes[21] mi := &file_daemon_started_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1551,7 +1440,7 @@ func (x *CloseConnectionRequest) String() string {
func (*CloseConnectionRequest) ProtoMessage() {} func (*CloseConnectionRequest) ProtoMessage() {}
func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[21] mi := &file_daemon_started_service_proto_msgTypes[19]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1564,7 +1453,7 @@ func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead.
func (*CloseConnectionRequest) Descriptor() ([]byte, []int) { func (*CloseConnectionRequest) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{21} return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
} }
func (x *CloseConnectionRequest) GetId() string { func (x *CloseConnectionRequest) GetId() string {
@@ -1583,7 +1472,7 @@ type DeprecatedWarnings struct {
func (x *DeprecatedWarnings) Reset() { func (x *DeprecatedWarnings) Reset() {
*x = DeprecatedWarnings{} *x = DeprecatedWarnings{}
mi := &file_daemon_started_service_proto_msgTypes[22] mi := &file_daemon_started_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1595,7 +1484,7 @@ func (x *DeprecatedWarnings) String() string {
func (*DeprecatedWarnings) ProtoMessage() {} func (*DeprecatedWarnings) ProtoMessage() {}
func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message { func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[22] mi := &file_daemon_started_service_proto_msgTypes[20]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1608,7 +1497,7 @@ func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead. // Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead.
func (*DeprecatedWarnings) Descriptor() ([]byte, []int) { func (*DeprecatedWarnings) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{22} return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
} }
func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning { func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning {
@@ -1629,7 +1518,7 @@ type DeprecatedWarning struct {
func (x *DeprecatedWarning) Reset() { func (x *DeprecatedWarning) Reset() {
*x = DeprecatedWarning{} *x = DeprecatedWarning{}
mi := &file_daemon_started_service_proto_msgTypes[23] mi := &file_daemon_started_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1641,7 +1530,7 @@ func (x *DeprecatedWarning) String() string {
func (*DeprecatedWarning) ProtoMessage() {} func (*DeprecatedWarning) ProtoMessage() {}
func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message { func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[23] mi := &file_daemon_started_service_proto_msgTypes[21]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1654,7 +1543,7 @@ func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead. // Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead.
func (*DeprecatedWarning) Descriptor() ([]byte, []int) { func (*DeprecatedWarning) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{23} return file_daemon_started_service_proto_rawDescGZIP(), []int{21}
} }
func (x *DeprecatedWarning) GetMessage() string { func (x *DeprecatedWarning) GetMessage() string {
@@ -1678,50 +1567,6 @@ func (x *DeprecatedWarning) GetMigrationLink() string {
return "" return ""
} }
type StartedAt struct {
state protoimpl.MessageState `protogen:"open.v1"`
StartedAt int64 `protobuf:"varint,1,opt,name=startedAt,proto3" json:"startedAt,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StartedAt) Reset() {
*x = StartedAt{}
mi := &file_daemon_started_service_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StartedAt) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StartedAt) ProtoMessage() {}
func (x *StartedAt) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StartedAt.ProtoReflect.Descriptor instead.
func (*StartedAt) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{24}
}
func (x *StartedAt) GetStartedAt() int64 {
if x != nil {
return x.StartedAt
}
return 0
}
type Log_Message struct { type Log_Message struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"`
@@ -1732,7 +1577,7 @@ type Log_Message struct {
func (x *Log_Message) Reset() { func (x *Log_Message) Reset() {
*x = Log_Message{} *x = Log_Message{}
mi := &file_daemon_started_service_proto_msgTypes[25] mi := &file_daemon_started_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1744,7 +1589,7 @@ func (x *Log_Message) String() string {
func (*Log_Message) ProtoMessage() {} func (*Log_Message) ProtoMessage() {}
func (x *Log_Message) ProtoReflect() protoreflect.Message { func (x *Log_Message) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[25] mi := &file_daemon_started_service_proto_msgTypes[22]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1778,7 +1623,7 @@ var File_daemon_started_service_proto protoreflect.FileDescriptor
const file_daemon_started_service_proto_rawDesc = "" + const file_daemon_started_service_proto_rawDesc = "" +
"\n" + "\n" +
"\x1cdaemon/started_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xad\x01\n" + "\x1cdaemon/started_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\x1a\x13daemon/helper.proto\"\xad\x01\n" +
"\rServiceStatus\x122\n" + "\rServiceStatus\x122\n" +
"\x06status\x18\x01 \x01(\x0e2\x1a.daemon.ServiceStatus.TypeR\x06status\x12\"\n" + "\x06status\x18\x01 \x01(\x0e2\x1a.daemon.ServiceStatus.TypeR\x06status\x12\"\n" +
"\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\"D\n" + "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\"D\n" +
@@ -1845,21 +1690,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" + "\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" + "\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" +
"\x1cSetSystemProxyEnabledRequest\x12\x18\n" + "\x1cSetSystemProxyEnabledRequest\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\"9\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\"\x9d\x01\n" +
"\x1bSubscribeConnectionsRequest\x12\x1a\n" + "\x1bSubscribeConnectionsRequest\x12\x1a\n" +
"\binterval\x18\x01 \x01(\x03R\binterval\"\xea\x01\n" + "\binterval\x18\x01 \x01(\x03R\binterval\x120\n" +
"\x0fConnectionEvent\x12/\n" + "\x06filter\x18\x02 \x01(\x0e2\x18.daemon.ConnectionFilterR\x06filter\x120\n" +
"\x04type\x18\x01 \x01(\x0e2\x1b.daemon.ConnectionEventTypeR\x04type\x12\x0e\n" + "\x06sortBy\x18\x03 \x01(\x0e2\x18.daemon.ConnectionSortByR\x06sortBy\"C\n" +
"\x02id\x18\x02 \x01(\tR\x02id\x122\n" + "\vConnections\x124\n" +
"\n" + "\vconnections\x18\x01 \x03(\v2\x12.daemon.ConnectionR\vconnections\"\xde\x04\n" +
"connection\x18\x03 \x01(\v2\x12.daemon.ConnectionR\n" +
"connection\x12 \n" +
"\vuplinkDelta\x18\x04 \x01(\x03R\vuplinkDelta\x12$\n" +
"\rdownlinkDelta\x18\x05 \x01(\x03R\rdownlinkDelta\x12\x1a\n" +
"\bclosedAt\x18\x06 \x01(\x03R\bclosedAt\"Y\n" +
"\x10ConnectionEvents\x12/\n" +
"\x06events\x18\x01 \x03(\v2\x17.daemon.ConnectionEventR\x06events\x12\x14\n" +
"\x05reset\x18\x02 \x01(\bR\x05reset\"\x95\x05\n" +
"\n" + "\n" +
"Connection\x12\x0e\n" + "Connection\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" +
@@ -1883,14 +1720,7 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x04rule\x18\x12 \x01(\tR\x04rule\x12\x1a\n" + "\x04rule\x18\x12 \x01(\tR\x04rule\x12\x1a\n" +
"\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" + "\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" +
"\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" + "\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" +
"\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" + "\tchainList\x18\x15 \x03(\tR\tchainList\"(\n" +
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa3\x01\n" +
"\vProcessInfo\x12\x1c\n" +
"\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" +
"\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" +
"\buserName\x18\x03 \x01(\tR\buserName\x12 \n" +
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12 \n" +
"\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\n" +
"\x16CloseConnectionRequest\x12\x0e\n" + "\x16CloseConnectionRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" + "\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
"\x12DeprecatedWarnings\x125\n" + "\x12DeprecatedWarnings\x125\n" +
@@ -1898,9 +1728,7 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x11DeprecatedWarning\x12\x18\n" + "\x11DeprecatedWarning\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" + "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" +
"\timpending\x18\x02 \x01(\bR\timpending\x12$\n" + "\timpending\x18\x02 \x01(\bR\timpending\x12$\n" +
"\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink\")\n" + "\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink*U\n" +
"\tStartedAt\x12\x1c\n" +
"\tstartedAt\x18\x01 \x01(\x03R\tstartedAt*U\n" +
"\bLogLevel\x12\t\n" + "\bLogLevel\x12\t\n" +
"\x05PANIC\x10\x00\x12\t\n" + "\x05PANIC\x10\x00\x12\t\n" +
"\x05FATAL\x10\x01\x12\t\n" + "\x05FATAL\x10\x01\x12\t\n" +
@@ -1908,18 +1736,23 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x04WARN\x10\x03\x12\b\n" + "\x04WARN\x10\x03\x12\b\n" +
"\x04INFO\x10\x04\x12\t\n" + "\x04INFO\x10\x04\x12\t\n" +
"\x05DEBUG\x10\x05\x12\t\n" + "\x05DEBUG\x10\x05\x12\t\n" +
"\x05TRACE\x10\x06*i\n" + "\x05TRACE\x10\x06*3\n" +
"\x13ConnectionEventType\x12\x18\n" + "\x10ConnectionFilter\x12\a\n" +
"\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" + "\x03ALL\x10\x00\x12\n" +
"\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" + "\n" +
"\x17CONNECTION_EVENT_CLOSED\x10\x022\xe5\v\n" + "\x06ACTIVE\x10\x01\x12\n" +
"\n" +
"\x06CLOSED\x10\x02*<\n" +
"\x10ConnectionSortBy\x12\b\n" +
"\x04DATE\x10\x00\x12\v\n" +
"\aTRAFFIC\x10\x01\x12\x11\n" +
"\rTOTAL_TRAFFIC\x10\x022\xf8\v\n" +
"\x0eStartedService\x12=\n" + "\x0eStartedService\x12=\n" +
"\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" + "\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" +
"\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" + "\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" +
"\x16SubscribeServiceStatus\x12\x16.google.protobuf.Empty\x1a\x15.daemon.ServiceStatus\"\x000\x01\x127\n" + "\x16SubscribeServiceStatus\x12\x16.google.protobuf.Empty\x1a\x15.daemon.ServiceStatus\"\x000\x01\x127\n" +
"\fSubscribeLog\x12\x16.google.protobuf.Empty\x1a\v.daemon.Log\"\x000\x01\x12G\n" + "\fSubscribeLog\x12\x16.google.protobuf.Empty\x1a\v.daemon.Log\"\x000\x01\x12G\n" +
"\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12=\n" + "\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12E\n" +
"\tClearLogs\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12E\n" +
"\x0fSubscribeStatus\x12\x1e.daemon.SubscribeStatusRequest\x1a\x0e.daemon.Status\"\x000\x01\x12=\n" + "\x0fSubscribeStatus\x12\x1e.daemon.SubscribeStatusRequest\x1a\x0e.daemon.Status\"\x000\x01\x12=\n" +
"\x0fSubscribeGroups\x12\x16.google.protobuf.Empty\x1a\x0e.daemon.Groups\"\x000\x01\x12G\n" + "\x0fSubscribeGroups\x12\x16.google.protobuf.Empty\x1a\x0e.daemon.Groups\"\x000\x01\x12G\n" +
"\x12GetClashModeStatus\x12\x16.google.protobuf.Empty\x1a\x17.daemon.ClashModeStatus\"\x00\x12C\n" + "\x12GetClashModeStatus\x12\x16.google.protobuf.Empty\x1a\x17.daemon.ClashModeStatus\"\x00\x12C\n" +
@@ -1929,12 +1762,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" + "\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" +
"\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" + "\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" +
"\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" + "\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" +
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Y\n" + "\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n" +
"\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x18.daemon.ConnectionEvents\"\x000\x01\x12K\n" + "\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x13.daemon.Connections\"\x000\x01\x12K\n" +
"\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" + "\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" +
"\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" + "\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" +
"\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\n" + "\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12J\n" +
"\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" "\x15SubscribeHelperEvents\x12\x16.google.protobuf.Empty\x1a\x15.daemon.HelperRequest\"\x000\x01\x12F\n" +
"\x12SendHelperResponse\x12\x16.daemon.HelperResponse\x1a\x16.google.protobuf.Empty\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
var ( var (
file_daemon_started_service_proto_rawDescOnce sync.Once file_daemon_started_service_proto_rawDescOnce sync.Once
@@ -1949,101 +1783,100 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte {
} }
var ( var (
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3) file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 26) file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
file_daemon_started_service_proto_goTypes = []any{ file_daemon_started_service_proto_goTypes = []any{
(LogLevel)(0), // 0: daemon.LogLevel (LogLevel)(0), // 0: daemon.LogLevel
(ConnectionEventType)(0), // 1: daemon.ConnectionEventType (ConnectionFilter)(0), // 1: daemon.ConnectionFilter
(ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type (ConnectionSortBy)(0), // 2: daemon.ConnectionSortBy
(*ServiceStatus)(nil), // 3: daemon.ServiceStatus (ServiceStatus_Type)(0), // 3: daemon.ServiceStatus.Type
(*ReloadServiceRequest)(nil), // 4: daemon.ReloadServiceRequest (*ServiceStatus)(nil), // 4: daemon.ServiceStatus
(*SubscribeStatusRequest)(nil), // 5: daemon.SubscribeStatusRequest (*ReloadServiceRequest)(nil), // 5: daemon.ReloadServiceRequest
(*Log)(nil), // 6: daemon.Log (*SubscribeStatusRequest)(nil), // 6: daemon.SubscribeStatusRequest
(*DefaultLogLevel)(nil), // 7: daemon.DefaultLogLevel (*Log)(nil), // 7: daemon.Log
(*Status)(nil), // 8: daemon.Status (*DefaultLogLevel)(nil), // 8: daemon.DefaultLogLevel
(*Groups)(nil), // 9: daemon.Groups (*Status)(nil), // 9: daemon.Status
(*Group)(nil), // 10: daemon.Group (*Groups)(nil), // 10: daemon.Groups
(*GroupItem)(nil), // 11: daemon.GroupItem (*Group)(nil), // 11: daemon.Group
(*URLTestRequest)(nil), // 12: daemon.URLTestRequest (*GroupItem)(nil), // 12: daemon.GroupItem
(*SelectOutboundRequest)(nil), // 13: daemon.SelectOutboundRequest (*URLTestRequest)(nil), // 13: daemon.URLTestRequest
(*SetGroupExpandRequest)(nil), // 14: daemon.SetGroupExpandRequest (*SelectOutboundRequest)(nil), // 14: daemon.SelectOutboundRequest
(*ClashMode)(nil), // 15: daemon.ClashMode (*SetGroupExpandRequest)(nil), // 15: daemon.SetGroupExpandRequest
(*ClashModeStatus)(nil), // 16: daemon.ClashModeStatus (*ClashMode)(nil), // 16: daemon.ClashMode
(*SystemProxyStatus)(nil), // 17: daemon.SystemProxyStatus (*ClashModeStatus)(nil), // 17: daemon.ClashModeStatus
(*SetSystemProxyEnabledRequest)(nil), // 18: daemon.SetSystemProxyEnabledRequest (*SystemProxyStatus)(nil), // 18: daemon.SystemProxyStatus
(*SubscribeConnectionsRequest)(nil), // 19: daemon.SubscribeConnectionsRequest (*SetSystemProxyEnabledRequest)(nil), // 19: daemon.SetSystemProxyEnabledRequest
(*ConnectionEvent)(nil), // 20: daemon.ConnectionEvent (*SubscribeConnectionsRequest)(nil), // 20: daemon.SubscribeConnectionsRequest
(*ConnectionEvents)(nil), // 21: daemon.ConnectionEvents (*Connections)(nil), // 21: daemon.Connections
(*Connection)(nil), // 22: daemon.Connection (*Connection)(nil), // 22: daemon.Connection
(*ProcessInfo)(nil), // 23: daemon.ProcessInfo (*CloseConnectionRequest)(nil), // 23: daemon.CloseConnectionRequest
(*CloseConnectionRequest)(nil), // 24: daemon.CloseConnectionRequest (*DeprecatedWarnings)(nil), // 24: daemon.DeprecatedWarnings
(*DeprecatedWarnings)(nil), // 25: daemon.DeprecatedWarnings (*DeprecatedWarning)(nil), // 25: daemon.DeprecatedWarning
(*DeprecatedWarning)(nil), // 26: daemon.DeprecatedWarning (*Log_Message)(nil), // 26: daemon.Log.Message
(*StartedAt)(nil), // 27: daemon.StartedAt (*emptypb.Empty)(nil), // 27: google.protobuf.Empty
(*Log_Message)(nil), // 28: daemon.Log.Message (*HelperResponse)(nil), // 28: daemon.HelperResponse
(*emptypb.Empty)(nil), // 29: google.protobuf.Empty (*HelperRequest)(nil), // 29: daemon.HelperRequest
} }
) )
var file_daemon_started_service_proto_depIdxs = []int32{ var file_daemon_started_service_proto_depIdxs = []int32{
2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type 3, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
28, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 26, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel
10, // 3: daemon.Groups.group:type_name -> daemon.Group 11, // 3: daemon.Groups.group:type_name -> daemon.Group
11, // 4: daemon.Group.items:type_name -> daemon.GroupItem 12, // 4: daemon.Group.items:type_name -> daemon.GroupItem
1, // 5: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType 1, // 5: daemon.SubscribeConnectionsRequest.filter:type_name -> daemon.ConnectionFilter
22, // 6: daemon.ConnectionEvent.connection:type_name -> daemon.Connection 2, // 6: daemon.SubscribeConnectionsRequest.sortBy:type_name -> daemon.ConnectionSortBy
20, // 7: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent 22, // 7: daemon.Connections.connections:type_name -> daemon.Connection
23, // 8: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo 25, // 8: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning
26, // 9: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning 0, // 9: daemon.Log.Message.level:type_name -> daemon.LogLevel
0, // 10: daemon.Log.Message.level:type_name -> daemon.LogLevel 27, // 10: daemon.StartedService.StopService:input_type -> google.protobuf.Empty
29, // 11: daemon.StartedService.StopService:input_type -> google.protobuf.Empty 27, // 11: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty
29, // 12: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty 27, // 12: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
29, // 13: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty 27, // 13: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
29, // 14: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty 27, // 14: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
29, // 15: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty 6, // 15: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
29, // 16: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty 27, // 16: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
5, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest 27, // 17: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
29, // 18: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty 27, // 18: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
29, // 19: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty 16, // 19: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
29, // 20: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty 13, // 20: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
15, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode 14, // 21: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
12, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest 15, // 22: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
13, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest 27, // 23: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
14, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest 19, // 24: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
29, // 25: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty 20, // 25: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
18, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest 23, // 26: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
19, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest 27, // 27: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
24, // 28: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest 27, // 28: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
29, // 29: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty 27, // 29: daemon.StartedService.SubscribeHelperEvents:input_type -> google.protobuf.Empty
29, // 30: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty 28, // 30: daemon.StartedService.SendHelperResponse:input_type -> daemon.HelperResponse
29, // 31: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty 27, // 31: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
29, // 32: daemon.StartedService.StopService:output_type -> google.protobuf.Empty 27, // 32: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
29, // 33: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty 4, // 33: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
3, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus 7, // 34: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
6, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log 8, // 35: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
7, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel 9, // 36: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
29, // 37: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty 10, // 37: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
8, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status 17, // 38: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
9, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups 16, // 39: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
16, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus 27, // 40: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
15, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode 27, // 41: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
29, // 42: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty 27, // 42: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
29, // 43: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty 27, // 43: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
29, // 44: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty 18, // 44: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
29, // 45: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty 27, // 45: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
17, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus 21, // 46: daemon.StartedService.SubscribeConnections:output_type -> daemon.Connections
29, // 47: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty 27, // 47: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents 27, // 48: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
29, // 49: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty 24, // 49: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
29, // 50: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty 29, // 50: daemon.StartedService.SubscribeHelperEvents:output_type -> daemon.HelperRequest
25, // 51: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings 27, // 51: daemon.StartedService.SendHelperResponse:output_type -> google.protobuf.Empty
27, // 52: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt 31, // [31:52] is the sub-list for method output_type
32, // [32:53] is the sub-list for method output_type 10, // [10:31] is the sub-list for method input_type
11, // [11:32] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee
11, // [11:11] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name
0, // [0:11] is the sub-list for field type_name
} }
func init() { file_daemon_started_service_proto_init() } func init() { file_daemon_started_service_proto_init() }
@@ -2051,13 +1884,14 @@ func file_daemon_started_service_proto_init() {
if File_daemon_started_service_proto != nil { if File_daemon_started_service_proto != nil {
return return
} }
file_daemon_helper_proto_init()
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)),
NumEnums: 3, NumEnums: 4,
NumMessages: 26, NumMessages: 23,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -4,6 +4,7 @@ package daemon;
option go_package = "github.com/sagernet/sing-box/daemon"; option go_package = "github.com/sagernet/sing-box/daemon";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "daemon/helper.proto";
service StartedService { service StartedService {
rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty); rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty);
@@ -12,7 +13,6 @@ service StartedService {
rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {} rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {}
rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {} rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {}
rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {} rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {}
rpc ClearLogs(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc SubscribeStatus(SubscribeStatusRequest) returns(stream Status) {} rpc SubscribeStatus(SubscribeStatusRequest) returns(stream Status) {}
rpc SubscribeGroups(google.protobuf.Empty) returns(stream Groups) {} rpc SubscribeGroups(google.protobuf.Empty) returns(stream Groups) {}
@@ -27,11 +27,13 @@ service StartedService {
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {} rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {} rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {} rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream Connections) {}
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {} rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {} rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {} rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
rpc SubscribeHelperEvents(google.protobuf.Empty) returns(stream HelperRequest) {}
rpc SendHelperResponse(HelperResponse) returns(google.protobuf.Empty) {}
} }
message ServiceStatus { message ServiceStatus {
@@ -143,26 +145,24 @@ message SetSystemProxyEnabledRequest {
message SubscribeConnectionsRequest { message SubscribeConnectionsRequest {
int64 interval = 1; int64 interval = 1;
ConnectionFilter filter = 2;
ConnectionSortBy sortBy = 3;
} }
enum ConnectionEventType { enum ConnectionFilter {
CONNECTION_EVENT_NEW = 0; ALL = 0;
CONNECTION_EVENT_UPDATE = 1; ACTIVE = 1;
CONNECTION_EVENT_CLOSED = 2; CLOSED = 2;
} }
message ConnectionEvent { enum ConnectionSortBy {
ConnectionEventType type = 1; DATE = 0;
string id = 2; TRAFFIC = 1;
Connection connection = 3; TOTAL_TRAFFIC = 2;
int64 uplinkDelta = 4;
int64 downlinkDelta = 5;
int64 closedAt = 6;
} }
message ConnectionEvents { message Connections {
repeated ConnectionEvent events = 1; repeated Connection connections = 1;
bool reset = 2;
} }
message Connection { message Connection {
@@ -187,15 +187,6 @@ message Connection {
string outbound = 19; string outbound = 19;
string outboundType = 20; string outboundType = 20;
repeated string chainList = 21; 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 { message CloseConnectionRequest {
@@ -210,8 +201,4 @@ message DeprecatedWarning {
string message = 1; string message = 1;
bool impending = 2; bool impending = 2;
string migrationLink = 3; string migrationLink = 3;
}
message StartedAt {
int64 startedAt = 1;
} }

View File

@@ -20,7 +20,6 @@ const (
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
@@ -35,7 +34,8 @@ const (
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" StartedService_SubscribeHelperEvents_FullMethodName = "/daemon.StartedService/SubscribeHelperEvents"
StartedService_SendHelperResponse_FullMethodName = "/daemon.StartedService/SendHelperResponse"
) )
// StartedServiceClient is the client API for StartedService service. // StartedServiceClient is the client API for StartedService service.
@@ -47,7 +47,6 @@ type StartedServiceClient interface {
SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], 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) 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) 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) 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) 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) GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error)
@@ -58,11 +57,12 @@ type StartedServiceClient interface {
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, 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) GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, 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) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error)
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, 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) 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) GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error) SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error)
SendHelperResponse(ctx context.Context, in *HelperResponse, opts ...grpc.CallOption) (*emptypb.Empty, error)
} }
type startedServiceClient struct { type startedServiceClient struct {
@@ -141,16 +141,6 @@ func (c *startedServiceClient) GetDefaultLogLevel(ctx context.Context, in *empty
return out, nil 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) { func (c *startedServiceClient) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[2], StartedService_SubscribeStatus_FullMethodName, cOpts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[2], StartedService_SubscribeStatus_FullMethodName, cOpts...)
@@ -278,13 +268,13 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
return out, nil return out, nil
} }
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) { func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
x := &grpc.GenericClientStream[SubscribeConnectionsRequest, ConnectionEvents]{ClientStream: stream} x := &grpc.GenericClientStream[SubscribeConnectionsRequest, Connections]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil { if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err return nil, err
} }
@@ -295,7 +285,7 @@ func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *Sub
} }
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // 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] type StartedService_SubscribeConnectionsClient = grpc.ServerStreamingClient[Connections]
func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
@@ -327,10 +317,29 @@ func (c *startedServiceClient) GetDeprecatedWarnings(ctx context.Context, in *em
return out, nil return out, nil
} }
func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error) { func (c *startedServiceClient) SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StartedAt) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeHelperEvents_FullMethodName, cOpts...)
err := c.cc.Invoke(ctx, StartedService_GetStartedAt_FullMethodName, in, out, cOpts...) if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, HelperRequest]{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_SubscribeHelperEventsClient = grpc.ServerStreamingClient[HelperRequest]
func (c *startedServiceClient) SendHelperResponse(ctx context.Context, in *HelperResponse, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_SendHelperResponse_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -346,7 +355,6 @@ type StartedServiceServer interface {
SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error
SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error
GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error)
ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error
SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error
GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error)
@@ -357,11 +365,12 @@ type StartedServiceServer interface {
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error
SendHelperResponse(context.Context, *HelperResponse) (*emptypb.Empty, error)
mustEmbedUnimplementedStartedServiceServer() mustEmbedUnimplementedStartedServiceServer()
} }
@@ -373,87 +382,87 @@ type StartedServiceServer interface {
type UnimplementedStartedServiceServer struct{} type UnimplementedStartedServiceServer struct{}
func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method StopService not implemented") return nil, status.Errorf(codes.Unimplemented, "method StopService not implemented")
} }
func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ReloadService not implemented") return nil, status.Errorf(codes.Unimplemented, "method ReloadService not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error { func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error {
return status.Error(codes.Unimplemented, "method SubscribeServiceStatus not implemented") return status.Errorf(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error { func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error {
return status.Error(codes.Unimplemented, "method SubscribeLog not implemented") return status.Errorf(codes.Unimplemented, "method SubscribeLog not implemented")
} }
func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) { func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) {
return nil, status.Error(codes.Unimplemented, "method GetDefaultLogLevel not implemented") return nil, status.Errorf(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 { func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error {
return status.Error(codes.Unimplemented, "method SubscribeStatus not implemented") return status.Errorf(codes.Unimplemented, "method SubscribeStatus not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error { func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error {
return status.Error(codes.Unimplemented, "method SubscribeGroups not implemented") return status.Errorf(codes.Unimplemented, "method SubscribeGroups not implemented")
} }
func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) { func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) {
return nil, status.Error(codes.Unimplemented, "method GetClashModeStatus not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetClashModeStatus not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error { func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error {
return status.Error(codes.Unimplemented, "method SubscribeClashMode not implemented") return status.Errorf(codes.Unimplemented, "method SubscribeClashMode not implemented")
} }
func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetClashMode not implemented") return nil, status.Errorf(codes.Unimplemented, "method SetClashMode not implemented")
} }
func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method URLTest not implemented") return nil, status.Errorf(codes.Unimplemented, "method URLTest not implemented")
} }
func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SelectOutbound not implemented") return nil, status.Errorf(codes.Unimplemented, "method SelectOutbound not implemented")
} }
func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetGroupExpand not implemented") return nil, status.Errorf(codes.Unimplemented, "method SetGroupExpand not implemented")
} }
func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) { func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) {
return nil, status.Error(codes.Unimplemented, "method GetSystemProxyStatus not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
} }
func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented") return nil, status.Errorf(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error { func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error {
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented") return status.Errorf(codes.Unimplemented, "method SubscribeConnections not implemented")
} }
func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CloseConnection not implemented") return nil, status.Errorf(codes.Unimplemented, "method CloseConnection not implemented")
} }
func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CloseAllConnections not implemented") return nil, status.Errorf(codes.Unimplemented, "method CloseAllConnections not implemented")
} }
func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) { func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) {
return nil, status.Error(codes.Unimplemented, "method GetDeprecatedWarnings not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetDeprecatedWarnings not implemented")
} }
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) { func (UnimplementedStartedServiceServer) SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error {
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented") return status.Errorf(codes.Unimplemented, "method SubscribeHelperEvents not implemented")
}
func (UnimplementedStartedServiceServer) SendHelperResponse(context.Context, *HelperResponse) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SendHelperResponse not implemented")
} }
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {} func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
@@ -466,7 +475,7 @@ type UnsafeStartedServiceServer interface {
} }
func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) { func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) {
// If the following call panics, it indicates UnimplementedStartedServiceServer was // If the following call pancis, it indicates UnimplementedStartedServiceServer was
// embedded by pointer and is nil. This will cause panics if an // embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization // unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O. // time to prevent it from happening at runtime later due to I/O.
@@ -552,24 +561,6 @@ func _StartedService_GetDefaultLogLevel_Handler(srv interface{}, ctx context.Con
return interceptor(ctx, in, info, handler) 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 { func _StartedService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeStatusRequest) m := new(SubscribeStatusRequest)
if err := stream.RecvMsg(m); err != nil { if err := stream.RecvMsg(m); err != nil {
@@ -734,11 +725,11 @@ func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.S
if err := stream.RecvMsg(m); err != nil { if err := stream.RecvMsg(m); err != nil {
return err return err
} }
return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, ConnectionEvents]{ServerStream: stream}) return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, Connections]{ServerStream: stream})
} }
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // 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] type StartedService_SubscribeConnectionsServer = grpc.ServerStreamingServer[Connections]
func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CloseConnectionRequest) in := new(CloseConnectionRequest)
@@ -794,20 +785,31 @@ func _StartedService_GetDeprecatedWarnings_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _StartedService_SubscribeHelperEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
in := new(emptypb.Empty) m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeHelperEvents(m, &grpc.GenericServerStream[emptypb.Empty, HelperRequest]{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_SubscribeHelperEventsServer = grpc.ServerStreamingServer[HelperRequest]
func _StartedService_SendHelperResponse_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelperResponse)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(StartedServiceServer).GetStartedAt(ctx, in) return srv.(StartedServiceServer).SendHelperResponse(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: StartedService_GetStartedAt_FullMethodName, FullMethod: StartedService_SendHelperResponse_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).GetStartedAt(ctx, req.(*emptypb.Empty)) return srv.(StartedServiceServer).SendHelperResponse(ctx, req.(*HelperResponse))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
@@ -831,10 +833,6 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetDefaultLogLevel", MethodName: "GetDefaultLogLevel",
Handler: _StartedService_GetDefaultLogLevel_Handler, Handler: _StartedService_GetDefaultLogLevel_Handler,
}, },
{
MethodName: "ClearLogs",
Handler: _StartedService_ClearLogs_Handler,
},
{ {
MethodName: "GetClashModeStatus", MethodName: "GetClashModeStatus",
Handler: _StartedService_GetClashModeStatus_Handler, Handler: _StartedService_GetClashModeStatus_Handler,
@@ -876,8 +874,8 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
Handler: _StartedService_GetDeprecatedWarnings_Handler, Handler: _StartedService_GetDeprecatedWarnings_Handler,
}, },
{ {
MethodName: "GetStartedAt", MethodName: "SendHelperResponse",
Handler: _StartedService_GetStartedAt_Handler, Handler: _StartedService_SendHelperResponse_Handler,
}, },
}, },
Streams: []grpc.StreamDesc{ Streams: []grpc.StreamDesc{
@@ -911,6 +909,11 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
Handler: _StartedService_SubscribeConnections_Handler, Handler: _StartedService_SubscribeConnections_Handler,
ServerStreams: true, ServerStreams: true,
}, },
{
StreamName: "SubscribeHelperEvents",
Handler: _StartedService_SubscribeHelperEvents_Handler,
ServerStreams: true,
},
}, },
Metadata: "daemon/started_service.proto", Metadata: "daemon/started_service.proto",
} }

View File

@@ -95,20 +95,6 @@ func (c *Client) Start() {
} }
} }
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
for _, record := range response.Ns {
if soa, isSOA := record.(*dns.SOA); isSOA {
soaTTL := soa.Header().Ttl
soaMinimum := soa.Minttl
if soaTTL < soaMinimum {
return soaTTL, true
}
return soaMinimum, true
}
}
return 0, false
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) { func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
if len(message.Question) == 0 { if len(message.Question) == 0 {
if c.logger != nil { if c.logger != nil {
@@ -144,11 +130,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if c.cache != nil { if c.cache != nil {
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{})) cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded { if loaded {
select { <-cond
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else { } else {
defer func() { defer func() {
c.cacheLock.Delete(question) c.cacheLock.Delete(question)
@@ -158,11 +140,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} else if c.transportCache != nil { } else if c.transportCache != nil {
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{})) cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
if loaded { if loaded {
select { <-cond
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else { } else {
defer func() { defer func() {
c.transportCacheLock.Delete(question) c.transportCacheLock.Delete(question)
@@ -236,7 +214,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
response.Answer = append(response.Answer, validResponse.Answer...) response.Answer = append(response.Answer, validResponse.Answer...)
} }
}*/ }*/
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError) disableCache = disableCache || response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0
if responseChecker != nil { if responseChecker != nil {
var rejected bool var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses // TODO: add accept_any rule and support to check response instead of addresses
@@ -273,17 +251,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} }
} }
var timeToLive uint32 var timeToLive uint32
if len(response.Answer) == 0 { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA { for _, record := range recordList {
timeToLive = soaTTL if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
} timeToLive = record.Header().Ttl
}
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
} }
} }
} }
@@ -361,6 +332,64 @@ 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)
response6, _ := c.loadResponse(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if response4 != nil || response6 != nil {
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 { func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
if strategy == C.DomainStrategyPreferIPv6 { if strategy == C.DomainStrategyPreferIPv6 {
return append(response6, response4...) return append(response6, response4...)

View File

@@ -213,94 +213,96 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
} }
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String())) r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
var ( var (
response *mDNS.Msg
transport adapter.DNSTransport transport adapter.DNSTransport
err error err error
) )
var metadata *adapter.InboundContext response, cached := r.client.ExchangeCache(ctx, message)
ctx, metadata = adapter.ExtendContext(ctx) if !cached {
metadata.Destination = M.Socksaddr{} var metadata *adapter.InboundContext
metadata.QueryType = message.Question[0].Qtype ctx, metadata = adapter.ExtendContext(ctx)
switch metadata.QueryType { metadata.Destination = M.Socksaddr{}
case mDNS.TypeA: metadata.QueryType = message.Question[0].Qtype
metadata.IPVersion = 4 switch metadata.QueryType {
case mDNS.TypeAAAA: case mDNS.TypeA:
metadata.IPVersion = 6 metadata.IPVersion = 4
} case mDNS.TypeAAAA:
metadata.Domain = FqdnToDomain(message.Question[0].Name) metadata.IPVersion = 6
if options.Transport != nil { }
transport = options.Transport metadata.Domain = FqdnToDomain(message.Question[0].Name)
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { 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 { if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy() options.Strategy = r.defaultDomainStrategy
} }
if !options.ClientSubnet.IsValid() { response, err = r.client.Exchange(ctx, transport, message, options, nil)
options.ClientSubnet = legacyTransport.LegacyClientSubnet() } else {
} var (
} rule adapter.DNSRule
if options.Strategy == C.DomainStrategyAsIS { ruleIndex int
options.Strategy = r.defaultDomainStrategy )
} ruleIndex = -1
response, err = r.client.Exchange(ctx, transport, message, options, nil) for {
} else { dnsCtx := adapter.OverrideContext(ctx)
var ( dnsOptions := options
rule adapter.DNSRule transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
ruleIndex int if rule != nil {
) switch action := rule.Action().(type) {
ruleIndex = -1 case *R.RuleActionReject:
for { switch action.Method {
dnsCtx := adapter.OverrideContext(ctx) case C.RuleActionRejectMethodDefault:
dnsOptions := options return &mDNS.Msg{
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions) MsgHdr: mDNS.MsgHdr{
if rule != nil { Id: message.Id,
switch action := rule.Action().(type) { Rcode: mDNS.RcodeRefused,
case *R.RuleActionReject: Response: true,
switch action.Method { },
case C.RuleActionRejectMethodDefault: Question: []mDNS.Question{message.Question[0]},
return &mDNS.Msg{ }, nil
MsgHdr: mDNS.MsgHdr{ case C.RuleActionRejectMethodDrop:
Id: message.Id, return nil, tun.ErrDrop
Rcode: mDNS.RcodeRefused, }
Response: true, case *R.RuleActionPredefined:
}, return action.Response(message), nil
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
} }
case *R.RuleActionPredefined:
return action.Response(message), nil
} }
} var responseCheck func(responseAddrs []netip.Addr) bool
var responseCheck func(responseAddrs []netip.Addr) bool if rule != nil && rule.WithAddressLimit() {
if rule != nil && rule.WithAddressLimit() { responseCheck = func(responseAddrs []netip.Addr) bool {
responseCheck = func(responseAddrs []netip.Addr) bool { metadata.DestinationAddresses = responseAddrs
metadata.DestinationAddresses = responseAddrs return rule.MatchAddressLimit(metadata)
return rule.MatchAddressLimit(metadata) }
} }
} if dnsOptions.Strategy == C.DomainStrategyAsIS {
if dnsOptions.Strategy == C.DomainStrategyAsIS { dnsOptions.Strategy = r.defaultDomainStrategy
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>"))
} }
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 { if err != nil {
@@ -324,6 +326,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) { func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
var ( var (
responseAddrs []netip.Addr responseAddrs []netip.Addr
cached bool
err error err error
) )
printResult := func() { printResult := func() {
@@ -343,6 +346,13 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err = E.Cause(err, "lookup ", domain) 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) r.logger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.ExtendContext(ctx) ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{} metadata.Destination = M.Socksaddr{}
@@ -375,13 +385,16 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
if rule != nil { if rule != nil {
switch action := rule.Action().(type) { switch action := rule.Action().(type) {
case *R.RuleActionReject: case *R.RuleActionReject:
return nil, &R.RejectedError{Cause: action.Error(ctx)} switch action.Method {
case C.RuleActionRejectMethodDefault:
return nil, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined: case *R.RuleActionPredefined:
responseAddrs = nil
if action.Rcode != mDNS.RcodeSuccess { if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode) err = RcodeError(action.Rcode)
} else { } else {
err = nil
for _, answer := range action.Answer { for _, answer := range action.Answer {
switch record := answer.(type) { switch record := answer.(type) {
case *mDNS.A: case *mDNS.A:
@@ -446,6 +459,6 @@ func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
func (r *Router) ResetNetwork() { func (r *Router) ResetNetwork() {
r.ClearCache() r.ClearCache()
for _, transport := range r.transport.Transports() { 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] interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
transportLock sync.RWMutex transportLock sync.RWMutex
updatedAt time.Time updatedAt time.Time
lastError error
servers []M.Socksaddr servers []M.Socksaddr
search []string search []string
ndots int ndots int
@@ -93,7 +92,7 @@ func (t *Transport) Start(stage adapter.StartStage) error {
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated) t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
} }
go func() { go func() {
_, err := t.fetch() _, err := t.Fetch()
if err != nil { if err != nil {
t.logger.Error(E.Cause(err, "fetch DNS servers")) t.logger.Error(E.Cause(err, "fetch DNS servers"))
} }
@@ -108,15 +107,8 @@ func (t *Transport) Close() error {
return nil 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) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
servers, err := t.fetch() servers, err := t.Fetch()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -136,20 +128,11 @@ func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []
} }
} }
func (t *Transport) Fetch() []M.Socksaddr { func (t *Transport) Fetch() ([]M.Socksaddr, error) {
servers, _ := t.fetch()
return servers
}
func (t *Transport) fetch() ([]M.Socksaddr, error) {
t.transportLock.RLock() t.transportLock.RLock()
updatedAt := t.updatedAt updatedAt := t.updatedAt
lastError := t.lastError
servers := t.servers servers := t.servers
t.transportLock.RUnlock() t.transportLock.RUnlock()
if lastError != nil {
return nil, lastError
}
if time.Since(updatedAt) < C.DHCPTTL { if time.Since(updatedAt) < C.DHCPTTL {
return servers, nil return servers, nil
} }
@@ -160,7 +143,7 @@ func (t *Transport) fetch() ([]M.Socksaddr, error) {
} }
err := t.updateServers() err := t.updateServers()
if err != nil { if err != nil {
return servers, err return nil, err
} }
return t.servers, nil return t.servers, nil
} }
@@ -190,15 +173,12 @@ func (t *Transport) updateServers() error {
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout) fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
err = t.fetchServers0(fetchCtx, iface) err = t.fetchServers0(fetchCtx, iface)
cancel() cancel()
t.updatedAt = time.Now()
if err != nil { if err != nil {
t.lastError = err
return err return err
} else if len(t.servers) == 0 { } else if len(t.servers) == 0 {
t.lastError = E.New("dhcp: empty DNS servers response") return E.New("dhcp: empty DNS servers response")
return t.lastError
} else { } else {
t.lastError = nil t.updatedAt = time.Now()
return nil return nil
} }
} }
@@ -263,7 +243,6 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
defer buffer.Release() defer buffer.Release()
for { for {
buffer.Reset()
_, _, err := buffer.ReadPacketFrom(packetConn) _, _, err := buffer.ReadPacketFrom(packetConn)
if err != nil { if err != nil {
if errors.Is(err, io.ErrShortBuffer) { 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 { func (s *MemoryStorage) FakeIPReset() error {
s.addressAccess.Lock()
s.domainAccess.Lock()
s.addressCache = make(map[netip.Addr]string) s.addressCache = make(map[netip.Addr]string)
s.domainCache4 = make(map[string]netip.Addr) s.domainCache4 = make(map[string]netip.Addr)
s.domainCache6 = make(map[string]netip.Addr) s.domainCache6 = make(map[string]netip.Addr)
s.domainAccess.Unlock()
s.addressAccess.Unlock()
return nil return nil
} }

View File

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

View File

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

View File

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

View File

@@ -75,6 +75,5 @@ func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
http2Transport: &http2.Transport{ http2Transport: &http2.Transport{
DialTLSContext: h.http2Transport.DialTLSContext, DialTLSContext: h.http2Transport.DialTLSContext,
}, },
fallback: h.fallback,
} }
} }

View File

@@ -53,15 +53,13 @@ func (t *Transport) Start(stage adapter.StartStage) error {
switch stage { switch stage {
case adapter.StartStateInitialize: case adapter.StartStateInitialize:
if !t.preferGo { if !t.preferGo {
if isSystemdResolvedManaged() { resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger)
resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger) if err == nil {
err = resolvedResolver.Start()
if err == nil { if err == nil {
err = resolvedResolver.Start() t.resolved = resolvedResolver
if err == nil { } else {
t.resolved = resolvedResolver t.logger.Warn(E.Cause(err, "initialize resolved resolver"))
} else {
t.logger.Warn(E.Cause(err, "initialize resolved resolver"))
}
} }
} }
} }
@@ -76,9 +74,6 @@ func (t *Transport) Close() error {
return nil return nil
} }
func (t *Transport) Reset() {
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if t.resolved != nil { if t.resolved != nil {
resolverObject := t.resolved.Object() resolverObject := t.resolved.Object()
@@ -87,11 +82,12 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
} }
} }
question := message.Question[0] question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) addresses := t.hosts.Lookup(domain)
if len(addresses) > 0 { if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} }
} }
return t.exchange(ctx, message, question.Name) return t.exchange(ctx, message, domain)
} }

View File

@@ -43,7 +43,7 @@ type Transport struct {
type dhcpTransport interface { type dhcpTransport interface {
adapter.DNSTransport adapter.DNSTransport
Fetch() []M.Socksaddr Fetch() ([]M.Socksaddr, error)
Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, 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 break
} }
} }
if t.fallback { if !C.IsIos {
t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger) if t.fallback {
if t.dhcpTransport != nil { t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger)
err := t.dhcpTransport.Start(stage) if t.dhcpTransport != nil {
if err != nil { err := t.dhcpTransport.Start(stage)
return err if err != nil {
return err
}
} }
} }
} }
@@ -92,32 +94,29 @@ 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) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0] question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) addresses := t.hosts.Lookup(domain)
if len(addresses) > 0 { if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} }
} }
if !t.fallback { if !t.fallback {
return t.exchange(ctx, message, question.Name) return t.exchange(ctx, message, domain)
} }
if t.dhcpTransport != nil { if !C.IsIos {
dhcpTransports := t.dhcpTransport.Fetch() if t.dhcpTransport != nil {
if len(dhcpTransports) > 0 { dhcpTransports, _ := t.dhcpTransport.Fetch()
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports) if len(dhcpTransports) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
}
} }
} }
if t.preferGo { if t.preferGo {
// Assuming the user knows what they are doing, we still execute the query which will fail. // Assuming the user knows what they are doing, we still execute the query which will fail.
return t.exchange(ctx, message, question.Name) return t.exchange(ctx, message, domain)
} }
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
var network string var network string
@@ -126,7 +125,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
} else { } else {
network = "ip6" network = "ip6"
} }
addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name) addresses, err := t.resolver.LookupNetIP(ctx, network, domain)
if err != nil { if err != nil {
var dnsError *net.DNSError var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound { if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -136,5 +135,9 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
} }
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil 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

@@ -1,11 +1,9 @@
package local package local
import ( import (
"bufio"
"context" "context"
"errors" "errors"
"os" "os"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -24,25 +22,6 @@ import (
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
func isSystemdResolvedManaged() bool {
resolvContent, err := os.Open("/etc/resolv.conf")
if err != nil {
return false
}
defer resolvContent.Close()
scanner := bufio.NewScanner(resolvContent)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || line[0] != '#' {
return false
}
if strings.Contains(line, "systemd-resolved") {
return true
}
}
return false
}
type DBusResolvedResolver struct { type DBusResolvedResolver struct {
ctx context.Context ctx context.Context
logger logger.ContextLogger logger logger.ContextLogger
@@ -209,7 +188,7 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
int32(defaultInterface.Index), int32(defaultInterface.Index),
) )
if call.Err != nil { if call.Err != nil {
return nil, call.Err return nil, err
} }
var linkPath dbus.ObjectPath var linkPath dbus.ObjectPath
err = call.Store(&linkPath) err = call.Store(&linkPath)
@@ -235,12 +214,15 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
return nil, E.New("No appropriate name servers or networks for name found") return nil, E.New("No appropriate name servers or networks for name found")
} }
} }
return nil, E.New("link has no DNS servers configured") return &ResolvedObject{
BusObject: dbusObject,
}, nil
} else {
return &ResolvedObject{
BusObject: dbusObject,
InterfaceIndex: int32(defaultInterface.Index),
}, nil
} }
return &ResolvedObject{
BusObject: dbusObject,
InterfaceIndex: int32(defaultInterface.Index),
}, nil
} }
func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) { func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) {

View File

@@ -9,10 +9,6 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
) )
func isSystemdResolvedManaged() bool {
return false
}
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) { func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,9 @@ package transport
import ( import (
"context" "context"
"net"
"os"
"sync" "sync"
"sync/atomic"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
@@ -27,23 +28,15 @@ func RegisterUDP(registry *dns.TransportRegistry) {
} }
type UDPTransport struct { type UDPTransport struct {
*BaseTransport dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer dialer N.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
udpSize atomic.Int32 udpSize int
tcpTransport *TCPTransport
connector *Connector[*Connection] access sync.Mutex
conn *dnsConnection
callbackAccess sync.RWMutex done chan struct{}
queryId uint16
callbacks map[uint16]*udpCallback
}
type udpCallback struct {
access sync.Mutex
response *mDNS.Msg
done chan struct{}
} }
func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) { 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 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 { func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
t := &UDPTransport{ return &UDPTransport{
BaseTransport: NewBaseTransport(adapter, logger), TransportAdapter: adapter,
dialer: dialerInstance, logger: logger,
serverAddr: serverAddr, dialer: dialer,
callbacks: make(map[uint16]*udpCallback), 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 { func (t *UDPTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer) return dialer.InitializeDetour(t.dialer)
} }
func (t *UDPTransport) Close() error { func (t *UDPTransport) Close() error {
return E.Errors(t.BaseTransport.Close(), t.connector.Close()) t.access.Lock()
} defer t.access.Unlock()
close(t.done)
func (t *UDPTransport) Reset() { t.done = make(chan struct{})
t.connector.Reset() return nil
}
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")
}
}
} }
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { 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) response, err := t.exchange(ctx, message)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if response.Truncated { if response.Truncated {
t.Logger.InfoContext(ctx, "response truncated, retrying with TCP") t.logger.InfoContext(ctx, "response truncated, retrying with TCP")
return t.exchangeTCP(ctx, message) return t.tcpTransport.Exchange(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")
} }
return response, nil return response, nil
} }
func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
t.access.Lock()
if edns0Opt := message.IsEdns0(); edns0Opt != nil { if edns0Opt := message.IsEdns0(); edns0Opt != nil {
udpSize := int32(edns0Opt.UDPSize()) if udpSize := int(edns0Opt.UDPSize()); udpSize > t.udpSize {
for { t.udpSize = udpSize
current := t.udpSize.Load() close(t.done)
if udpSize <= current { t.done = make(chan struct{})
break
}
if t.udpSize.CompareAndSwap(current, udpSize) {
t.connector.Reset()
break
}
} }
} }
t.access.Unlock()
conn, err := t.connector.Get(ctx) conn, err := t.open(ctx)
if err != nil { if err != nil {
return nil, err 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()) buffer := buf.NewSize(1 + message.Len())
defer buffer.Release() defer buffer.Release()
exMessage := *message exMessage := *message
exMessage.Compress = true exMessage.Compress = true
originalId := message.Id messageId := message.Id
exMessage.Id = queryId 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()) rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, err = conn.Write(rawMessage) _, err = conn.Write(rawMessage)
if err != nil { if err != nil {
conn.CloseWithError(err) conn.Close(err)
return nil, E.Cause(err, "write request") return nil, err
} }
select { select {
case <-callback.done: case <-callback.done:
callback.response.Id = originalId callback.message.Id = messageId
return callback.response, nil return callback.message, nil
case <-conn.Done(): case <-conn.done:
return nil, conn.CloseError() return nil, conn.err
case <-t.CloseContext().Done(): case <-t.done:
return nil, ErrTransportClosed return nil, os.ErrClosed
case <-ctx.Done(): case <-ctx.Done():
conn.Close(ctx.Err())
return nil, 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 { for {
buffer := buf.NewSize(int(t.udpSize.Load())) buffer := buf.NewSize(t.udpSize)
_, err := buffer.ReadOnceFrom(conn) _, err := buffer.ReadOnceFrom(conn)
if err != nil { if err != nil {
buffer.Release() buffer.Release()
conn.CloseWithError(err) conn.Close(err)
return return
} }
var message mDNS.Msg var message mDNS.Msg
err = message.Unpack(buffer.Bytes()) err = message.Unpack(buffer.Bytes())
buffer.Release() buffer.Release()
if err != nil { if err != nil {
t.Logger.Debug("discarded malformed UDP response: ", err) conn.Close(err)
continue return
} }
conn.access.RLock()
t.callbackAccess.RLock() callback, loaded := conn.callbacks[message.Id]
callback, loaded := t.callbacks[message.Id] conn.access.RUnlock()
t.callbackAccess.RUnlock()
if !loaded { if !loaded {
continue continue
} }
callback.access.Lock() callback.access.Lock()
select { select {
case <-callback.done: case <-callback.done:
default: default:
callback.response = &message callback.message = &message
close(callback.done) close(callback.done)
} }
callback.access.Unlock() 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,524 +2,10 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.13.0-rc.5 #### 1.13.0-alpha.20
* 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 * 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
* 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:
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:
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.12
* Fixes and improvements
#### 1.13.0-alpha.26
* Update quic-go to v0.55.0
* Fix memory leak in hysteria2
* Fixes and improvements
#### 1.12.11
* Fixes and improvements
#### 1.13.0-alpha.24
* Add Claude Code Multiplexer service **1**
* Fixes and improvements
**1**:
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).
#### 1.13.0-alpha.23
* Fix compatibility with MPTCP **1**
* Fixes and improvements
**1**:
`auto_redirect` now rejects MPTCP connections by default to fix compatibility issues,
but you can change it to bypass the sing-box via the new `exclude_mptcp` option.
See [TUN](/configuration/inbound/tun/#exclude_mptcp).
#### 1.13.0-alpha.22
* Update uTLS to v1.8.1 **1**
* Fixes and improvements
**1**:
This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected,
see https://github.com/refraction-networking/utls/pull/375.
#### 1.12.10
* Update uTLS to v1.8.1 **1**
* Fixes and improvements
**1**:
This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected,
see https://github.com/refraction-networking/utls/pull/375.
#### 1.13.0-alpha.21
* Fix missing mTLS support in client options **1**
* Fixes and improvements
See [TLS](/configuration/shared/tls/).
#### 1.12.9 #### 1.12.9
* Fixes and improvements * Fixes and improvements
@@ -641,8 +127,7 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
**7**: **7**:
@@ -704,8 +189,7 @@ See [Tun](/configuration/inbound/tun/#loopback_address).
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack. We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
The following data was tested The following data was tested using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
| Version | Stack | MTU | Upload | Download | | Version | Stack | MTU | Upload | Download |
|-------------|--------|-------|--------|----------| |-------------|--------|-------|--------|----------|
@@ -724,8 +208,8 @@ using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/interna
**18**: **18**:
We continue to experience issues updating our sing-box apps on the App Store and Play Store. We continue to experience issues updating our sing-box apps on the App Store and Play Store.
Until we rewrite and resubmit the apps, they are considered irrecoverable. Until we rewrite and resubmit the apps, they are considered irrecoverable.
Therefore, after this release, we will not be repeating this notice unless there is new information. Therefore, after this release, we will not be repeating this notice unless there is new information.
### 1.11.15 ### 1.11.15
@@ -1006,8 +490,7 @@ See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/conf
**2**: **2**:
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, `resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action).
see [Route Action](/configuration/route/rule_action).
**3**: **3**:
@@ -1038,8 +521,7 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
### 1.11.3 ### 1.11.3

View File

@@ -9,7 +9,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
!!! failure "" !!! failure ""
Due to non-technical reasons, we are temporarily unable to update the sing-box app on the App Store and release the standalone version of the macOS client (TestFlight users are not affected) We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected).
## :material-graph: Requirements ## :material-graph: Requirements
@@ -18,7 +18,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :material-download: Download ## :material-download: Download
* ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~ * [App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)
* TestFlight (Beta) * TestFlight (Beta)
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai) TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
@@ -26,15 +26,15 @@ TestFlight quota is only available to [sponsors](https://github.com/sponsors/nek
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot) Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
or sending us your Apple ID [via email](mailto:contact@sagernet.org). or sending us your Apple ID [via email](mailto:contact@sagernet.org).
## ~~:material-file-download: Download (macOS standalone version)~~ ## :material-file-download: Download (macOS standalone version)
* ~~[Homebrew Cask](https://formulae.brew.sh/cask/sfm)~~ * [Homebrew Cask](https://formulae.brew.sh/cask/sfm)
```bash ```bash
# brew install sfm brew install sfm
``` ```
* ~~[GitHub Releases](https://github.com/SagerNet/sing-box/releases)~~ * [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
## :material-source-repository: Source code ## :material-source-repository: Source code

View File

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

View File

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

View File

@@ -412,7 +412,7 @@ Match default interface address.
!!! quote "" !!! quote ""
Only supported in graphical clients on Android and Apple platforms, or on Linux. Only supported in graphical clients on Android and Apple platforms.
Match WiFi SSID. Match WiFi SSID.
@@ -420,7 +420,7 @@ Match WiFi SSID.
!!! quote "" !!! quote ""
Only supported in graphical clients on Android and Apple platforms, or on Linux. Only supported in graphical clients on Android and Apple platforms.
Match WiFi BSSID. Match WiFi BSSID.

View File

@@ -411,7 +411,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! quote "" !!! quote ""
仅在 Android 与 Apple 平台图形客户端和 Linux 中支持。 仅在 Android 与 Apple 平台图形客户端中支持。
匹配 WiFi SSID。 匹配 WiFi SSID。
@@ -419,7 +419,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! quote "" !!! quote ""
仅在 Android 与 Apple 平台图形客户端和 Linux 中支持。 仅在 Android 与 Apple 平台图形客户端中支持。
匹配 WiFi BSSID。 匹配 WiFi BSSID。

View File

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

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