Compare commits

..

106 Commits

Author SHA1 Message Date
世界
4e73574144 documentation: Bump version 2025-12-25 16:43:00 +08:00
世界
8881882326 Ignore darwin IP_DONTFRAG error when not supported 2025-12-25 16:43:00 +08:00
世界
b675ed2563 Update tailscale to v1.92.4 2025-12-25 16:43:00 +08:00
世界
a44f8c7b5d Update cronet-go to v143.0.7499.109-1 2025-12-25 16:43:00 +08:00
世界
003cf13898 platform: Split library for Android SDK 21 and 23 2025-12-25 16:43:00 +08:00
世界
2f906adfa1 Fix missing RootPoolFromContext and TimeFuncFromContext in HTTP clients 2025-12-25 14:51:59 +08:00
世界
bef0a2f240 documentation: Minor fixes 2025-12-25 14:51:59 +08:00
世界
e27a335ee0 documentation: Add Wi-Fi state shared page 2025-12-25 14:51:59 +08:00
世界
a1694d4c7b Fix missing build constraints for linux wifi state monitor 2025-12-25 14:51:59 +08:00
世界
48e5344cea Update dependencies 2025-12-25 14:51:59 +08:00
世界
6e59a76941 Update quic-go to v0.58.0 2025-12-25 14:51:59 +08:00
世界
6a7264aa91 Add Chrome Root Store certificate option
Adds `chrome` as a new certificate store option alongside `mozilla`.
Both stores filter out China-based CA certificates.
2025-12-25 14:51:59 +08:00
世界
bc23473411 Fix cronet-go crash 2025-12-25 14:51:59 +08:00
世界
8171e792cc Add trace logging for lifecycle calls
Log start/close operations with timing information for debugging.
2025-12-25 14:51:59 +08:00
世界
589e4e5bd7 documentation: Minor fixes 2025-12-25 14:51:59 +08:00
世界
7dd91362b5 Remove certificate_public_key_sha256 for naive 2025-12-25 14:51:59 +08:00
世界
0e79256f15 platform: Use new crash log api 2025-12-25 14:51:59 +08:00
世界
05169b09ad Fix naive network 2025-12-25 14:51:59 +08:00
世界
99c125d8f3 Add QUIC support for naiveproxy 2025-12-25 14:51:59 +08:00
世界
e473c64cd6 Add ECH support for NaiveProxy outbound and tls.ech.query_server_name option
- Enable ECH for NaiveProxy outbound with DNS resolver integration
- Add query_server_name option to override domain for ECH HTTPS record queries
- Update cronet-go dependency and remove windows_386 support
2025-12-25 14:51:59 +08:00
世界
be7254c335 Fix naiveproxy build 2025-12-25 14:51:58 +08:00
世界
bf0e432340 Add OpenAI Codex Multiplexer service 2025-12-25 14:51:58 +08:00
世界
d592e2d12a Update pricing for CCM service 2025-12-25 14:51:58 +08:00
世界
dd164d9150 release: Upload only other apks 2025-12-25 14:49:49 +08:00
世界
84b277615c Fix bugs and add UoT option for naiveproxy outbound 2025-12-25 14:49:49 +08:00
世界
13e425d5c3 Add naiveproxy outbound 2025-12-25 14:49:49 +08:00
世界
626ef0b427 Apply ping destination filter for Windows 2025-12-25 14:49:48 +08:00
世界
277c643c3e platform: Add UsePlatformWIFIMonitor to gRPC interface
Align dev-next-grpc with wip2 by adding UsePlatformWIFIMonitor()
to the new PlatformInterface, allowing platform clients to indicate
they handle WIFI monitoring themselves.
2025-12-25 14:49:48 +08:00
世界
7a4c70ede9 daemon: Add clear logs 2025-12-25 14:49:48 +08:00
世界
55df080e2a Revert "Stop using DHCP on iOS and tvOS" 2025-12-25 14:49:48 +08:00
世界
71253f800e platform: Refactoring libbox to use gRPC-based protocol 2025-12-25 14:49:48 +08:00
世界
30ef92ec7b Add Windows WI-FI state support 2025-12-25 14:49:47 +08:00
世界
5ce866cc8a Add Linux WI-FI state support
Support monitoring WIFI state on Linux through:
- NetworkManager (D-Bus)
- IWD (D-Bus)
- wpa_supplicant (control socket)
- ConnMan (D-Bus)
2025-12-25 14:49:47 +08:00
世界
eca6a5da18 Add more tcp keep alive options
Also update default TCP keep-alive initial period from 10 minutes to 5 minutes.
2025-12-25 14:49:47 +08:00
世界
37a43dd63a Update quic-go to v0.57.1 2025-12-25 14:49:47 +08:00
世界
2f377b2cdf Fix read credentials for ccm service 2025-12-25 14:49:47 +08:00
世界
fac4068214 Add claude code multiplexer service 2025-12-25 14:49:46 +08:00
世界
ee07065f7b Fix compatibility with MPTCP 2025-12-25 14:49:45 +08:00
世界
f70867e0a9 Use a more conservative strategy for resolving with systemd-resolved for local DNS server 2025-12-25 14:49:45 +08:00
世界
bf43a6655e Fix missing mTLS support in client options 2025-12-25 14:49:45 +08:00
世界
bf055b8ae2 Add curve preferences, pinned public key SHA256 and mTLS for TLS options 2025-12-25 14:49:45 +08:00
世界
f88d249f03 Fix WireGuard input packet 2025-12-25 14:49:45 +08:00
世界
10d6d22b73 Update tfo-go to latest 2025-12-25 14:49:45 +08:00
世界
65e7649952 Remove compatibility codes 2025-12-25 14:49:45 +08:00
世界
d01534aa5c Do not use linkname by default to simplify debugging 2025-12-25 14:49:44 +08:00
世界
3efe0fdfdc documentation: Update chinese translations 2025-12-25 14:49:44 +08:00
世界
67a0c19b07 Update quic-go to v0.55.0 2025-12-25 14:49:44 +08:00
世界
3546a9368b Update WireGuard and Tailscale 2025-12-25 14:49:44 +08:00
世界
8ab5c7695f Fix preConnectionCopy 2025-12-25 14:49:44 +08:00
世界
07190d8d8a Fix ping domain 2025-12-25 14:49:24 +08:00
世界
8e627088c6 release: Fix linux build 2025-12-25 14:49:24 +08:00
世界
f306f704bc Improve ktls rx error handling 2025-12-25 14:49:24 +08:00
世界
537ca35cfe Improve compatibility for kTLS 2025-12-25 14:49:24 +08:00
世界
84a0f240f9 ktls: Add warning for inappropriate scenarios 2025-12-25 14:49:24 +08:00
世界
7f13a66e12 Add support for kTLS
Reference: https://gitlab.com/go-extension/tls
2025-12-25 14:49:23 +08:00
世界
301e829266 Add proxy support for ICMP echo request 2025-12-25 14:49:23 +08:00
世界
b04310f285 Fix resolve using resolved 2025-12-25 14:48:56 +08:00
世界
8acef05e95 documentation: Update behavior of local DNS server on darwin 2025-12-25 14:48:56 +08:00
世界
6922ec1070 Remove use of ldflags -checklinkname=0 on darwin 2025-12-25 14:48:56 +08:00
世界
9964bc39da Fix legacy DNS config 2025-12-25 14:48:56 +08:00
世界
644cd773c7 Fix rule-set format 2025-12-25 14:48:56 +08:00
世界
032e00f38d documentation: Remove outdated icons 2025-12-25 14:48:56 +08:00
世界
f9a9845901 documentation: Improve local DNS server 2025-12-25 14:48:52 +08:00
世界
b1fae028ce Stop using DHCP on iOS and tvOS
We do not have the `com.apple.developer.networking.multicast` entitlement and are unable to obtain it for non-technical reasons.
2025-12-25 14:48:52 +08:00
世界
07d9ec4f68 Improve local DNS server on darwin
We mistakenly believed that `libresolv`'s `search` function worked correctly in NetworkExtension, but it seems only `getaddrinfo` does.

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

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

In addition, this commit also improves the DHCP DNS server to use the same robust query logic as `local`.
2025-12-25 14:48:52 +08:00
世界
61cecf0b01 Use resolved in local DNS server if available 2025-12-25 14:48:52 +08:00
xchacha20-poly1305
6aa6ee2572 Fix rule set version 2025-12-25 14:48:52 +08:00
世界
591665a302 documentation: Add preferred_by route rule item 2025-12-25 14:48:52 +08:00
世界
1c2d38fcab Add preferred_by route rule item 2025-12-25 14:48:52 +08:00
世界
1357294a63 documentation: Add interface address rule items 2025-12-25 14:48:52 +08:00
世界
a5135e33fd Add interface address rule items 2025-12-25 14:48:51 +08:00
世界
0f772f7bbe Fix ECH retry support 2025-12-25 14:48:51 +08:00
neletor
65f5f406b3 Add support for ech retry configs 2025-12-25 14:48:51 +08:00
Zephyruso
96f1f9e205 Add /dns/flush-clash meta api 2025-12-25 14:48:51 +08:00
世界
f56d9ab945 Bump version 2025-12-25 14:47:10 +08:00
世界
86fabd6a22 Update Mozilla certificates 2025-12-25 14:42:18 +08:00
世界
24a1e7cee4 Ignore darwin IP_DONTFRAG error when not supported 2025-12-25 14:40:48 +08:00
世界
223dd8bb1a Fix TCP DNS response buffer 2025-12-22 13:51:00 +08:00
世界
68448de7d0 Fix missing RootPoolFromContext and TimeFuncFromContext in HTTP clients 2025-12-22 13:50:57 +08:00
世界
1ebff74c21 Fix DNS cache not working when domain strategy is set
The cache lookup was performed before rule matching, using the caller's
strategy (usually AsIS/0) instead of the resolved strategy. This caused
cache misses when ipv4_only was configured globally but the cache lookup
expected both A and AAAA records.

Remove LookupCache and ExchangeCache from Router, as the cache checks
inside client.Lookup and client.Exchange already handle caching correctly
after rule matching with the proper strategy and transport.
2025-12-21 16:59:10 +08:00
世界
f0cd3422c1 Bump version 2025-12-14 00:09:19 +08:00
世界
e385a98ced Update Go to 1.25.5 2025-12-13 20:11:29 +08:00
世界
670f32baee Fix naive inbound 2025-12-12 21:19:28 +08:00
世界
2747a00ba2 Fix tailscale destination 2025-12-01 15:02:04 +08:00
世界
48e76038d0 Update Go to 1.25.4 2025-11-16 09:53:10 +08:00
世界
6421252d44 release: Fix windows7 build 2025-11-16 09:09:34 +08:00
世界
216c4c8bd4 Fix adapter handler 2025-11-16 08:34:46 +08:00
世界
5841d410a1 ssm-api: Fix save cache 2025-11-04 11:00:43 +08:00
Kumiko as a Service
63c8207d7a Use --no-cache --upgrade option in apk add
No need for separate upgrade / cache cleanup steps.

Signed-off-by: Kumiko as a Service <Dreista@users.noreply.github.com>
2025-11-04 11:00:41 +08:00
世界
54ed58499d Bump version 2025-10-27 18:04:24 +08:00
世界
b1bdc18c85 Fix socks response 2025-10-27 18:03:05 +08:00
世界
a38030cc0b Fix memory leak in hysteria2 2025-10-24 10:52:08 +08:00
世界
4626aa2cb0 Bump version 2025-10-21 21:39:28 +08:00
世界
5a40b673a4 Update dependencies 2025-10-21 21:39:28 +08:00
世界
541f63fee4 redirect: Fix compatibility with /product/bin/su 2025-10-21 21:27:15 +08:00
世界
5de6f4a14f Fix tailscale not enforcing NoLogsNoSupport 2025-10-16 22:30:08 +08:00
世界
5658830077 Fix trailing dot handling in local DNS transport 2025-10-16 21:43:12 +08:00
世界
0e50edc009 documentation: Add appreciate for Warp 2025-10-16 21:43:12 +08:00
世界
444f454810 Bump version 2025-10-14 23:43:36 +08:00
世界
d0e1fd6c7e Update Go to 1.25.3 2025-10-14 23:43:36 +08:00
世界
17b4d1e010 Update uTLS to v1.8.1 2025-10-14 23:40:19 +08:00
世界
06791470c9 Fix DNS reject panic 2025-10-14 23:40:19 +08:00
世界
ef14c8ca0e Disable TCP slow open for anytls
Fixes #3459
2025-10-14 23:40:19 +08:00
世界
36dc883c7c Fix DNS negative caching to comply with RFC 2308 2025-10-09 23:45:23 +08:00
Mahdi
6557bd7029 Fix dns cache in lookup 2025-10-09 23:45:23 +08:00
世界
41b30c91d9 Improve HTTPS DNS transport 2025-10-09 23:45:23 +08:00
163 changed files with 11542 additions and 2344 deletions

1
.github/CRONET_GO_VERSION vendored Normal file
View File

@@ -0,0 +1 @@
b0385d27c2ab659d9532d71f301deb6599c44a79

View File

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

13
.github/update_cronet.sh vendored Executable file
View File

@@ -0,0 +1,13 @@
#!/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/HEAD > "$SCRIPT_DIR/CRONET_GO_VERSION"

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -69,13 +69,25 @@ jobs:
strategy:
matrix:
include:
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
- { os: linux, arch: amd64, variant: purego, naive: true, openwrt: "x86_64" }
- { os: linux, arch: amd64, variant: glibc, naive: true }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: arm64, variant: purego, naive: true, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { 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, openwrt: "i386_pentium4" }
- { 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", 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: 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: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
@@ -87,35 +99,28 @@ jobs:
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64 }
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: "386" }
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: arm64 }
- { os: darwin, arch: amd64 }
- { os: darwin, arch: arm64 }
- { os: darwin, arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
- { 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" }
- { os: android, arch: arm64, ndk: "aarch64-linux-android23" }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi23" }
- { os: android, arch: amd64, ndk: "x86_64-linux-android23" }
- { os: android, arch: "386", ndk: "i686-linux-android23" }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }}
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.6
go-version: ~1.24.10
- name: Cache Go for Windows 7
if: matrix.legacy_win7
id: cache-go-for-windows7
@@ -123,7 +128,7 @@ jobs:
with:
path: |
~/go/go_win7
key: go_win7_1251
key: go_win7_1255
- name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
run: |-
@@ -139,6 +144,45 @@ jobs:
with:
ndk-version: r28
local-cache: true
- name: Clone cronet-go
if: matrix.naive
run: |
set -xeuo pipefail
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
git init ~/cronet-go
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
~/cronet-go/naiveproxy/src/out/sysroot-build
key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium toolchain
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
if [[ "${{ matrix.variant }}" == "musl" ]]; then
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
else
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} download-toolchain
fi
- name: Set Chromium toolchain environment
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
if [[ "${{ matrix.variant }}" == "musl" ]]; then
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
else
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} env >> $GITHUB_ENV
fi
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -146,10 +190,70 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0'
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"
fi
if [[ "${{ matrix.variant }}" == "purego" ]]; then
TAGS="${TAGS},with_purego"
elif [[ "${{ matrix.variant }}" == "musl" ]]; then
TAGS="${TAGS},with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build
if: matrix.os != 'android'
- name: Build (purego)
if: matrix.variant == 'purego'
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 }} -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 }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
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 }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-variant)
if: matrix.os != 'android' && matrix.variant == ''
run: |
set -xeuo pipefail
mkdir -p dist
@@ -193,6 +297,11 @@ jobs:
elif [[ -n "${{ matrix.legacy_name }}" ]]; then
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
fi
if [[ "${{ matrix.variant }}" == "glibc" ]]; then
DIR_NAME="${DIR_NAME}-glibc"
elif [[ "${{ matrix.variant }}" == "musl" ]]; then
DIR_NAME="${DIR_NAME}-musl"
fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
@@ -260,8 +369,12 @@ jobs:
-p "dist/openwrt.deb" \
--architecture all \
dist/sing-box=/usr/bin/sing-box
SUFFIX=""
if [[ "${{ matrix.variant }}" == "musl" ]]; then
SUFFIX="_musl"
fi
for architecture in ${{ matrix.openwrt }}; do
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}${SUFFIX}.ipk"
done
rm "dist/openwrt.deb"
- name: Archive
@@ -275,15 +388,177 @@ jobs:
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
else
cp sing-box "${DIR_NAME}"
if [ -f libcronet.so ]; then
cp libcronet.so "${DIR_NAME}"
fi
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
fi
rm -r "${DIR_NAME}"
- name: Cleanup
run: rm -f dist/sing-box dist/libcronet.so
- 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 }} -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
run: rm dist/sing-box
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }}
name: binary-darwin_${{ matrix.arch }}${{ 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 }} -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 }} -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"
build_android:
name: Build Android
@@ -300,7 +575,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -348,9 +623,9 @@ jobs:
- name: Build
run: |-
mkdir clients/android/app/libs
cp libbox.aar clients/android/app/libs
cp *.aar clients/android/app/libs
cd clients/android
./gradlew :app:assemblePlayRelease :app:assembleOtherRelease
./gradlew :app:assembleOtherRelease :app:assembleOtherLegacyRelease
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
@@ -358,8 +633,18 @@ jobs:
- name: Prepare upload
run: |-
mkdir -p dist
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
cp clients/android/app/build/outputs/apk/other/release/*-universal.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/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
uses: actions/upload-artifact@v4
with:
@@ -380,7 +665,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -421,7 +706,7 @@ jobs:
run: |-
go run -v ./cmd/internal/update_android_version --ci
mkdir clients/android/app/libs
cp libbox.aar clients/android/app/libs
cp *.aar clients/android/app/libs
cd clients/android
echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json
./gradlew :app:publishPlayReleaseBundle
@@ -433,7 +718,7 @@ jobs:
build_apple:
name: Build Apple clients
runs-on: macos-26
if: false
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'
needs:
- calculate_version
strategy:
@@ -479,7 +764,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Set tag
if: matrix.if
run: |-
@@ -598,7 +883,7 @@ jobs:
--app-drop-link 0 0 \
--skip-jenkins \
SFM.dmg "${{ matrix.export_path }}/SFM.app"
xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password"
xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password"
cd "${{ matrix.archive }}"
zip -r SFM.dSYMs.zip dSYMs
popd
@@ -619,6 +904,8 @@ jobs:
needs:
- calculate_version
- build
- build_darwin
- build_windows
- build_android
- build_apple
steps:

View File

@@ -1,6 +1,10 @@
name: Publish Docker Images
on:
#push:
# branches:
# - main-next
# - dev-next
release:
types:
- published
@@ -13,8 +17,134 @@ env:
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
jobs:
build:
build_binary:
name: Build binary
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" }
# Non-naive builds
- { arch: arm, goarm: "6", docker_platform: "linux/arm/v6" }
- { arch: ppc64le, docker_platform: "linux/ppc64le" }
- { arch: riscv64, docker_platform: "linux/riscv64" }
- { 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.4
- 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: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
~/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}\" -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
- 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}\" -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
- name: Prepare artifact
run: |
platform=${{ matrix.docker_platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
# Rename binary to include arch info for Dockerfile.binary
BINARY_NAME="sing-box-${{ matrix.arch }}"
if [[ -n "${{ matrix.goarm }}" ]]; then
BINARY_NAME="${BINARY_NAME}v${{ matrix.goarm }}"
fi
mv sing-box "${BINARY_NAME}"
echo "BINARY_NAME=${BINARY_NAME}" >> $GITHUB_ENV
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: binary-${{ env.PLATFORM_PAIR }}
path: ${{ env.BINARY_NAME }}
if-no-files-found: error
retention-days: 1
build_docker:
name: Build Docker image
runs-on: ubuntu-latest
needs:
- build_binary
strategy:
fail-fast: true
matrix:
@@ -47,6 +177,16 @@ jobs:
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Download binary
uses: actions/download-artifact@v5
with:
name: binary-${{ env.PLATFORM_PAIR }}
path: .
- name: Prepare binary
run: |
# Find and make the binary executable
chmod +x sing-box-*
ls -la sing-box-*
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
@@ -68,8 +208,7 @@ jobs:
with:
platforms: ${{ matrix.platform }}
context: .
build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
file: Dockerfile.binary
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
@@ -87,7 +226,7 @@ jobs:
merge:
runs-on: ubuntu-latest
needs:
- build
- build_docker
steps:
- name: Get commit to build
id: ref
@@ -121,6 +260,7 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
if: github.event_name != 'push'
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
@@ -128,6 +268,7 @@ jobs:
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
if: github.event_name != 'push'
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}

View File

@@ -1,6 +1,10 @@
name: Build Linux Packages
on:
#push:
# branches:
# - main-next
# - dev-next
workflow_dispatch:
inputs:
version:
@@ -30,7 +34,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -52,11 +56,13 @@ jobs:
strategy:
matrix:
include:
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: "386", debian: i386, rpm: i386 }
# Naive-enabled builds (musl)
- { os: linux, arch: amd64, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: arm64, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: "386", naive: true, debian: i386, rpm: i386 }
- { os: linux, arch: arm, goarm: "7", naive: true, debian: armhf, rpm: armv7hl, pacman: armv7hl }
# Non-naive builds (unsupported architectures)
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
@@ -71,13 +77,38 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
- name: Setup Android NDK
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1
go-version: ^1.25.5
- 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: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
ndk-version: r28
local-cache: true
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
~/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
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -85,9 +116,27 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0'
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
- name: Build (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 }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-naive)
if: ${{ ! matrix.naive }}
run: |
set -xeuo pipefail
mkdir -p dist
@@ -185,5 +234,6 @@ jobs:
path: dist
merge-multiple: true
- name: Publish packages
if: github.event_name != 'push'
run: |-
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/

View File

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

View File

@@ -1,213 +0,0 @@
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,15 +13,13 @@ RUN set -ex \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0" \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0" \
-o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \
&& apk upgrade \
&& apk add bash tzdata ca-certificates nftables \
&& rm -rf /var/cache/apk/*
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"]

8
Dockerfile.binary Normal file
View File

@@ -0,0 +1,8 @@
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"]

View File

@@ -1,6 +1,6 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0
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
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
@@ -249,8 +249,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios
lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.8
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.8
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.10
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.10
docs:
venv/bin/mkdocs serve

View File

@@ -1,3 +1,11 @@
> 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
The universal proxy platform.

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import (
"encoding/binary"
"time"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/varbin"
)
@@ -14,6 +15,7 @@ type ClashServer interface {
ConnectionTracker
Mode() string
ModeList() []string
SetModeUpdateHook(hook *observable.Subscriber[struct{}])
HistoryStorage() URLTestHistoryStorage
}
@@ -23,7 +25,7 @@ type URLTestHistory struct {
}
type URLTestHistoryStorage interface {
SetHook(hook chan<- struct{})
SetHook(hook *observable.Subscriber[struct{}])
LoadURLTestHistory(tag string) *URLTestHistory
DeleteURLTestHistory(tag string)
StoreURLTestHistory(tag string, history *URLTestHistory)

View File

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

View File

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

View File

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

View File

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

49
box.go
View File

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

View File

@@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
_ "github.com/sagernet/gomobile"
@@ -46,7 +47,7 @@ var (
sharedFlags []string
debugFlags []string
sharedTags []string
macOSTags []string
darwinTags []string
memcTags []string
notMemcTags []string
debugTags []string
@@ -62,16 +63,34 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -checklinkname=0")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
macOSTags = append(macOSTags, "with_dhcp")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
darwinTags = append(darwinTags, "with_dhcp")
memcTags = append(memcTags, "with_tailscale")
notMemcTags = append(notMemcTags, "with_low_memory")
debugTags = append(debugTags, "debug")
}
func buildAndroid() {
build_shared.FindSDK()
type AndroidBuildConfig struct {
AndroidAPI int
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
javaHome := os.Getenv("JAVA_HOME")
if javaHome == "" {
@@ -87,21 +106,24 @@ func buildAndroid() {
if !strings.Contains(javaVersion, "openjdk 17") {
log.Fatal("java version should be openjdk 17")
}
}
var bindTarget string
func getAndroidBindTarget() string {
if platform != "" {
bindTarget = platform
return platform
} else if debugEnabled {
bindTarget = "android/arm64"
} else {
bindTarget = "android"
return "android/arm64"
}
return "android"
}
func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
args := []string{
"bind",
"-v",
"-o", config.OutputName,
"-target", bindTarget,
"-androidapi", "21",
"-androidapi", strconv.Itoa(config.AndroidAPI),
"-javapkg=io.nekohasekai",
"-libname=box",
}
@@ -112,34 +134,59 @@ func buildAndroid() {
args = append(args, debugFlags...)
}
tags := append(sharedTags, memcTags...)
if debugEnabled {
tags = append(tags, debugTags...)
}
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "-tags", strings.Join(config.Tags, ","))
args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
err = command.Run()
err := command.Run()
if err != nil {
log.Fatal(err)
}
const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.IsDir(copyPath) {
copyPath, _ = filepath.Abs(copyPath)
err = rw.CopyFile(name, filepath.Join(copyPath, name))
err = rw.CopyFile(config.OutputName, filepath.Join(copyPath, config.OutputName))
if err != nil {
log.Fatal(err)
}
log.Info("copied to ", copyPath)
log.Info("copied ", config.OutputName, " to ", copyPath)
}
}
func buildAndroid() {
build_shared.FindSDK()
checkJavaVersion()
bindTarget := getAndroidBindTarget()
// Build main variant (SDK 23)
mainTags := append([]string{}, sharedTags...)
mainTags = append(mainTags, memcTags...)
if debugEnabled {
mainTags = append(mainTags, debugTags...)
}
buildAndroidVariant(AndroidBuildConfig{
AndroidAPI: 23,
OutputName: "libbox.aar",
Tags: mainTags,
}, bindTarget)
// Build legacy variant (SDK 21, no naive outbound)
legacyTags := filterTags(sharedTags, "with_naive_outbound")
legacyTags = append(legacyTags, memcTags...)
if debugEnabled {
legacyTags = append(legacyTags, debugTags...)
}
buildAndroidVariant(AndroidBuildConfig{
AndroidAPI: 21,
OutputName: "libbox-legacy.aar",
Tags: legacyTags,
}, bindTarget)
}
func buildApple() {
var bindTarget string
if platform != "" {
@@ -158,9 +205,7 @@ func buildApple() {
"-tags-not-macos=with_low_memory",
}
if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ","))
} else {
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
}
if !debugEnabled {
@@ -169,7 +214,7 @@ func buildApple() {
args = append(args, debugFlags...)
}
tags := sharedTags
tags := append(sharedTags, darwinTags...)
if withTailscale {
tags = append(tags, memcTags...)
}

View File

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

2817
common/certificate/chrome.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
//go:build linux
package settings
import (
@@ -134,7 +136,9 @@ func (m *connmanMonitor) monitorSignals(ctx context.Context, signalChan chan *db
if !ok {
return
}
if signal.Name == "PropertyChanged" {
// godbus Signal.Name uses "interface.member" format (e.g. "net.connman.Service.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()
if state != lastState {
lastState = state
@@ -154,6 +158,10 @@ func (m *connmanMonitor) Close() error {
close(m.signalChan)
}
if m.conn != nil {
m.conn.RemoveMatchSignal(
dbus.WithMatchInterface("net.connman.Service"),
dbus.WithMatchSender("net.connman"),
)
return m.conn.Close()
}
return nil

View File

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

View File

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

View File

@@ -8,15 +8,21 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
)
var wpaSocketCounter atomic.Uint64
type wpaSupplicantMonitor struct {
socketPath string
callback func(adapter.WIFIState)
cancel context.CancelFunc
socketPath string
callback func(adapter.WIFIState)
cancel context.CancelFunc
monitorConn *net.UnixConn
connMutex sync.Mutex
}
func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
@@ -31,7 +37,8 @@ func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, err
continue
}
socketPath := filepath.Join(socketDir, entry.Name())
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"}
id := wpaSocketCounter.Add(1)
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"}
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
if err != nil {
@@ -45,7 +52,8 @@ func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, err
}
func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState {
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"}
id := wpaSocketCounter.Add(1)
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
if err != nil {
@@ -85,8 +93,11 @@ 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) {
_, err := conn.Write([]byte(command + "\n"))
_, err := conn.Write([]byte(command))
if err != nil {
return "", err
}
@@ -121,6 +132,8 @@ func (m *wpaSupplicantMonitor) Start() error {
func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) {
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"}
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
@@ -130,7 +143,14 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
}
defer conn.Close()
_, err = conn.Write([]byte("ATTACH\n"))
m.connMutex.Lock()
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 {
return
}
@@ -144,6 +164,12 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
for {
select {
case <-ctx.Done():
debounceMutex.Lock()
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceMutex.Unlock()
conn.Write([]byte("DETACH"))
return
default:
}
@@ -151,6 +177,14 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
n, err := conn.Read(buf)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
select {
case <-ctx.Done():
return
default:
}
consecutiveErrors++
if consecutiveErrors > 10 {
return
@@ -162,11 +196,18 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
msg := string(buf[:n])
if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") {
state := m.ReadWIFIState()
if state != lastState {
lastState = state
m.callback(state)
debounceMutex.Lock()
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
state := m.ReadWIFIState()
if state != lastState {
lastState = state
m.callback(state)
}
})
debounceMutex.Unlock()
}
}
}
@@ -175,5 +216,10 @@ func (m *wpaSupplicantMonitor) Close() error {
if m.cancel != nil {
m.cancel()
}
m.connMutex.Lock()
if m.monitorConn != nil {
m.monitorConn.Close()
}
m.connMutex.Unlock()
return nil
}

View File

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

View File

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

@@ -114,13 +114,17 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
switch dnsOptions.Provider {
case C.DNSProviderAliDNS:
solver.DNSProvider = &alidns.Provider{
AccKeyID: dnsOptions.AliDNSOptions.AccessKeyID,
AccKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
RegionID: dnsOptions.AliDNSOptions.RegionID,
CredentialInfo: alidns.CredentialInfo{
AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID,
AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
RegionID: dnsOptions.AliDNSOptions.RegionID,
SecurityToken: dnsOptions.AliDNSOptions.SecurityToken,
},
}
case C.DNSProviderCloudflare:
solver.DNSProvider = &cloudflare.Provider{
APIToken: dnsOptions.CloudflareOptions.APIToken,
APIToken: dnsOptions.CloudflareOptions.APIToken,
ZoneToken: dnsOptions.CloudflareOptions.ZoneToken,
}
default:
return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider)

View File

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

View File

@@ -169,6 +169,35 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
}
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}
if options.ECH != nil && options.ECH.Enabled {
var err error

View File

@@ -222,6 +222,35 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
}
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)
if err != nil {
return nil, err

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,15 +7,12 @@ import (
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"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/include"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/sing/service/pause"
)
@@ -29,23 +26,12 @@ type Instance struct {
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 {
ctx := s.baseContext()
options, err := parseConfig(ctx, configContent)
options, err := parseConfig(s.ctx, configContent)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(s.ctx)
defer cancel()
instance, err := box.New(box.Options{
Context: ctx,
@@ -58,7 +44,7 @@ func (s *StartedService) CheckConfig(configContent string) error {
}
func (s *StartedService) FormatConfig(configContent string) (string, error) {
options, err := parseConfig(s.baseContext(), configContent)
options, err := parseConfig(s.ctx, configContent)
if err != nil {
return "", err
}
@@ -79,7 +65,7 @@ type OverrideOptions struct {
}
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
ctx := s.baseContext()
ctx := s.ctx
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
ctx, cancel := context.WithCancel(include.Context(ctx))
options, err := parseConfig(ctx, profileContent)

View File

@@ -1,11 +1,5 @@
package daemon
import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/option"
)
type PlatformHandler interface {
ServiceStop() error
ServiceReload() error
@@ -13,10 +7,3 @@ type PlatformHandler interface {
SetSystemProxyEnabled(enabled bool) error
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)
type StartedService struct {
ctx context.Context
platform PlatformInterface
platformHandler PlatformHandler
debug bool
logMaxLines int
workingDirectory string
tempDirectory string
userID int
groupID int
systemProxyEnabled bool
ctx context.Context
// platform adapter.PlatformInterface
handler PlatformHandler
debug bool
logMaxLines int
// workingDirectory string
// tempDirectory string
// userID int
// groupID int
// systemProxyEnabled bool
serviceAccess sync.RWMutex
serviceStatus *ServiceStatus
serviceStatusSubscriber *observable.Subscriber[*ServiceStatus]
@@ -58,30 +58,30 @@ type StartedService struct {
}
type ServiceOptions struct {
Context context.Context
Platform PlatformInterface
PlatformHandler PlatformHandler
Debug bool
LogMaxLines int
WorkingDirectory string
TempDirectory string
UserID int
GroupID int
SystemProxyEnabled bool
Context context.Context
// Platform adapter.PlatformInterface
Handler PlatformHandler
Debug bool
LogMaxLines int
// WorkingDirectory string
// TempDirectory string
// UserID int
// GroupID int
// SystemProxyEnabled bool
}
func NewStartedService(options ServiceOptions) *StartedService {
s := &StartedService{
ctx: options.Context,
platform: options.Platform,
platformHandler: options.PlatformHandler,
debug: options.Debug,
logMaxLines: options.LogMaxLines,
workingDirectory: options.WorkingDirectory,
tempDirectory: options.TempDirectory,
userID: options.UserID,
groupID: options.GroupID,
systemProxyEnabled: options.SystemProxyEnabled,
ctx: options.Context,
// platform: options.Platform,
handler: options.Handler,
debug: options.Debug,
logMaxLines: options.LogMaxLines,
// workingDirectory: options.WorkingDirectory,
// tempDirectory: options.TempDirectory,
// userID: options.UserID,
// groupID: options.GroupID,
// systemProxyEnabled: options.SystemProxyEnabled,
serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE},
serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4),
logSubscriber: observable.NewSubscriber[*log.Entry](128),
@@ -117,6 +117,46 @@ func (s *StartedService) updateStatusError(err error) error {
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 {
s.serviceAccess.Lock()
switch s.serviceStatus.Status {
@@ -125,6 +165,13 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
s.serviceAccess.Unlock()
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.resetLogs()
instance, err := s.newInstance(profileContent, options)
@@ -132,6 +179,10 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
return s.updateStatusError(err)
}
s.instance = instance
instance.urlTestHistoryStorage.SetHook(s.urlTestSubscriber)
if instance.clashServer != nil {
instance.clashServer.SetModeUpdateHook(s.clashModeSubscriber)
}
s.serviceAccess.Unlock()
err = instance.Start()
s.serviceAccess.Lock()
@@ -173,12 +224,11 @@ func (s *StartedService) CloseService() error {
func (s *StartedService) SetError(err error) {
s.serviceAccess.Lock()
s.updateStatusError(err)
s.serviceAccess.Unlock()
s.WriteMessage(log.LevelError, err.Error())
}
func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
err := s.platformHandler.ServiceStop()
err := s.handler.ServiceStop()
if err != nil {
return nil, err
}
@@ -186,7 +236,7 @@ func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty)
}
func (s *StartedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
err := s.platformHandler.ServiceReload()
err := s.handler.ServiceReload()
if err != nil {
return nil, err
}
@@ -227,8 +277,8 @@ func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerSt
for element := s.logLines.Front(); element != nil; element = element.Next() {
savedLines = append(savedLines, element.Value)
}
s.logAccess.Unlock()
subscription, done, err := s.logObserver.Subscribe()
s.logAccess.Unlock()
if err != nil {
return err
}
@@ -252,30 +302,33 @@ func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerSt
case <-server.Context().Done():
return server.Context().Err()
case message := <-subscription:
var rawMessage Log
if message == nil {
err = server.Send(&Log{Reset_: true})
if err != nil {
return err
}
continue
rawMessage.Reset_ = true
} else {
rawMessage.Messages = append(rawMessage.Messages, &Log_Message{
Level: LogLevel(message.Level),
Message: message.Message,
})
}
messages := []*Log_Message{{
Level: LogLevel(message.Level),
Message: message.Message,
}}
fetch:
for {
select {
case message = <-subscription:
messages = append(messages, &Log_Message{
Level: LogLevel(message.Level),
Message: message.Message,
})
if message == nil {
rawMessage.Messages = nil
rawMessage.Reset_ = true
} else {
rawMessage.Messages = append(rawMessage.Messages, &Log_Message{
Level: LogLevel(message.Level),
Message: message.Message,
})
}
default:
break fetch
}
}
err = server.Send(&Log{Messages: messages})
err = server.Send(&rawMessage)
if err != nil {
return err
}
@@ -298,6 +351,11 @@ func (s *StartedService) GetDefaultLogLevel(ctx context.Context, empty *emptypb.
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 {
interval := time.Duration(request.Interval)
if interval <= 0 {
@@ -335,7 +393,9 @@ func (s *StartedService) readStatus() *Status {
status.Memory = memory.Inuse()
status.Goroutines = int32(runtime.NumGoroutine())
status.ConnectionsOut = int32(conntrack.Count())
s.serviceAccess.RLock()
nowService := s.instance
s.serviceAccess.RUnlock()
if nowService != nil {
if clashServer := nowService.clashServer; clashServer != nil {
status.TrafficAvailable = true
@@ -348,6 +408,10 @@ func (s *StartedService) readStatus() *Status {
}
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()
if err != nil {
return err
@@ -355,18 +419,16 @@ func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.Serve
defer s.urlTestObserver.UnSubscribe(subscription)
for {
s.serviceAccess.RLock()
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:
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return os.ErrInvalid
}
groups := s.readGroups()
s.serviceAccess.RUnlock()
err = server.Send(groups)
if err != nil {
return err
}
select {
case <-subscription:
case <-s.ctx.Done():
@@ -443,12 +505,27 @@ func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.
}
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()
if err != nil {
return err
}
defer s.clashModeObserver.UnSubscribe(subscription)
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 {
case <-subscription:
case <-s.ctx.Done():
@@ -458,16 +535,6 @@ func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.Se
case <-done:
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
}
}
}
@@ -504,12 +571,7 @@ func (s *StartedService) URLTest(ctx context.Context, request *URLTestRequest) (
if isURLTest {
go urlTest.CheckOutbounds()
} else {
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")
}
historyStorage := boxService.urlTestHistoryStorage
outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
itOutbound, _ := boxService.instance.Outbound().Outbound(it)
@@ -566,6 +628,7 @@ func (s *StartedService) SelectOutbound(ctx context.Context, request *SelectOutb
if !selector.SelectOutbound(request.OutboundTag) {
return nil, E.New("outbound not found in selector: ", request.OutboundTag)
}
s.urlTestObserver.Emit(struct{}{})
return &emptypb.Empty{}, nil
}
@@ -589,11 +652,11 @@ func (s *StartedService) SetGroupExpand(ctx context.Context, request *SetGroupEx
}
func (s *StartedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) {
return s.platformHandler.SystemProxyStatus()
return s.handler.SystemProxyStatus()
}
func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
err := s.platformHandler.SetSystemProxyEnabled(request.Enabled)
err := s.handler.SetSystemProxyEnabled(request.Enabled)
if err != nil {
return nil, err
}
@@ -601,13 +664,11 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
}
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[Connections]) error {
s.serviceAccess.RLock()
switch s.serviceStatus.Status {
case ServiceStatus_STARTING, ServiceStatus_STARTED:
default:
s.serviceAccess.RUnlock()
return os.ErrInvalid
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
ticker := time.NewTicker(time.Duration(request.Interval))
@@ -755,15 +816,15 @@ func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
func (s *StartedService) WriteMessage(level log.Level, message string) {
item := &log.Entry{Level: level, Message: message}
s.logSubscriber.Emit(item)
s.logAccess.Lock()
s.logLines.PushBack(item)
if s.logLines.Len() > s.logMaxLines {
s.logLines.Remove(s.logLines.Front())
}
s.logAccess.Unlock()
s.logSubscriber.Emit(item)
if s.debug {
s.platformHandler.WriteDebugMessage(message)
s.handler.WriteDebugMessage(message)
}
}

View File

@@ -1746,13 +1746,14 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x10ConnectionSortBy\x12\b\n" +
"\x04DATE\x10\x00\x12\v\n" +
"\aTRAFFIC\x10\x01\x12\x11\n" +
"\rTOTAL_TRAFFIC\x10\x022\xf8\v\n" +
"\rTOTAL_TRAFFIC\x10\x022\xb7\f\n" +
"\x0eStartedService\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" +
"\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" +
"\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12E\n" +
"\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12=\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" +
"\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" +
@@ -1835,45 +1836,47 @@ var file_daemon_started_service_proto_depIdxs = []int32{
27, // 12: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
27, // 13: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
27, // 14: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
6, // 15: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
27, // 16: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
27, // 17: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
27, // 18: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
16, // 19: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
13, // 20: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
14, // 21: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
15, // 22: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
27, // 23: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
19, // 24: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
20, // 25: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
23, // 26: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
27, // 27: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
27, // 28: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
27, // 29: daemon.StartedService.SubscribeHelperEvents:input_type -> google.protobuf.Empty
28, // 30: daemon.StartedService.SendHelperResponse:input_type -> daemon.HelperResponse
27, // 31: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
27, // 32: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
4, // 33: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
7, // 34: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
8, // 35: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
9, // 36: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
10, // 37: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
17, // 38: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
16, // 39: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
27, // 40: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
27, // 41: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
27, // 42: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
27, // 43: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
18, // 44: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
27, // 45: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
21, // 46: daemon.StartedService.SubscribeConnections:output_type -> daemon.Connections
27, // 47: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
27, // 48: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
24, // 49: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
29, // 50: daemon.StartedService.SubscribeHelperEvents:output_type -> daemon.HelperRequest
27, // 51: daemon.StartedService.SendHelperResponse:output_type -> google.protobuf.Empty
31, // [31:52] is the sub-list for method output_type
10, // [10:31] is the sub-list for method input_type
27, // 15: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
6, // 16: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
27, // 17: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
27, // 18: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
27, // 19: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
16, // 20: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
13, // 21: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
14, // 22: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
15, // 23: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
27, // 24: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
19, // 25: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
20, // 26: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
23, // 27: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
27, // 28: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
27, // 29: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
27, // 30: daemon.StartedService.SubscribeHelperEvents:input_type -> google.protobuf.Empty
28, // 31: daemon.StartedService.SendHelperResponse:input_type -> daemon.HelperResponse
27, // 32: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
27, // 33: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
4, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
7, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
8, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
27, // 37: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
9, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
10, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
17, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
16, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
27, // 42: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
27, // 43: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
27, // 44: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
27, // 45: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
18, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
27, // 47: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.Connections
27, // 49: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
27, // 50: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
24, // 51: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
29, // 52: daemon.StartedService.SubscribeHelperEvents:output_type -> daemon.HelperRequest
27, // 53: daemon.StartedService.SendHelperResponse:output_type -> google.protobuf.Empty
32, // [32:54] is the sub-list for method output_type
10, // [10:32] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name

View File

@@ -13,6 +13,7 @@ service StartedService {
rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {}
rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {}
rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {}
rpc ClearLogs(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc SubscribeStatus(SubscribeStatusRequest) returns(stream Status) {}
rpc SubscribeGroups(google.protobuf.Empty) returns(stream Groups) {}

View File

@@ -20,6 +20,7 @@ const (
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
@@ -47,6 +48,7 @@ type StartedServiceClient interface {
SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error)
SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error)
GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error)
ClearLogs(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error)
SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error)
GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error)
@@ -141,6 +143,16 @@ func (c *startedServiceClient) GetDefaultLogLevel(ctx context.Context, in *empty
return out, nil
}
func (c *startedServiceClient) ClearLogs(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_ClearLogs_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[2], StartedService_SubscribeStatus_FullMethodName, cOpts...)
@@ -355,6 +367,7 @@ type StartedServiceServer interface {
SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error
SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error
GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error)
ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error
SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error
GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error)
@@ -401,6 +414,10 @@ func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *em
return nil, status.Errorf(codes.Unimplemented, "method GetDefaultLogLevel not implemented")
}
func (UnimplementedStartedServiceServer) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ClearLogs not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error {
return status.Errorf(codes.Unimplemented, "method SubscribeStatus not implemented")
}
@@ -561,6 +578,24 @@ func _StartedService_GetDefaultLogLevel_Handler(srv interface{}, ctx context.Con
return interceptor(ctx, in, info, handler)
}
func _StartedService_ClearLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).ClearLogs(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_ClearLogs_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).ClearLogs(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeStatusRequest)
if err := stream.RecvMsg(m); err != nil {
@@ -833,6 +868,10 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetDefaultLogLevel",
Handler: _StartedService_GetDefaultLogLevel_Handler,
},
{
MethodName: "ClearLogs",
Handler: _StartedService_ClearLogs_Handler,
},
{
MethodName: "GetClashModeStatus",
Handler: _StartedService_GetClashModeStatus_Handler,

View File

@@ -95,6 +95,20 @@ 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) {
if len(message.Question) == 0 {
if c.logger != nil {
@@ -214,7 +228,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
response.Answer = append(response.Answer, validResponse.Answer...)
}
}*/
disableCache = disableCache || response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil {
var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses
@@ -251,10 +265,17 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
}
var timeToLive uint32
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
if len(response.Answer) == 0 {
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
timeToLive = soaTTL
}
}
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
}
}
}
}
@@ -332,64 +353,6 @@ 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 {
if strategy == C.DomainStrategyPreferIPv6 {
return append(response6, response4...)

View File

@@ -213,97 +213,95 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
}
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
var (
response *mDNS.Msg
transport adapter.DNSTransport
err error
)
response, cached := r.client.ExchangeCache(ctx, message)
if !cached {
var metadata *adapter.InboundContext
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
var metadata *adapter.InboundContext
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
options.Strategy = legacyTransport.LegacyStrategy()
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else {
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else {
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
}
}
if err != nil {
return nil, err
@@ -326,7 +324,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
var (
responseAddrs []netip.Addr
cached bool
err error
)
printResult := func() {
@@ -346,13 +343,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err = E.Cause(err, "lookup ", domain)
}
}
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached {
if len(responseAddrs) == 0 {
return nil, E.New("lookup ", domain, ": empty result (cached)")
}
return responseAddrs, nil
}
r.logger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
@@ -385,12 +375,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return nil, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
return nil, &R.RejectedError{Cause: action.Error(ctx)}
case *R.RuleActionPredefined:
if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode)

View File

@@ -49,6 +49,7 @@ type Transport struct {
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
transportLock sync.RWMutex
updatedAt time.Time
lastError error
servers []M.Socksaddr
search []string
ndots int
@@ -92,7 +93,7 @@ func (t *Transport) Start(stage adapter.StartStage) error {
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
}
go func() {
_, err := t.Fetch()
_, err := t.fetch()
if err != nil {
t.logger.Error(E.Cause(err, "fetch DNS servers"))
}
@@ -108,7 +109,7 @@ func (t *Transport) Close() 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 {
return nil, err
}
@@ -128,11 +129,20 @@ func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []
}
}
func (t *Transport) Fetch() ([]M.Socksaddr, error) {
func (t *Transport) Fetch() []M.Socksaddr {
servers, _ := t.fetch()
return servers
}
func (t *Transport) fetch() ([]M.Socksaddr, error) {
t.transportLock.RLock()
updatedAt := t.updatedAt
lastError := t.lastError
servers := t.servers
t.transportLock.RUnlock()
if lastError != nil {
return nil, lastError
}
if time.Since(updatedAt) < C.DHCPTTL {
return servers, nil
}
@@ -143,7 +153,7 @@ func (t *Transport) Fetch() ([]M.Socksaddr, error) {
}
err := t.updateServers()
if err != nil {
return nil, err
return servers, err
}
return t.servers, nil
}
@@ -173,12 +183,15 @@ func (t *Transport) updateServers() error {
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
err = t.fetchServers0(fetchCtx, iface)
cancel()
t.updatedAt = time.Now()
if err != nil {
t.lastError = err
return err
} else if len(t.servers) == 0 {
return E.New("dhcp: empty DNS servers response")
t.lastError = E.New("dhcp: empty DNS servers response")
return t.lastError
} else {
t.updatedAt = time.Now()
t.lastError = nil
return nil
}
}

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ type Transport struct {
type dhcpTransport interface {
adapter.DNSTransport
Fetch() ([]M.Socksaddr, error)
Fetch() []M.Socksaddr
Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error)
}
@@ -74,14 +74,12 @@ func (t *Transport) Start(stage adapter.StartStage) error {
break
}
}
if !C.IsIos {
if t.fallback {
t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger)
if t.dhcpTransport != nil {
err := t.dhcpTransport.Start(stage)
if err != nil {
return err
}
if t.fallback {
t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger)
if t.dhcpTransport != nil {
err := t.dhcpTransport.Start(stage)
if err != nil {
return err
}
}
}
@@ -96,27 +94,24 @@ func (t *Transport) Close() error {
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(domain)
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
if !t.fallback {
return t.exchange(ctx, message, domain)
return t.exchange(ctx, message, question.Name)
}
if !C.IsIos {
if t.dhcpTransport != nil {
dhcpTransports, _ := t.dhcpTransport.Fetch()
if len(dhcpTransports) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
}
if t.dhcpTransport != nil {
dhcpTransports := t.dhcpTransport.Fetch()
if len(dhcpTransports) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
}
}
if t.preferGo {
// Assuming the user knows what they are doing, we still execute the query which will fail.
return t.exchange(ctx, message, domain)
return t.exchange(ctx, message, question.Name)
}
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
var network string
@@ -125,7 +120,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
} else {
network = "ip6"
}
addresses, err := t.resolver.LookupNetIP(ctx, network, domain)
addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -135,9 +130,5 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
}
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
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.")
}
return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.")
}

View File

@@ -1,9 +1,11 @@
package local
import (
"bufio"
"context"
"errors"
"os"
"strings"
"sync"
"sync/atomic"
@@ -22,6 +24,25 @@ import (
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 {
ctx context.Context
logger logger.ContextLogger
@@ -188,7 +209,7 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
int32(defaultInterface.Index),
)
if call.Err != nil {
return nil, err
return nil, call.Err
}
var linkPath dbus.ObjectPath
err = call.Store(&linkPath)
@@ -214,15 +235,12 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
return nil, E.New("No appropriate name servers or networks for name found")
}
}
return &ResolvedObject{
BusObject: dbusObject,
}, nil
} else {
return &ResolvedObject{
BusObject: dbusObject,
InterfaceIndex: int32(defaultInterface.Index),
}, nil
return nil, E.New("link has no DNS servers configured")
}
return &ResolvedObject{
BusObject: dbusObject,
InterfaceIndex: int32(defaultInterface.Index),
}, nil
}
func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) {

View File

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

View File

@@ -2,10 +2,216 @@
icon: material/alert-decagram
---
#### 1.13.0-alpha.20
#### 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
* Fixes and improvements
@@ -127,7 +333,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
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 from [MetaCubeX/go](https://github.com/MetaCubeX/go).
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
**7**:
@@ -189,7 +396,8 @@ See [Tun](/configuration/inbound/tun/#loopback_address).
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
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.
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.
| Version | Stack | MTU | Upload | Download |
|-------------|--------|-------|--------|----------|
@@ -208,8 +416,8 @@ The following data was tested using [tun_bench](https://github.com/SagerNet/sing
**18**:
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.
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.
Therefore, after this release, we will not be repeating this notice unless there is new information.
### 1.11.15
@@ -490,7 +698,8 @@ See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/conf
**2**:
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action).
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions,
see [Route Action](/configuration/route/rule_action).
**3**:
@@ -521,7 +730,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
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 from [MetaCubeX/go](https://github.com/MetaCubeX/go).
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
### 1.11.3

View File

@@ -9,7 +9,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
!!! failure ""
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).
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)
## :material-graph: Requirements
@@ -18,7 +18,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :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 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)
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
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,9 +48,9 @@
}
```
### Listen Fields
### 监听字段
See [Listen Fields](/configuration/shared/listen/) for details.
参阅 [监听字段](/zh/configuration/shared/listen/)
### 字段

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [exclude_mptcp](#exclude_mptcp)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [loopback_address](#loopback_address)
@@ -63,6 +67,7 @@ icon: material/new-box
"auto_redirect": true,
"auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024",
"exclude_mptcp": false,
"loopback_address": [
"10.7.0.1"
],
@@ -278,6 +283,20 @@ Connection output mark used by `auto_redirect`.
`0x2024` is used by default.
#### exclude_mptcp
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled.
MPTCP cannot be transparently proxied due to protocol limitations.
Such traffic is usually created by Apple systems.
When enabled, MPTCP connections will bypass sing-box and connect directly, otherwise, will be rejected to avoid errors by default.
#### loopback_address
!!! question "Since sing-box 1.12.0"

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [exclude_mptcp](#exclude_mptcp)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [loopback_address](#loopback_address)
@@ -63,6 +67,7 @@ icon: material/new-box
"auto_redirect": true,
"auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024",
"exclude_mptcp": false,
"loopback_address": [
"10.7.0.1"
],
@@ -277,6 +282,20 @@ tun 接口的 IPv6 前缀。
默认使用 `0x2024`
#### exclude_mptcp
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅支持 Linux且需要 nftables`auto_route``auto_redirect` 已启用。
由于协议限制MPTCP 无法被透明代理。
此类流量通常由 Apple 系统创建。
启用时MPTCP 连接将绕过 sing-box 直接连接,否则,将被拒绝以避免错误。
#### loopback_address
!!! question "自 sing-box 1.12.0 起"

View File

@@ -36,6 +36,7 @@
| `dns` | [DNS](./dns/) |
| `selector` | [Selector](./selector/) |
| `urltest` | [URLTest](./urltest/) |
| `naive` | [NaiveProxy](./naive/) |
#### tag

View File

@@ -36,6 +36,7 @@
| `dns` | [DNS](./dns/) |
| `selector` | [Selector](./selector/) |
| `urltest` | [URLTest](./urltest/) |
| `naive` | [NaiveProxy](./naive/) |
#### tag

View File

@@ -0,0 +1,114 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.13.0"
### Structure
```json
{
"type": "naive",
"tag": "naive-out",
"server": "127.0.0.1",
"server_port": 443,
"username": "sekai",
"password": "password",
"insecure_concurrency": 0,
"extra_headers": {},
"udp_over_tcp": false | {},
"quic": false,
"quic_congestion_control": "",
"tls": {},
... // Dial Fields
}
```
!!! warning "Platform Support"
NaiveProxy outbound is only available on Apple platforms, Android, Windows and certain Linux builds.
**Official Release Build Variants:**
| Build Variant | Platforms | Description |
|---------------|-----------|-------------|
| (default) | Linux amd64/arm64 | purego build with `libcronet.so` included |
| `-glibc` | Linux 386/amd64/arm/arm64 | CGO build dynamically linked with glibc, requires glibc >= 2.31 |
| `-musl` | Linux 386/amd64/arm/arm64 | CGO build statically linked with musl, no system requirements |
| (default) | Windows amd64/arm64 | purego build with `libcronet.dll` included |
**Runtime Requirements:**
- **Linux purego**: `libcronet.so` must be in the same directory as the sing-box binary or in system library path
- **Windows**: `libcronet.dll` must be in the same directory as `sing-box.exe` or in a directory listed in `PATH`
For self-built binaries, see [Build from source](/installation/build-from-source/#with_naive_outbound).
### Fields
#### server
==Required==
The server address.
#### server_port
==Required==
The server port.
#### username
Authentication username.
#### password
Authentication password.
#### insecure_concurrency
Number of concurrent tunnel connections. Multiple connections make the tunneling easier to detect through traffic analysis, which defeats the purpose of NaiveProxy's design to resist traffic analysis.
#### extra_headers
Extra headers to send in HTTP requests.
#### udp_over_tcp
UDP over TCP protocol settings.
See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details.
#### quic
Use QUIC instead of HTTP/2.
#### quic_congestion_control
QUIC congestion control algorithm.
| Algorithm | Description |
|-----------|-------------|
| `bbr` | BBR |
| `bbr2` | BBRv2 |
| `cubic` | CUBIC |
| `reno` | New Reno |
`bbr` is used by default (the default of QUICHE, used by Chromium which NaiveProxy is based on).
#### tls
==Required==
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
Only `server_name`, `certificate`, `certificate_path` and `ech` are supported.
Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis, and should not be used in production.
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -0,0 +1,114 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.13.0 起"
### 结构
```json
{
"type": "naive",
"tag": "naive-out",
"server": "127.0.0.1",
"server_port": 443,
"username": "sekai",
"password": "password",
"insecure_concurrency": 0,
"extra_headers": {},
"udp_over_tcp": false | {},
"quic": false,
"quic_congestion_control": "",
"tls": {},
... // 拨号字段
}
```
!!! warning "平台支持"
NaiveProxy 出站仅在 Apple 平台、Android、Windows 和特定 Linux 构建上可用。
**官方发布版本区别:**
| 构建变体 | 平台 | 说明 |
|-----------|------------------------|------------------------------------------|
| (默认) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` |
| `-glibc` | Linux 386/amd64/arm/arm64 | CGO 构建,动态链接 glibc要求 glibc >= 2.31 |
| `-musl` | Linux 386/amd64/arm/arm64 | CGO 构建,静态链接 musl无系统要求 |
| (默认) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` |
**运行时要求:**
- **Linux purego**`libcronet.so` 必须位于 sing-box 二进制文件相同目录或系统库路径中
- **Windows**`libcronet.dll` 必须位于 `sing-box.exe` 相同目录或 `PATH` 中的任意目录
自行构建请参阅 [从源代码构建](/zh/installation/build-from-source/#with_naive_outbound)。
### 字段
#### server
==必填==
服务器地址。
#### server_port
==必填==
服务器端口。
#### username
认证用户名。
#### password
认证密码。
#### insecure_concurrency
并发隧道连接数。多连接使隧道更容易被流量分析检测,违背 NaiveProxy 抵抗流量分析的设计目的。
#### extra_headers
HTTP 请求中发送的额外头部。
#### udp_over_tcp
UDP over TCP 配置。
参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。
#### quic
使用 QUIC 代替 HTTP/2。
#### quic_congestion_control
QUIC 拥塞控制算法。
| 算法 | 描述 |
|------|------|
| `bbr` | BBR |
| `bbr2` | BBRv2 |
| `cubic` | CUBIC |
| `reno` | New Reno |
默认使用 `bbr`NaiveProxy 基于的 Chromium 使用的 QUICHE 的默认值)。
#### tls
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
只有 `server_name``certificate``certificate_path``ech` 是被支持的。
自签名证书会显著改变流量行为,违背了 NaiveProxy 旨在抵抗流量分析的设计初衷,不应该在生产环境中使用。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。

View File

@@ -66,7 +66,7 @@ UDP 包中继模式
#### udp_over_stream
这是 TUIC 的 [UDP over TCP 协议](/configuration/shared/udp-over-tcp/) 移植, 旨在提供 TUIC 不提供的 基于 QUIC 流的 UDP 中继模式。 由于它是一个附加协议,因此您需要使用 sing-box 或其他兼容的程序作为服务器。
这是 TUIC 的 [UDP over TCP 协议](/zh/configuration/shared/udp-over-tcp/) 移植, 旨在提供 TUIC 不提供的 基于 QUIC 流的 UDP 中继模式。 由于它是一个附加协议,因此您需要使用 sing-box 或其他兼容的程序作为服务器。
此模式在正确的 UDP 代理场景中没有任何积极作用,仅适用于中继流式 UDP 流量(基本上是 QUIC 流)。

View File

@@ -62,7 +62,7 @@ icon: material/alert-decagram
!!! question "自 sing-box 1.8.0 起"
一组 [规则集](/configuration/rule-set/)。
一组 [规则集](/zh/configuration/rule-set/)。
#### final

View File

@@ -428,20 +428,16 @@ Match default interface address.
#### wifi_ssid
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match WiFi SSID.
See [Wi-Fi State](/configuration/shared/wifi-state/) for details.
#### wifi_bssid
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match WiFi BSSID.
See [Wi-Fi State](/configuration/shared/wifi-state/) for details.
#### preferred_by
!!! question "Since sing-box 1.13.0"

View File

@@ -425,20 +425,16 @@ icon: material/new-box
#### wifi_ssid
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配 WiFi SSID。
参阅 [Wi-Fi 状态](/zh/configuration/shared/wifi-state/)。
#### wifi_bssid
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配 WiFi BSSID。
参阅 [Wi-Fi 状态](/zh/configuration/shared/wifi-state/)。
#### preferred_by
!!! question "自 sing-box 1.13.0 起"

View File

@@ -0,0 +1,106 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.13.0"
# CCM
CCM (Claude Code Multiplexer) service is a multiplexing service that allows you to access your local Claude Code subscription remotely through custom tokens.
It handles OAuth authentication with Claude's API on your local machine while allowing remote Claude Code to authenticate using Auth Tokens via the `ANTHROPIC_AUTH_TOKEN` environment variable.
### Structure
```json
{
"type": "ccm",
... // Listen Fields
"credential_path": "",
"usages_path": "",
"users": [],
"headers": {},
"detour": "",
"tls": {}
}
```
### Listen Fields
See [Listen Fields](/configuration/shared/listen/) for details.
### Fields
#### credential_path
Path to the Claude Code OAuth credentials file.
If not specified, defaults to:
- `$CLAUDE_CONFIG_DIR/.credentials.json` if `CLAUDE_CONFIG_DIR` environment variable is set
- `~/.claude/.credentials.json` otherwise
On macOS, credentials are read from the system keychain first, then fall back to the file if unavailable.
Refreshed tokens are automatically written back to the same location.
#### usages_path
Path to the file for storing aggregated API usage statistics.
Usage tracking is disabled if not specified.
When enabled, the service tracks and saves comprehensive statistics including:
- Request counts
- Token usage (input, output, cache read, cache creation)
- Calculated costs in USD based on Claude API pricing
Statistics are organized by model, context window (200k standard vs 1M premium), and optionally by user when authentication is enabled.
The statistics file is automatically saved every minute and upon service shutdown.
#### users
List of authorized users for token authentication.
If empty, no authentication is required.
Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value.
#### headers
Custom HTTP headers to send to the Claude API.
These headers will override any existing headers with the same name.
#### detour
Outbound tag for connecting to the Claude API.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
### Example
```json
{
"services": [
{
"type": "ccm",
"listen": "127.0.0.1",
"listen_port": 8080
}
]
}
```
Connect to the CCM service:
```bash
export ANTHROPIC_BASE_URL="http://127.0.0.1:8080"
export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context"
claude
```

View File

@@ -0,0 +1,106 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.13.0 起"
# CCM
CCMClaude Code 多路复用器)服务是一个多路复用服务,允许您通过自定义令牌远程访问本地的 Claude Code 订阅。
它在本地机器上处理与 Claude API 的 OAuth 身份验证,同时允许远程 Claude Code 通过 `ANTHROPIC_AUTH_TOKEN` 环境变量使用认证令牌进行身份验证。
### 结构
```json
{
"type": "ccm",
... // 监听字段
"credential_path": "",
"usages_path": "",
"users": [],
"headers": {},
"detour": "",
"tls": {}
}
```
### 监听字段
参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。
### 字段
#### credential_path
Claude Code OAuth 凭据文件的路径。
如果未指定,默认值为:
- 如果设置了 `CLAUDE_CONFIG_DIR` 环境变量,则使用 `$CLAUDE_CONFIG_DIR/.credentials.json`
- 否则使用 `~/.claude/.credentials.json`
在 macOS 上,首先从系统钥匙串读取凭据,如果不可用则回退到文件。
刷新的令牌会自动写回相同位置。
#### usages_path
用于存储聚合 API 使用统计信息的文件路径。
如果未指定,使用跟踪将被禁用。
启用后,服务会跟踪并保存全面的统计信息,包括:
- 请求计数
- 令牌使用量(输入、输出、缓存读取、缓存创建)
- 基于 Claude API 定价计算的美元成本
统计信息按模型、上下文窗口200k 标准版 vs 1M 高级版)以及可选的用户(启用身份验证时)进行组织。
统计文件每分钟自动保存一次,并在服务关闭时保存。
#### users
用于令牌身份验证的授权用户列表。
如果为空,则不需要身份验证。
Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
#### headers
发送到 Claude API 的自定义 HTTP 头。
这些头会覆盖同名的现有头。
#### detour
用于连接 Claude API 的出站标签。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
### 示例
```json
{
"services": [
{
"type": "ccm",
"listen": "127.0.0.1",
"listen_port": 8080
}
]
}
```
连接到 CCM 服务:
```bash
export ANTHROPIC_BASE_URL="http://127.0.0.1:8080"
export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context"
claude
```

View File

@@ -23,7 +23,9 @@ icon: material/new-box
| Type | Format |
|------------|------------------------|
| `ccm` | [CCM](./ccm) |
| `derp` | [DERP](./derp) |
| `ocm` | [OCM](./ocm) |
| `resolved` | [Resolved](./resolved) |
| `ssm-api` | [SSM API](./ssm-api) |

View File

@@ -23,7 +23,9 @@ icon: material/new-box
| 类型 | 格式 |
|-----------|------------------------|
| `ccm` | [CCM](./ccm) |
| `derp` | [DERP](./derp) |
| `ocm` | [OCM](./ocm) |
| `resolved`| [Resolved](./resolved) |
| `ssm-api` | [SSM API](./ssm-api) |

View File

@@ -0,0 +1,171 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.13.0"
# OCM
OCM (OpenAI Codex Multiplexer) service is a multiplexing service that allows you to access your local OpenAI Codex subscription remotely through custom tokens.
It handles OAuth authentication with OpenAI's API on your local machine while allowing remote clients to authenticate using custom tokens.
### Structure
```json
{
"type": "ocm",
... // Listen Fields
"credential_path": "",
"usages_path": "",
"users": [],
"headers": {},
"detour": "",
"tls": {}
}
```
### Listen Fields
See [Listen Fields](/configuration/shared/listen/) for details.
### Fields
#### credential_path
Path to the OpenAI OAuth credentials file.
If not specified, defaults to `~/.codex/auth.json`.
Refreshed tokens are automatically written back to the same location.
#### usages_path
Path to the file for storing aggregated API usage statistics.
Usage tracking is disabled if not specified.
When enabled, the service tracks and saves comprehensive statistics including:
- Request counts
- Token usage (input, output, cached)
- Calculated costs in USD based on OpenAI API pricing
Statistics are organized by model and optionally by user when authentication is enabled.
The statistics file is automatically saved every minute and upon service shutdown.
#### users
List of authorized users for token authentication.
If empty, no authentication is required.
Object format:
```json
{
"name": "",
"token": ""
}
```
Object fields:
- `name`: Username identifier for tracking purposes.
- `token`: Bearer token for authentication. Clients authenticate by setting the `Authorization: Bearer <token>` header.
#### headers
Custom HTTP headers to send to the OpenAI API.
These headers will override any existing headers with the same name.
#### detour
Outbound tag for connecting to the OpenAI API.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
### Example
#### Server
```json
{
"services": [
{
"type": "ocm",
"listen": "127.0.0.1",
"listen_port": 8080
}
]
}
```
#### Client
Add to `~/.codex/config.toml`:
```toml
[model_providers.ocm]
name = "OCM Proxy"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = false
```
Then run:
```bash
codex --model-provider ocm
```
### Example with Authentication
#### Server
```json
{
"services": [
{
"type": "ocm",
"listen": "0.0.0.0",
"listen_port": 8080,
"usages_path": "./codex-usages.json",
"users": [
{
"name": "alice",
"token": "sk-alice-secret-token"
},
{
"name": "bob",
"token": "sk-bob-secret-token"
}
]
}
]
}
```
#### Client
Add to `~/.codex/config.toml`:
```toml
[model_providers.ocm]
name = "OCM Proxy"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = false
experimental_bearer_token = "sk-alice-secret-token"
```
Then run:
```bash
codex --model-provider ocm
```

View File

@@ -0,0 +1,171 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.13.0 起"
# OCM
OCMOpenAI Codex 多路复用器)服务是一个多路复用服务,允许您通过自定义令牌远程访问本地的 OpenAI Codex 订阅。
它在本地机器上处理与 OpenAI API 的 OAuth 身份验证,同时允许远程客户端使用自定义令牌进行身份验证。
### 结构
```json
{
"type": "ocm",
... // 监听字段
"credential_path": "",
"usages_path": "",
"users": [],
"headers": {},
"detour": "",
"tls": {}
}
```
### 监听字段
参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。
### 字段
#### credential_path
OpenAI OAuth 凭据文件的路径。
如果未指定,默认值为 `~/.codex/auth.json`
刷新的令牌会自动写回相同位置。
#### usages_path
用于存储聚合 API 使用统计信息的文件路径。
如果未指定,使用跟踪将被禁用。
启用后,服务会跟踪并保存全面的统计信息,包括:
- 请求计数
- 令牌使用量(输入、输出、缓存)
- 基于 OpenAI API 定价计算的美元成本
统计信息按模型以及可选的用户(启用身份验证时)进行组织。
统计文件每分钟自动保存一次,并在服务关闭时保存。
#### users
用于令牌身份验证的授权用户列表。
如果为空,则不需要身份验证。
对象格式:
```json
{
"name": "",
"token": ""
}
```
对象字段:
- `name`:用于跟踪的用户名标识符。
- `token`:用于身份验证的 Bearer 令牌。客户端通过设置 `Authorization: Bearer <token>` 头进行身份验证。
#### headers
发送到 OpenAI API 的自定义 HTTP 头。
这些头会覆盖同名的现有头。
#### detour
用于连接 OpenAI API 的出站标签。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
### 示例
#### 服务端
```json
{
"services": [
{
"type": "ocm",
"listen": "127.0.0.1",
"listen_port": 8080
}
]
}
```
#### 客户端
`~/.codex/config.toml` 中添加:
```toml
[model_providers.ocm]
name = "OCM Proxy"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = false
```
然后运行:
```bash
codex --model-provider ocm
```
### 带身份验证的示例
#### 服务端
```json
{
"services": [
{
"type": "ocm",
"listen": "0.0.0.0",
"listen_port": 8080,
"usages_path": "./codex-usages.json",
"users": [
{
"name": "alice",
"token": "sk-alice-secret-token"
},
{
"name": "bob",
"token": "sk-bob-secret-token"
}
]
}
]
}
```
#### 客户端
`~/.codex/config.toml` 中添加:
```toml
[model_providers.ocm]
name = "OCM Proxy"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = false
experimental_bearer_token = "sk-alice-secret-token"
```
然后运行:
```bash
codex --model-provider ocm
```

View File

@@ -2,6 +2,12 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive)
:material-plus: [tcp_keep_alive](#tcp_keep_alive)
:material-plus: [tcp_keep_alive_interval](#tcp_keep_alive_interval)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [domain_resolver](#domain_resolver)
@@ -29,8 +35,11 @@ icon: material/new-box
"connect_timeout": "",
"tcp_fast_open": false,
"tcp_multi_path": false,
"disable_tcp_keep_alive": false,
"tcp_keep_alive": "",
"tcp_keep_alive_interval": "",
"udp_fragment": false,
"domain_resolver": "", // or {}
"network_strategy": "",
"network_type": [],
@@ -112,6 +121,30 @@ Enable TCP Fast Open.
Enable TCP Multi Path.
#### disable_tcp_keep_alive
!!! question "Since sing-box 1.13.0"
Disable TCP keep alive.
#### tcp_keep_alive
!!! question "Since sing-box 1.13.0"
Default value changed from `10m` to `5m`.
TCP keep alive initial period.
`5m` will be used by default.
#### tcp_keep_alive_interval
!!! question "Since sing-box 1.13.0"
TCP keep alive interval.
`75s` will be used by default.
#### udp_fragment
Enable UDP fragmentation.

View File

@@ -2,6 +2,12 @@
icon: material/new-box
---
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive)
:material-plus: [tcp_keep_alive](#tcp_keep_alive)
:material-plus: [tcp_keep_alive_interval](#tcp_keep_alive_interval)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [domain_resolver](#domain_resolver)
@@ -29,7 +35,11 @@ icon: material/new-box
"connect_timeout": "",
"tcp_fast_open": false,
"tcp_multi_path": false,
"disable_tcp_keep_alive": false,
"tcp_keep_alive": "",
"tcp_keep_alive_interval": "",
"udp_fragment": false,
"domain_resolver": "", // 或 {}
"network_strategy": "",
"network_type": [],
@@ -109,6 +119,30 @@ icon: material/new-box
启用 TCP Multi Path。
#### disable_tcp_keep_alive
!!! question "自 sing-box 1.13.0 起"
禁用 TCP keep alive。
#### tcp_keep_alive
!!! question "自 sing-box 1.13.0 起"
默认值从 `10m` 更改为 `5m`
TCP keep alive 初始周期。
默认使用 `5m`
#### tcp_keep_alive_interval
!!! question "自 sing-box 1.13.0 起"
TCP keep alive 间隔。
默认使用 `75s`
#### udp_fragment
启用 UDP 分段。

View File

@@ -1,9 +1,18 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [alidns.security_token](#security_token)
:material-plus: [cloudflare.zone_token](#zone_token)
### Structure
```json
{
"provider": "",
... // Provider Fields
}
```
@@ -17,15 +26,31 @@
"provider": "alidns",
"access_key_id": "",
"access_key_secret": "",
"region_id": ""
"region_id": "",
"security_token": ""
}
```
##### security_token
!!! question "Since sing-box 1.13.0"
The Security Token for STS temporary credentials.
#### Cloudflare
```json
{
"provider": "cloudflare",
"api_token": ""
"api_token": "",
"zone_token": ""
}
```
```
##### zone_token
!!! question "Since sing-box 1.13.0"
Optional API token with `Zone:Read` permission.
When provided, allows `api_token` to be scoped to a single zone.

View File

@@ -1,9 +1,18 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [alidns.security_token](#security_token)
:material-plus: [cloudflare.zone_token](#zone_token)
### 结构
```json
{
"provider": "",
... // 提供商字段
}
```
@@ -17,15 +26,31 @@
"provider": "alidns",
"access_key_id": "",
"access_key_secret": "",
"region_id": ""
"region_id": "",
"security_token": ""
}
```
##### security_token
!!! question "自 sing-box 1.13.0 起"
用于 STS 临时凭证的安全令牌。
#### Cloudflare
```json
{
"provider": "cloudflare",
"api_token": ""
"api_token": "",
"zone_token": ""
}
```
```
##### zone_token
!!! question "自 sing-box 1.13.0 起"
具有 `Zone:Read` 权限的可选 API 令牌。
提供后可将 `api_token` 限定到单个区域。

View File

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive)
:material-alert: [tcp_keep_alive](#tcp_keep_alive)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [netns](#netns)
@@ -29,6 +34,9 @@ icon: material/new-box
"netns": "",
"tcp_fast_open": false,
"tcp_multi_path": false,
"disable_tcp_keep_alive": false,
"tcp_keep_alive": "",
"tcp_keep_alive_interval": "",
"udp_fragment": false,
"udp_timeout": "",
"detour": "",
@@ -101,6 +109,28 @@ Enable TCP Fast Open.
Enable TCP Multi Path.
#### disable_tcp_keep_alive
!!! question "Since sing-box 1.13.0"
Disable TCP keep alive.
#### tcp_keep_alive
!!! question "Since sing-box 1.13.0"
Default value changed from `10m` to `5m`.
TCP keep alive initial period.
`5m` will be used by default.
#### tcp_keep_alive_interval
TCP keep alive interval.
`75s` will be used by default.
#### udp_fragment
Enable UDP fragmentation.

View File

@@ -2,7 +2,12 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.12.0"
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive)
:material-alert: [tcp_keep_alive](#tcp_keep_alive)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [netns](#netns)
:material-plus: [bind_interface](#bind_interface)
@@ -29,6 +34,9 @@ icon: material/new-box
"netns": "",
"tcp_fast_open": false,
"tcp_multi_path": false,
"disable_tcp_keep_alive": false,
"tcp_keep_alive": "",
"tcp_keep_alive_interval": "",
"udp_fragment": false,
"udp_timeout": "",
"detour": "",
@@ -101,6 +109,28 @@ icon: material/new-box
启用 TCP Multi Path。
#### disable_tcp_keep_alive
!!! question "自 sing-box 1.13.0 起"
禁用 TCP keep alive。
#### tcp_keep_alive
!!! question "自 sing-box 1.13.0 起"
默认值从 `10m` 更改为 `5m`
TCP keep alive 初始周期。
默认使用 `5m`
#### tcp_keep_alive_interval
TCP keep alive 间隔。
默认使用 `75s`
#### udp_fragment
启用 UDP 分段。

View File

@@ -8,10 +8,13 @@ icon: material/new-box
:material-plus: [kernel_rx](#kernel_rx)
:material-plus: [curve_preferences](#curve_preferences)
:material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256)
:material-plus: [client_authentication](#client_authentication)
:material-plus: [client_certificate](#client_certificate)
:material-plus: [client_certificate_path](#client_certificate_path)
:material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256)
:material-plus: [client_key](#client_key)
:material-plus: [client_key_path](#client_key_path)
:material-plus: [client_authentication](#client_authentication)
:material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256)
:material-plus: [ech.query_server_name](#query_server_name)
!!! quote "Changes in sing-box 1.12.0"
@@ -101,9 +104,14 @@ icon: material/new-box
"min_version": "",
"max_version": "",
"cipher_suites": [],
"curve_preferences": [],
"certificate": "",
"certificate_path": "",
"certificate_public_key_sha256": [],
"client_certificate": [],
"client_certificate_path": "",
"client_key": [],
"client_key_path": "",
"fragment": false,
"fragment_fallback_delay": "",
"record_fragment": false,
@@ -111,6 +119,7 @@ icon: material/new-box
"enabled": false,
"config": [],
"config_path": "",
"query_server_name": "",
// Deprecated
"pq_signature_schemes_enabled": false,
@@ -258,6 +267,38 @@ openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform d
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
```
#### client_certificate
!!! question "Since sing-box 1.13.0"
==Client only==
Client certificate chain line array, in PEM format.
#### client_certificate_path
!!! question "Since sing-box 1.13.0"
==Client only==
The path to client certificate chain, in PEM format.
#### client_key
!!! question "Since sing-box 1.13.0"
==Client only==
Client private key line array, in PEM format.
#### client_key_path
!!! question "Since sing-box 1.13.0"
==Client only==
The path to client private key, in PEM format.
#### key
==Server only==
@@ -466,6 +507,16 @@ The path to ECH configuration, in PEM format.
If empty, load from DNS will be attempted.
#### query_server_name
!!! question "Since sing-box 1.13.0"
==Client only==
Overrides the domain name used for ECH HTTPS record queries.
If empty, `server_name` is used for queries.
#### fragment
!!! question "Since sing-box 1.12.0"

View File

@@ -4,14 +4,17 @@ icon: material/new-box
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [kernel_tx](#kernel_tx)
:material-plus: [kernel_rx](#kernel_rx)
:material-plus: [curve_preferences](#curve_preferences)
:material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256)
:material-plus: [client_authentication](#client_authentication)
:material-plus: [client_certificate](#client_certificate)
:material-plus: [client_certificate_path](#client_certificate_path)
:material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256)
:material-plus: [kernel_tx](#kernel_tx)
:material-plus: [kernel_rx](#kernel_rx)
:material-plus: [curve_preferences](#curve_preferences)
:material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256)
:material-plus: [client_certificate](#client_certificate)
:material-plus: [client_certificate_path](#client_certificate_path)
:material-plus: [client_key](#client_key)
:material-plus: [client_key_path](#client_key_path)
:material-plus: [client_authentication](#client_authentication)
:material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256)
:material-plus: [ech.query_server_name](#query_server_name)
!!! quote "sing-box 1.12.0 中的更改"
@@ -101,9 +104,14 @@ icon: material/new-box
"min_version": "",
"max_version": "",
"cipher_suites": [],
"curve_preferences": [],
"certificate": "",
"certificate_path": "",
"certificate_public_key_sha256": [],
"client_certificate": [],
"client_certificate_path": "",
"client_key": [],
"client_key_path": "",
"fragment": false,
"fragment_fallback_delay": "",
"record_fragment": false,
@@ -111,6 +119,7 @@ icon: material/new-box
"enabled": false,
"config": [],
"config_path": "",
"query_server_name": "",
// 废弃的
"pq_signature_schemes_enabled": false,
@@ -253,6 +262,38 @@ openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform d
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
```
#### client_certificate
!!! question "自 sing-box 1.13.0 起"
==仅客户端==
客户端证书链行数组PEM 格式。
#### client_certificate_path
!!! question "自 sing-box 1.13.0 起"
==仅客户端==
客户端证书链路径PEM 格式。
#### client_key
!!! question "自 sing-box 1.13.0 起"
==仅客户端==
客户端私钥行数组PEM 格式。
#### client_key_path
!!! question "自 sing-box 1.13.0 起"
==仅客户端==
客户端私钥路径PEM 格式。
#### key
==仅服务器==
@@ -464,6 +505,16 @@ ECH 配置路径PEM 格式。
如果为空,将尝试从 DNS 加载。
#### query_server_name
!!! question "自 sing-box 1.13.0 起"
==仅客户端==
覆盖用于 ECH HTTPS 记录查询的域名。
如果为空,使用 `server_name` 查询。
#### fragment
!!! question "自 sing-box 1.12.0 起"
@@ -569,7 +620,7 @@ MAC 密钥。
ACME DNS01 验证字段。如果配置,将禁用其他验证方法。
参阅 [DNS01 验证字段](/configuration/shared/dns01_challenge/)。
参阅 [DNS01 验证字段](/zh/configuration/shared/dns01_challenge/)。
### Reality 字段

View File

@@ -0,0 +1,41 @@
---
icon: material/new-box
---
# Wi-Fi State
!!! quote "Changes in sing-box 1.13.0"
:material-plus: Linux support
:material-plus: Windows support
sing-box can monitor Wi-Fi state to enable routing rules based on `wifi_ssid` and `wifi_bssid`.
### Platform Support
| Platform | Support | Notes |
|-----------------|------------------|--------------------------|
| Android | :material-check: | In graphical client |
| Apple platforms | :material-check: | In graphical clients |
| Linux | :material-check: | Requires supported daemon |
| Windows | :material-check: | WLAN API |
| Others | :material-close: | |
### Linux
!!! question "Since sing-box 1.13.0"
The following backends are supported and will be auto-detected in order of priority:
| Backend | Interface |
|------------------|-------------|
| NetworkManager | D-Bus |
| IWD | D-Bus |
| wpa_supplicant | Unix socket |
| ConnMan | D-Bus |
### Windows
!!! question "Since sing-box 1.13.0"
Uses Windows WLAN API.

View File

@@ -0,0 +1,41 @@
---
icon: material/new-box
---
# Wi-Fi 状态
!!! quote "sing-box 1.13.0 的变更"
:material-plus: Linux 支持
:material-plus: Windows 支持
sing-box 可以监控 Wi-Fi 状态,以启用基于 `wifi_ssid``wifi_bssid` 的路由规则。
### 平台支持
| 平台 | 支持 | 备注 |
|-----------------|------------------|----------------|
| Android | :material-check: | 仅图形客户端 |
| Apple 平台 | :material-check: | 仅图形客户端 |
| Linux | :material-check: | 需要支持的守护进程 |
| Windows | :material-check: | WLAN API |
| 其他 | :material-close: | |
### Linux
!!! question "自 sing-box 1.13.0 起"
支持以下后端,将按优先级顺序自动探测:
| 后端 | 接口 |
|------------------|-------------|
| NetworkManager | D-Bus |
| IWD | D-Bus |
| wpa_supplicant | Unix socket |
| ConnMan | D-Bus |
### Windows
!!! question "自 sing-box 1.13.0 起"
使用 Windows WLAN API。

View File

@@ -95,7 +95,7 @@ GeoIP 已废弃且将在 sing-box 1.12.0 中被移除。
maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过,
且现有的实现均存在内存使用大与管理困难的问题。
sing-box 1.8.0 引入了[规则集](/configuration/rule-set/)
sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/)
可以完全替代 GeoIP 参阅 [迁移指南](/zh/migration/#geoip)。
#### Geosite
@@ -105,7 +105,7 @@ Geosite 已废弃且将在 sing-box 1.12.0 中被移除。
Geosite即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案,
存在着包括缺少维护、规则不准确和管理困难内的大量问题。
sing-box 1.8.0 引入了[规则集](/configuration/rule-set/)
sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/)
可以完全替代 Geosite参阅 [迁移指南](/zh/migration/#geosite)。
## 1.6.0

View File

@@ -57,6 +57,45 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale) |
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale) |
| `with_naive_outbound` | :material-close: | Build with NaiveProxy outbound support, see [NaiveProxy outbound](/configuration/outbound/naive/). |
It is not recommended to change the default build tag list unless you really know what you are adding.
## :material-layers: with_naive_outbound
NaiveProxy outbound requires special build configurations depending on your target platform.
### Supported Platforms
| Platform | Architectures | Mode | Requirements |
|-----------------|------------------------|--------|---------------------------------------------------|
| Linux | amd64, arm64 | purego | None (library included in official releases) |
| Linux | 386, amd64, arm, arm64 | CGO | Chromium toolchain, glibc >= 2.31 at runtime |
| Linux (musl) | 386, amd64, arm, arm64 | CGO | Chromium toolchain |
| Windows | amd64, arm64 | purego | None (library included in official releases) |
| Apple platforms | * | CGO | Xcode |
| Android | * | CGO | Android NDK |
### Windows
Use `with_purego` tag.
For official releases, `libcronet.dll` is included in the archive. For self-built binaries, download from [cronet-go releases](https://github.com/sagernet/cronet-go/releases) and place in the same directory as `sing-box.exe` or in a directory listed in `PATH`.
### Linux (purego, amd64/arm64 only)
Use `with_purego` tag.
For official releases, `libcronet.so` is included in the archive. For self-built binaries, download from [cronet-go releases](https://github.com/sagernet/cronet-go/releases) and place in the same directory as sing-box binary or in system library path.
### Linux (CGO)
See [cronet-go](https://github.com/sagernet/cronet-go#linux-build-instructions).
- **glibc build**: Requires glibc >= 2.31 at runtime
- **musl build**: Use `with_musl` tag, statically linked, no runtime requirements
### Apple platforms / Android
See [cronet-go](https://github.com/sagernet/cronet-go).

View File

@@ -62,5 +62,44 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale) |
| `with_naive_outbound` | :material-close: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/zh/configuration/outbound/naive/)。 |
除非您确实知道您正在启用什么,否则不建议更改默认构建标签列表。
## :material-layers: with_naive_outbound
NaiveProxy 出站需要根据目标平台进行特殊的构建配置。
### 支持的平台
| 平台 | 架构 | 模式 | 要求 |
|---------------|------------------------|--------|--------------------------------|
| Linux | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
| Linux | 386, amd64, arm, arm64 | CGO | Chromium 工具链,运行时需要 glibc >= 2.31 |
| Linux (musl) | 386, amd64, arm, arm64 | CGO | Chromium 工具链 |
| Windows | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
| Apple 平台 | * | CGO | Xcode |
| Android | * | CGO | Android NDK |
### Windows
使用 `with_purego` 标记。
官方发布版本已包含 `libcronet.dll`。自行构建时,从 [cronet-go releases](https://github.com/sagernet/cronet-go/releases) 下载并放置在 `sing-box.exe` 相同目录或 `PATH` 中的任意目录。
### Linux (purego, 仅 amd64/arm64)
使用 `with_purego` 标记。
官方发布版本已包含 `libcronet.so`。自行构建时,从 [cronet-go releases](https://github.com/sagernet/cronet-go/releases) 下载并放置在 sing-box 二进制文件相同目录或系统库路径中。
### Linux (CGO)
参阅 [cronet-go](https://github.com/sagernet/cronet-go#linux-build-instructions)。
- **glibc 构建**:运行时需要 glibc >= 2.31
- **musl 构建**:使用 `with_musl` 标记,静态链接,无运行时要求
### Apple 平台 / Android
参阅 [cronet-go](https://github.com/sagernet/cronet-go)。

View File

@@ -10,8 +10,8 @@ DNS 服务器已经重构。
!!! info "引用"
[DNS 服务器](/configuration/dns/server/) /
[旧 DNS 服务器](/configuration/dns/server/legacy/)
[DNS 服务器](/zh/configuration/dns/server/) /
[旧 DNS 服务器](/zh/configuration/dns/server/legacy/)
=== "Local"

View File

@@ -11,16 +11,22 @@ the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohas
![](https://nekohasekai.github.io/sponsor-images/sponsors.svg)
### Special Sponsors
## Commercial Sponsors
**Viral Tech, Inc.**
> [Warp](https://go.warp.dev/sing-box), Built for coding with multiple AI agents.
[![](https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png)](https://go.warp.dev/sing-box)
## Special Sponsors
> Viral Tech, Inc.
Helping us re-list sing-box apps on the Apple Store.
---
[![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://www.jetbrains.com)
> [JetBrains](https://www.jetbrains.com)
Free license for the amazing IDEs.
---
[![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://www.jetbrains.com)

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