mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-21 08:44:24 +00:00
Compare commits
134
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9442d14b1 | ||
|
|
7dd51eb244 | ||
|
|
f269dee097 | ||
|
|
60094a12b4 | ||
|
|
dee34e0300 | ||
|
|
b2bb7bf347 | ||
|
|
5be93998c6 | ||
|
|
364c6a6e6a | ||
|
|
cb165a381c | ||
|
|
6b5ee55ba2 | ||
|
|
2283c95513 | ||
|
|
7054cac512 | ||
|
|
78d44d52d9 | ||
|
|
1a773a425a | ||
|
|
194ebd18b6 | ||
|
|
80c1171410 | ||
|
|
67fb235ee8 | ||
|
|
02537831e1 | ||
|
|
f9fcb8964f | ||
|
|
a691a44023 | ||
|
|
a364ff4794 | ||
|
|
bff5acf292 | ||
|
|
52f9bad034 | ||
|
|
a0d775ea66 | ||
|
|
79fc394d67 | ||
|
|
f95aa6ac11 | ||
|
|
6e3c86f518 | ||
|
|
7a3d4e4a8e | ||
|
|
351ce12f73 | ||
|
|
85c4a530b7 | ||
|
|
cdea9e87f1 | ||
|
|
21ba30b8c3 | ||
|
|
07130365d1 | ||
|
|
eb7deeae8a | ||
|
|
fd9a076d25 | ||
|
|
d2cf9747d7 | ||
|
|
d41e1745e6 | ||
|
|
0efa644942 | ||
|
|
021e0877a3 | ||
|
|
ddbf34639a | ||
|
|
5956780ba3 | ||
|
|
d75ac4ea21 | ||
|
|
4537a1ac00 | ||
|
|
af6e64c3b6 | ||
|
|
e6a1a3073e | ||
|
|
83aec4222f | ||
|
|
17c52d03b3 | ||
|
|
111ecb3483 | ||
|
|
6b6c20a248 | ||
|
|
4abede8ceb | ||
|
|
0d91c45479 | ||
|
|
ca7216a381 | ||
|
|
19f57607a9 | ||
|
|
886422d2e0 | ||
|
|
1ea3bb4563 | ||
|
|
78afbb2e62 | ||
|
|
6cfc2a86f8 | ||
|
|
55faa763f9 | ||
|
|
466eb0245a | ||
|
|
aae5f7ff02 | ||
|
|
311d9b51eb | ||
|
|
197cf99b73 | ||
|
|
aab74fea51 | ||
|
|
f5e8cdae19 | ||
|
|
60f0a0369b | ||
|
|
6a7f11fca3 | ||
|
|
afb0b30558 | ||
|
|
bea01fe80a | ||
|
|
fbb74b13e6 | ||
|
|
7b1d7e1c85 | ||
|
|
3f0958bf58 | ||
|
|
18b0317018 | ||
|
|
9be5fcf4e1 | ||
|
|
9cf7a7a28a | ||
|
|
a1f948c330 | ||
|
|
cd568a563b | ||
|
|
96b004bab1 | ||
|
|
5ae597bfba | ||
|
|
c0b8fe8b57 | ||
|
|
f5f4995c32 | ||
|
|
d7cc756eba | ||
|
|
9fa1890aea | ||
|
|
394285b78b | ||
|
|
491813c360 | ||
|
|
474aa5b5d9 | ||
|
|
6439d67043 | ||
|
|
e63f3b6be2 | ||
|
|
2c379103f3 | ||
|
|
1ac1a339cb | ||
|
|
dc3b490149 | ||
|
|
6c87a0c044 | ||
|
|
b5d5338e52 | ||
|
|
73a4196cb3 | ||
|
|
9713a546e6 | ||
|
|
54e5c497bb | ||
|
|
c827ec8303 | ||
|
|
b7eb49bb87 | ||
|
|
378f151c75 | ||
|
|
8d0500eded | ||
|
|
70d0ba7279 | ||
|
|
6d1fc214c1 | ||
|
|
046c46eacb | ||
|
|
89ca622e99 | ||
|
|
f909997082 | ||
|
|
6364d9a5f1 | ||
|
|
d6ec014ca4 | ||
|
|
b2a5ac3f60 | ||
|
|
515a73e4ee | ||
|
|
0c041aa7f6 | ||
|
|
0d7fae13bc | ||
|
|
0265f31f84 | ||
|
|
4ef3432c0d | ||
|
|
288411b0b9 | ||
|
|
6a6a1e0a31 | ||
|
|
217f77643f | ||
|
|
7afda58818 | ||
|
|
a25ad8ce52 | ||
|
|
64605fc47a | ||
|
|
08703c9035 | ||
|
|
94ae161956 | ||
|
|
5c7467aaba | ||
|
|
e4a19a2a7c | ||
|
|
4bc15be97c | ||
|
|
ad8d053985 | ||
|
|
21b8fcacc6 | ||
|
|
0944cd9d7b | ||
|
|
bdd86c1fef | ||
|
|
a556d49491 | ||
|
|
6c4589c933 | ||
|
|
b807ff30d0 | ||
|
|
3e185071eb | ||
|
|
650ef881c8 | ||
|
|
f0a2c348f6 | ||
|
|
df34f5068b |
@@ -1 +1 @@
|
||||
45832ab074849607406baa3e3a2c4660274602ed
|
||||
0d28acc44093df24b2526dea3d6ffefd6b0a54f0
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
--- a/src/internal/cpu/cpu_arm64_ios.go
|
||||
+++ b/src/internal/cpu/cpu_arm64_ios.go
|
||||
@@ -0,0 +1,14 @@
|
||||
+// Copyright 2020 The Go Authors. All rights reserved.
|
||||
+// Use of this source code is governed by a BSD-style
|
||||
+// license that can be found in the LICENSE file.
|
||||
+
|
||||
+//go:build arm64 && ios
|
||||
+
|
||||
+package cpu
|
||||
+
|
||||
+func osInit() {
|
||||
+ ARM64.HasAES = true
|
||||
+ ARM64.HasPMULL = true
|
||||
+ ARM64.HasSHA1 = true
|
||||
+ ARM64.HasSHA2 = true
|
||||
+}
|
||||
--- a/src/internal/cpu/cpu_arm64_other.go
|
||||
+++ b/src/internal/cpu/cpu_arm64_other.go
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
-//go:build arm64 && !linux && !freebsd && !android && (!darwin || ios) && !openbsd
|
||||
+//go:build arm64 && !linux && !freebsd && !android && !darwin && !openbsd
|
||||
|
||||
package cpu
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Go's internal/cpu never detects ARM64 features on GOOS=ios, so crypto/aes,
|
||||
# AES-GCM and crypto/sha256 use their generic implementations on iOS and tvOS.
|
||||
# See https://github.com/SagerNet/sing-box/issues/4486
|
||||
#
|
||||
# Only the ARMv8.0 cryptographic extensions are asserted; ARMv8.1 atomics and
|
||||
# SHA-512 are absent on the A8/A9 devices still supported by the deployment targets.
|
||||
#
|
||||
# Remove once the Go release used by the Apple library build includes the fix.
|
||||
|
||||
export GOTOOLCHAIN=local
|
||||
|
||||
GOROOT="$(go env GOROOT)"
|
||||
PATCH_FILE="$(cd "$(dirname "$0")" && pwd)/go_ios_cpu_features.patch"
|
||||
|
||||
cd "$GOROOT"
|
||||
if [[ -f src/internal/cpu/cpu_arm64_ios.go ]]; then
|
||||
echo "already patched"
|
||||
else
|
||||
patch --verbose -p1 < "$PATCH_FILE"
|
||||
fi
|
||||
|
||||
CPU_FILES="$(GOOS=ios GOARCH=arm64 CGO_ENABLED=0 go list -f '{{.GoFiles}}' internal/cpu)"
|
||||
echo "internal/cpu files for ios/arm64: $CPU_FILES"
|
||||
case "$CPU_FILES" in
|
||||
*cpu_arm64_ios.go*) ;;
|
||||
*)
|
||||
echo "patch is not effective" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.26.7"
|
||||
VERSION="1.26.8"
|
||||
PATCH_COMMITS=(
|
||||
"f080b0c6346eb690c0dc82497b35925f385b35ac"
|
||||
"2d9c12887c342fb9051d231aa5388743cb7e9cb6"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.26.7"
|
||||
VERSION="1.26.8"
|
||||
PATCH_COMMITS=(
|
||||
"a4ae550aa148b04c9d4890e98bee63aede5c4b53"
|
||||
"95b851f661584711faa8115b3234a461044f4510"
|
||||
|
||||
+29
-23
@@ -54,7 +54,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
if: ${{ ! matrix.legacy_win7 }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/go/go_win7
|
||||
key: go_win7_1267
|
||||
key: go_win7_1268
|
||||
- name: Setup Go for Windows 7
|
||||
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
||||
env:
|
||||
@@ -488,7 +488,7 @@ jobs:
|
||||
if: ${{ ! matrix.legacy_osx }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -503,7 +503,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/go/go_osx
|
||||
key: go_osx_1267
|
||||
key: go_osx_1268
|
||||
- name: Setup Go for macOS 10.13
|
||||
if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true'
|
||||
env:
|
||||
@@ -525,7 +525,7 @@ jobs:
|
||||
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
|
||||
else
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
TAGS=$(sed 's/,with_usbip//' release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Set shared ldflags
|
||||
@@ -587,7 +587,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
@@ -690,7 +690,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
@@ -754,7 +754,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -862,7 +862,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -975,7 +975,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
@@ -1087,7 +1087,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
@@ -1214,7 +1214,7 @@ jobs:
|
||||
path: 'dist'
|
||||
build_apple_library:
|
||||
name: Build Apple library (${{ matrix.artifact }})
|
||||
runs-on: ${{ matrix.build && 'macos-26' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ matrix.build && 'xcode-27' || 'ubuntu-latest' }}
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone' || inputs.build == 'iOS-jailbreak'
|
||||
needs:
|
||||
- calculate_version
|
||||
@@ -1244,8 +1244,11 @@ jobs:
|
||||
if: matrix.build
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Patch Go for iOS
|
||||
if: matrix.build && (matrix.platform == 'ios/arm64' || matrix.platform == 'tvos/arm64')
|
||||
run: .github/patch_go_for_ios.sh
|
||||
- name: Cache Go modules
|
||||
if: matrix.build
|
||||
uses: actions/cache@v4
|
||||
@@ -1258,13 +1261,15 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/Library/Caches/go-build
|
||||
key: libbox-go-build-${{ matrix.artifact }}-1.26.7-${{ hashFiles('go.sum') }}
|
||||
restore-keys: libbox-go-build-${{ matrix.artifact }}-1.26.7-
|
||||
key: libbox-go-build-${{ matrix.artifact }}-1.26.8-${{ hashFiles('go.sum') }}
|
||||
restore-keys: libbox-go-build-${{ matrix.artifact }}-1.26.8-
|
||||
- name: Set tag
|
||||
if: matrix.build
|
||||
run: git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Build library
|
||||
if: matrix.build
|
||||
env:
|
||||
GOTOOLCHAIN: local
|
||||
run: |-
|
||||
make lib_install
|
||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
@@ -1280,7 +1285,7 @@ jobs:
|
||||
compression-level: 1
|
||||
build_apple:
|
||||
name: Build Apple release clients
|
||||
runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ matrix.if && 'xcode-27' || 'ubuntu-latest' }}
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone' || inputs.build == 'iOS-jailbreak'
|
||||
needs:
|
||||
- calculate_version
|
||||
@@ -1326,7 +1331,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
if: matrix.if
|
||||
@@ -1370,10 +1375,10 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ matrix.type == 'standalone' && 'clients/apple/build/SFM.System-*.dd' || matrix.type == 'jailbreak' && 'clients/apple/build/jailbreak/DerivedData' || format('{0}/DerivedData', runner.temp) }}
|
||||
key: apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ github.sha }}
|
||||
key: apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ github.ref_name }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-
|
||||
apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-
|
||||
apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ github.ref_name }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-
|
||||
apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ github.ref_name }}-
|
||||
- name: Setup App Store Connect key
|
||||
if: matrix.if && matrix.type != 'jailbreak' && github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -1554,7 +1559,7 @@ jobs:
|
||||
--api-key $ASC_KEY_ID \
|
||||
--api-issuer 00000000-0000-0000-0000-000000000000
|
||||
- name: Publish to TestFlight
|
||||
if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/testing'
|
||||
if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
arguments=(publish_testflight "${{ matrix.platform }}")
|
||||
if [[ -n "$TEST_MESSAGE" ]]; then
|
||||
@@ -1591,6 +1596,7 @@ jobs:
|
||||
make -C clients/apple build_ios_deb
|
||||
mkdir -p dist
|
||||
cp "clients/apple/build/jailbreak/SFI-${VERSION}-iphoneos-arm64.deb" dist
|
||||
cp "clients/apple/build/jailbreak/SFI-${VERSION}-iphoneos-arm64e.deb" dist
|
||||
env:
|
||||
XCODEBUILD_CLONED_SOURCE_PACKAGES_DIR_PATH: ${{ runner.temp }}/SourcePackages
|
||||
- name: Report cache directory sizes
|
||||
@@ -1611,7 +1617,7 @@ jobs:
|
||||
path: 'dist'
|
||||
build_apple_app_store:
|
||||
name: Build Apple App Store clients
|
||||
runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ matrix.if && 'xcode-27' || 'ubuntu-latest' }}
|
||||
if: github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS')
|
||||
needs:
|
||||
- calculate_version
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
|
||||
@@ -18,11 +18,17 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
@@ -56,11 +62,17 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
@@ -115,11 +127,17 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
@@ -148,11 +166,17 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
@@ -181,11 +205,17 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
@@ -214,11 +244,17 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
@@ -247,11 +283,17 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
@@ -296,11 +338,17 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
@@ -353,11 +401,17 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
@@ -423,11 +477,15 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_trusttunnel
|
||||
|
||||
@@ -174,6 +174,7 @@ build_ios_deb:
|
||||
|
||||
upload_ios_deb:
|
||||
ghr --replace --draft --prerelease "v${VERSION}" ../sing-box-for-apple/build/jailbreak/"SFI-${VERSION}-iphoneos-arm64.deb"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" ../sing-box-for-apple/build/jailbreak/"SFI-${VERSION}-iphoneos-arm64e.deb"
|
||||
|
||||
release_ios: build_ios upload_ios_app_store
|
||||
|
||||
@@ -273,7 +274,7 @@ update_apple_version:
|
||||
go run ./cmd/internal/update_apple_version
|
||||
|
||||
update_macos_version:
|
||||
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
|
||||
MACOS_PROJECT_VERSION=$(shell go run ./cmd/internal/app_store_connect next_project_version macos) go run ./cmd/internal/update_apple_version
|
||||
|
||||
release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
||||
|
||||
@@ -282,11 +283,13 @@ release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||
publish_testflight:
|
||||
go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
prepare_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect prepare_app_store
|
||||
submit_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect submit_app_store $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
release_app_store: release_ios release_tvos submit_app_store
|
||||
|
||||
publish_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect publish_app_store
|
||||
go run -v ./cmd/internal/app_store_connect publish_app_store $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
test:
|
||||
@go test -v ./... && \
|
||||
|
||||
@@ -2,86 +2,43 @@ package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
var _ adapter.CertificateProviderManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.CertificateProviderRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
providers []adapter.CertificateProviderService
|
||||
providerByTag map[string]adapter.CertificateProviderService
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager {
|
||||
func NewManager(registry adapter.CertificateProviderRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
providerByTag: make(map[string]adapter.CertificateProviderService),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
providers := m.providers
|
||||
m.access.Unlock()
|
||||
for _, provider := range providers {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
m.logger.Trace(stage, " ", name)
|
||||
startTime := time.Now()
|
||||
err := adapter.LegacyStart(provider, stage)
|
||||
err := scope.Start(name, provider, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
return err
|
||||
}
|
||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
providers := m.providers
|
||||
m.providers = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, provider := range providers {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
m.logger.Trace("close ", name)
|
||||
startTime := time.Now()
|
||||
monitor.Start("close ", name)
|
||||
err = E.Append(err, provider.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", name)
|
||||
})
|
||||
monitor.Finish()
|
||||
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) CertificateProviders() []adapter.CertificateProviderService {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
@@ -95,29 +52,6 @@ func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
|
||||
return provider, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.providerByTag, tag)
|
||||
index := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
|
||||
return it == provider
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid certificate provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:index], m.providers[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return provider.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error {
|
||||
provider, err := m.registry.Create(ctx, logger, tag, providerType, options)
|
||||
if err != nil {
|
||||
@@ -125,32 +59,9 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
m.logger.Trace(stage, " ", name)
|
||||
startTime := time.Now()
|
||||
err = adapter.LegacyStart(provider, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
}
|
||||
if existsProvider, loaded := m.providerByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsProvider.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
|
||||
return it == existsProvider
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid certificate provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
|
||||
_, loaded := m.providerByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate certificate provider tag: ", tag)
|
||||
}
|
||||
m.providers = append(m.providers, provider)
|
||||
m.providerByTag[tag] = provider
|
||||
|
||||
@@ -33,6 +33,5 @@ type CertificateProviderManager interface {
|
||||
Lifecycle
|
||||
CertificateProviders() []CertificateProviderService
|
||||
Get(tag string) (CertificateProviderService, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -45,26 +43,6 @@ type DNSQueryOptions struct {
|
||||
RemoveClientSubnet bool
|
||||
}
|
||||
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (DNSQueryOptions, error) {
|
||||
if options == nil || options.Server == "" {
|
||||
return DNSQueryOptions{}, nil
|
||||
}
|
||||
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||
transport, loaded := transportManager.Transport(options.Server)
|
||||
if !loaded {
|
||||
return DNSQueryOptions{}, E.New("domain resolver not found: " + options.Server)
|
||||
}
|
||||
return DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
DisableOptimisticCache: options.DisableOptimisticCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
Timeout: time.Duration(options.Timeout),
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RDRCStore interface {
|
||||
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
|
||||
SaveRDRC(transportName string, qName string, qType uint16) error
|
||||
@@ -111,6 +89,5 @@ type DNSTransportManager interface {
|
||||
Transport(tag string) (DNSTransport, bool)
|
||||
Default() DNSTransport
|
||||
FakeIP() FakeIPTransport
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,8 +21,8 @@ type EndpointRegistry interface {
|
||||
|
||||
type EndpointManager interface {
|
||||
Lifecycle
|
||||
StartEndpoint(endpoint Endpoint) error
|
||||
Endpoints() []Endpoint
|
||||
Get(tag string) (Endpoint, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error
|
||||
}
|
||||
|
||||
+12
-89
@@ -2,46 +2,36 @@ package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.EndpointManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.EndpointRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
scope *adapter.Scope
|
||||
endpoints []adapter.Endpoint
|
||||
endpointByTag map[string]adapter.Endpoint
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager {
|
||||
func NewManager(registry adapter.EndpointRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpointByTag: make(map[string]adapter.Endpoint),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
if stage == adapter.StartStateInitialize {
|
||||
m.scope = scope
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
if stage == adapter.StartStateStart {
|
||||
// started with outbound manager
|
||||
m.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -49,38 +39,16 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Unlock()
|
||||
for _, endpoint := range endpoints {
|
||||
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err := adapter.LegacyStart(endpoint, stage)
|
||||
done()
|
||||
err := scope.Start(name, endpoint, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
endpoints := m.endpoints
|
||||
m.endpoints = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, endpoint := range endpoints {
|
||||
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
||||
done := adapter.LogElapsed(m.logger, "close ", name)
|
||||
monitor.Start("close ", name)
|
||||
err = E.Append(err, endpoint.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", name)
|
||||
})
|
||||
monitor.Finish()
|
||||
done()
|
||||
}
|
||||
return nil
|
||||
func (m *Manager) StartEndpoint(endpoint adapter.Endpoint) error {
|
||||
return m.scope.Start("endpoint/"+endpoint.Type()+"["+endpoint.Tag()+"]", endpoint, adapter.StartStateStart)
|
||||
}
|
||||
|
||||
func (m *Manager) Endpoints() []adapter.Endpoint {
|
||||
@@ -96,29 +64,6 @@ func (m *Manager) Get(tag string) (adapter.Endpoint, bool) {
|
||||
return endpoint, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
endpoint, found := m.endpointByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.endpointByTag, tag)
|
||||
index := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
|
||||
return it == endpoint
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid endpoint index")
|
||||
}
|
||||
m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return endpoint.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||
endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
|
||||
if err != nil {
|
||||
@@ -126,31 +71,9 @@ 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 {
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err = adapter.LegacyStart(endpoint, stage)
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsEndpoint.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
|
||||
return it == existsEndpoint
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid endpoint index")
|
||||
}
|
||||
m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...)
|
||||
_, loaded := m.endpointByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate endpoint tag: ", tag)
|
||||
}
|
||||
m.endpoints = append(m.endpoints, endpoint)
|
||||
m.endpointByTag[tag] = endpoint
|
||||
|
||||
+1
-10
@@ -8,18 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
)
|
||||
|
||||
type ClashServer interface {
|
||||
LifecycleService
|
||||
Mode() string
|
||||
ModeList() []string
|
||||
SetMode(mode string)
|
||||
AddModeUpdateHook(hook *observable.Subscriber[struct{}])
|
||||
}
|
||||
|
||||
type URLTestHistory struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
@@ -43,7 +34,7 @@ type CacheFile interface {
|
||||
|
||||
StoreWARPConfig() bool
|
||||
StoreMASQUEConfig() bool
|
||||
StoreProviders() bool
|
||||
StoreSubscriptions() bool
|
||||
|
||||
StoreDNS() bool
|
||||
DNSCacheStore
|
||||
|
||||
+10
-1
@@ -41,7 +41,6 @@ type InboundManager interface {
|
||||
Lifecycle
|
||||
Inbounds() []Inbound
|
||||
Get(tag string) (Inbound, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
|
||||
}
|
||||
|
||||
@@ -191,6 +190,16 @@ func DNSTransportTagFromContext(ctx context.Context) (string, bool) {
|
||||
return transportTag, loaded
|
||||
}
|
||||
|
||||
func ContextForMultiplexSession(ctx context.Context) context.Context {
|
||||
var sessionContext InboundContext
|
||||
metadata := ContextFrom(ctx)
|
||||
if metadata != nil {
|
||||
sessionContext.Outbound = metadata.Outbound
|
||||
}
|
||||
ctx = ContextWithDNSTransportTag(ctx, "")
|
||||
return WithContext(ctx, &sessionContext)
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
||||
return context.WithValue(ctx, (*inboundContextKey)(nil), inboundContext)
|
||||
}
|
||||
|
||||
@@ -2,84 +2,45 @@ package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.InboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.InboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
inbounds []adapter.Inbound
|
||||
inboundByTag map[string]adapter.Inbound
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
|
||||
func NewManager(registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
inboundByTag: make(map[string]adapter.Inbound),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
inbounds := m.inbounds
|
||||
m.access.Unlock()
|
||||
for _, inbound := range inbounds {
|
||||
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err := adapter.LegacyStart(inbound, stage)
|
||||
done()
|
||||
err := scope.Start(name, inbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
inbounds := m.inbounds
|
||||
m.inbounds = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, inbound := range inbounds {
|
||||
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
|
||||
done := adapter.LogElapsed(m.logger, "close ", name)
|
||||
monitor.Start("close ", name)
|
||||
err = E.Append(err, inbound.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", name)
|
||||
})
|
||||
monitor.Finish()
|
||||
done()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Inbounds() []adapter.Inbound {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
@@ -96,29 +57,6 @@ func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
|
||||
return m.endpoint.Get(tag)
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
inbound, found := m.inboundByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.inboundByTag, tag)
|
||||
index := common.Index(m.inbounds, func(it adapter.Inbound) bool {
|
||||
return it == inbound
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return inbound.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||
inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
|
||||
if err != nil {
|
||||
@@ -126,31 +64,9 @@ 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 {
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err = adapter.LegacyStart(inbound, stage)
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsInbound.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool {
|
||||
return it == existsInbound
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...)
|
||||
_, loaded := m.inboundByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate inbound tag: ", tag)
|
||||
}
|
||||
m.inbounds = append(m.inbounds, inbound)
|
||||
m.inboundByTag[tag] = inbound
|
||||
|
||||
+98
-46
@@ -2,13 +2,15 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
type SimpleLifecycle interface {
|
||||
@@ -48,8 +50,7 @@ func (s StartStage) String() string {
|
||||
}
|
||||
|
||||
type Lifecycle interface {
|
||||
Start(stage StartStage) error
|
||||
Close() error
|
||||
Start(stage StartStage, scope *Scope) error
|
||||
}
|
||||
|
||||
type LifecycleService interface {
|
||||
@@ -57,55 +58,106 @@ type LifecycleService interface {
|
||||
Lifecycle
|
||||
}
|
||||
|
||||
func getServiceName(service any) string {
|
||||
if named, ok := service.(interface {
|
||||
Type() string
|
||||
Tag() string
|
||||
}); ok {
|
||||
tag := named.Tag()
|
||||
if tag != "" {
|
||||
return named.Type() + "[" + tag + "]"
|
||||
}
|
||||
return named.Type()
|
||||
}
|
||||
t := reflect.TypeOf(service)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
return strings.ToLower(t.Name())
|
||||
type Scope struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger log.ContextLogger
|
||||
access sync.Mutex
|
||||
cleanups list.List[func() error]
|
||||
children map[Lifecycle]*scopeChild
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
|
||||
for _, service := range services {
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := getServiceName(service)
|
||||
done := LogElapsed(logger, stage, " ", name)
|
||||
err = service.Start(stage)
|
||||
done()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type scopeChild struct {
|
||||
scope *Scope
|
||||
cleanup *list.Element[func() error]
|
||||
}
|
||||
|
||||
func NewScope(ctx context.Context, logger log.ContextLogger) *Scope {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &Scope{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
children: make(map[Lifecycle]*scopeChild),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scope) Context() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
||||
func (s *Scope) Add(cleanup func() error) {
|
||||
s.access.Lock()
|
||||
s.cleanups.PushBack(cleanup)
|
||||
s.access.Unlock()
|
||||
}
|
||||
|
||||
func (s *Scope) Start(name string, component Lifecycle, stage StartStage) error {
|
||||
s.access.Lock()
|
||||
err := s.ctx.Err()
|
||||
if err != nil {
|
||||
s.access.Unlock()
|
||||
return err
|
||||
}
|
||||
child, loaded := s.children[component]
|
||||
if !loaded {
|
||||
childScope := NewScope(s.ctx, s.logger)
|
||||
child = &scopeChild{scope: childScope}
|
||||
child.cleanup = s.cleanups.PushBack(func() error {
|
||||
done := LogElapsed(s.logger, "close ", name)
|
||||
monitor := taskmonitor.New(s.logger, C.StopTimeout)
|
||||
monitor.Start("close ", name)
|
||||
closeErr := childScope.Close()
|
||||
monitor.Finish()
|
||||
done()
|
||||
if closeErr != nil {
|
||||
return E.Cause(closeErr, "close ", name)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
s.children[component] = child
|
||||
}
|
||||
s.access.Unlock()
|
||||
done := LogElapsed(s.logger, stage, " ", name)
|
||||
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||
monitor.Start(stage, " ", name)
|
||||
err = component.Start(stage, child.scope)
|
||||
monitor.Finish()
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartNamed(ctx context.Context, logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
|
||||
for _, service := range services {
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := LogElapsed(logger, stage, " ", service.Name())
|
||||
err = service.Start(stage)
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, stage.String(), " ", service.Name())
|
||||
}
|
||||
func (s *Scope) Remove(component Lifecycle) error {
|
||||
s.access.Lock()
|
||||
child, loaded := s.children[component]
|
||||
if !loaded {
|
||||
s.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
delete(s.children, component)
|
||||
cleanup := s.cleanups.Remove(child.cleanup)
|
||||
s.access.Unlock()
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
func (s *Scope) Close() error {
|
||||
s.access.Lock()
|
||||
s.cancel()
|
||||
var cleanups []func() error
|
||||
for element := s.cleanups.Back(); element != nil; element = element.Prev() {
|
||||
cleanups = append(cleanups, element.Value)
|
||||
}
|
||||
s.cleanups.Init()
|
||||
s.children = nil
|
||||
s.access.Unlock()
|
||||
var err error
|
||||
for _, cleanup := range cleanups {
|
||||
err = E.Errors(err, cleanup())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func LogElapsed(logger log.ContextLogger, description ...any) func() {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package adapter
|
||||
|
||||
func LegacyStart(starter any, stage StartStage) error {
|
||||
if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle {
|
||||
return lifecycle.Start(stage)
|
||||
}
|
||||
switch stage {
|
||||
case StartStateInitialize:
|
||||
if preStarter, isPreStarter := starter.(interface {
|
||||
PreStart() error
|
||||
}); isPreStarter {
|
||||
return preStarter.PreStart()
|
||||
}
|
||||
case StartStateStart:
|
||||
if starter, isStarter := starter.(interface {
|
||||
Start() error
|
||||
}); isStarter {
|
||||
return starter.Start()
|
||||
}
|
||||
case StartStateStarted:
|
||||
if postStarter, isPostStarter := starter.(interface {
|
||||
PostStart() error
|
||||
}); isPostStarter {
|
||||
return postStarter.PostStart()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type lifecycleServiceWrapper struct {
|
||||
SimpleLifecycle
|
||||
name string
|
||||
}
|
||||
|
||||
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
|
||||
return &lifecycleServiceWrapper{
|
||||
SimpleLifecycle: service,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Name() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
|
||||
return LegacyStart(l.SimpleLifecycle, stage)
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Close() error {
|
||||
return l.SimpleLifecycle.Close()
|
||||
}
|
||||
@@ -51,3 +51,12 @@ type OutboundManager interface {
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||
}
|
||||
|
||||
type IdleConnectionKeeper interface {
|
||||
SetKeepIdleConnections(keep bool)
|
||||
CloseIdleConnections()
|
||||
}
|
||||
|
||||
type Referrer interface {
|
||||
References() []string
|
||||
}
|
||||
|
||||
+103
-133
@@ -2,29 +2,25 @@ package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var _ adapter.OutboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.OutboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
defaultTag string
|
||||
access sync.RWMutex
|
||||
started bool
|
||||
startAccess sync.Mutex
|
||||
scope *adapter.Scope
|
||||
stage adapter.StartStage
|
||||
outbounds []adapter.Outbound
|
||||
outboundByTag map[string]adapter.Outbound
|
||||
@@ -33,9 +29,8 @@ type Manager struct {
|
||||
defaultOutboundFallback func() (adapter.Outbound, error)
|
||||
}
|
||||
|
||||
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
||||
func NewManager(registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
defaultTag: defaultTag,
|
||||
@@ -48,14 +43,13 @@ func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, e
|
||||
m.defaultOutboundFallback = defaultOutboundFallback
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.scope = scope
|
||||
m.stage = stage
|
||||
if stage == adapter.StartStateStart {
|
||||
if stage == adapter.StartStateInitialize {
|
||||
if m.defaultTag != "" && m.defaultOutbound == nil {
|
||||
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
||||
if !loaded {
|
||||
@@ -74,27 +68,27 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.outboundByTag[directOutbound.Tag()] = directOutbound
|
||||
m.defaultOutbound = directOutbound
|
||||
}
|
||||
outbounds := m.outbounds
|
||||
m.access.Unlock()
|
||||
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
||||
} else {
|
||||
outbounds := m.outbounds
|
||||
m.access.Unlock()
|
||||
for _, outbound := range outbounds {
|
||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err := adapter.LegacyStart(outbound, stage)
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
}
|
||||
outbounds := m.outbounds
|
||||
m.access.Unlock()
|
||||
if stage == adapter.StartStateStart {
|
||||
return m.startOutbounds(scope, append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
||||
}
|
||||
for _, outbound := range outbounds {
|
||||
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
|
||||
if !isLifecycle {
|
||||
continue
|
||||
}
|
||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||
err := scope.Start(name, lifecycle, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
monitor := taskmonitor.New(m.logger, C.StartTimeout)
|
||||
func (m *Manager) startOutbounds(scope *adapter.Scope, outbounds []adapter.Outbound) error {
|
||||
started := make(map[string]bool)
|
||||
for {
|
||||
canContinue := false
|
||||
@@ -112,27 +106,21 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
}
|
||||
started[outboundTag] = true
|
||||
canContinue = true
|
||||
if endpoint, isEndpoint := outboundToStart.(adapter.Endpoint); isEndpoint {
|
||||
err := m.endpoint.StartEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
lifecycle, isLifecycle := outboundToStart.(adapter.Lifecycle)
|
||||
if !isLifecycle {
|
||||
continue
|
||||
}
|
||||
name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]"
|
||||
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
|
||||
done := adapter.LogElapsed(m.logger, "start ", name)
|
||||
monitor.Start("start ", name)
|
||||
err := starter.Start(adapter.StartStateStart)
|
||||
monitor.Finish()
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ", name)
|
||||
}
|
||||
} else if starter, isStarter := outboundToStart.(interface {
|
||||
Start() error
|
||||
}); isStarter {
|
||||
done := adapter.LogElapsed(m.logger, "start ", name)
|
||||
monitor.Start("start ", name)
|
||||
err := starter.Start()
|
||||
monitor.Finish()
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ", name)
|
||||
}
|
||||
err := scope.Start(name, lifecycle, adapter.StartStateStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(started) == len(outbounds) {
|
||||
@@ -152,9 +140,9 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
if common.Contains(oTree, problemOutboundTag) {
|
||||
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
|
||||
}
|
||||
m.access.Lock()
|
||||
problemOutbound := m.outboundByTag[problemOutboundTag]
|
||||
m.access.Unlock()
|
||||
problemOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
|
||||
return it.Tag() == problemOutboundTag
|
||||
})
|
||||
if problemOutbound == nil {
|
||||
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]")
|
||||
}
|
||||
@@ -165,33 +153,6 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
m.access.Lock()
|
||||
if !m.started {
|
||||
m.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
outbounds := m.outbounds
|
||||
m.outbounds = nil
|
||||
m.access.Unlock()
|
||||
var err error
|
||||
for _, outbound := range outbounds {
|
||||
if closer, isCloser := outbound.(io.Closer); isCloser {
|
||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||
done := adapter.LogElapsed(m.logger, "close ", name)
|
||||
monitor.Start("close ", name)
|
||||
err = E.Append(err, closer.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", name)
|
||||
})
|
||||
monitor.Finish()
|
||||
done()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Outbounds() []adapter.Outbound {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
@@ -215,47 +176,38 @@ func (m *Manager) Default() adapter.Outbound {
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
outbound, found := m.outboundByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.outboundByTag, tag)
|
||||
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it == outbound
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid inbound index")
|
||||
dependBy := m.dependByTag[tag]
|
||||
if len(dependBy) > 0 {
|
||||
m.access.Unlock()
|
||||
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
||||
}
|
||||
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
|
||||
started := m.started
|
||||
delete(m.outboundByTag, tag)
|
||||
m.outbounds = common.Filter(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it != outbound
|
||||
})
|
||||
m.removeDependencies(outbound)
|
||||
if m.defaultOutbound == outbound {
|
||||
if len(m.outbounds) > 0 {
|
||||
m.defaultOutbound = m.outbounds[0]
|
||||
m.logger.Notice("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
} else {
|
||||
m.defaultOutbound = nil
|
||||
}
|
||||
}
|
||||
dependBy := m.dependByTag[tag]
|
||||
if len(dependBy) > 0 {
|
||||
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
||||
scope := m.scope
|
||||
m.access.Unlock()
|
||||
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
|
||||
if scope == nil || !isLifecycle {
|
||||
return nil
|
||||
}
|
||||
dependencies := outbound.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
if len(m.dependByTag[dependency]) == 1 {
|
||||
delete(m.dependByTag, dependency)
|
||||
} else {
|
||||
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
|
||||
return it != tag
|
||||
})
|
||||
}
|
||||
}
|
||||
if started {
|
||||
return common.Close(outbound)
|
||||
}
|
||||
return nil
|
||||
return scope.Remove(lifecycle)
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error {
|
||||
@@ -266,45 +218,63 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.started {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.RLock()
|
||||
scope := m.scope
|
||||
stage := m.stage
|
||||
m.access.RUnlock()
|
||||
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
|
||||
if scope != nil && isLifecycle {
|
||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err = adapter.LegacyStart(outbound, stage)
|
||||
done()
|
||||
for _, startStage := range adapter.ListStartStages[:stage+1] {
|
||||
err = scope.Start(name, lifecycle, startStage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
return E.Errors(err, scope.Remove(lifecycle))
|
||||
}
|
||||
}
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = common.Close(existsOutbound)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it == existsOutbound
|
||||
existsOutbound, loaded := m.outboundByTag[tag]
|
||||
if loaded {
|
||||
m.outbounds = common.Filter(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it != existsOutbound
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
|
||||
m.removeDependencies(existsOutbound)
|
||||
}
|
||||
m.outbounds = append(m.outbounds, outbound)
|
||||
m.outboundByTag[tag] = outbound
|
||||
dependencies := outbound.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
for _, dependency := range outbound.Dependencies() {
|
||||
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
|
||||
}
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) || (loaded && m.defaultOutbound == existsOutbound) {
|
||||
m.defaultOutbound = outbound
|
||||
if m.started {
|
||||
m.logger.Notice("updated default outbound to ", outbound.Tag())
|
||||
}
|
||||
}
|
||||
m.access.Unlock()
|
||||
if !loaded || scope == nil {
|
||||
return nil
|
||||
}
|
||||
existsLifecycle, isExistsLifecycle := existsOutbound.(adapter.Lifecycle)
|
||||
if !isExistsLifecycle {
|
||||
return nil
|
||||
}
|
||||
err = scope.Remove(existsLifecycle)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) removeDependencies(outbound adapter.Outbound) {
|
||||
tag := outbound.Tag()
|
||||
for _, dependency := range outbound.Dependencies() {
|
||||
dependBy := common.Filter(m.dependByTag[dependency], func(it string) bool {
|
||||
return it != tag
|
||||
})
|
||||
if len(dependBy) == 0 {
|
||||
delete(m.dependByTag, dependency)
|
||||
} else {
|
||||
m.dependByTag[dependency] = dependBy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Outbounds() []Outbound
|
||||
|
||||
+39
-18
@@ -28,6 +28,7 @@ type Adapter struct {
|
||||
logger log.ContextLogger
|
||||
providerType string
|
||||
providerTag string
|
||||
outboundAccess sync.RWMutex
|
||||
outbounds []adapter.Outbound
|
||||
outboundsByTag map[string]adapter.Outbound
|
||||
ticker *time.Ticker
|
||||
@@ -36,11 +37,12 @@ type Adapter struct {
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.ProviderUpdateCallback]
|
||||
|
||||
link string
|
||||
enabled bool
|
||||
removeEmojis bool
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
link string
|
||||
enabled bool
|
||||
removeEmojis bool
|
||||
overrideDialerOptions *option.DialerOptions
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.OutboundManager, logFactory log.Factory, logger log.ContextLogger, providerTag string, providerType string, options option.ProviderHealthCheckOptions) Adapter {
|
||||
@@ -75,12 +77,19 @@ func (a *Adapter) SetRemoveEmojis(remove bool) {
|
||||
a.removeEmojis = remove
|
||||
}
|
||||
|
||||
func (a *Adapter) SetOverrideDialerOptions(options *option.DialerOptions) {
|
||||
a.overrideDialerOptions = options
|
||||
}
|
||||
|
||||
func (a *Adapter) Start() error {
|
||||
a.history = service.PtrFromContext[urltest.HistoryStorage](a.ctx)
|
||||
if a.history == nil {
|
||||
a.history = urltest.NewHistoryStorage()
|
||||
}
|
||||
go a.loopCheck()
|
||||
if a.enabled {
|
||||
a.ticker = time.NewTicker(a.interval)
|
||||
go a.loopCheck()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -93,13 +102,14 @@ func (a *Adapter) Tag() string {
|
||||
}
|
||||
|
||||
func (a *Adapter) Outbounds() []adapter.Outbound {
|
||||
a.outboundAccess.RLock()
|
||||
defer a.outboundAccess.RUnlock()
|
||||
return a.outbounds
|
||||
}
|
||||
|
||||
func (a *Adapter) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
if a.outboundsByTag == nil {
|
||||
return nil, false
|
||||
}
|
||||
a.outboundAccess.RLock()
|
||||
defer a.outboundAccess.RUnlock()
|
||||
detour, ok := a.outboundsByTag[tag]
|
||||
return detour, ok
|
||||
}
|
||||
@@ -119,6 +129,11 @@ func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Ou
|
||||
oldOptByTag[opt.Tag] = opt
|
||||
}
|
||||
for i, opt := range newOpts {
|
||||
if a.overrideDialerOptions != nil {
|
||||
if wrapper, ok := opt.Options.(option.DialerOptionsWrapper); ok {
|
||||
wrapper.ReplaceDialerOptions(*a.overrideDialerOptions)
|
||||
}
|
||||
}
|
||||
var tag string
|
||||
if opt.Tag != "" {
|
||||
tag = F.ToString(a.providerTag, "/", opt.Tag)
|
||||
@@ -146,11 +161,13 @@ func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Ou
|
||||
outbounds = append(outbounds, outbound)
|
||||
outboundsByTag[tag] = outbound
|
||||
}
|
||||
a.outboundAccess.Lock()
|
||||
a.outbounds = outbounds
|
||||
a.outboundsByTag = outboundsByTag
|
||||
a.outboundAccess.Unlock()
|
||||
if a.enabled && a.history != nil {
|
||||
go a.HealthCheck(a.ctx)
|
||||
}
|
||||
a.outbounds = outbounds
|
||||
a.outboundsByTag = outboundsByTag
|
||||
}
|
||||
|
||||
func (a *Adapter) HealthCheck(ctx context.Context) (map[string]uint16, error) {
|
||||
@@ -176,14 +193,21 @@ func (a *Adapter) UpdateGroups() {
|
||||
for element := a.callbacks.Front(); element != nil; element = element.Next() {
|
||||
element.Value(a.providerTag)
|
||||
}
|
||||
history := service.PtrFromContext[urltest.HistoryStorage](a.ctx)
|
||||
if history != nil {
|
||||
history.NotifyUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Close() error {
|
||||
if a.ticker != nil {
|
||||
a.ticker.Stop()
|
||||
}
|
||||
a.outboundAccess.Lock()
|
||||
outbounds := a.outbounds
|
||||
a.outbounds = nil
|
||||
a.outboundsByTag = nil
|
||||
a.outboundAccess.Unlock()
|
||||
var err error
|
||||
for _, ob := range outbounds {
|
||||
if err2 := a.outbound.Remove(ob.Tag()); err2 != nil {
|
||||
@@ -196,10 +220,6 @@ func (a *Adapter) Close() error {
|
||||
}
|
||||
|
||||
func (a *Adapter) loopCheck() {
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
a.ticker = time.NewTicker(a.interval)
|
||||
a.healthcheck(a.ctx)
|
||||
for {
|
||||
select {
|
||||
@@ -220,7 +240,7 @@ func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
|
||||
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
|
||||
var resultAccess sync.Mutex
|
||||
checked := make(map[string]bool)
|
||||
for _, detour := range a.outbounds {
|
||||
for _, detour := range a.Outbounds() {
|
||||
tag := detour.Tag()
|
||||
if checked[tag] {
|
||||
continue
|
||||
@@ -251,7 +271,8 @@ func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
|
||||
}
|
||||
|
||||
func (a *Adapter) removeUseless(newOpts []option.Outbound) {
|
||||
if len(a.outbounds) == 0 {
|
||||
outbounds := a.Outbounds()
|
||||
if len(outbounds) == 0 {
|
||||
return
|
||||
}
|
||||
exists := make(map[string]bool)
|
||||
@@ -264,7 +285,7 @@ func (a *Adapter) removeUseless(newOpts []option.Outbound) {
|
||||
}
|
||||
exists[tag] = true
|
||||
}
|
||||
for _, opt := range a.outbounds {
|
||||
for _, opt := range outbounds {
|
||||
if !exists[opt.Tag()] {
|
||||
if err := a.outbound.Remove(opt.Tag()); err != nil {
|
||||
a.logger.Error(err, "close outbound [", opt.Tag(), "]")
|
||||
|
||||
+40
-72
@@ -2,80 +2,46 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var _ adapter.ProviderManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.ProviderRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
startAccess sync.Mutex
|
||||
scope *adapter.Scope
|
||||
stage adapter.StartStage
|
||||
providers []adapter.Provider
|
||||
providerByTag map[string]adapter.Provider
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewManager(logger logger.ContextLogger, registry adapter.ProviderRegistry) *Manager {
|
||||
func NewManager(registry adapter.ProviderRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
providerByTag: make(map[string]adapter.Provider),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Initialize() {
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.scope = scope
|
||||
m.stage = stage
|
||||
providers := m.providers
|
||||
m.access.Unlock()
|
||||
for _, provider := range providers {
|
||||
err := adapter.LegacyStart(provider, stage)
|
||||
err := scope.Start("provider/"+provider.Type()+"["+provider.Tag()+"]", provider, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
m.access.Lock()
|
||||
if !m.started {
|
||||
m.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
providers := m.providers
|
||||
m.providers = nil
|
||||
m.access.Unlock()
|
||||
var err error
|
||||
for _, provider := range providers {
|
||||
if closer, isCloser := provider.(io.Closer); isCloser {
|
||||
monitor.Start("close provider/", provider.Type(), "[", provider.Tag(), "]")
|
||||
err = E.Append(err, closer.Close(), func(err error) error {
|
||||
return E.Cause(err, "close provider/", provider.Type(), "[", provider.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -95,6 +61,8 @@ func (m *Manager) Get(tag string) (adapter.Provider, bool) {
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
if !found {
|
||||
@@ -102,56 +70,56 @@ func (m *Manager) Remove(tag string) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.providerByTag, tag)
|
||||
index := common.Index(m.providers, func(it adapter.Provider) bool {
|
||||
return it == provider
|
||||
m.providers = common.Filter(m.providers, func(it adapter.Provider) bool {
|
||||
return it != provider
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:index], m.providers[index+1:]...)
|
||||
started := m.started
|
||||
scope := m.scope
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return common.Close(provider)
|
||||
if scope == nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return scope.Remove(provider)
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) error {
|
||||
if tag == "" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
provider, err := m.registry.CreateProvider(ctx, router, logFactory, tag, providerType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(provider, stage)
|
||||
scope := m.scope
|
||||
stage := m.stage
|
||||
m.access.Unlock()
|
||||
if scope != nil {
|
||||
name := "provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
for _, startStage := range adapter.ListStartStages[:stage+1] {
|
||||
err = scope.Start(name, provider, startStage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
|
||||
return E.Errors(err, scope.Remove(provider))
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsProvider, loaded := m.providerByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = common.Close(existsProvider)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close provider", provider.Type(), "[", existsProvider.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.providers, func(it adapter.Provider) bool {
|
||||
return it == existsProvider
|
||||
m.access.Lock()
|
||||
existsProvider, loaded := m.providerByTag[tag]
|
||||
if loaded {
|
||||
m.providers = common.Filter(m.providers, func(it adapter.Provider) bool {
|
||||
return it != existsProvider
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
|
||||
}
|
||||
m.providers = append(m.providers, provider)
|
||||
m.providerByTag[tag] = provider
|
||||
m.access.Unlock()
|
||||
if !loaded || scope == nil {
|
||||
return nil
|
||||
}
|
||||
err = scope.Remove(existsProvider)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+5
-1
@@ -93,7 +93,11 @@ func JudgeFlow(router Router, inbound string, inboundType string, network uint8,
|
||||
case PreMatchDrop:
|
||||
return tun.FlowVerdict{Action: tun.ActionDrop}
|
||||
case PreMatchBypass:
|
||||
return tun.FlowVerdict{Action: tun.ActionBypass}
|
||||
port, isPort := result.Outbound.(tun.Port)
|
||||
if !isPort {
|
||||
return tun.FlowVerdict{Action: tun.ActionBypass}
|
||||
}
|
||||
return tun.FlowVerdict{Action: tun.ActionBypass, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker}
|
||||
case PreMatchHijackDNS:
|
||||
return tun.FlowVerdict{Action: tun.ActionHijackDNS}
|
||||
default:
|
||||
|
||||
@@ -22,6 +22,5 @@ type ServiceManager interface {
|
||||
Lifecycle
|
||||
Services() []Service
|
||||
Get(tag string) (Service, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
|
||||
}
|
||||
|
||||
@@ -2,82 +2,43 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.ServiceManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.ServiceRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
services []adapter.Service
|
||||
serviceByTag map[string]adapter.Service
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
|
||||
func NewManager(registry adapter.ServiceRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
serviceByTag: make(map[string]adapter.Service),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
services := m.services
|
||||
m.access.Unlock()
|
||||
for _, service := range services {
|
||||
name := "service/" + service.Type() + "[" + service.Tag() + "]"
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err := adapter.LegacyStart(service, stage)
|
||||
done()
|
||||
err := scope.Start(name, service, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
services := m.services
|
||||
m.services = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, service := range services {
|
||||
name := "service/" + service.Type() + "[" + service.Tag() + "]"
|
||||
done := adapter.LogElapsed(m.logger, "close ", name)
|
||||
monitor.Start("close ", name)
|
||||
err = E.Append(err, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", name)
|
||||
})
|
||||
monitor.Finish()
|
||||
done()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Services() []adapter.Service {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
@@ -91,29 +52,6 @@ func (m *Manager) Get(tag string) (adapter.Service, bool) {
|
||||
return service, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
service, found := m.serviceByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.serviceByTag, tag)
|
||||
index := common.Index(m.services, func(it adapter.Service) bool {
|
||||
return it == service
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid service index")
|
||||
}
|
||||
m.services = append(m.services[:index], m.services[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return service.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
|
||||
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
|
||||
if err != nil {
|
||||
@@ -121,31 +59,9 @@ 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 {
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err = adapter.LegacyStart(service, stage)
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsService, loaded := m.serviceByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsService.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
|
||||
return it == existsService
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid service index")
|
||||
}
|
||||
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
|
||||
_, loaded := m.serviceByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate service tag: ", tag)
|
||||
}
|
||||
m.services = append(m.services, service)
|
||||
m.serviceByTag[tag] = service
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package adapter
|
||||
|
||||
import "context"
|
||||
|
||||
type keepSessionKey struct{}
|
||||
|
||||
func ContextWithKeepSession(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, (*keepSessionKey)(nil), true)
|
||||
}
|
||||
|
||||
func KeepSessionFromContext(ctx context.Context) bool {
|
||||
keep, _ := ctx.Value((*keepSessionKey)(nil)).(bool)
|
||||
return keep
|
||||
}
|
||||
@@ -22,3 +22,8 @@ type V2RayClientTransport interface {
|
||||
DialContext(ctx context.Context) (net.Conn, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type V2RayMultiplexClientTransport interface {
|
||||
V2RayClientTransport
|
||||
MultiplexEnabled() bool
|
||||
}
|
||||
|
||||
@@ -2,11 +2,7 @@ package box
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -28,6 +24,7 @@ import (
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/clashmode"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -44,10 +41,8 @@ import (
|
||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
ctx context.Context
|
||||
createdAt time.Time
|
||||
debugOptions option.DebugOptions
|
||||
debugHTTPServer *http.Server
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
@@ -61,9 +56,11 @@ type Box struct {
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
referenceManager *route.ReferenceManager
|
||||
httpClientService adapter.LifecycleService
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
ntpService *ntp.Service
|
||||
scope *adapter.Scope
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@@ -169,7 +166,7 @@ func New(options Options) (*Box, error) {
|
||||
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
||||
needCacheFile = true
|
||||
}
|
||||
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
||||
if experimentalOptions.ClashAPI != nil {
|
||||
needClashAPI = true
|
||||
}
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
@@ -192,7 +189,7 @@ func New(options Options) (*Box, error) {
|
||||
logFactory, err := log.New(log.Options{
|
||||
Context: ctx,
|
||||
Options: common.PtrValueOrDefault(options.Log),
|
||||
Observable: needClashAPI || needAPIService,
|
||||
Observable: needClashAPI && experimentalOptions.ClashAPI.ExternalController != "",
|
||||
DefaultWriter: defaultLogWriter,
|
||||
BaseTime: createdAt,
|
||||
PlatformWriter: options.PlatformLogWriter,
|
||||
@@ -223,13 +220,13 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.NetworkNamespaceManager](ctx, netnsManager)
|
||||
internalServices = append(internalServices, netnsManager)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||
providerManager := provider.NewManager(logFactory.NewLogger("provider"), providerRegistry)
|
||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
|
||||
endpointManager := endpoint.NewManager(endpointRegistry)
|
||||
inboundManager := inbound.NewManager(inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(outboundRegistry, endpointManager, routeOptions.Final)
|
||||
providerManager := provider.NewManager(providerRegistry)
|
||||
dnsTransportManager := dns.NewTransportManager(dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(serviceRegistry)
|
||||
certificateProviderManager := boxCertificate.NewManager(certificateProviderRegistry)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
@@ -243,13 +240,13 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
}
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
// Must register after ConnectionManager: the Apple HTTP engine's proxy bridge reads it from the context when Manager.Start resolves the default client.
|
||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
||||
@@ -260,12 +257,21 @@ func New(options Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize router")
|
||||
}
|
||||
if needClashAPI || needAPIService {
|
||||
if needClashAPI || needAPIService || options.PlatformLogWriter != nil {
|
||||
trafficManager := trafficcontrol.NewManager(outboundManager)
|
||||
service.MustRegisterPtr(ctx, trafficManager)
|
||||
router.AppendTracker(trafficManager)
|
||||
internalServices = append(internalServices, trafficManager)
|
||||
var clashDefaultMode string
|
||||
if experimentalOptions.ClashAPI != nil {
|
||||
clashDefaultMode = experimentalOptions.ClashAPI.DefaultMode
|
||||
}
|
||||
clashMode := clashmode.NewManager(ctx, logFactory.NewLogger("clash-mode"), clashDefaultMode, clashmode.CalculateModeList(options.Options))
|
||||
service.MustRegisterPtr(ctx, clashMode)
|
||||
internalServices = append(internalServices, clashMode)
|
||||
}
|
||||
referenceManager := route.NewReferenceManager(ctx, logFactory.NewLogger("reference"), options.Options)
|
||||
internalServices = append(internalServices, referenceManager)
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
if ntpOptions.Enabled {
|
||||
@@ -460,13 +466,10 @@ func New(options Options) (*Box, error) {
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
if needClashAPI {
|
||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(experimentalOptions.ClashAPI))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create clash-server")
|
||||
}
|
||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||
internalServices = append(internalServices, clashServer)
|
||||
}
|
||||
if needV2RayAPI {
|
||||
@@ -480,6 +483,7 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||
}
|
||||
}
|
||||
var ntpService *ntp.Service
|
||||
if ntpOptions.Enabled {
|
||||
if ntpOptions.WriteToSystem {
|
||||
err = adapter.CheckSecurityFeature(ctx, "NTP `write_to_system`")
|
||||
@@ -491,7 +495,7 @@ func New(options Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create NTP service")
|
||||
}
|
||||
ntpService := ntp.NewService(ntp.Options{
|
||||
ntpService = ntp.NewService(ntp.Options{
|
||||
Context: ctx,
|
||||
Dialer: ntpDialer,
|
||||
Logger: logFactory.NewLogger("ntp"),
|
||||
@@ -500,10 +504,8 @@ func New(options Options) (*Box, error) {
|
||||
WriteToSystem: ntpOptions.WriteToSystem,
|
||||
})
|
||||
timeService.TimeService = ntpService
|
||||
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||
}
|
||||
return &Box{
|
||||
ctx: ctx,
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
@@ -515,28 +517,21 @@ func New(options Options) (*Box, error) {
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
referenceManager: referenceManager,
|
||||
httpClientService: httpClientService,
|
||||
createdAt: createdAt,
|
||||
debugOptions: debugOptions,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
ntpService: ntpService,
|
||||
scope: adapter.NewScope(ctx, logFactory.Logger()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Box) PreStart() error {
|
||||
err := s.preStart()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
println(err.Error())
|
||||
debug.PrintStack()
|
||||
panic("panic on early close: " + fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
@@ -547,15 +542,6 @@ func (s *Box) PreStart() error {
|
||||
func (s *Box) Start() error {
|
||||
err := s.start()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
println(err.Error())
|
||||
debug.PrintStack()
|
||||
println("panic on early start: " + fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
@@ -563,6 +549,31 @@ func (s *Box) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type boxComponent struct {
|
||||
name string
|
||||
lifecycle adapter.Lifecycle
|
||||
}
|
||||
|
||||
func (s *Box) startComponents(stage adapter.StartStage, components ...boxComponent) error {
|
||||
for _, component := range components {
|
||||
err := s.scope.Start(component.name, component.lifecycle, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) startInternalServices(stage adapter.StartStage) error {
|
||||
for _, lifecycleService := range s.internalService {
|
||||
err := s.scope.Start(lifecycleService.Name(), lifecycleService, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) preStart() error {
|
||||
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||
monitor.Start("start logger")
|
||||
@@ -571,28 +582,46 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "start logger")
|
||||
}
|
||||
s.scope.Add(s.logFactory.Close)
|
||||
applyDebugOptions(s.debugOptions)
|
||||
s.debugHTTPServer, err = startDebugHTTPServer(s.debugOptions)
|
||||
debugHTTPServer, err := startDebugHTTPServer(s.debugOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||
if debugHTTPServer != nil {
|
||||
s.scope.Add(debugHTTPServer.Close)
|
||||
}
|
||||
err = s.startInternalServices(adapter.StartStateInitialize) // cache-file clash-api v2ray-api
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.provider, s.inbound, s.endpoint, s.service, s.certificateProvider)
|
||||
err = s.startComponents(adapter.StartStateInitialize,
|
||||
boxComponent{s.httpClientService.Name(), s.httpClientService},
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.outbound, s.provider, s.dnsTransport, s.network, s.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
|
||||
err = s.startComponents(adapter.StartStateStart,
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{s.httpClientService.Name(), s.httpClientService},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -604,35 +633,65 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStart, s.internalService)
|
||||
err = s.startInternalServices(adapter.StartStateStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.endpoint)
|
||||
if s.ntpService != nil {
|
||||
done := adapter.LogElapsed(s.logger, "start ntp service")
|
||||
err = s.ntpService.Start()
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ntp service")
|
||||
}
|
||||
s.scope.Add(s.ntpService.Close)
|
||||
}
|
||||
err = s.startComponents(adapter.StartStateStart,
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.certificateProvider)
|
||||
err = s.startComponents(adapter.StartStatePostStart,
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.inbound, s.service)
|
||||
err = s.startInternalServices(adapter.StartStatePostStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStatePostStart, s.outbound, s.provider, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
|
||||
err = s.startComponents(adapter.StartStateStarted,
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStatePostStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.provider, s.endpoint, s.certificateProvider, s.inbound, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStarted, s.internalService)
|
||||
err = s.startInternalServices(adapter.StartStateStarted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -640,62 +699,7 @@ func (s *Box) start() error {
|
||||
}
|
||||
|
||||
func (s *Box) Close() error {
|
||||
select {
|
||||
case <-s.done:
|
||||
return os.ErrClosed
|
||||
default:
|
||||
close(s.done)
|
||||
}
|
||||
var err error
|
||||
if s.debugHTTPServer != nil {
|
||||
err = E.Append(err, s.debugHTTPServer.Close(), func(err error) error {
|
||||
return E.Cause(err, "close debug HTTP server")
|
||||
})
|
||||
s.debugHTTPServer = nil
|
||||
}
|
||||
for _, closeItem := range []struct {
|
||||
name string
|
||||
service adapter.Lifecycle
|
||||
}{
|
||||
{"service", s.service},
|
||||
{"inbound", s.inbound},
|
||||
{"certificate-provider", s.certificateProvider},
|
||||
{"provider", s.provider},
|
||||
{"endpoint", s.endpoint},
|
||||
{"outbound", s.outbound},
|
||||
{"router", s.router},
|
||||
{"connection", s.connection},
|
||||
{"dns-router", s.dnsRouter},
|
||||
{"dns-transport", s.dnsTransport},
|
||||
{"network", s.network},
|
||||
} {
|
||||
done := adapter.LogElapsed(s.logger, "close ", closeItem.name)
|
||||
err = E.Append(err, closeItem.service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", closeItem.name)
|
||||
})
|
||||
done()
|
||||
}
|
||||
if s.httpClientService != nil {
|
||||
s.logger.Trace("close ", s.httpClientService.Name())
|
||||
startTime := time.Now()
|
||||
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", s.httpClientService.Name())
|
||||
})
|
||||
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
for _, lifecycleService := range s.internalService {
|
||||
done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name())
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", lifecycleService.Name())
|
||||
})
|
||||
done()
|
||||
}
|
||||
done := adapter.LogElapsed(s.logger, "close logger")
|
||||
err = E.Append(err, s.logFactory.Close(), func(err error) error {
|
||||
return E.Cause(err, "close logger")
|
||||
})
|
||||
done()
|
||||
return err
|
||||
return s.scope.Close()
|
||||
}
|
||||
|
||||
func (s *Box) Network() adapter.NetworkManager {
|
||||
@@ -718,6 +722,14 @@ func (s *Box) Endpoint() adapter.EndpointManager {
|
||||
return s.endpoint
|
||||
}
|
||||
|
||||
func (s *Box) CreatedAt() time.Time {
|
||||
return s.createdAt
|
||||
}
|
||||
|
||||
func (s *Box) CloseIdleConnections() {
|
||||
s.referenceManager.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (s *Box) LogFactory() log.Factory {
|
||||
return s.logFactory
|
||||
}
|
||||
|
||||
Submodule clients/android deleted from eb87216961
Submodule clients/apple deleted from c19945f65b
+1
-1
Submodule clients/desktop updated: cebee0d527...50f5038491
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/asc-go/asc"
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -32,18 +34,37 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "list_builds":
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatal("platform required: ios, macos, or tvos")
|
||||
}
|
||||
err := listBuilds(ctx, os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "remove_testflight":
|
||||
if len(os.Args) < 5 {
|
||||
log.Fatal("usage: remove_testflight <platform> <version> <build>")
|
||||
}
|
||||
err := removeTestflight(ctx, os.Args[2], os.Args[3], os.Args[4])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "cancel_app_store":
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatal("platform required: ios, macos, or tvos")
|
||||
}
|
||||
err := cancelAppStore(ctx, os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "prepare_app_store":
|
||||
err := prepareAppStore(ctx)
|
||||
case "submit_app_store":
|
||||
err := submitAppStore(ctx, os.Args[2:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "publish_app_store":
|
||||
err := publishAppStore(ctx)
|
||||
err := publishAppStore(ctx, os.Args[2:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -55,8 +76,35 @@ func main() {
|
||||
const (
|
||||
appID = "6785326793"
|
||||
groupID = "39f9ebdc-05d4-421f-9595-dae71df227c4"
|
||||
|
||||
applePath = "../sing-box-for-apple"
|
||||
appStorePromotionalText = "The universal proxy platform."
|
||||
appStoreWhatsNew = "Fixes and improvements"
|
||||
buildPollInterval = 15 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
appStorePlatforms = []string{"ios", "tvos"}
|
||||
platformDirectories = map[asc.Platform]string{
|
||||
asc.PlatformIOS: "SFI",
|
||||
asc.PlatformMACOS: "SFM",
|
||||
asc.PlatformTVOS: "SFT",
|
||||
}
|
||||
)
|
||||
|
||||
func parsePlatform(name string) (asc.Platform, error) {
|
||||
switch name {
|
||||
case "ios":
|
||||
return asc.PlatformIOS, nil
|
||||
case "macos":
|
||||
return asc.PlatformMACOS, nil
|
||||
case "tvos":
|
||||
return asc.PlatformTVOS, nil
|
||||
default:
|
||||
return "", E.New("unknown platform: ", name)
|
||||
}
|
||||
}
|
||||
|
||||
func createClient(expireDuration time.Duration) *asc.Client {
|
||||
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
|
||||
if err != nil {
|
||||
@@ -70,16 +118,9 @@ func createClient(expireDuration time.Duration) *asc.Client {
|
||||
}
|
||||
|
||||
func fetchNextProjectVersion(ctx context.Context, platformName string) error {
|
||||
var platform asc.Platform
|
||||
switch platformName {
|
||||
case "ios":
|
||||
platform = asc.PlatformIOS
|
||||
case "macos":
|
||||
platform = asc.PlatformMACOS
|
||||
case "tvos":
|
||||
platform = asc.PlatformTVOS
|
||||
default:
|
||||
return E.New("unknown platform: ", platformName)
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := &asc.ListBuildsQuery{
|
||||
@@ -115,20 +156,55 @@ func fetchNextProjectVersion(ctx context.Context, platformName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func listBuilds(ctx context.Context, platformName string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
betaBuildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
|
||||
return it.ID
|
||||
})
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
Include: []string{"preReleaseVersion"},
|
||||
Sort: []string{"-uploadedDate"},
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, build := range builds.Data {
|
||||
var betaGroup string
|
||||
if slices.Contains(betaBuildIDs, build.ID) {
|
||||
betaGroup = " testflight"
|
||||
}
|
||||
var versionString string
|
||||
if build.Relationships != nil && build.Relationships.PreReleaseVersion != nil && build.Relationships.PreReleaseVersion.Data != nil {
|
||||
for _, included := range builds.Included {
|
||||
preReleaseVersion := included.PrereleaseVersion()
|
||||
if preReleaseVersion != nil && preReleaseVersion.ID == build.Relationships.PreReleaseVersion.Data.ID {
|
||||
versionString = *preReleaseVersion.Attributes.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
os.Stdout.WriteString(F.ToString(versionString, " (", *build.Attributes.Version, ") ", *build.Attributes.ProcessingState, " ", build.Attributes.UploadedDate.Time.Format(time.RFC3339), betaGroup, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishTestflight(ctx context.Context) error {
|
||||
if len(os.Args) < 3 {
|
||||
return E.New("platform required: ios, macos, or tvos")
|
||||
}
|
||||
var platform asc.Platform
|
||||
switch os.Args[2] {
|
||||
case "ios":
|
||||
platform = asc.PlatformIOS
|
||||
case "macos":
|
||||
platform = asc.PlatformMACOS
|
||||
case "tvos":
|
||||
platform = asc.PlatformTVOS
|
||||
default:
|
||||
return E.New("unknown platform: ", os.Args[2])
|
||||
platform, err := parsePlatform(os.Args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tagVersion, err := build_shared.ReadTagVersion()
|
||||
@@ -136,6 +212,8 @@ func publishTestflight(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
tag := tagVersion.VersionString()
|
||||
testFlightVersion := build_shared.TestFlightVersion(tagVersion)
|
||||
projectVersion := os.Getenv(strings.ToUpper(os.Args[2]) + "_PROJECT_VERSION")
|
||||
|
||||
releaseNotes := F.ToString("sing-box ", tagVersion.String())
|
||||
if len(os.Args) >= 4 {
|
||||
@@ -153,27 +231,37 @@ func publishTestflight(ctx context.Context) error {
|
||||
return it.ID
|
||||
})
|
||||
|
||||
waitingForProcess := false
|
||||
log.Info(string(platform), " list builds")
|
||||
query := &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
FilterPreReleaseVersionVersion: []string{testFlightVersion},
|
||||
Sort: []string{"-uploadedDate"},
|
||||
Limit: 1,
|
||||
}
|
||||
if projectVersion != "" {
|
||||
query.FilterVersion = []string{projectVersion}
|
||||
}
|
||||
log.Info(string(platform), " ", testFlightVersion, " (", projectVersion, ") list builds")
|
||||
for {
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
})
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Info(string(platform), " ", testFlightVersion, " waiting for build upload")
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
build := builds.Data[0]
|
||||
log.Info(string(platform), " ", tag, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
|
||||
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
|
||||
log.Info(string(platform), " ", tag, " waiting for process")
|
||||
time.Sleep(15 * time.Second)
|
||||
log.Info(string(platform), " ", testFlightVersion, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
|
||||
if projectVersion == "" && common.Contains(buildIDs, build.ID) {
|
||||
log.Info(string(platform), " ", testFlightVersion, " build ", *build.Attributes.Version, " already published, waiting for new upload")
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
if *build.Attributes.ProcessingState != "VALID" {
|
||||
waitingForProcess = true
|
||||
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||
time.Sleep(15 * time.Second)
|
||||
log.Info(string(platform), " ", testFlightVersion, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " list localizations")
|
||||
@@ -198,7 +286,7 @@ func publishTestflight(ctx context.Context) error {
|
||||
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
|
||||
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
|
||||
log.Info("waiting for process")
|
||||
time.Sleep(15 * time.Second)
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -226,227 +314,341 @@ func publishTestflight(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelAppStore(ctx context.Context, platform string) error {
|
||||
switch platform {
|
||||
case "ios":
|
||||
platform = string(asc.PlatformIOS)
|
||||
case "macos":
|
||||
platform = string(asc.PlatformMACOS)
|
||||
case "tvos":
|
||||
platform = string(asc.PlatformTVOS)
|
||||
}
|
||||
tag, err := build_shared.ReadTag()
|
||||
func removeTestflight(ctx context.Context, platformName string, versionString string, buildNumber string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
for {
|
||||
log.Info(platform, " list versions")
|
||||
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
})
|
||||
if isRetryable(response) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||
return *it.Attributes.VersionString == tag
|
||||
})
|
||||
if version.ID == "" {
|
||||
return nil
|
||||
}
|
||||
log.Info(platform, " ", tag, " get submission")
|
||||
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
||||
if response != nil && response.StatusCode == http.StatusNotFound {
|
||||
return nil
|
||||
}
|
||||
if isRetryable(response) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(platform, " ", tag, " delete submission")
|
||||
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
FilterPreReleaseVersionVersion: []string{versionString},
|
||||
FilterVersion: []string{buildNumber},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) != 1 {
|
||||
return E.New(string(platform), " ", versionString, " (", buildNumber, ") matched ", len(builds.Data), " builds")
|
||||
}
|
||||
build := builds.Data[0]
|
||||
log.Info(string(platform), " ", versionString, " (", buildNumber, ") remove from beta group")
|
||||
_, err = client.Builds.RemoveAccessForBetaGroupsFromBuild(ctx, build.ID, []string{groupID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareAppStore(ctx context.Context) error {
|
||||
func cancelAppStore(ctx context.Context, platformName string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
for _, platform := range []asc.Platform{
|
||||
asc.PlatformIOS,
|
||||
asc.PlatformMACOS,
|
||||
asc.PlatformTVOS,
|
||||
} {
|
||||
log.Info(string(platform), " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
})
|
||||
log.Info(string(platform), " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterVersionString: []string{tag},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(versions.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
versionID := versions.Data[0].ID
|
||||
log.Info(string(platform), " ", tag, " list review submissions")
|
||||
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterState: []string{
|
||||
string(asc.ReviewSubmissionStateReadyForReview),
|
||||
string(asc.ReviewSubmissionStateWaitingForReview),
|
||||
string(asc.ReviewSubmissionStateInReview),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, submission := range submissions.Data {
|
||||
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, submission.ID, versionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||
return *it.Attributes.VersionString == tag
|
||||
})
|
||||
log.Info(string(platform), " ", tag, " list builds")
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
})
|
||||
if !containsVersion {
|
||||
continue
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " cancel review submission ", submission.ID)
|
||||
_, _, err = client.Submission.UpdateReviewSubmission(ctx, submission.ID, nil, common.Ptr(true))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Fatal(string(platform), " ", tag, " no build found")
|
||||
}
|
||||
buildID := common.Ptr(builds.Data[0].ID)
|
||||
if version.ID == "" {
|
||||
log.Info(string(platform), " ", tag, " create version")
|
||||
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
|
||||
Platform: platform,
|
||||
VersionString: tag,
|
||||
}, appID, buildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version = newVersion.Data
|
||||
|
||||
} else {
|
||||
log.Info(string(platform), " ", tag, " check build")
|
||||
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||
asc.AppStoreVersionStateRejected,
|
||||
asc.AppStoreVersionStateDeveloperRejected:
|
||||
case asc.AppStoreVersionStateWaitingForReview,
|
||||
asc.AppStoreVersionStateInReview,
|
||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if submission != nil {
|
||||
log.Info(string(platform), " ", tag, " delete submission")
|
||||
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
default:
|
||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " update build")
|
||||
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
response.Write(os.Stderr)
|
||||
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
|
||||
}
|
||||
} else {
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||
asc.AppStoreVersionStateRejected,
|
||||
asc.AppStoreVersionStateDeveloperRejected:
|
||||
case asc.AppStoreVersionStateWaitingForReview,
|
||||
asc.AppStoreVersionStateInReview,
|
||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
continue
|
||||
default:
|
||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " list localization")
|
||||
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
|
||||
return *it.Attributes.Locale == "en-US"
|
||||
})
|
||||
if localization.ID == "" {
|
||||
log.Info(string(platform), " ", tag, " no en-US localization found")
|
||||
}
|
||||
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
||||
log.Info(string(platform), " ", tag, " update localization")
|
||||
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
|
||||
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
|
||||
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " create submission")
|
||||
fixSubmit:
|
||||
for {
|
||||
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
|
||||
if err != nil {
|
||||
switch response.StatusCode {
|
||||
case http.StatusInternalServerError:
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch response.StatusCode {
|
||||
case http.StatusCreated:
|
||||
break fixSubmit
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishAppStore(ctx context.Context) error {
|
||||
tag, err := build_shared.ReadTag()
|
||||
func reviewSubmissionContainsVersion(ctx context.Context, client *asc.Client, submissionID string, versionID string) (bool, error) {
|
||||
items, _, err := client.Submission.ListItemsForReviewSubmission(ctx, submissionID, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return slices.ContainsFunc(items.Data, func(it asc.ReviewSubmissionItem) bool {
|
||||
return it.Relationships != nil && it.Relationships.AppStoreVersion != nil &&
|
||||
it.Relationships.AppStoreVersion.Data != nil && it.Relationships.AppStoreVersion.Data.ID == versionID
|
||||
}), nil
|
||||
}
|
||||
|
||||
func submitAppStore(ctx context.Context, platformNames []string) error {
|
||||
if len(platformNames) == 0 {
|
||||
platformNames = appStorePlatforms
|
||||
}
|
||||
client := createClient(20 * time.Minute)
|
||||
for _, platformName := range platformNames {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionString, err := build_shared.ReadAppleMarketingVersion(applePath, platformDirectories[platform])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = submitAppStoreVersion(ctx, client, platform, versionString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func submitAppStoreVersion(ctx context.Context, client *asc.Client, platform asc.Platform, versionString string) error {
|
||||
log.Info(string(platform), " ", versionString, " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
for _, platform := range []asc.Platform{
|
||||
asc.PlatformIOS,
|
||||
asc.PlatformMACOS,
|
||||
asc.PlatformTVOS,
|
||||
} {
|
||||
log.Info(string(platform), " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
localVersion := badversion.Parse(versionString)
|
||||
var version asc.AppStoreVersion
|
||||
for _, remoteVersion := range versions.Data {
|
||||
remoteVersionString := *remoteVersion.Attributes.VersionString
|
||||
if remoteVersionString == versionString {
|
||||
version = remoteVersion
|
||||
continue
|
||||
}
|
||||
if !badversion.Parse(remoteVersionString).LessThan(localVersion) {
|
||||
return E.New(string(platform), " ", versionString, " is not newer than App Store version ", remoteVersionString)
|
||||
}
|
||||
}
|
||||
if version.ID != "" {
|
||||
state := *version.Attributes.AppStoreState
|
||||
switch state {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||
asc.AppStoreVersionStateRejected,
|
||||
asc.AppStoreVersionStateDeveloperRejected,
|
||||
asc.AppStoreVersionStateMetadataRejected,
|
||||
asc.AppStoreVersionStateInvalidBinary:
|
||||
log.Info(string(platform), " ", versionString, " found version in state ", string(state))
|
||||
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
|
||||
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
|
||||
return nil
|
||||
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
log.Info(string(platform), " ", versionString, " approved, run publish_app_store to release")
|
||||
return nil
|
||||
default:
|
||||
return E.New(string(platform), " ", versionString, " already released: ", string(state))
|
||||
}
|
||||
} else {
|
||||
log.Info(string(platform), " ", versionString, " create version")
|
||||
created, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
|
||||
Platform: platform,
|
||||
ReleaseType: common.Ptr("MANUAL"),
|
||||
VersionString: versionString,
|
||||
}, appID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version = created.Data
|
||||
}
|
||||
|
||||
log.Info(string(platform), " ", versionString, " list localizations")
|
||||
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
|
||||
return *it.Attributes.Locale == "en-US"
|
||||
})
|
||||
if localization.ID == "" {
|
||||
log.Info(string(platform), " ", versionString, " create en-US localization")
|
||||
_, _, err = client.Apps.CreateAppStoreVersionLocalization(ctx, asc.AppStoreVersionLocalizationCreateRequestAttributes{
|
||||
Locale: "en-US",
|
||||
PromotionalText: common.Ptr(appStorePromotionalText),
|
||||
WhatsNew: common.Ptr(appStoreWhatsNew),
|
||||
}, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if localization.Attributes.PromotionalText == nil || *localization.Attributes.PromotionalText != appStorePromotionalText ||
|
||||
localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew != appStoreWhatsNew {
|
||||
log.Info(string(platform), " ", versionString, " update en-US localization")
|
||||
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
|
||||
PromotionalText: common.Ptr(appStorePromotionalText),
|
||||
WhatsNew: common.Ptr(appStoreWhatsNew),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||
return *it.Attributes.VersionString == tag
|
||||
}
|
||||
|
||||
var build asc.Build
|
||||
for {
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
FilterPreReleaseVersionVersion: []string{versionString},
|
||||
Sort: []string{"-uploadedDate"},
|
||||
Limit: 1,
|
||||
})
|
||||
switch *version.Attributes.AppStoreState {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Info(string(platform), " ", versionString, " waiting for build upload")
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
build = builds.Data[0]
|
||||
processingState := *build.Attributes.ProcessingState
|
||||
if processingState == "VALID" {
|
||||
break
|
||||
}
|
||||
if processingState == "PROCESSING" {
|
||||
log.Info(string(platform), " ", versionString, " waiting for build ", *build.Attributes.Version, ": ", processingState)
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
return E.New(string(platform), " ", versionString, " build ", *build.Attributes.Version, " ", processingState)
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " found build ", *build.Attributes.Version)
|
||||
|
||||
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != build.ID {
|
||||
log.Info(string(platform), " ", versionString, " attach build ", *build.Attributes.Version)
|
||||
_, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, common.Ptr(build.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info(string(platform), " ", versionString, " list review submissions")
|
||||
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterState: []string{
|
||||
string(asc.ReviewSubmissionStateReadyForReview),
|
||||
string(asc.ReviewSubmissionStateWaitingForReview),
|
||||
string(asc.ReviewSubmissionStateInReview),
|
||||
string(asc.ReviewSubmissionStateUnresolvedIssues),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
submission asc.ReviewSubmission
|
||||
submissionHasItem bool
|
||||
)
|
||||
for _, remoteSubmission := range submissions.Data {
|
||||
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, remoteSubmission.ID, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state := *remoteSubmission.Attributes.State
|
||||
switch state {
|
||||
case asc.ReviewSubmissionStateReadyForReview:
|
||||
submission = remoteSubmission
|
||||
submissionHasItem = containsVersion
|
||||
case asc.ReviewSubmissionStateWaitingForReview, asc.ReviewSubmissionStateInReview:
|
||||
if containsVersion {
|
||||
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
|
||||
return nil
|
||||
}
|
||||
return E.New(string(platform), " review submission ", remoteSubmission.ID, " with another version is ", string(state))
|
||||
default:
|
||||
return E.New(string(platform), " review submission ", remoteSubmission.ID, " is ", string(state))
|
||||
}
|
||||
}
|
||||
if submission.ID == "" {
|
||||
log.Info(string(platform), " ", versionString, " create review submission")
|
||||
created, _, err := client.Submission.CreateReviewSubmission(ctx, appID, common.Ptr(platform))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
submission = created.Data
|
||||
}
|
||||
if !submissionHasItem {
|
||||
log.Info(string(platform), " ", versionString, " add version to review submission ", submission.ID)
|
||||
_, _, err = client.Submission.CreateReviewSubmissionItemForAppStoreVersion(ctx, submission.ID, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " submit review submission ", submission.ID)
|
||||
_, _, err = client.Submission.UpdateReviewSubmission(ctx, submission.ID, common.Ptr(true), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishAppStore(ctx context.Context, platformNames []string) error {
|
||||
if len(platformNames) == 0 {
|
||||
platformNames = appStorePlatforms
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
for _, platformName := range platformNames {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionString, err := build_shared.ReadAppleMarketingVersion(applePath, platformDirectories[platform])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterVersionString: []string{versionString},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(versions.Data) == 0 {
|
||||
return E.New(string(platform), " ", versionString, " not found")
|
||||
}
|
||||
version := versions.Data[0]
|
||||
state := *version.Attributes.AppStoreState
|
||||
switch state {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
|
||||
log.Fatal(string(platform), " ", tag, " not submitted")
|
||||
case asc.AppStoreVersionStateWaitingForReview,
|
||||
asc.AppStoreVersionStateInReview:
|
||||
log.Warn(string(platform), " ", tag, " waiting for review")
|
||||
return E.New(string(platform), " ", versionString, " not submitted")
|
||||
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
|
||||
log.Warn(string(platform), " ", versionString, " waiting for review")
|
||||
continue
|
||||
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
default:
|
||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||
return E.New(string(platform), " ", versionString, " unknown state ", string(state))
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " release")
|
||||
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -454,15 +656,3 @@ func publishAppStore(ctx context.Context) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isRetryable(response *asc.Response) bool {
|
||||
if response == nil {
|
||||
return false
|
||||
}
|
||||
switch response.StatusCode {
|
||||
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,5 +39,5 @@ func ReadTagVersion() (badversion.Version, error) {
|
||||
}
|
||||
|
||||
func TestFlightVersion(version badversion.Version) string {
|
||||
return F.ToString(version.Major, ".", version.Minor, ".10")
|
||||
return F.ToString(version.Major, ".", version.Minor, ".1000")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package build_shared
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
func ReadAppleMarketingVersion(applePath string, directory string) (string, error) {
|
||||
projectFile, err := os.Open(filepath.Join(applePath, "sing-box.xcodeproj", "project.pbxproj"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer projectFile.Close()
|
||||
var project map[string]any
|
||||
err = plist.NewDecoder(projectFile).Decode(&project)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var marketingVersion string
|
||||
for _, object := range project["objects"].(map[string]any) {
|
||||
buildSettings, isMap := object.(map[string]any)["buildSettings"].(map[string]any)
|
||||
if !isMap {
|
||||
continue
|
||||
}
|
||||
infoPlistFile, isString := buildSettings["INFOPLIST_FILE"].(string)
|
||||
if !isString || !strings.HasPrefix(infoPlistFile, directory+"/") {
|
||||
continue
|
||||
}
|
||||
version, isString := buildSettings["MARKETING_VERSION"].(string)
|
||||
if !isString {
|
||||
continue
|
||||
}
|
||||
if marketingVersion != "" && marketingVersion != version {
|
||||
return "", E.New("inconsistent marketing versions in ", directory, ": ", marketingVersion, ", ", version)
|
||||
}
|
||||
marketingVersion = version
|
||||
}
|
||||
if marketingVersion == "" {
|
||||
return "", E.New("marketing version not found in ", directory)
|
||||
}
|
||||
return marketingVersion, nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -30,7 +31,7 @@ func check() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
ctx, cancel := context.WithCancel(service.ExtendContext(globalCtx))
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -136,7 +137,7 @@ func create(options option.Options) (*box.Box, context.CancelFunc, error) {
|
||||
}
|
||||
options.Log.DisableColor = true
|
||||
}
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
ctx, cancel := context.WithCancel(service.ExtendContext(globalCtx))
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/sagernet/sing-box"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -30,7 +31,7 @@ func createPreStartedClient() (*box.Box, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
|
||||
instance, err := box.New(box.Options{Context: service.ExtendContext(globalCtx), Options: options})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create service")
|
||||
}
|
||||
|
||||
@@ -80,3 +80,7 @@ func (c *ReadWaitConn) Upstream() any {
|
||||
func (c *ReadWaitConn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ReadWaitConn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
+42
-33
@@ -31,6 +31,7 @@ type Store struct {
|
||||
certificatePaths []string
|
||||
certificateDirectoryPaths []string
|
||||
watcher *fswatch.Watcher
|
||||
anchorPEM []byte
|
||||
//nolint:unused // populated only on darwin && cgo via the storePlatform embed.
|
||||
platform storePlatform
|
||||
}
|
||||
@@ -92,10 +93,12 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
||||
}
|
||||
store.watcher = watcher
|
||||
}
|
||||
err := store.update()
|
||||
currentPool, anchorPEM, err := store.load()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initializing certificate store")
|
||||
}
|
||||
store.currentPool = currentPool
|
||||
store.anchorPEM = anchorPEM
|
||||
return store, nil
|
||||
}
|
||||
|
||||
@@ -103,31 +106,28 @@ func (s *Store) Name() string {
|
||||
return "certificate"
|
||||
}
|
||||
|
||||
func (s *Store) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
if s.watcher != nil {
|
||||
return s.watcher.Start()
|
||||
func (s *Store) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
switch stage {
|
||||
case adapter.StartStateInitialize:
|
||||
s.access.Lock()
|
||||
err := s.updatePlatformLocked(s.anchorPEM)
|
||||
s.access.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scope.Add(s.closePlatform)
|
||||
case adapter.StartStateStart:
|
||||
if s.watcher != nil {
|
||||
err := s.watcher.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scope.Add(s.watcher.Close)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() error {
|
||||
watcher := s.watcher
|
||||
s.watcher = nil
|
||||
|
||||
var closeErr error
|
||||
if watcher != nil {
|
||||
closeErr = watcher.Close()
|
||||
}
|
||||
platformErr := s.closePlatform()
|
||||
if platformErr != nil {
|
||||
closeErr = platformErr
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func (s *Store) Pool() *x509.CertPool {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
@@ -143,38 +143,50 @@ func (s *Store) ExclusiveAnchors() bool {
|
||||
}
|
||||
|
||||
func (s *Store) update() error {
|
||||
currentPool, err := s.newBasePool()
|
||||
currentPool, anchorPEM, err := s.load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
s.currentPool = currentPool
|
||||
s.anchorPEM = anchorPEM
|
||||
return s.updatePlatformLocked(anchorPEM)
|
||||
}
|
||||
|
||||
func (s *Store) load() (*x509.CertPool, []byte, error) {
|
||||
currentPool, err := s.newBasePool()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pemBuffer := new(bytes.Buffer)
|
||||
switch s.storeType {
|
||||
case C.CertificateStoreMozilla:
|
||||
pemContent := mozillaIncludedPEM()
|
||||
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
|
||||
return E.New("invalid Mozilla included certificate PEM")
|
||||
return nil, nil, E.New("invalid Mozilla included certificate PEM")
|
||||
}
|
||||
appendPEMBlock(pemBuffer, string(pemContent))
|
||||
case C.CertificateStoreChrome:
|
||||
pemContent := chromeIncludedPEM()
|
||||
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
|
||||
return E.New("invalid Chrome included certificate PEM")
|
||||
return nil, nil, E.New("invalid Chrome included certificate PEM")
|
||||
}
|
||||
appendPEMBlock(pemBuffer, string(pemContent))
|
||||
}
|
||||
if s.certificate != "" {
|
||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||
return E.New("invalid certificate PEM strings")
|
||||
return nil, nil, E.New("invalid certificate PEM strings")
|
||||
}
|
||||
appendPEMBlock(pemBuffer, s.certificate)
|
||||
}
|
||||
for _, path := range s.certificatePaths {
|
||||
pemContent, err := filemanager.ReadFile(s.ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||
return E.New("invalid certificate PEM file: ", path)
|
||||
return nil, nil, E.New("invalid certificate PEM file: ", path)
|
||||
}
|
||||
appendPEMBlock(pemBuffer, string(pemContent))
|
||||
}
|
||||
@@ -195,12 +207,9 @@ func (s *Store) update() error {
|
||||
}
|
||||
}
|
||||
if firstErr != nil {
|
||||
return firstErr
|
||||
return nil, nil, firstErr
|
||||
}
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
s.currentPool = currentPool
|
||||
return s.updatePlatformLocked(pemBuffer.Bytes())
|
||||
return currentPool, pemBuffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func appendPEMBlock(buffer *bytes.Buffer, block string) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package cloudflare
|
||||
|
||||
const (
|
||||
ApiUrl = "https://api.cloudflareclient.com"
|
||||
ApiVersion = "v0a4471"
|
||||
ConnectSNI = "consumer-masque.cloudflareclient.com"
|
||||
ApiUrl = "https://api.cloudflareclient.com"
|
||||
ApiVersion = "v0a4471"
|
||||
ConnectSNI = "consumer-masque.cloudflareclient.com"
|
||||
L4ConnectSNI = "consumer-masque-proxy.cloudflareclient.com"
|
||||
// unused for now
|
||||
ZeroTierSNI = "zt-masque.cloudflareclient.com"
|
||||
ConnectURI = "https://cloudflareaccess.com"
|
||||
|
||||
@@ -199,7 +199,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
} else {
|
||||
udpFragment = options.UDPFragmentDefault
|
||||
}
|
||||
if !udpFragment {
|
||||
if udpFragment {
|
||||
dialer.Control = control.Append(dialer.Control, control.EnableUDPFragment())
|
||||
listener.Control = control.Append(listener.Control, control.EnableUDPFragment())
|
||||
} else {
|
||||
dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment())
|
||||
listener.Control = control.Append(listener.Control, control.DisableUDPFragment())
|
||||
}
|
||||
@@ -431,6 +434,15 @@ func (d *DefaultDialer) trackConn(ctx context.Context, destination M.Socksaddr,
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
if nativeConn, isUDPConn := conn.(*net.UDPConn); isUDPConn {
|
||||
var rawConn syscall.RawConn
|
||||
rawConn, err = nativeConn.SyscallConn()
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
conn = &udpConn{Conn: conn, rawConn: rawConn}
|
||||
}
|
||||
if d.connectionManager != nil {
|
||||
conn = d.connectionManager.TrackConn(conn)
|
||||
}
|
||||
@@ -439,9 +451,17 @@ func (d *DefaultDialer) trackConn(ctx context.Context, destination M.Socksaddr,
|
||||
if recorder != nil {
|
||||
recorder.CountConnectionOpened()
|
||||
attribution := d.dialAttribution(ctx, destination)
|
||||
outboundCounter := recorder.TrafficCounter(powerreport.TrafficOutbound, attribution.Outbound)
|
||||
dnsCounter := recorder.TrafficCounter(powerreport.TrafficDNSTransport, attribution.DNS)
|
||||
outboundCounter.CountDial()
|
||||
dnsCounter.CountDial()
|
||||
conn = bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) {
|
||||
outboundCounter.CountIn(n)
|
||||
dnsCounter.CountIn(n)
|
||||
recorder.Touch(powerreport.DirectionInbound, int(n), attribution)
|
||||
}}, []N.CountFunc{func(n int64) {
|
||||
outboundCounter.CountOut(n)
|
||||
dnsCounter.CountOut(n)
|
||||
recorder.Touch(powerreport.DirectionOutbound, int(n), attribution)
|
||||
}})
|
||||
}
|
||||
@@ -461,9 +481,17 @@ func (d *DefaultDialer) trackPacketConn(ctx context.Context, destination M.Socks
|
||||
if recorder != nil {
|
||||
recorder.CountConnectionOpened()
|
||||
attribution := d.dialAttribution(ctx, destination)
|
||||
outboundCounter := recorder.TrafficCounter(powerreport.TrafficOutbound, attribution.Outbound)
|
||||
dnsCounter := recorder.TrafficCounter(powerreport.TrafficDNSTransport, attribution.DNS)
|
||||
outboundCounter.CountDial()
|
||||
dnsCounter.CountDial()
|
||||
conn = bufio.NewNetPacketConn(bufio.NewCounterPacketConn(bufio.NewPacketConn(conn), []N.CountFunc{func(n int64) {
|
||||
outboundCounter.CountIn(n)
|
||||
dnsCounter.CountIn(n)
|
||||
recorder.Touch(powerreport.DirectionInbound, int(n), attribution)
|
||||
}}, []N.CountFunc{func(n int64) {
|
||||
outboundCounter.CountOut(n)
|
||||
dnsCounter.CountOut(n)
|
||||
recorder.Touch(powerreport.DirectionOutbound, int(n), attribution)
|
||||
}}))
|
||||
}
|
||||
|
||||
+62
-64
@@ -63,75 +63,31 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
}
|
||||
}
|
||||
if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||
var defaultOptions adapter.NetworkOptions
|
||||
if networkManager != nil {
|
||||
defaultOptions = networkManager.DefaultOptions()
|
||||
}
|
||||
var (
|
||||
server string
|
||||
dnsQueryOptions adapter.DNSQueryOptions
|
||||
resolveFallbackDelay time.Duration
|
||||
server string
|
||||
dnsQueryOptions adapter.DNSQueryOptions
|
||||
)
|
||||
if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" {
|
||||
var transport adapter.DNSTransport
|
||||
if !options.DirectResolver {
|
||||
var loaded bool
|
||||
transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server)
|
||||
if !loaded {
|
||||
return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server)
|
||||
}
|
||||
hasDomainResolver := dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != ""
|
||||
if options.DirectResolver {
|
||||
if !hasDomainResolver {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
}
|
||||
var strategy C.DomainStrategy
|
||||
if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy)
|
||||
} else if
|
||||
//nolint:staticcheck
|
||||
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
//nolint:staticcheck
|
||||
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
|
||||
}
|
||||
server = dialOptions.DomainResolver.Server
|
||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
Timeout: time.Duration(dialOptions.DomainResolver.Timeout),
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
}
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else if options.DirectResolver {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
dnsQueryOptions = domainResolveQueryOptions(dialOptions.DomainResolver)
|
||||
} else {
|
||||
if defaultOptions.DomainResolver != "" {
|
||||
dnsQueryOptions = defaultOptions.DomainResolveOptions
|
||||
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
|
||||
if !loaded {
|
||||
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
|
||||
}
|
||||
dnsQueryOptions.Transport = transport
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else {
|
||||
transports := dnsTransport.Transports()
|
||||
if len(transports) < 2 {
|
||||
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||
} else if options.NewDialer {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
} else {
|
||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||
}
|
||||
dnsQueryOptions, err = NewDNSQueryOptions(options.Context, dialOptions.DomainResolver, options.NewDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if
|
||||
}
|
||||
if hasDomainResolver {
|
||||
server = dialOptions.DomainResolver.Server
|
||||
}
|
||||
if
|
||||
//nolint:staticcheck
|
||||
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) && (!hasDomainResolver || dialOptions.DomainResolver.Strategy == option.DomainStrategy(C.DomainStrategyAsIS)) {
|
||||
//nolint:staticcheck
|
||||
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
//nolint:staticcheck
|
||||
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
|
||||
}
|
||||
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
|
||||
}
|
||||
dialer = NewResolveDialer(
|
||||
options.Context,
|
||||
@@ -139,12 +95,54 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
dialOptions.Detour == "" && !dialOptions.TCPFastOpen,
|
||||
server,
|
||||
dnsQueryOptions,
|
||||
resolveFallbackDelay,
|
||||
time.Duration(dialOptions.FallbackDelay),
|
||||
)
|
||||
}
|
||||
return dialer, nil
|
||||
}
|
||||
|
||||
func NewDNSQueryOptions(ctx context.Context, domainResolver *option.DomainResolveOptions, newDialer bool) (adapter.DNSQueryOptions, error) {
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](ctx)
|
||||
if domainResolver != nil && domainResolver.Server != "" {
|
||||
transport, loaded := dnsTransport.Transport(domainResolver.Server)
|
||||
if !loaded {
|
||||
return adapter.DNSQueryOptions{}, E.New("domain resolver not found: ", domainResolver.Server)
|
||||
}
|
||||
dnsQueryOptions := domainResolveQueryOptions(domainResolver)
|
||||
dnsQueryOptions.Transport = transport
|
||||
return dnsQueryOptions, nil
|
||||
}
|
||||
defaultOptions := service.FromContext[adapter.NetworkManager](ctx).DefaultOptions()
|
||||
if defaultOptions.DomainResolver != "" {
|
||||
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
|
||||
if !loaded {
|
||||
return adapter.DNSQueryOptions{}, E.New("default domain resolver not found: ", defaultOptions.DomainResolver)
|
||||
}
|
||||
dnsQueryOptions := defaultOptions.DomainResolveOptions
|
||||
dnsQueryOptions.Transport = transport
|
||||
return dnsQueryOptions, nil
|
||||
}
|
||||
if len(dnsTransport.Transports()) < 2 {
|
||||
return adapter.DNSQueryOptions{Transport: dnsTransport.Default()}, nil
|
||||
}
|
||||
if newDialer {
|
||||
return adapter.DNSQueryOptions{}, E.New("missing domain resolver for domain server address")
|
||||
}
|
||||
deprecated.Report(ctx, deprecated.OptionMissingDomainResolver)
|
||||
return adapter.DNSQueryOptions{}, nil
|
||||
}
|
||||
|
||||
func domainResolveQueryOptions(domainResolver *option.DomainResolveOptions) adapter.DNSQueryOptions {
|
||||
return adapter.DNSQueryOptions{
|
||||
Strategy: C.DomainStrategy(domainResolver.Strategy),
|
||||
Timeout: time.Duration(domainResolver.Timeout),
|
||||
DisableCache: domainResolver.DisableCache,
|
||||
DisableOptimisticCache: domainResolver.DisableOptimisticCache,
|
||||
RewriteTTL: domainResolver.RewriteTTL,
|
||||
ClientSubnet: domainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
}
|
||||
}
|
||||
|
||||
type ParallelInterfaceDialer interface {
|
||||
N.Dialer
|
||||
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||
|
||||
+13
-2
@@ -20,6 +20,7 @@ import (
|
||||
type slowOpenConn struct {
|
||||
dialer *tfo.Dialer
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
network string
|
||||
destination M.Socksaddr
|
||||
conn atomic.Pointer[net.TCPConn]
|
||||
@@ -39,9 +40,11 @@ func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, de
|
||||
return dialer.Dialer.DialContext(ctx, network, destination.AddrString())
|
||||
}
|
||||
}
|
||||
dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
|
||||
return &slowOpenConn{
|
||||
dialer: dialer,
|
||||
ctx: ctx,
|
||||
ctx: dialCtx,
|
||||
cancel: cancel,
|
||||
network: network,
|
||||
destination: destination,
|
||||
create: make(chan struct{}),
|
||||
@@ -86,7 +89,14 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
if err != nil {
|
||||
c.err = err
|
||||
} else {
|
||||
c.conn.Store(conn.(*net.TCPConn))
|
||||
select {
|
||||
case <-c.done:
|
||||
conn.Close()
|
||||
err = os.ErrClosed
|
||||
c.err = err
|
||||
default:
|
||||
c.conn.Store(conn.(*net.TCPConn))
|
||||
}
|
||||
}
|
||||
n = len(b)
|
||||
close(c.create)
|
||||
@@ -96,6 +106,7 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
func (c *slowOpenConn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
close(c.done)
|
||||
c.cancel()
|
||||
conn := c.conn.Load()
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type udpConn struct {
|
||||
net.Conn
|
||||
rawConn syscall.RawConn
|
||||
}
|
||||
|
||||
func (c *udpConn) Read(b []byte) (n int, err error) {
|
||||
n, err = c.Conn.Read(b)
|
||||
if n == 0 && err == nil {
|
||||
var isEOF bool
|
||||
c.rawConn.Control(func(fd uintptr) {
|
||||
isEOF = control.IsUDPEOF(fd)
|
||||
})
|
||||
if isEOF {
|
||||
err = io.EOF
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *udpConn) SyscallConn() (syscall.RawConn, error) {
|
||||
return c.rawConn, nil
|
||||
}
|
||||
|
||||
func (c *udpConn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func (c *udpConn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *udpConn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
_ syscall.Conn = (*udpConn)(nil)
|
||||
_ common.WithUpstream = (*udpConn)(nil)
|
||||
_ N.ReaderWithUpstream = (*udpConn)(nil)
|
||||
_ N.WriterWithUpstream = (*udpConn)(nil)
|
||||
)
|
||||
@@ -1,29 +1,33 @@
|
||||
package v2rayhttp
|
||||
package force_close
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type efaceWords struct {
|
||||
typ unsafe.Pointer
|
||||
data unsafe.Pointer
|
||||
}
|
||||
|
||||
func ResetTransport(rawTransport http.RoundTripper) http.RoundTripper {
|
||||
switch transport := rawTransport.(type) {
|
||||
case *http.Transport:
|
||||
transport.CloseIdleConnections()
|
||||
return transport.Clone()
|
||||
case *http2.Transport:
|
||||
closeHTTP2Connections(transport)
|
||||
CloseHTTP2Connections(transport)
|
||||
return transport
|
||||
case *http3.Transport:
|
||||
transport.Close()
|
||||
return transport
|
||||
default:
|
||||
panic(E.New("unknown transport type: ", reflect.TypeOf(transport)))
|
||||
}
|
||||
}
|
||||
|
||||
type efaceWords struct {
|
||||
typ unsafe.Pointer
|
||||
data unsafe.Pointer
|
||||
}
|
||||
+15
-15
@@ -1,6 +1,6 @@
|
||||
//go:build go1.27 && badlinkname
|
||||
|
||||
package v2rayhttp
|
||||
package force_close
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -16,20 +16,7 @@ import (
|
||||
// -checklinkname does not inspect. This declaration must precede the linkname declarations.
|
||||
var _ *http.Transport
|
||||
|
||||
// net/http/internal/http2.Transport
|
||||
type internalTransport struct {
|
||||
t1 [2]uintptr // TransportConfig
|
||||
connPool *clientConnPool
|
||||
}
|
||||
|
||||
// net/http/internal/http2.clientConnPool
|
||||
type clientConnPool struct {
|
||||
t *internalTransport
|
||||
mu sync.Mutex
|
||||
conns map[string][]unsafe.Pointer // key is host:port, value is []*ClientConn
|
||||
}
|
||||
|
||||
func closeHTTP2Connections(transport *http2.Transport) {
|
||||
func CloseHTTP2Connections(transport *http2.Transport) {
|
||||
h2Transport := transportFromH1Transport(transportInit(transport))
|
||||
t := (*internalTransport)((*efaceWords)(unsafe.Pointer(&h2Transport)).data)
|
||||
if t == nil {
|
||||
@@ -45,6 +32,19 @@ func closeHTTP2Connections(transport *http2.Transport) {
|
||||
}
|
||||
}
|
||||
|
||||
// net/http/internal/http2.Transport
|
||||
type internalTransport struct {
|
||||
t1 [2]uintptr // TransportConfig
|
||||
connPool *clientConnPool
|
||||
}
|
||||
|
||||
// net/http/internal/http2.clientConnPool
|
||||
type clientConnPool struct {
|
||||
t *internalTransport
|
||||
mu sync.Mutex
|
||||
conns map[string][]unsafe.Pointer // key is host:port, value is []*ClientConn
|
||||
}
|
||||
|
||||
//go:linkname transportInit golang.org/x/net/http2.(*Transport).init
|
||||
func transportInit(t *http2.Transport) *http.Transport
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
//go:build go1.27 && !badlinkname
|
||||
|
||||
package v2rayhttp
|
||||
package force_close
|
||||
|
||||
import "golang.org/x/net/http2"
|
||||
|
||||
func closeHTTP2Connections(transport *http2.Transport) {
|
||||
func CloseHTTP2Connections(transport *http2.Transport) {
|
||||
transport.CloseIdleConnections()
|
||||
}
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package v2rayhttp
|
||||
package force_close
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@@ -9,13 +9,7 @@ import (
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type clientConnPool struct {
|
||||
t *http2.Transport
|
||||
mu sync.Mutex
|
||||
conns map[string][]*http2.ClientConn // key is host:port
|
||||
}
|
||||
|
||||
func closeHTTP2Connections(transport *http2.Transport) {
|
||||
func CloseHTTP2Connections(transport *http2.Transport) {
|
||||
connPool := transportConnPool(transport)
|
||||
p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data)
|
||||
p.mu.Lock()
|
||||
@@ -27,5 +21,11 @@ func closeHTTP2Connections(transport *http2.Transport) {
|
||||
}
|
||||
}
|
||||
|
||||
type clientConnPool struct {
|
||||
t *http2.Transport
|
||||
mu sync.Mutex
|
||||
conns map[string][]*http2.ClientConn // key is host:port
|
||||
}
|
||||
|
||||
//go:linkname transportConnPool golang.org/x/net/http2.(*Transport).connPool
|
||||
func transportConnPool(t *http2.Transport) http2.ClientConnPool
|
||||
@@ -40,10 +40,12 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string,
|
||||
return nil, err
|
||||
}
|
||||
return &ManagedTransport{
|
||||
dialer: rawDialer,
|
||||
headers: headers,
|
||||
host: host,
|
||||
tag: tag,
|
||||
dialer: rawDialer,
|
||||
headers: headers,
|
||||
host: host,
|
||||
tag: tag,
|
||||
detour: options.Detour,
|
||||
defaultOutbound: options.DefaultOutbound,
|
||||
factory: func() (innerTransport, error) {
|
||||
return newAppleTransport(ctx, logger, rawDialer, options)
|
||||
},
|
||||
@@ -69,11 +71,13 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string,
|
||||
return nil, err
|
||||
}
|
||||
managedTransport := &ManagedTransport{
|
||||
cheapRebuild: cheapRebuild,
|
||||
dialer: rawDialer,
|
||||
headers: headers,
|
||||
host: host,
|
||||
tag: tag,
|
||||
cheapRebuild: cheapRebuild,
|
||||
dialer: rawDialer,
|
||||
headers: headers,
|
||||
host: host,
|
||||
tag: tag,
|
||||
detour: options.Detour,
|
||||
defaultOutbound: options.DefaultOutbound,
|
||||
factory: func() (innerTransport, error) {
|
||||
return newTransport(rawDialer, baseTLSConfig, options)
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
stdTLS "crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -172,41 +173,49 @@ func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Re
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request, authority string) (*http.Response, error) {
|
||||
ctx, cancel := context.WithCancel(request.Context())
|
||||
defer cancel()
|
||||
type result struct {
|
||||
response *http.Response
|
||||
err error
|
||||
h3 bool
|
||||
}
|
||||
results := make(chan result, 2)
|
||||
startRoundTrip := func(request *http.Request, useH3 bool) {
|
||||
request = request.WithContext(ctx)
|
||||
var (
|
||||
response *http.Response
|
||||
err error
|
||||
)
|
||||
if useH3 {
|
||||
response, err = t.h3Transport.RoundTrip(request)
|
||||
} else {
|
||||
response, err = t.h2FallbackRoundTrip(request)
|
||||
}
|
||||
results <- result{response: response, err: err, h3: useH3}
|
||||
var (
|
||||
results = make(chan result, 2)
|
||||
cancels []context.CancelFunc
|
||||
received int
|
||||
)
|
||||
startRoundTrip := func(useH3 bool) {
|
||||
ctx, cancel := context.WithCancel(request.Context())
|
||||
cancels = append(cancels, cancel)
|
||||
raceRequest := cloneRequestForRetry(request).WithContext(ctx)
|
||||
go func() {
|
||||
var (
|
||||
response *http.Response
|
||||
err error
|
||||
)
|
||||
if useH3 {
|
||||
response, err = t.h3Transport.RoundTrip(raceRequest)
|
||||
} else {
|
||||
response, err = t.h2FallbackRoundTrip(raceRequest)
|
||||
}
|
||||
results <- result{response: response, err: err, h3: useH3}
|
||||
}()
|
||||
}
|
||||
goroutines := 1
|
||||
received := 0
|
||||
drainRemaining := func() {
|
||||
cancel()
|
||||
for range goroutines - received {
|
||||
finish := func(winner int) {
|
||||
for index, cancel := range cancels {
|
||||
if index != winner {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
for range len(cancels) - received {
|
||||
go func() {
|
||||
loser := <-results
|
||||
if loser.response != nil && loser.response.Body != nil {
|
||||
if loser.response != nil {
|
||||
loser.response.Body.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
go startRoundTrip(cloneRequestForRetry(request), true)
|
||||
startRoundTrip(true)
|
||||
timer := time.NewTimer(t.fallbackDelay)
|
||||
defer timer.Stop()
|
||||
var (
|
||||
@@ -216,39 +225,40 @@ func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request, autho
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
if goroutines == 1 {
|
||||
goroutines++
|
||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
||||
if len(cancels) == 1 {
|
||||
startRoundTrip(false)
|
||||
}
|
||||
case raceResult := <-results:
|
||||
received++
|
||||
if raceResult.err == nil {
|
||||
winner := 1
|
||||
if raceResult.h3 {
|
||||
winner = 0
|
||||
t.clearH3Broken(authority)
|
||||
}
|
||||
drainRemaining()
|
||||
finish(winner)
|
||||
raceResult.response.Body = &cancelOnCloseBody{ReadCloser: raceResult.response.Body, cancel: cancels[winner]}
|
||||
return raceResult.response, nil
|
||||
}
|
||||
if raceResult.h3 {
|
||||
t.markH3Broken(authority)
|
||||
h3Err = raceResult.err
|
||||
if goroutines == 1 {
|
||||
goroutines++
|
||||
if len(cancels) == 1 {
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
||||
startRoundTrip(false)
|
||||
}
|
||||
} else {
|
||||
fallbackErr = raceResult.err
|
||||
}
|
||||
if received < goroutines {
|
||||
if received < len(cancels) {
|
||||
continue
|
||||
}
|
||||
drainRemaining()
|
||||
finish(-1)
|
||||
switch {
|
||||
case h3Err != nil && fallbackErr != nil:
|
||||
return nil, E.Errors(h3Err, fallbackErr)
|
||||
@@ -261,6 +271,17 @@ func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request, autho
|
||||
}
|
||||
}
|
||||
|
||||
type cancelOnCloseBody struct {
|
||||
io.ReadCloser
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (b *cancelOnCloseBody) Close() error {
|
||||
err := b.ReadCloser.Close()
|
||||
b.cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *http3FallbackTransport) h2FallbackRoundTrip(request *http.Request) (*http.Response, error) {
|
||||
if fallback, isFallback := t.h2Fallback.(*http2FallbackTransport); isFallback {
|
||||
return fallback.roundTrip(request, true)
|
||||
|
||||
@@ -25,10 +25,12 @@ type ManagedTransport struct {
|
||||
factory func() (innerTransport, error)
|
||||
cheapRebuild bool
|
||||
|
||||
dialer N.Dialer
|
||||
headers http.Header
|
||||
host string
|
||||
tag string
|
||||
dialer N.Dialer
|
||||
headers http.Header
|
||||
host string
|
||||
tag string
|
||||
detour string
|
||||
defaultOutbound bool
|
||||
}
|
||||
|
||||
type transportEpoch struct {
|
||||
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var (
|
||||
_ adapter.HTTPClientManager = (*Manager)(nil)
|
||||
_ adapter.LifecycleService = (*Manager)(nil)
|
||||
_ adapter.Referrer = (*Manager)(nil)
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
@@ -59,7 +61,7 @@ func (m *Manager) Name() string {
|
||||
return "http-client"
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
@@ -70,6 +72,7 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
m.defaultTransport = sharedTransport
|
||||
}
|
||||
scope.Add(m.close)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -161,12 +164,9 @@ func (m *Manager) ResetNetwork() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
func (m *Manager) close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.managedTransports == nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
for _, transport := range m.managedTransports {
|
||||
err = E.Append(err, transport.close(), func(err error) error {
|
||||
@@ -177,3 +177,27 @@ func (m *Manager) Close() error {
|
||||
m.sharedTransports = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) References() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
var references []string
|
||||
var needDefault bool
|
||||
for _, transport := range m.managedTransports {
|
||||
if transport.detour != "" {
|
||||
references = append(references, transport.detour)
|
||||
} else if transport.defaultOutbound {
|
||||
needDefault = true
|
||||
}
|
||||
}
|
||||
if needDefault {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](m.ctx)
|
||||
if outboundManager != nil {
|
||||
defaultOutbound := outboundManager.Default()
|
||||
if defaultOutbound != nil {
|
||||
references = append(references, defaultOutbound.Tag())
|
||||
}
|
||||
}
|
||||
}
|
||||
return references
|
||||
}
|
||||
|
||||
@@ -8,26 +8,12 @@ import (
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
/*type GroupedConn interface {
|
||||
MarkAsInternal()
|
||||
}
|
||||
|
||||
func MarkAsInternal(conn any) {
|
||||
if groupedConn, isGroupConn := common.Cast[GroupedConn](conn); isGroupConn {
|
||||
groupedConn.MarkAsInternal()
|
||||
}
|
||||
}*/
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
group *Group
|
||||
element *list.Element[*groupConnItem]
|
||||
}
|
||||
|
||||
/*func (c *Conn) MarkAsInternal() {
|
||||
c.element.Value.internal = true
|
||||
}*/
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.group.access.Lock()
|
||||
c.group.connections.Remove(c.element)
|
||||
@@ -48,20 +34,20 @@ func (c *Conn) Upstream() any {
|
||||
}
|
||||
|
||||
type PacketConn struct {
|
||||
net.PacketConn
|
||||
N.NetPacketConn
|
||||
group *Group
|
||||
element *list.Element[*groupConnItem]
|
||||
}
|
||||
|
||||
/*func (c *PacketConn) MarkAsInternal() {
|
||||
c.element.Value.internal = true
|
||||
}*/
|
||||
func newPacketConn(group *Group, conn net.PacketConn, element *list.Element[*groupConnItem]) *PacketConn {
|
||||
return &PacketConn{NetPacketConn: bufio.NewPacketConn(conn), group: group, element: element}
|
||||
}
|
||||
|
||||
func (c *PacketConn) Close() error {
|
||||
c.group.access.Lock()
|
||||
c.group.connections.Remove(c.element)
|
||||
c.group.access.Unlock()
|
||||
return c.PacketConn.Close()
|
||||
return c.NetPacketConn.Close()
|
||||
}
|
||||
|
||||
func (c *PacketConn) ReaderReplaceable() bool {
|
||||
@@ -73,7 +59,7 @@ func (c *PacketConn) WriterReplaceable() bool {
|
||||
}
|
||||
|
||||
func (c *PacketConn) Upstream() any {
|
||||
return bufio.NewPacketConn(c.PacketConn)
|
||||
return c.NetPacketConn
|
||||
}
|
||||
|
||||
type SingPacketConn struct {
|
||||
|
||||
@@ -35,7 +35,7 @@ func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool, isProvider b
|
||||
g.access.Lock()
|
||||
defer g.access.Unlock()
|
||||
item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
|
||||
return &PacketConn{PacketConn: conn, group: g, element: item}
|
||||
return newPacketConn(g, conn, item)
|
||||
}
|
||||
|
||||
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool, isProvider bool) N.PacketConn {
|
||||
@@ -47,19 +47,17 @@ func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool, isProvider
|
||||
|
||||
func (g *Group) Interrupt(interruptExternalConnections bool) {
|
||||
g.access.Lock()
|
||||
var toDelete []*list.Element[*groupConnItem]
|
||||
var toClose []io.Closer
|
||||
for element := g.connections.Front(); element != nil; element = element.Next() {
|
||||
var closers []io.Closer
|
||||
for element := g.connections.Front(); element != nil; {
|
||||
nextElement := element.Next()
|
||||
if !element.Value.isExternal || interruptExternalConnections {
|
||||
toDelete = append(toDelete, element)
|
||||
toClose = append(toClose, element.Value.conn)
|
||||
closers = append(closers, element.Value.conn)
|
||||
g.connections.Remove(element)
|
||||
}
|
||||
}
|
||||
for _, element := range toDelete {
|
||||
g.connections.Remove(element)
|
||||
element = nextElement
|
||||
}
|
||||
g.access.Unlock()
|
||||
for _, conn := range toClose {
|
||||
_ = conn.Close()
|
||||
for _, closer := range closers {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
+13
-5
@@ -264,16 +264,24 @@ func (j *ClientHello) parseExtensions(exs []byte) error {
|
||||
return &ParseError{LengthErr, 19}
|
||||
}
|
||||
versionsLen := int(sex[0])
|
||||
for i := 0; i < versionsLen; i += 2 {
|
||||
versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:]))
|
||||
sex = sex[versionExtensionHeaderLen:]
|
||||
if len(sex) < versionsLen {
|
||||
return &ParseError{LengthErr, 21}
|
||||
}
|
||||
for i := range versionsLen / 2 {
|
||||
versions = append(versions, binary.BigEndian.Uint16(sex[i*2:]))
|
||||
}
|
||||
case signatureAlgorithmsExtensionType:
|
||||
if len(sex) < signatureAlgorithmsExtensionHeaderLen {
|
||||
return &ParseError{LengthErr, 20}
|
||||
}
|
||||
ssaLen := binary.BigEndian.Uint16(sex)
|
||||
for i := 0; i < int(ssaLen); i += 2 {
|
||||
signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:]))
|
||||
ssaLen := int(binary.BigEndian.Uint16(sex))
|
||||
sex = sex[signatureAlgorithmsExtensionHeaderLen:]
|
||||
if len(sex) < ssaLen {
|
||||
return &ParseError{LengthErr, 22}
|
||||
}
|
||||
for i := range ssaLen / 2 {
|
||||
signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[i*2:]))
|
||||
}
|
||||
}
|
||||
exs = exs[4+exLen:]
|
||||
|
||||
@@ -89,7 +89,7 @@ func (l *Listener) Start() error {
|
||||
if common.Contains(l.network, N.NetworkUDP) {
|
||||
_, err := l.ListenUDP()
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Errors(err, l.Close())
|
||||
}
|
||||
l.packetOutboundClosed = make(chan struct{})
|
||||
l.packetOutbound = make(chan *N.PacketBuffer, 64)
|
||||
@@ -109,11 +109,11 @@ func (l *Listener) Start() error {
|
||||
}
|
||||
systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS, nil)
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize system proxy")
|
||||
return E.Errors(E.Cause(err, "initialize system proxy"), l.Close())
|
||||
}
|
||||
err = systemProxy.Enable()
|
||||
if err != nil {
|
||||
return E.Errors(E.Cause(err, "set system proxy"), systemProxy.Close())
|
||||
return E.Errors(E.Cause(err, "set system proxy"), systemProxy.Close(), l.Close())
|
||||
}
|
||||
l.systemProxy = systemProxy
|
||||
}
|
||||
|
||||
@@ -84,22 +84,32 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
|
||||
func (l *Listener) loopTCPIn() {
|
||||
tcpListener := l.tcpListener
|
||||
var metadata adapter.InboundContext
|
||||
var (
|
||||
metadata adapter.InboundContext
|
||||
acceptDelay time.Duration
|
||||
)
|
||||
for {
|
||||
conn, err := tcpListener.Accept()
|
||||
if err != nil {
|
||||
//nolint:staticcheck
|
||||
if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() {
|
||||
l.logger.Error(err)
|
||||
continue
|
||||
}
|
||||
if l.shutdown.Load() && E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
//nolint:staticcheck
|
||||
if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() {
|
||||
if acceptDelay == 0 {
|
||||
acceptDelay = 5 * time.Millisecond
|
||||
} else {
|
||||
acceptDelay = min(2*acceptDelay, time.Second)
|
||||
}
|
||||
l.logger.Error(err, ", retrying in ", acceptDelay)
|
||||
time.Sleep(acceptDelay)
|
||||
continue
|
||||
}
|
||||
l.tcpListener.Close()
|
||||
l.logger.Error("tcp listener closed: ", err)
|
||||
continue
|
||||
return
|
||||
}
|
||||
acceptDelay = 0
|
||||
//nolint:staticcheck
|
||||
metadata.InboundDetour = l.listenOptions.Detour
|
||||
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
|
||||
|
||||
@@ -42,7 +42,9 @@ func (l *Listener) ListenUDPWithConfig(listenConfig net.ListenConfig) (net.Packe
|
||||
} else {
|
||||
udpFragment = l.listenOptions.UDPFragmentDefault
|
||||
}
|
||||
if !udpFragment {
|
||||
if udpFragment {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.EnableUDPFragment())
|
||||
} else {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment())
|
||||
}
|
||||
if l.tproxy {
|
||||
|
||||
@@ -51,9 +51,9 @@ type clientDialer struct {
|
||||
}
|
||||
|
||||
func (d *clientDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
return d.Dialer.DialContext(adapter.OverrideContext(ctx), network, destination)
|
||||
return d.Dialer.DialContext(adapter.ContextForMultiplexSession(ctx), network, destination)
|
||||
}
|
||||
|
||||
func (d *clientDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return d.Dialer.ListenPacket(adapter.OverrideContext(ctx), destination)
|
||||
return d.Dialer.ListenPacket(adapter.ContextForMultiplexSession(ctx), destination)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ type Manager struct {
|
||||
namespaces []option.NetworkNamespace
|
||||
holderArgs []string
|
||||
paths map[string]string
|
||||
holders []*holder
|
||||
}
|
||||
|
||||
type holder struct {
|
||||
@@ -64,7 +63,7 @@ func (m *Manager) Name() string {
|
||||
return "netns"
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
if stage != adapter.StartStateInitialize {
|
||||
return nil
|
||||
}
|
||||
@@ -80,7 +79,7 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
return E.Cause(err, "network namespace[", namespace.Tag, "]")
|
||||
}
|
||||
case C.NetNsTypeUnshare:
|
||||
err := m.startNamespace(namespace)
|
||||
err := m.startNamespace(namespace, scope)
|
||||
if err != nil {
|
||||
return E.Cause(err, "network namespace[", namespace.Tag, "]")
|
||||
}
|
||||
@@ -89,7 +88,7 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) startNamespace(namespace option.NetworkNamespace) error {
|
||||
func (m *Manager) startNamespace(namespace option.NetworkNamespace, scope *adapter.Scope) error {
|
||||
if len(m.holderArgs) == 0 {
|
||||
return E.New("unshare network namespace is only supported in `sing-box run`")
|
||||
}
|
||||
@@ -97,7 +96,13 @@ func (m *Manager) startNamespace(namespace option.NetworkNamespace) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.holders = append(m.holders, created)
|
||||
scope.Add(func() error {
|
||||
created.pipeWriter.Close()
|
||||
if created.pidFile != "" {
|
||||
os.Remove(created.pidFile)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
pid := created.command.Process.Pid
|
||||
if created.pidFile != "" {
|
||||
err = os.WriteFile(created.pidFile, []byte(strconv.Itoa(pid)+"\n"), 0o644)
|
||||
@@ -115,17 +120,6 @@ func (m *Manager) startNamespace(namespace option.NetworkNamespace) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
for _, created := range m.holders {
|
||||
created.pipeWriter.Close()
|
||||
if created.pidFile != "" {
|
||||
os.Remove(created.pidFile)
|
||||
}
|
||||
}
|
||||
m.holders = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) ResolvePath(nameOrPath string) string {
|
||||
path, loaded := m.paths[nameOrPath]
|
||||
if loaded && path != "" {
|
||||
|
||||
@@ -22,11 +22,7 @@ func (m *Manager) Name() string {
|
||||
return "netns"
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ package netns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
@@ -39,11 +41,11 @@ func TestUnshareNamespace(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = manager.Start(adapter.StartStateInitialize)
|
||||
scope := adapter.NewScope(context.Background(), log.NewNOPFactory().Logger())
|
||||
err = manager.Start(adapter.StartStateInitialize, scope)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
pipeReader.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
pidLine, err := bufio.NewReader(pipeReader).ReadString('\n')
|
||||
@@ -71,7 +73,7 @@ func TestUnshareNamespace(t *testing.T) {
|
||||
t.Fatal("holder is in the current netns")
|
||||
}
|
||||
|
||||
err = manager.Close()
|
||||
err = scope.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ import (
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
)
|
||||
|
||||
const pathProc = "/proc"
|
||||
const (
|
||||
pathProc = "/proc"
|
||||
|
||||
processPathsAllUsers = ^uint32(0)
|
||||
)
|
||||
|
||||
var _ Searcher = (*linuxSearcher)(nil)
|
||||
|
||||
@@ -101,6 +105,8 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
source = netip.AddrPortFrom(source.Addr().Unmap(), source.Port())
|
||||
destination = netip.AddrPortFrom(destination.Addr().Unmap(), destination.Port())
|
||||
family, protocol, err := socketDiagSettings(network, source)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
@@ -118,28 +124,33 @@ func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.Addr
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
return querySocketDiagOnce(family, protocol, source)
|
||||
return dumpSocketDiag(family, protocol, source, destination)
|
||||
}
|
||||
|
||||
// The socket keeps the uid it was created with, while /proc reflects the
|
||||
// current uid of the process, so a socket created before a privilege drop
|
||||
// only appears under a scan of all users.
|
||||
func (s *linuxSearcher) findProcessPath(targetInode, uid uint32) (string, error) {
|
||||
if cached, ok := s.processPathCache.Get(uid); ok {
|
||||
if processPath, found := cached.entries[targetInode]; found {
|
||||
for _, scanUID := range []uint32{uid, processPathsAllUsers} {
|
||||
if cached, ok := s.processPathCache.Get(scanUID); ok {
|
||||
if processPath, found := cached.entries[targetInode]; found {
|
||||
return processPath, nil
|
||||
}
|
||||
}
|
||||
processPaths, err := buildProcessPaths(scanUID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.processPathCache.Add(scanUID, &uidProcessPaths{entries: processPaths})
|
||||
processPath, found := processPaths[targetInode]
|
||||
if found {
|
||||
return processPath, nil
|
||||
}
|
||||
}
|
||||
processPaths, err := buildProcessPathsByUID(uid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.processPathCache.Add(uid, &uidProcessPaths{entries: processPaths})
|
||||
processPath, found := processPaths[targetInode]
|
||||
if !found {
|
||||
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
|
||||
}
|
||||
return processPath, nil
|
||||
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
|
||||
}
|
||||
|
||||
func buildProcessPathsByUID(uid uint32) (map[uint32]string, error) {
|
||||
func buildProcessPaths(uid uint32) (map[uint32]string, error) {
|
||||
files, err := os.ReadDir(pathProc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -157,7 +168,7 @@ func buildProcessPathsByUID(uid uint32) (map[uint32]string, error) {
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
if uid != processPathsAllUsers && info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
continue
|
||||
}
|
||||
processPath := filepath.Join(pathProc, file.Name())
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -19,6 +20,7 @@ const (
|
||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
|
||||
socketDiagResponseMinSize = 72
|
||||
socketDiagByFamily = 20
|
||||
socketDiagTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
type socketDiagConn struct {
|
||||
@@ -28,6 +30,13 @@ type socketDiagConn struct {
|
||||
fd int
|
||||
}
|
||||
|
||||
type socketDiagEntry struct {
|
||||
source netip.AddrPort
|
||||
destination netip.AddrPort
|
||||
uid uint32
|
||||
inode uint32
|
||||
}
|
||||
|
||||
func socketDiagConnIndex(family, protocol uint8) int {
|
||||
index := 0
|
||||
if protocol == syscall.IPPROTO_UDP {
|
||||
@@ -83,15 +92,6 @@ func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) {
|
||||
fd, err := openSocketDiag()
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "dial netlink")
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
|
||||
}
|
||||
|
||||
func (c *socketDiagConn) ensureOpenLocked() error {
|
||||
if c.fd != -1 {
|
||||
return nil
|
||||
@@ -109,12 +109,12 @@ func openSocketDiag() (int, error) {
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
timeout := &syscall.Timeval{Usec: 100}
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil {
|
||||
timeout := syscall.NsecToTimeval(socketDiagTimeout.Nanoseconds())
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &timeout); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil {
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &timeout); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
@@ -196,36 +196,132 @@ func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) {
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "parse netlink message")
|
||||
}
|
||||
return unpackSocketDiagMessages(messages)
|
||||
}
|
||||
|
||||
func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) {
|
||||
for _, message := range messages {
|
||||
switch message.Header.Type {
|
||||
case syscall.NLMSG_DONE:
|
||||
continue
|
||||
case syscall.NLMSG_ERROR:
|
||||
err = unpackSocketDiagError(&message)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
case socketDiagByFamily:
|
||||
inode, uid = unpackSocketDiagResponse(&message)
|
||||
if inode != 0 || uid != 0 {
|
||||
return inode, uid, nil
|
||||
entry, valid := unpackSocketDiagEntry(&message)
|
||||
if valid && (entry.inode != 0 || entry.uid != 0) {
|
||||
return entry.inode, entry.uid, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
}
|
||||
|
||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||
if len(msg.Data) < socketDiagResponseMinSize {
|
||||
return 0, 0
|
||||
// The dump only filters by port (inet_diag_dump_icsk, udp_dump), so every socket
|
||||
// in the namespace with the requested local port is returned regardless of address.
|
||||
// A dual-stack socket carrying a v4-mapped address is only listed under AF_INET6.
|
||||
func dumpSocketDiag(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
families := []uint8{family}
|
||||
if family == syscall.AF_INET {
|
||||
families = append(families, syscall.AF_INET6)
|
||||
}
|
||||
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
|
||||
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
|
||||
return inode, uid
|
||||
for _, dumpFamily := range families {
|
||||
inode, uid, err = dumpSocketDiagFamily(dumpFamily, protocol, source, destination)
|
||||
if err == nil || !errors.Is(err, ErrNotFound) {
|
||||
return inode, uid, err
|
||||
}
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
}
|
||||
|
||||
func dumpSocketDiagFamily(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
fd, err := openSocketDiag()
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "dial netlink")
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
_, err = syscall.Write(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "write netlink request")
|
||||
}
|
||||
var (
|
||||
localMatch socketDiagEntry
|
||||
hasLocalMatch bool
|
||||
wildcardMatch socketDiagEntry
|
||||
hasWildcardMatch bool
|
||||
buffer = make([]byte, 64<<10)
|
||||
n int
|
||||
messages []syscall.NetlinkMessage
|
||||
)
|
||||
for {
|
||||
n, err = syscall.Read(fd, buffer)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "read netlink response")
|
||||
}
|
||||
messages, err = syscall.ParseNetlinkMessage(buffer[:n])
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "parse netlink message")
|
||||
}
|
||||
if len(messages) == 0 {
|
||||
return 0, 0, E.New("empty netlink response")
|
||||
}
|
||||
for _, message := range messages {
|
||||
switch message.Header.Type {
|
||||
case syscall.NLMSG_DONE:
|
||||
if hasLocalMatch {
|
||||
return localMatch.inode, localMatch.uid, nil
|
||||
}
|
||||
if hasWildcardMatch {
|
||||
return wildcardMatch.inode, wildcardMatch.uid, nil
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
case syscall.NLMSG_ERROR:
|
||||
err = unpackSocketDiagError(&message)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
case socketDiagByFamily:
|
||||
entry, valid := unpackSocketDiagEntry(&message)
|
||||
if !valid || (entry.inode == 0 && entry.uid == 0) || entry.source.Port() != source.Port() {
|
||||
continue
|
||||
}
|
||||
if entry.source.Addr() == source.Addr() && (!destination.IsValid() || entry.destination == destination) {
|
||||
return entry.inode, entry.uid, nil
|
||||
}
|
||||
if protocol != syscall.IPPROTO_UDP {
|
||||
continue
|
||||
}
|
||||
if !hasLocalMatch && entry.source.Addr() == source.Addr() {
|
||||
hasLocalMatch = true
|
||||
localMatch = entry
|
||||
}
|
||||
if !hasWildcardMatch && entry.source.Addr().IsUnspecified() {
|
||||
hasWildcardMatch = true
|
||||
wildcardMatch = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unpackSocketDiagEntry(msg *syscall.NetlinkMessage) (socketDiagEntry, bool) {
|
||||
if len(msg.Data) < socketDiagResponseMinSize {
|
||||
return socketDiagEntry{}, false
|
||||
}
|
||||
data := msg.Data
|
||||
var sourceAddr, destinationAddr netip.Addr
|
||||
switch data[0] {
|
||||
case syscall.AF_INET:
|
||||
sourceAddr = netip.AddrFrom4([4]byte(data[8:12]))
|
||||
destinationAddr = netip.AddrFrom4([4]byte(data[24:28]))
|
||||
case syscall.AF_INET6:
|
||||
sourceAddr = netip.AddrFrom16([16]byte(data[8:24])).Unmap()
|
||||
destinationAddr = netip.AddrFrom16([16]byte(data[24:40])).Unmap()
|
||||
default:
|
||||
return socketDiagEntry{}, false
|
||||
}
|
||||
return socketDiagEntry{
|
||||
source: netip.AddrPortFrom(sourceAddr, binary.BigEndian.Uint16(data[4:6])),
|
||||
destination: netip.AddrPortFrom(destinationAddr, binary.BigEndian.Uint16(data[6:8])),
|
||||
uid: binary.NativeEndian.Uint32(data[64:68]),
|
||||
inode: binary.NativeEndian.Uint32(data[68:72]),
|
||||
}, true
|
||||
}
|
||||
|
||||
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//go:build linux
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func socketInode(t *testing.T, conn syscall.Conn) uint32 {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
require.NoError(t, err)
|
||||
var inode uint32
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
var stat syscall.Stat_t
|
||||
require.NoError(t, syscall.Fstat(int(fd), &stat))
|
||||
inode = uint32(stat.Ino)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return inode
|
||||
}
|
||||
|
||||
func TestDumpSocketDiagTCP(t *testing.T) {
|
||||
t.Parallel()
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
first, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2)}}).Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer first.Close()
|
||||
second, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(M.AddrPortFromNet(first.LocalAddr()).Port())}}).Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer second.Close()
|
||||
destination := M.AddrPortFromNet(listener.Addr())
|
||||
for _, conn := range []net.Conn{second, first} {
|
||||
inode, uid, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), destination)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode)
|
||||
require.Equal(t, uint32(os.Getuid()), uid)
|
||||
inode, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode)
|
||||
}
|
||||
_, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(first.LocalAddr()).Port()), destination)
|
||||
require.ErrorIs(t, err, ErrNotFound)
|
||||
}
|
||||
|
||||
func TestDumpSocketDiagUDP(t *testing.T) {
|
||||
t.Parallel()
|
||||
first, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 2)})
|
||||
require.NoError(t, err)
|
||||
defer first.Close()
|
||||
port := M.AddrPortFromNet(first.LocalAddr()).Port()
|
||||
second, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(port)})
|
||||
require.NoError(t, err)
|
||||
defer second.Close()
|
||||
for _, conn := range []*net.UDPConn{second, first} {
|
||||
inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, socketInode(t, conn), inode)
|
||||
}
|
||||
_, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), port), netip.AddrPort{})
|
||||
require.ErrorIs(t, err, ErrNotFound)
|
||||
|
||||
wildcard, err := net.ListenUDP("udp4", &net.UDPAddr{})
|
||||
require.NoError(t, err)
|
||||
defer wildcard.Close()
|
||||
inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(wildcard.LocalAddr()).Port()), netip.AddrPort{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, socketInode(t, wildcard), inode)
|
||||
}
|
||||
|
||||
func TestLinuxSearcherFindProcessInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
searcher, err := NewSearcher(Config{Logger: log.NewNOPFactory().NewLogger("test")})
|
||||
require.NoError(t, err)
|
||||
defer searcher.Close()
|
||||
executable, err := os.Executable()
|
||||
require.NoError(t, err)
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
tcpConn, err := net.Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer tcpConn.Close()
|
||||
info, err := searcher.FindProcessInfo(context.Background(), N.NetworkTCP, M.AddrPortFromNet(tcpConn.LocalAddr()), M.AddrPortFromNet(listener.Addr()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, executable, info.ProcessPath)
|
||||
require.Equal(t, int32(os.Getuid()), info.UserId)
|
||||
|
||||
udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{})
|
||||
require.NoError(t, err)
|
||||
defer udpConn.Close()
|
||||
info, err = searcher.FindProcessInfo(context.Background(), N.NetworkUDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), M.AddrPortFromNet(udpConn.LocalAddr()).Port()), netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 53))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, executable, info.ProcessPath)
|
||||
}
|
||||
@@ -23,6 +23,6 @@ func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Rea
|
||||
}
|
||||
}
|
||||
metadata.Protocol = C.ProtocolHTTP
|
||||
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||
metadata.Domain = M.ParseSocksaddr(request.Host).Fqdn
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -245,6 +245,9 @@ func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, pack
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if length > uint64(decryptedReader.Len()) {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
index := len(decrypted) - decryptedReader.Len()
|
||||
fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]})
|
||||
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
|
||||
@@ -291,7 +294,7 @@ func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, pack
|
||||
find:
|
||||
for {
|
||||
for _, fragment := range fragments {
|
||||
if fragment.offset == index {
|
||||
if fragment.offset == index && fragment.length > 0 {
|
||||
buffer.Write(fragment.payload)
|
||||
index = fragment.offset + fragment.length
|
||||
length++
|
||||
|
||||
+6
-1
@@ -49,9 +49,14 @@ func (w *acmeWrapper) Start() error {
|
||||
Logger: w.zapLogger,
|
||||
})
|
||||
config = certmagic.New(cache, *config)
|
||||
err := config.ManageSync(w.ctx, w.domain)
|
||||
if err != nil {
|
||||
cache.Stop()
|
||||
return err
|
||||
}
|
||||
w.cfg = config
|
||||
w.cache = cache
|
||||
return w.cfg.ManageSync(w.ctx, w.domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *acmeWrapper) Close() error {
|
||||
|
||||
@@ -45,10 +45,11 @@ import (
|
||||
var _ ConfigCompat = (*RealityClientConfig)(nil)
|
||||
|
||||
type RealityClientConfig struct {
|
||||
ctx context.Context
|
||||
uClient *UTLSClientConfig
|
||||
publicKey []byte
|
||||
shortID [8]byte
|
||||
ctx context.Context
|
||||
uClient *UTLSClientConfig
|
||||
publicKey []byte
|
||||
shortID [8]byte
|
||||
supportX25519MLKEM768 bool
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
@@ -84,7 +85,7 @@ func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAd
|
||||
return nil, E.New("invalid short_id")
|
||||
}
|
||||
|
||||
var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}
|
||||
var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID, options.Reality.SupportX25519MLKEM768}
|
||||
if options.KernelRx || options.KernelTx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
@@ -145,22 +146,24 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, extension := range uConn.Extensions {
|
||||
if ce, ok := extension.(*utls.SupportedCurvesExtension); ok {
|
||||
ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool {
|
||||
return curveID != utls.X25519MLKEM768
|
||||
})
|
||||
if !e.supportX25519MLKEM768 {
|
||||
for _, extension := range uConn.Extensions {
|
||||
if ce, ok := extension.(*utls.SupportedCurvesExtension); ok {
|
||||
ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool {
|
||||
return curveID != utls.X25519MLKEM768
|
||||
})
|
||||
}
|
||||
if ks, ok := extension.(*utls.KeyShareExtension); ok {
|
||||
ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool {
|
||||
return share.Group != utls.X25519MLKEM768
|
||||
})
|
||||
}
|
||||
}
|
||||
if ks, ok := extension.(*utls.KeyShareExtension); ok {
|
||||
ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool {
|
||||
return share.Group != utls.X25519MLKEM768
|
||||
})
|
||||
err = uConn.BuildHandshakeState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err = uConn.BuildHandshakeState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(uConfig.NextProtos) > 0 {
|
||||
for _, extension := range uConn.Extensions {
|
||||
@@ -271,6 +274,7 @@ func (e *RealityClientConfig) Clone() Config {
|
||||
e.uClient.Clone().(*UTLSClientConfig),
|
||||
e.publicKey,
|
||||
e.shortID,
|
||||
e.supportX25519MLKEM768,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,5 +347,5 @@ func (c *realityClientConnWrapper) ReaderReplaceable() bool {
|
||||
}
|
||||
|
||||
func (c *realityClientConnWrapper) WriterReplaceable() bool {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -254,5 +254,5 @@ func (c *realityConnWrapper) ReaderReplaceable() bool {
|
||||
}
|
||||
|
||||
func (c *realityConnWrapper) WriterReplaceable() bool {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -57,21 +57,26 @@ func (p *sharedCertificateProvider) GetACMENextProtos() []string {
|
||||
}
|
||||
|
||||
type inlineCertificateProvider struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
provider adapter.CertificateProviderService
|
||||
scope *adapter.Scope
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) Start() error {
|
||||
p.scope = adapter.NewScope(p.ctx, p.logger)
|
||||
name := "certificate-provider/" + p.provider.Type()
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err := adapter.LegacyStart(p.provider, stage)
|
||||
err := p.scope.Start(name, p.provider, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Errors(err, p.scope.Close())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) Close() error {
|
||||
return p.provider.Close()
|
||||
return p.scope.Close()
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
@@ -548,6 +553,8 @@ func newCertificateProvider(ctx context.Context, logger log.ContextLogger, optio
|
||||
return nil, E.Cause(err, "create inline certificate provider")
|
||||
}
|
||||
return &inlineCertificateProvider{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
provider: provider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func NewConn(conn net.Conn, ctx context.Context, splitPacket bool, splitRecord b
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
if !c.firstPacketWritten {
|
||||
if !c.firstPacketWritten && len(b) > 0 {
|
||||
defer func() {
|
||||
c.firstPacketWritten = true
|
||||
}()
|
||||
|
||||
@@ -45,7 +45,6 @@ type Manager struct {
|
||||
|
||||
eventSubscriber *observable.Subscriber[ConnectionEvent]
|
||||
eventObserver *observable.Observer[ConnectionEvent]
|
||||
cleaner *cleanup.Cleaner
|
||||
}
|
||||
|
||||
func NewManager(outbound adapter.OutboundManager) *Manager {
|
||||
@@ -59,20 +58,15 @@ func (m *Manager) Name() string {
|
||||
return "traffic manager"
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
if stage == adapter.StartStateInitialize {
|
||||
m.eventObserver = observable.NewObserver(m.eventSubscriber, 64)
|
||||
m.cleaner = cleanup.Add(m.Clear)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
if m.cleaner != nil {
|
||||
m.cleaner.Close()
|
||||
}
|
||||
if m.eventObserver != nil {
|
||||
return m.eventObserver.Close()
|
||||
scope.Add(m.eventObserver.Close)
|
||||
cleaner := cleanup.Add(m.Clear)
|
||||
scope.Add(func() error {
|
||||
cleaner.Close()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build with_quic
|
||||
|
||||
package urltest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-quic"
|
||||
)
|
||||
|
||||
func contextWithQUICKeepSession(ctx context.Context) context.Context {
|
||||
return qtls.ContextWithKeepSession(ctx)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !with_quic
|
||||
|
||||
package urltest
|
||||
|
||||
import "context"
|
||||
|
||||
func contextWithQUICKeepSession(ctx context.Context) context.Context {
|
||||
return ctx
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-mux"
|
||||
"github.com/sagernet/sing-snell"
|
||||
"github.com/sagernet/sing/common"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -81,7 +83,11 @@ func (s *HistoryStorage) Close() error {
|
||||
func URLTest(ctx context.Context, link string, detour N.Dialer) (uint16, error) {
|
||||
multiplexOutbound, isMultiplexOutbound := common.Cast[adapter.OutboundWithMultiplex](detour)
|
||||
if isMultiplexOutbound && multiplexOutbound.MultiplexEnabled() {
|
||||
_, err := urlTest(ctx, link, detour)
|
||||
warmContext := adapter.ContextWithKeepSession(ctx)
|
||||
warmContext = mux.ContextWithKeepSession(warmContext)
|
||||
warmContext = contextWithQUICKeepSession(warmContext)
|
||||
warmContext = snell.ContextWithKeepSession(warmContext)
|
||||
_, err := urlTest(warmContext, link, detour)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build !(ios && tvos)
|
||||
|
||||
package constant
|
||||
|
||||
const IsTvOS = false
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build ios && tvos
|
||||
|
||||
package constant
|
||||
|
||||
const IsTvOS = true
|
||||
@@ -3,8 +3,6 @@ package daemon
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
const defaultAttachedLogMaxLines = 3000
|
||||
@@ -20,9 +18,9 @@ func NewAttachedService(ctx context.Context) *StartedService {
|
||||
s.serviceStatus = &ServiceStatus{Status: ServiceStatus_STARTED}
|
||||
s.startedAt = time.Now()
|
||||
instance.urlTestHistoryStorage.AddUpdateHook(s.urlTestSubscriber)
|
||||
if instance.clashServer != nil {
|
||||
instance.clashServer.AddModeUpdateHook(s.clashModeSubscriber)
|
||||
if instance.clashMode != nil {
|
||||
instance.clashMode.AddUpdateHook(s.clashModeSubscriber)
|
||||
}
|
||||
instance.logFactory.(log.ObservableFactory).AttachPlatformWriter(s)
|
||||
instance.logFactory.AttachPlatformWriter(s)
|
||||
return s
|
||||
}
|
||||
|
||||
+34
-4
@@ -9,13 +9,16 @@ import (
|
||||
"github.com/sagernet/sing-box/common/trafficcontrol"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/clashmode"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/experimental/locale"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/service/powerreport"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
@@ -25,10 +28,11 @@ type Instance struct {
|
||||
cancel context.CancelFunc
|
||||
instance *box.Box
|
||||
connectionManager adapter.ConnectionManager
|
||||
clashServer adapter.ClashServer
|
||||
clashMode *clashmode.Manager
|
||||
trafficManager *trafficcontrol.Manager
|
||||
cacheFile adapter.CacheFile
|
||||
pauseManager pause.Manager
|
||||
pauseCallback *list.Element[pause.Callback]
|
||||
urlTestHistoryStorage *urltest.HistoryStorage
|
||||
outboundManager adapter.OutboundManager
|
||||
endpointManager adapter.EndpointManager
|
||||
@@ -38,6 +42,7 @@ type Instance struct {
|
||||
func (s *StartedService) CheckConfig(ctx context.Context, configContent string) error {
|
||||
selectedLocale := locale.FromContext(ctx)
|
||||
ctx, _ = locale.ContextWithLocale(s.ctx, selectedLocale.Locale)
|
||||
ctx = service.ExtendContext(ctx)
|
||||
options, err := parseConfig(ctx, configContent)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -77,7 +82,7 @@ type OverrideOptions struct {
|
||||
ExcludePackage []string
|
||||
}
|
||||
|
||||
func (s *StartedService) newInstance(ctx context.Context, profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
|
||||
func (s *StartedService) newInstance(ctx context.Context, profileContent string, overrideOptions *OverrideOptions, reloading bool) (*Instance, error) {
|
||||
selectedLocale := locale.FromContext(ctx)
|
||||
ctx, _ = locale.ContextWithLocale(s.ctx, selectedLocale.Locale)
|
||||
ctx = service.ExtendContext(ctx)
|
||||
@@ -134,9 +139,10 @@ func (s *StartedService) newInstance(ctx context.Context, profileContent string,
|
||||
}
|
||||
i.instance = boxInstance
|
||||
i.connectionManager = service.FromContext[adapter.ConnectionManager](ctx)
|
||||
i.clashServer = service.FromContext[adapter.ClashServer](ctx)
|
||||
i.clashMode = service.PtrFromContext[clashmode.Manager](ctx)
|
||||
i.trafficManager = service.PtrFromContext[trafficcontrol.Manager](ctx)
|
||||
i.pauseManager = service.FromContext[pause.Manager](ctx)
|
||||
i.registerPowerReport(ctx, reloading)
|
||||
i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
|
||||
i.outboundManager = service.FromContext[adapter.OutboundManager](ctx)
|
||||
i.endpointManager = service.FromContext[adapter.EndpointManager](ctx)
|
||||
@@ -149,7 +155,7 @@ func attachInstance(ctx context.Context) *Instance {
|
||||
return &Instance{
|
||||
ctx: ctx,
|
||||
connectionManager: service.FromContext[adapter.ConnectionManager](ctx),
|
||||
clashServer: service.FromContext[adapter.ClashServer](ctx),
|
||||
clashMode: service.PtrFromContext[clashmode.Manager](ctx),
|
||||
trafficManager: service.PtrFromContext[trafficcontrol.Manager](ctx),
|
||||
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||
cacheFile: service.FromContext[adapter.CacheFile](ctx),
|
||||
@@ -166,10 +172,34 @@ func (i *Instance) Start() error {
|
||||
|
||||
func (i *Instance) Close() error {
|
||||
i.cancel()
|
||||
if i.pauseCallback != nil {
|
||||
i.pauseManager.UnregisterCallback(i.pauseCallback)
|
||||
i.pauseCallback = nil
|
||||
}
|
||||
i.urlTestHistoryStorage.Close()
|
||||
return i.instance.Close()
|
||||
}
|
||||
|
||||
func (i *Instance) registerPowerReport(ctx context.Context, reloading bool) {
|
||||
powerManager := service.FromContext[*powerreport.Manager](ctx)
|
||||
if powerManager == nil {
|
||||
return
|
||||
}
|
||||
recorder := powerManager.Recorder()
|
||||
if recorder != nil {
|
||||
recorder.RecordServiceStart(i.instance.CreatedAt(), reloading)
|
||||
}
|
||||
if i.pauseManager == nil {
|
||||
return
|
||||
}
|
||||
i.pauseCallback = i.pauseManager.RegisterCallback(func(event int) {
|
||||
recorder := powerManager.Recorder()
|
||||
if recorder != nil {
|
||||
recorder.RecordPauseEvent(event)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Instance) Box() *box.Box {
|
||||
return i.instance
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
type logRing struct {
|
||||
entries []*log.Entry
|
||||
maxLines int
|
||||
start int
|
||||
}
|
||||
|
||||
func (r *logRing) push(entry *log.Entry) {
|
||||
if r.maxLines <= 0 {
|
||||
return
|
||||
}
|
||||
if len(r.entries) < r.maxLines {
|
||||
r.entries = append(r.entries, entry)
|
||||
return
|
||||
}
|
||||
r.entries[r.start] = entry
|
||||
r.start++
|
||||
if r.start == len(r.entries) {
|
||||
r.start = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (r *logRing) array() []*log.Entry {
|
||||
result := make([]*log.Entry, 0, len(r.entries))
|
||||
result = append(result, r.entries[r.start:]...)
|
||||
result = append(result, r.entries[:r.start]...)
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *logRing) reset() {
|
||||
clear(r.entries)
|
||||
r.entries = r.entries[:0]
|
||||
r.start = 0
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -18,20 +17,20 @@ var _ ManagedServiceServer = (*ManagedService)(nil)
|
||||
type ManagedService struct {
|
||||
handler ManagedHandler
|
||||
debug bool
|
||||
oomReporter oomkiller.OOMReporter
|
||||
oomRecorder *oomkiller.Recorder
|
||||
}
|
||||
|
||||
type ManagedServiceOptions struct {
|
||||
Handler ManagedHandler
|
||||
Debug bool
|
||||
OOMReporter oomkiller.OOMReporter
|
||||
OOMRecorder *oomkiller.Recorder
|
||||
}
|
||||
|
||||
func NewManagedService(options ManagedServiceOptions) *ManagedService {
|
||||
return &ManagedService{
|
||||
handler: options.Handler,
|
||||
debug: options.Debug,
|
||||
oomReporter: options.OOMReporter,
|
||||
oomRecorder: options.OOMRecorder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,10 +86,10 @@ func (s *ManagedService) TriggerDebugCrash(ctx context.Context, request *DebugCr
|
||||
}
|
||||
|
||||
func (s *ManagedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
if s.oomReporter == nil {
|
||||
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
|
||||
if s.oomRecorder == nil {
|
||||
return nil, status.Error(codes.Unavailable, "OOM recorder not available")
|
||||
}
|
||||
return &emptypb.Empty{}, s.oomReporter.WriteReport(memory.Total())
|
||||
return &emptypb.Empty{}, s.oomRecorder.WriteReport()
|
||||
}
|
||||
|
||||
func (s *ManagedService) mustEmbedUnimplementedManagedServiceServer() {
|
||||
|
||||
+28
-29
@@ -20,10 +20,10 @@ import (
|
||||
"github.com/sagernet/sing-box/experimental/locale"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/protocol/group"
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
@@ -47,7 +47,6 @@ type StartedService struct {
|
||||
// platform adapter.PlatformInterface
|
||||
handler PlatformHandler
|
||||
debug bool
|
||||
logMaxLines int
|
||||
oomKillerEnabled bool
|
||||
oomKillerDisabled bool
|
||||
oomMemoryLimit uint64
|
||||
@@ -64,7 +63,7 @@ type StartedService struct {
|
||||
serviceStatusSubscriber *observable.Subscriber[*ServiceStatus]
|
||||
serviceStatusObserver *observable.Observer[*ServiceStatus]
|
||||
logAccess sync.RWMutex
|
||||
logLines list.List[*log.Entry]
|
||||
logLines logRing
|
||||
logSubscriber *observable.Subscriber[*log.Entry]
|
||||
logObserver *observable.Observer[*log.Entry]
|
||||
instance *Instance
|
||||
@@ -99,7 +98,7 @@ func NewStartedService(options ServiceOptions) *StartedService {
|
||||
// platform: options.Platform,
|
||||
handler: options.Handler,
|
||||
debug: options.Debug,
|
||||
logMaxLines: options.LogMaxLines,
|
||||
logLines: logRing{maxLines: options.LogMaxLines},
|
||||
oomKillerEnabled: options.OOMKillerEnabled,
|
||||
oomKillerDisabled: options.OOMKillerDisabled,
|
||||
oomMemoryLimit: options.OOMMemoryLimit,
|
||||
@@ -140,7 +139,7 @@ func (s *StartedService) GetVersion(ctx context.Context, empty *emptypb.Empty) (
|
||||
|
||||
func (s *StartedService) resetLogs() {
|
||||
s.logAccess.Lock()
|
||||
s.logLines = list.List[*log.Entry]{}
|
||||
s.logLines.reset()
|
||||
s.logAccess.Unlock()
|
||||
s.logSubscriber.Emit(nil)
|
||||
}
|
||||
@@ -261,15 +260,22 @@ func (s *StartedService) StartOrReloadService(ctx context.Context, profileConten
|
||||
s.instance = nil
|
||||
s.updateStatus(ServiceStatus_STOPPING)
|
||||
s.serviceAccess.Unlock()
|
||||
oomRecorder := service.FromContext[*oomkiller.Recorder](s.ctx)
|
||||
if oomRecorder != nil {
|
||||
oomRecorder.BeginReload()
|
||||
defer oomRecorder.EndReload()
|
||||
}
|
||||
_ = oldInstance.Close()
|
||||
runtimeDebug.FreeOSMemory()
|
||||
s.serviceAccess.Lock()
|
||||
}
|
||||
s.startInterrupted = false
|
||||
s.updateStatus(ServiceStatus_STARTING)
|
||||
s.resetLogs()
|
||||
if oldInstance == nil {
|
||||
s.resetLogs()
|
||||
}
|
||||
s.serviceAccess.Unlock()
|
||||
instance, err := s.newInstance(ctx, profileContent, options)
|
||||
instance, err := s.newInstance(ctx, profileContent, options, oldInstance != nil)
|
||||
if err != nil {
|
||||
s.serviceAccess.Lock()
|
||||
s.updateStatusError(err)
|
||||
@@ -277,8 +283,8 @@ func (s *StartedService) StartOrReloadService(ctx context.Context, profileConten
|
||||
return err
|
||||
}
|
||||
instance.urlTestHistoryStorage.AddUpdateHook(s.urlTestSubscriber)
|
||||
if instance.clashServer != nil {
|
||||
instance.clashServer.AddModeUpdateHook(s.clashModeSubscriber)
|
||||
if instance.clashMode != nil {
|
||||
instance.clashMode.AddUpdateHook(s.clashModeSubscriber)
|
||||
}
|
||||
s.serviceAccess.Lock()
|
||||
s.instance = instance
|
||||
@@ -378,12 +384,8 @@ func (s *StartedService) SubscribeServiceStatus(empty *emptypb.Empty, server grp
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerStreamingServer[Log]) error {
|
||||
var savedLines []*log.Entry
|
||||
s.logAccess.Lock()
|
||||
savedLines = make([]*log.Entry, 0, s.logLines.Len())
|
||||
for element := s.logLines.Front(); element != nil; element = element.Next() {
|
||||
savedLines = append(savedLines, element.Value)
|
||||
}
|
||||
savedLines := s.logLines.array()
|
||||
subscription, done, err := s.logObserver.Subscribe()
|
||||
s.logAccess.Unlock()
|
||||
if err != nil {
|
||||
@@ -635,14 +637,14 @@ func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
clashServer := s.instance.clashServer
|
||||
clashMode := s.instance.clashMode
|
||||
s.serviceAccess.RUnlock()
|
||||
if clashServer == nil {
|
||||
if clashMode == nil {
|
||||
return nil, status.Error(codes.NotFound, "clash mode not available")
|
||||
}
|
||||
return &ClashModeStatus{
|
||||
ModeList: clashServer.ModeList(),
|
||||
CurrentMode: clashServer.Mode(),
|
||||
ModeList: clashMode.ModeList(),
|
||||
CurrentMode: clashMode.Mode(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -665,12 +667,12 @@ func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.Se
|
||||
s.serviceAccess.RLock()
|
||||
var message *ClashMode
|
||||
if s.serviceStatus.Status == ServiceStatus_STARTED {
|
||||
clashServer := s.instance.clashServer
|
||||
if clashServer == nil {
|
||||
clashMode := s.instance.clashMode
|
||||
if clashMode == nil {
|
||||
s.serviceAccess.RUnlock()
|
||||
return status.Error(codes.NotFound, "clash mode not available")
|
||||
}
|
||||
message = &ClashMode{Mode: clashServer.Mode()}
|
||||
message = &ClashMode{Mode: clashMode.Mode()}
|
||||
} else {
|
||||
message = &ClashMode{}
|
||||
}
|
||||
@@ -700,12 +702,12 @@ func (s *StartedService) SetClashMode(ctx context.Context, request *ClashMode) (
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
clashServer := s.instance.clashServer
|
||||
clashMode := s.instance.clashMode
|
||||
s.serviceAccess.RUnlock()
|
||||
if clashServer == nil {
|
||||
if clashMode == nil {
|
||||
return nil, status.Error(codes.NotFound, "clash mode not available")
|
||||
}
|
||||
clashServer.SetMode(request.Mode)
|
||||
clashMode.SetMode(request.Mode)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
@@ -2092,10 +2094,7 @@ func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
|
||||
func (s *StartedService) WriteMessage(level log.Level, message string) {
|
||||
item := &log.Entry{Level: level, Message: message}
|
||||
s.logAccess.Lock()
|
||||
s.logLines.PushBack(item)
|
||||
if s.logLines.Len() > s.logMaxLines {
|
||||
s.logLines.Remove(s.logLines.Front())
|
||||
}
|
||||
s.logLines.push(item)
|
||||
s.logAccess.Unlock()
|
||||
s.logSubscriber.Emit(item)
|
||||
if s.debug {
|
||||
@@ -2106,7 +2105,7 @@ func (s *StartedService) WriteMessage(level log.Level, message string) {
|
||||
func (s *StartedService) SavedLog() []*log.Entry {
|
||||
s.logAccess.RLock()
|
||||
defer s.logAccess.RUnlock()
|
||||
return s.logLines.Array()
|
||||
return s.logLines.array()
|
||||
}
|
||||
|
||||
func (s *StartedService) Instance() *Instance {
|
||||
|
||||
+67
-39
@@ -48,7 +48,7 @@ type Client struct {
|
||||
networkManager adapter.NetworkManager
|
||||
logger logger.ContextLogger
|
||||
cache *freelru.Cache[dnsCacheKey, *dns.Msg]
|
||||
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
|
||||
cacheLock compatible.Map[dnsExchangeKey, chan struct{}]
|
||||
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
|
||||
}
|
||||
|
||||
@@ -95,6 +95,11 @@ type dnsCacheKey struct {
|
||||
environment uint64
|
||||
}
|
||||
|
||||
type dnsExchangeKey struct {
|
||||
dnsCacheKey
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (k dnsCacheKey) persistentName() string {
|
||||
name := k.transportTag
|
||||
if k.clientSubnet.IsValid() {
|
||||
@@ -251,6 +256,10 @@ func (o *exchangeOperation) release() {
|
||||
}
|
||||
|
||||
func (c *Client) beginExchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool, allowWait bool) (*exchangeOperation, *dns.Msg, exchangeStatus, error) {
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
return nil, nil, exchangeDone, err
|
||||
}
|
||||
transportStack := transportStackFromContext(ctx)
|
||||
if containsTransport(transportStack, transport.Tag()) {
|
||||
return nil, nil, exchangeDone, E.New("DNS resolution loop detected: ", formatTransportLoop(transportStack, transport.Tag()))
|
||||
@@ -291,8 +300,33 @@ func (c *Client) beginExchange(ctx context.Context, transport adapter.DNSTranspo
|
||||
if !disableCache {
|
||||
cacheKey := c.newCacheKey(transport, question, message, options)
|
||||
operation.cacheKey = cacheKey
|
||||
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
|
||||
if loaded {
|
||||
exchangeKey := dnsExchangeKey{dnsCacheKey: cacheKey, timeout: options.Timeout}
|
||||
cond, loaded := c.cacheLock.LoadOrStore(exchangeKey, make(chan struct{}))
|
||||
if !loaded {
|
||||
operation.releaseCond = func() {
|
||||
c.cacheLock.Delete(exchangeKey)
|
||||
close(cond)
|
||||
}
|
||||
}
|
||||
for {
|
||||
response, ttl, isStale := c.loadResponse(cacheKey)
|
||||
if response != nil {
|
||||
if isStale && !options.DisableOptimisticCache {
|
||||
c.backgroundRefreshDNS(transport, cacheKey, message.Copy(), options, responseChecker)
|
||||
logOptimisticResponse(c.logger, ctx, response)
|
||||
response.Id = message.Id
|
||||
operation.release()
|
||||
return nil, response, exchangeDone, nil
|
||||
} else if !isStale {
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
operation.release()
|
||||
return nil, response, exchangeDone, nil
|
||||
}
|
||||
}
|
||||
if !loaded {
|
||||
break
|
||||
}
|
||||
if !allowWait {
|
||||
return nil, nil, exchangeWait, nil
|
||||
}
|
||||
@@ -301,28 +335,13 @@ func (c *Client) beginExchange(ctx context.Context, transport adapter.DNSTranspo
|
||||
case <-ctx.Done():
|
||||
return nil, nil, exchangeDone, ctx.Err()
|
||||
}
|
||||
err = ctx.Err()
|
||||
if err != nil {
|
||||
return nil, nil, exchangeDone, err
|
||||
}
|
||||
cacheKey = c.newCacheKey(transport, question, message, options)
|
||||
operation.cacheKey = cacheKey
|
||||
} else {
|
||||
operation.releaseCond = func() {
|
||||
c.cacheLock.Delete(cacheKey)
|
||||
close(cond)
|
||||
}
|
||||
}
|
||||
response, ttl, isStale := c.loadResponse(cacheKey)
|
||||
if response != nil {
|
||||
if isStale && !options.DisableOptimisticCache {
|
||||
c.backgroundRefreshDNS(transport, cacheKey, message.Copy(), options, responseChecker)
|
||||
logOptimisticResponse(c.logger, ctx, response)
|
||||
response.Id = message.Id
|
||||
operation.release()
|
||||
return nil, response, exchangeDone, nil
|
||||
} else if !isStale {
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
operation.release()
|
||||
return nil, response, exchangeDone, nil
|
||||
}
|
||||
loaded = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,12 +398,17 @@ func (c *Client) finishExchange(transport adapter.DNSTransport, operation *excha
|
||||
}
|
||||
|
||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
|
||||
if options.Timeout == 0 {
|
||||
options.Timeout = c.timeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, options.Timeout)
|
||||
defer cancel()
|
||||
operation, earlyResponse, status, err := c.beginExchange(ctx, transport, message, options, responseChecker, true)
|
||||
if status != exchangeReady {
|
||||
return earlyResponse, err
|
||||
}
|
||||
defer operation.release()
|
||||
response, err := c.exchangeToTransport(operation.ctx, transport, operation.message, options.Timeout)
|
||||
response, err := c.exchangeToTransport(operation.ctx, transport, operation.message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -392,18 +416,26 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
}
|
||||
|
||||
func (c *Client) ExchangeAsync(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool, callback func(response *dns.Msg, err error)) {
|
||||
if options.Timeout == 0 {
|
||||
options.Timeout = c.timeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, options.Timeout)
|
||||
operation, earlyResponse, status, err := c.beginExchange(ctx, transport, message, options, responseChecker, false)
|
||||
switch status {
|
||||
case exchangeDone:
|
||||
cancel()
|
||||
callback(earlyResponse, err)
|
||||
return
|
||||
case exchangeWait:
|
||||
go func() {
|
||||
callback(c.Exchange(ctx, transport, message, options, responseChecker))
|
||||
response, exchangeErr := c.Exchange(ctx, transport, message, options, responseChecker)
|
||||
cancel()
|
||||
callback(response, exchangeErr)
|
||||
}()
|
||||
return
|
||||
}
|
||||
finish := func(response *dns.Msg, exchangeErr error) {
|
||||
cancel()
|
||||
if exchangeErr != nil {
|
||||
operation.release()
|
||||
callback(nil, exchangeErr)
|
||||
@@ -413,7 +445,7 @@ func (c *Client) ExchangeAsync(ctx context.Context, transport adapter.DNSTranspo
|
||||
operation.release()
|
||||
callback(finishedResponse, finishErr)
|
||||
}
|
||||
c.exchangeToTransportAsync(operation.ctx, transport, operation.message, options.Timeout, finish)
|
||||
c.exchangeToTransportAsync(operation.ctx, transport, operation.message, finish)
|
||||
}
|
||||
|
||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
||||
@@ -644,8 +676,14 @@ func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, key dnsCac
|
||||
}
|
||||
go func() {
|
||||
defer c.backgroundRefresh.Delete(key)
|
||||
timeout := options.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = c.timeout
|
||||
}
|
||||
ctx := adapter.ContextWithDNSTransportTag(c.ctx, transport.Tag())
|
||||
response, err := c.exchangeToTransport(ctx, transport, message, options.Timeout)
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
response, err := c.exchangeToTransport(ctx, transport, message)
|
||||
if err != nil {
|
||||
if c.logger != nil {
|
||||
c.logger.DebugContext(ctx, "optimistic refresh failed for ", FqdnToDomain(key.Name), ": ", err)
|
||||
@@ -718,12 +756,7 @@ func transportStackFromContext(ctx context.Context) []string {
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, timeout time.Duration) (*dns.Msg, error) {
|
||||
if timeout == 0 {
|
||||
timeout = c.timeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) {
|
||||
response, err := transport.Exchange(ctx, message)
|
||||
if err == nil {
|
||||
stripDNSPadding(response)
|
||||
@@ -745,13 +778,8 @@ func containsTransport(stack []string, tag string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) exchangeToTransportAsync(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, timeout time.Duration, callback func(response *dns.Msg, err error)) {
|
||||
if timeout == 0 {
|
||||
timeout = c.timeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
func (c *Client) exchangeToTransportAsync(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, callback func(response *dns.Msg, err error)) {
|
||||
transport.ExchangeAsync(ctx, message, func(response *dns.Msg, err error) {
|
||||
cancel()
|
||||
if err == nil {
|
||||
stripDNSPadding(response)
|
||||
callback(response, nil)
|
||||
|
||||
+32
-16
@@ -129,10 +129,19 @@ func (r *Router) Initialize(rules []option.DNSRule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) Start(stage adapter.StartStage) error {
|
||||
func (r *Router) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
monitor := taskmonitor.New(r.logger, C.StartTimeout)
|
||||
switch stage {
|
||||
case adapter.StartStateStart:
|
||||
scope.Add(func() error {
|
||||
r.rulesAccess.Lock()
|
||||
r.closing = true
|
||||
runtimeRules := r.rules
|
||||
r.rules = nil
|
||||
r.rulesAccess.Unlock()
|
||||
closeRules(runtimeRules)
|
||||
return nil
|
||||
})
|
||||
monitor.Start("initialize DNS client")
|
||||
r.client.Start()
|
||||
monitor.Finish()
|
||||
@@ -163,20 +172,6 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) Close() error {
|
||||
r.rulesAccess.Lock()
|
||||
if r.closing {
|
||||
r.rulesAccess.Unlock()
|
||||
return nil
|
||||
}
|
||||
r.closing = true
|
||||
runtimeRules := r.rules
|
||||
r.rules = nil
|
||||
r.rulesAccess.Unlock()
|
||||
closeRules(runtimeRules)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, dnsRuleModeFlags, error) {
|
||||
for i, ruleOptions := range r.rawRules {
|
||||
err := R.ValidateNoNestedDNSRuleActions(ruleOptions)
|
||||
@@ -1059,7 +1054,11 @@ func (r *Router) prepareExchange(ctx context.Context, message *mDNS.Msg) (*dnsEx
|
||||
if r.powerManager != nil {
|
||||
recorder := r.powerManager.Recorder()
|
||||
if recorder != nil {
|
||||
recorder.CountDNSQuery()
|
||||
var domain string
|
||||
if len(message.Question) == 1 {
|
||||
domain = message.Question[0].Name
|
||||
}
|
||||
recorder.CountDNSQuery(domain)
|
||||
}
|
||||
}
|
||||
if len(message.Question) != 1 {
|
||||
@@ -1073,6 +1072,19 @@ func (r *Router) prepareExchange(ctx context.Context, message *mDNS.Msg) (*dnsEx
|
||||
Question: message.Question,
|
||||
}, nil
|
||||
}
|
||||
if isResolverDiscoveryQuery(message.Question[0]) {
|
||||
r.logger.DebugContext(ctx, "rejected resolver discovery query ", FormatQuestion(message.Question[0].String()))
|
||||
return nil, &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Response: true,
|
||||
RecursionDesired: message.RecursionDesired,
|
||||
RecursionAvailable: true,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
},
|
||||
Question: message.Question,
|
||||
}, nil
|
||||
}
|
||||
r.rulesAccess.RLock()
|
||||
if r.closing {
|
||||
r.rulesAccess.RUnlock()
|
||||
@@ -1335,6 +1347,10 @@ response:
|
||||
return responseAddrs, err
|
||||
}
|
||||
|
||||
func isResolverDiscoveryQuery(question mDNS.Question) bool {
|
||||
return question.Qtype == mDNS.TypeSVCB && len(question.Name) > 5 && strings.EqualFold(question.Name[:5], "_dns.")
|
||||
}
|
||||
|
||||
func isAddressQuery(message *mDNS.Msg) bool {
|
||||
for _, question := range message.Question {
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS {
|
||||
|
||||
+2
-14
@@ -31,11 +31,7 @@ type fakeDNSTransport struct {
|
||||
firstQueried time.Time
|
||||
}
|
||||
|
||||
func (t *fakeDNSTransport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *fakeDNSTransport) Close() error {
|
||||
func (t *fakeDNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -90,11 +86,7 @@ type fakeDNSTransportManager struct {
|
||||
defaultTransport adapter.DNSTransport
|
||||
}
|
||||
|
||||
func (m *fakeDNSTransportManager) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeDNSTransportManager) Close() error {
|
||||
func (m *fakeDNSTransportManager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -115,10 +107,6 @@ func (m *fakeDNSTransportManager) FakeIP() adapter.FakeIPTransport {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeDNSTransportManager) Remove(tag string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeDNSTransportManager) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ type connPoolState[T comparable] struct {
|
||||
shared T
|
||||
hasShared bool
|
||||
sharedClaimed bool
|
||||
sharedUsers int
|
||||
sharedWaiters int
|
||||
sharedCtx context.Context
|
||||
sharedCancel context.CancelCauseFunc
|
||||
|
||||
@@ -122,6 +124,9 @@ func (p *ConnPool[T]) Release(conn T, reuse bool) {
|
||||
p.options.Close(conn, net.ErrClosed)
|
||||
return
|
||||
}
|
||||
if p.options.Mode == ConnPoolSingle && state.hasShared && state.shared == conn && state.sharedUsers > 0 {
|
||||
state.sharedUsers--
|
||||
}
|
||||
if p.options.Mode == ConnPoolOrdered {
|
||||
if _, idle := state.idleElements[conn]; !idle {
|
||||
state.idleElements[conn] = state.idle.PushBack(conn)
|
||||
@@ -182,6 +187,7 @@ func (p *ConnPool[T]) removeConn(state *connPoolState[T], conn T, cause error) {
|
||||
state.shared = zero
|
||||
state.hasShared = false
|
||||
state.sharedClaimed = false
|
||||
state.sharedUsers = 0
|
||||
state.sharedCtx = nil
|
||||
if state.sharedCancel != nil {
|
||||
state.sharedCancel(cause)
|
||||
@@ -196,6 +202,23 @@ func (p *ConnPool[T]) removeConn(state *connPoolState[T], conn T, cause error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ConnPool[T]) CloseIdle() {
|
||||
p.access.Lock()
|
||||
if p.closed {
|
||||
p.access.Unlock()
|
||||
return
|
||||
}
|
||||
state := p.state
|
||||
if p.options.Mode != ConnPoolSingle || !state.hasShared || state.sharedUsers > 0 || state.sharedWaiters > 0 {
|
||||
p.access.Unlock()
|
||||
return
|
||||
}
|
||||
conn := state.shared
|
||||
p.removeConn(state, conn, net.ErrClosed)
|
||||
p.access.Unlock()
|
||||
p.options.Close(conn, net.ErrClosed)
|
||||
}
|
||||
|
||||
func (p *ConnPool[T]) Reset() {
|
||||
p.access.Lock()
|
||||
if p.closed {
|
||||
@@ -309,6 +332,7 @@ func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Conte
|
||||
if p.options.IsAlive(conn) {
|
||||
created := !current.sharedClaimed
|
||||
current.sharedClaimed = true
|
||||
current.sharedUsers++
|
||||
connCtx := current.sharedCtx
|
||||
p.access.Unlock()
|
||||
return conn, connCtx, created, nil
|
||||
@@ -324,6 +348,7 @@ func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Conte
|
||||
current.connecting = &connPoolConnect[T]{done: make(chan struct{})}
|
||||
}
|
||||
state := current.connecting
|
||||
current.sharedWaiters++
|
||||
p.access.Unlock()
|
||||
|
||||
if startDial {
|
||||
@@ -338,9 +363,13 @@ func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Conte
|
||||
}
|
||||
return conn, connCtx, created, err
|
||||
case <-ctx.Done():
|
||||
p.access.Lock()
|
||||
current.sharedWaiters--
|
||||
p.access.Unlock()
|
||||
return zero, nil, false, ctx.Err()
|
||||
case <-current.ctx.Done():
|
||||
p.access.Lock()
|
||||
current.sharedWaiters--
|
||||
closed := p.closed
|
||||
p.access.Unlock()
|
||||
if closed {
|
||||
@@ -400,6 +429,7 @@ func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolCo
|
||||
var zero T
|
||||
|
||||
p.access.Lock()
|
||||
current.sharedWaiters--
|
||||
if state.err != nil {
|
||||
err := state.err
|
||||
p.access.Unlock()
|
||||
@@ -431,6 +461,7 @@ func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolCo
|
||||
|
||||
created := !current.sharedClaimed
|
||||
current.sharedClaimed = true
|
||||
current.sharedUsers++
|
||||
connCtx := current.sharedCtx
|
||||
p.access.Unlock()
|
||||
return conn, connCtx, created, false, nil
|
||||
|
||||
+33
-46
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
@@ -27,7 +26,6 @@ import (
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
@@ -54,7 +52,6 @@ type Transport struct {
|
||||
networkManager adapter.NetworkManager
|
||||
platformInterface adapter.PlatformInterface
|
||||
interfaceName string
|
||||
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
||||
updateAccess sync.Mutex
|
||||
updateCancel context.CancelFunc
|
||||
refreshAccess sync.Mutex
|
||||
@@ -70,6 +67,7 @@ type transportState struct {
|
||||
search []string
|
||||
servers []M.Socksaddr
|
||||
serverTransports []adapter.DNSTransport
|
||||
serverScope *adapter.Scope
|
||||
}
|
||||
|
||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
@@ -104,15 +102,29 @@ func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
scope.Add(func() error {
|
||||
t.Reset()
|
||||
return nil
|
||||
})
|
||||
if t.interfaceName == "" {
|
||||
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
|
||||
interfaceMonitor := t.networkManager.InterfaceMonitor()
|
||||
if interfaceMonitor == nil {
|
||||
return E.New("missing monitor for auto DHCP, set route.auto_detect_interface")
|
||||
}
|
||||
interfaceCallback := interfaceMonitor.RegisterCallback(func(defaultInterface *control.Interface, flags int) {
|
||||
t.interfaceUpdated(scope.Context())
|
||||
})
|
||||
scope.Add(func() error {
|
||||
interfaceMonitor.UnregisterCallback(interfaceCallback)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
go func() {
|
||||
err := t.fetch()
|
||||
err := t.fetch(scope.Context())
|
||||
if err != nil {
|
||||
if errors.Is(err, errInterfaceIsCellular) && t.optional {
|
||||
t.logger.Debug(E.Cause(errInterfaceIsCellular, "dhcp: fetch DNS servers"))
|
||||
@@ -124,32 +136,12 @@ func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
if t.interfaceCallback != nil {
|
||||
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
|
||||
}
|
||||
t.updateAccess.Lock()
|
||||
updateCancel := t.updateCancel
|
||||
t.updateCancel = nil
|
||||
t.updateAccess.Unlock()
|
||||
if updateCancel != nil {
|
||||
updateCancel()
|
||||
}
|
||||
t.refreshAccess.Lock()
|
||||
defer t.refreshAccess.Unlock()
|
||||
state := t.savedState.Swap(nil)
|
||||
if state != nil {
|
||||
closeServerTransports(state.serverTransports)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
t.refreshAccess.Lock()
|
||||
defer t.refreshAccess.Unlock()
|
||||
state := t.savedState.Swap(nil)
|
||||
if state != nil {
|
||||
closeServerTransports(state.serverTransports)
|
||||
if state != nil && state.serverScope != nil {
|
||||
state.serverScope.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,12 +157,6 @@ func (t *Transport) Environment() []string {
|
||||
return append(environment, state.search...)
|
||||
}
|
||||
|
||||
func closeServerTransports(serverTransports []adapter.DNSTransport) {
|
||||
for _, serverTransport := range serverTransports {
|
||||
serverTransport.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
done := make(chan struct{})
|
||||
var (
|
||||
@@ -207,7 +193,7 @@ func (t *Transport) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callba
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeCold(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
|
||||
err := t.fetch()
|
||||
err := t.fetch(t.ctx)
|
||||
if err != nil {
|
||||
callback(nil, E.Cause(err, "dhcp: fetch DNS servers"))
|
||||
return
|
||||
@@ -231,7 +217,7 @@ func (t *Transport) Fetch() []M.Socksaddr {
|
||||
return state.servers
|
||||
}
|
||||
|
||||
func (t *Transport) fetch() error {
|
||||
func (t *Transport) fetch(ctx context.Context) error {
|
||||
state := t.savedState.Load()
|
||||
if state != nil {
|
||||
if state.lastError != nil {
|
||||
@@ -252,7 +238,7 @@ func (t *Transport) fetch() error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return t.updateServersLocked(t.ctx)
|
||||
return t.updateServersLocked(ctx)
|
||||
}
|
||||
|
||||
func (t *Transport) startRefresh() {
|
||||
@@ -339,12 +325,13 @@ func (t *Transport) storeFailureLocked(err error) {
|
||||
newState.search = previousState.search
|
||||
newState.servers = previousState.servers
|
||||
newState.serverTransports = previousState.serverTransports
|
||||
newState.serverScope = previousState.serverScope
|
||||
}
|
||||
t.savedState.Store(newState)
|
||||
}
|
||||
|
||||
func (t *Transport) interfaceUpdated(defaultInterface *control.Interface, flags int) {
|
||||
updateContext, updateCancel := context.WithCancel(t.ctx)
|
||||
func (t *Transport) interfaceUpdated(ctx context.Context) {
|
||||
updateContext, updateCancel := context.WithCancel(ctx)
|
||||
t.updateAccess.Lock()
|
||||
previousCancel := t.updateCancel
|
||||
t.updateCancel = updateCancel
|
||||
@@ -480,25 +467,25 @@ func (t *Transport) recreateServersLocked(iface *control.Interface, dhcpPacket *
|
||||
}
|
||||
if serversUnchanged && previousState.serverTransports != nil {
|
||||
newState.serverTransports = previousState.serverTransports
|
||||
newState.serverScope = previousState.serverScope
|
||||
t.savedState.Store(newState)
|
||||
return nil
|
||||
}
|
||||
serverScope := adapter.NewScope(t.ctx, t.logger)
|
||||
serverTransports := make([]adapter.DNSTransport, 0, len(newState.servers))
|
||||
for _, serverAddr := range newState.servers {
|
||||
serverTransport := transport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), t.dialer, serverAddr)
|
||||
err := serverTransport.Start(adapter.StartStateStart)
|
||||
err := serverTransport.Start(adapter.StartStateStart, serverScope)
|
||||
if err != nil {
|
||||
for _, startedTransport := range serverTransports {
|
||||
startedTransport.Close()
|
||||
}
|
||||
return E.Cause(err, "initialize transport for ", serverAddr)
|
||||
return E.Errors(E.Cause(err, "initialize transport for ", serverAddr), serverScope.Close())
|
||||
}
|
||||
serverTransports = append(serverTransports, serverTransport)
|
||||
}
|
||||
newState.serverTransports = serverTransports
|
||||
newState.serverScope = serverScope
|
||||
t.savedState.Store(newState)
|
||||
if previousState != nil {
|
||||
closeServerTransports(previousState.serverTransports)
|
||||
if previousState != nil && previousState.serverScope != nil {
|
||||
previousState.serverScope.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -82,6 +82,70 @@ type sequentialCallState struct {
|
||||
continued bool
|
||||
}
|
||||
|
||||
func ExchangeParallel(ctx context.Context, exchangers []AsyncExchanger, accept func(response *mDNS.Msg, err error) bool, callback func(response *mDNS.Msg, err error)) {
|
||||
if len(exchangers) == 0 {
|
||||
callback(nil, E.New("missing exchangers"))
|
||||
return
|
||||
}
|
||||
exchangeCtx, cancel := context.WithCancel(ctx)
|
||||
parallel := ¶llelExchange{
|
||||
accept: accept,
|
||||
callback: callback,
|
||||
cancel: cancel,
|
||||
responses: make([]*mDNS.Msg, len(exchangers)),
|
||||
errors: make([]error, len(exchangers)),
|
||||
pending: len(exchangers),
|
||||
}
|
||||
for i, exchanger := range exchangers {
|
||||
exchanger(exchangeCtx, func(response *mDNS.Msg, err error) {
|
||||
parallel.complete(i, response, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type parallelExchange struct {
|
||||
access sync.Mutex
|
||||
accept func(response *mDNS.Msg, err error) bool
|
||||
callback func(response *mDNS.Msg, err error)
|
||||
cancel context.CancelFunc
|
||||
responses []*mDNS.Msg
|
||||
errors []error
|
||||
pending int
|
||||
completed bool
|
||||
}
|
||||
|
||||
func (p *parallelExchange) complete(index int, response *mDNS.Msg, err error) {
|
||||
p.access.Lock()
|
||||
if p.completed {
|
||||
p.access.Unlock()
|
||||
return
|
||||
}
|
||||
accepted := p.accept(response, err)
|
||||
if !accepted {
|
||||
p.responses[index] = response
|
||||
p.errors[index] = err
|
||||
p.pending--
|
||||
if p.pending > 0 {
|
||||
p.access.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
p.completed = true
|
||||
p.access.Unlock()
|
||||
p.cancel()
|
||||
if accepted {
|
||||
p.callback(response, err)
|
||||
return
|
||||
}
|
||||
for i, finalResponse := range p.responses {
|
||||
if p.errors[i] == nil {
|
||||
p.callback(finalResponse, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.callback(nil, E.Errors(p.errors...))
|
||||
}
|
||||
|
||||
func ExchangeNames(ctx context.Context, names []string, question mDNS.Question, exchangerFor func(fqdn string) AsyncExchanger, callback func(response *mDNS.Msg, err error)) {
|
||||
if len(names) == 0 {
|
||||
callback(nil, E.New("missing name candidates"))
|
||||
|
||||
@@ -45,15 +45,16 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return t.store.Start()
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
return t.store.Close()
|
||||
err := t.store.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scope.Add(t.store.Close)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
@@ -67,7 +68,11 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
|
||||
if question.Qtype == mDNS.TypeA && !t.inet4Enabled || question.Qtype == mDNS.TypeAAAA && !t.inet6Enabled {
|
||||
return dns.FixedResponseStatus(message, mDNS.RcodeSuccess), nil
|
||||
}
|
||||
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
|
||||
domain := dns.FqdnToDomain(question.Name)
|
||||
if len(domain) > 255 {
|
||||
return nil, E.New("domain name too long for fakeip")
|
||||
}
|
||||
address, err := t.store.Create(domain, question.Qtype == mDNS.TypeAAAA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -56,11 +56,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -58,11 +58,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+29
-8
@@ -34,7 +34,10 @@ import (
|
||||
|
||||
const MimeType = "application/dns-message"
|
||||
|
||||
var _ adapter.DNSTransport = (*HTTPSTransport)(nil)
|
||||
var (
|
||||
_ adapter.DNSTransport = (*HTTPSTransport)(nil)
|
||||
_ adapter.IdleConnectionKeeper = (*HTTPSTransport)(nil)
|
||||
)
|
||||
|
||||
func RegisterHTTPS(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTPS, NewHTTPS)
|
||||
@@ -48,6 +51,7 @@ type HTTPSTransport struct {
|
||||
headers http.Header
|
||||
serverAddr M.Socksaddr
|
||||
fallback *atomic.Bool
|
||||
keepIdle atomic.Bool
|
||||
transportAccess sync.Mutex
|
||||
transport *HTTPSTransportWrapper
|
||||
transportResetAt time.Time
|
||||
@@ -131,7 +135,7 @@ func NewHTTPSRaw(
|
||||
// plain HTTP DoH used by Tailscale
|
||||
fallback.Store(true)
|
||||
}
|
||||
return &HTTPSTransport{
|
||||
transport := &HTTPSTransport{
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
@@ -141,26 +145,40 @@ func NewHTTPSRaw(
|
||||
fallback: fallback,
|
||||
transport: NewHTTPSTransportWrapper(dialer, serverAddr, fallback),
|
||||
}
|
||||
transport.keepIdle.Store(true)
|
||||
return transport
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
||||
func (t *HTTPSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
scope.Add(func() error {
|
||||
t.Reset()
|
||||
return nil
|
||||
})
|
||||
return dialer.InitializeDetour(t.dialer)
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Close() error {
|
||||
t.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Reset() {
|
||||
t.transportAccess.Lock()
|
||||
defer t.transportAccess.Unlock()
|
||||
t.resetTransportLocked()
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) SetKeepIdleConnections(keep bool) {
|
||||
t.keepIdle.Store(keep)
|
||||
if !keep {
|
||||
t.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) CloseIdleConnections() {
|
||||
t.transportAccess.Lock()
|
||||
defer t.transportAccess.Unlock()
|
||||
t.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) resetTransportLocked() {
|
||||
oldTransport := t.transport
|
||||
t.transport = NewHTTPSTransportWrapper(t.dialer, t.serverAddr, t.fallback)
|
||||
@@ -171,6 +189,9 @@ func (t *HTTPSTransport) resetTransportLocked() {
|
||||
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
startAt := time.Now()
|
||||
response, err := t.exchange(ctx, message)
|
||||
if !t.keepIdle.Load() {
|
||||
t.CloseIdleConnections()
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
t.transportAccess.Lock()
|
||||
|
||||
@@ -111,6 +111,10 @@ func (h *HTTPSTransportWrapper) Close() {
|
||||
for _, trackedConn := range connections {
|
||||
trackedConn.Conn.Close()
|
||||
}
|
||||
h.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
|
||||
h.http2Transport.CloseIdleConnections()
|
||||
h.httpTransport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/sagernet/sing-box/dns/transport/mdns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -39,6 +38,7 @@ type Transport struct {
|
||||
preferGo bool
|
||||
resolved ResolvedResolver
|
||||
mdnsTransport adapter.DNSTransport
|
||||
mdnsScope *adapter.Scope
|
||||
configSource *systemconfig.Source
|
||||
system systemResolver
|
||||
serverSet atomic.Pointer[localServerSet]
|
||||
@@ -65,13 +65,15 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
t.preferredResolver.Start(stage)
|
||||
switch stage {
|
||||
case adapter.StartStateInitialize:
|
||||
scope.Add(t.configSource.Close)
|
||||
if !t.preferGo && isSystemdResolvedManaged() {
|
||||
resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger)
|
||||
if err == nil {
|
||||
scope.Add(resolvedResolver.Close)
|
||||
err = resolvedResolver.Start()
|
||||
if err == nil {
|
||||
t.resolved = resolvedResolver
|
||||
@@ -81,13 +83,26 @@ func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
}
|
||||
case adapter.StartStateStart:
|
||||
scope.Add(func() error {
|
||||
t.system.close()
|
||||
return nil
|
||||
})
|
||||
scope.Add(func() error {
|
||||
serverSet := t.serverSet.Swap(nil)
|
||||
if serverSet != nil {
|
||||
return serverSet.serverScope.Close()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if !C.IsDarwin {
|
||||
t.mdnsTransport = mdns.NewRawTransport(t.TransportAdapter, t.ctx, t.logger)
|
||||
t.mdnsScope = adapter.NewScope(t.ctx, t.logger)
|
||||
scope.Add(t.mdnsScope.Close)
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
if t.mdnsTransport != nil {
|
||||
err := t.mdnsTransport.Start(stage)
|
||||
err := t.mdnsTransport.Start(stage, t.mdnsScope)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,15 +111,6 @@ func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
serverSet := t.serverSet.Swap(nil)
|
||||
if serverSet != nil {
|
||||
serverSet.Close()
|
||||
}
|
||||
t.system.close()
|
||||
return common.Close(t.resolved, t.mdnsTransport, t.configSource)
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
serverSet := t.serverSet.Load()
|
||||
if serverSet != nil {
|
||||
|
||||
@@ -11,6 +11,5 @@ type ResolvedResolver interface {
|
||||
Close() error
|
||||
Reset()
|
||||
Environment() []string
|
||||
Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
|
||||
ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error))
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
@@ -17,7 +19,6 @@ import (
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
dnsTransport "github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/service/resolved"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
@@ -54,20 +55,51 @@ func isSystemdResolvedManaged() bool {
|
||||
type DBusResolvedResolver struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
interfaceFinder control.InterfaceFinder
|
||||
interfaceMonitor tun.DefaultInterfaceMonitor
|
||||
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
||||
networkMonitor tun.NetworkUpdateMonitor
|
||||
networkCallback *list.Element[tun.NetworkUpdateCallback]
|
||||
systemBus *dbus.Conn
|
||||
savedServerSet atomic.Pointer[resolvedServerSet]
|
||||
updateAccess sync.Mutex
|
||||
updateCancel context.CancelFunc
|
||||
updateRunAccess sync.Mutex
|
||||
closed bool
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
type resolvedServerSet struct {
|
||||
servers []resolvedServer
|
||||
signature []string
|
||||
scopes []*resolvedScope
|
||||
signature []string
|
||||
serverScope *adapter.Scope
|
||||
}
|
||||
|
||||
// Match levels of dns_scope_good_domain() in systemd-resolved: a routing or search
|
||||
// domain match yields its label count, and only scopes at the best level are queried.
|
||||
const (
|
||||
resolvedScopeNoMatch = -3
|
||||
resolvedScopeLastResort = -2
|
||||
resolvedScopeMaybe = -1
|
||||
)
|
||||
|
||||
// systemd-resolved only signals changes of the DNS server list, while clients such as
|
||||
// NetworkManager and systemd-networkd configure domains, default route and DNS over TLS
|
||||
// of a link in separate calls right after it; link state changes are not signaled at all.
|
||||
const resolvedUpdateDelay = time.Second
|
||||
|
||||
// SD_RESOLVED_DNS bit of org.freedesktop.resolve1.Link.ScopesMask, set when the link has a unicast DNS scope.
|
||||
const resolvedScopesMaskDNS = 1 << 0
|
||||
|
||||
// TRANSACTION_UDP_TIMEOUT_USEC in systemd-resolved (resolved-timeouts.h), after which a transaction
|
||||
// is resent to the next server of the scope.
|
||||
const resolvedServerTimeout = 5 * time.Second
|
||||
|
||||
type resolvedScope struct {
|
||||
domains []string
|
||||
defaultRoute bool
|
||||
fallback bool
|
||||
servers []resolvedServer
|
||||
currentServer atomic.Uint32
|
||||
}
|
||||
|
||||
type resolvedServer struct {
|
||||
@@ -75,14 +107,51 @@ type resolvedServer struct {
|
||||
fallbackTransport adapter.DNSTransport
|
||||
}
|
||||
|
||||
type resolvedScopeSpecification struct {
|
||||
interfaceName string
|
||||
dnsOverTLSMode string
|
||||
domains []string
|
||||
defaultRoute bool
|
||||
fallback bool
|
||||
servers []resolvedServerSpecification
|
||||
}
|
||||
|
||||
type resolvedServerSpecification struct {
|
||||
address netip.Addr
|
||||
port uint16
|
||||
serverName string
|
||||
}
|
||||
|
||||
type resolvedManagerDNS struct {
|
||||
InterfaceIndex int32
|
||||
Family int32
|
||||
Address []byte
|
||||
}
|
||||
|
||||
type resolvedManagerDNSEx struct {
|
||||
InterfaceIndex int32
|
||||
Family int32
|
||||
Address []byte
|
||||
Port uint16
|
||||
Name string
|
||||
}
|
||||
|
||||
type resolvedManagerDomain struct {
|
||||
InterfaceIndex int32
|
||||
Domain string
|
||||
RoutingOnly bool
|
||||
}
|
||||
|
||||
type resolvedManagerServer struct {
|
||||
interfaceIndex int32
|
||||
address []byte
|
||||
port uint16
|
||||
serverName string
|
||||
}
|
||||
|
||||
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
|
||||
interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor()
|
||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||
interfaceMonitor := networkManager.InterfaceMonitor()
|
||||
if interfaceMonitor == nil {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
@@ -93,7 +162,9 @@ func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (Reso
|
||||
return &DBusResolvedResolver{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
interfaceFinder: networkManager.InterfaceFinder(),
|
||||
interfaceMonitor: interfaceMonitor,
|
||||
networkMonitor: networkManager.NetworkMonitor(),
|
||||
systemBus: systemBus,
|
||||
}, nil
|
||||
}
|
||||
@@ -101,6 +172,9 @@ func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (Reso
|
||||
func (t *DBusResolvedResolver) Start() error {
|
||||
t.updateStatus(t.ctx)
|
||||
t.interfaceCallback = t.interfaceMonitor.RegisterCallback(t.updateDefaultInterface)
|
||||
if t.networkMonitor != nil {
|
||||
t.networkCallback = t.networkMonitor.RegisterCallback(t.postUpdateStatus)
|
||||
}
|
||||
err := t.systemBus.BusObject().AddMatchSignal(
|
||||
"org.freedesktop.DBus",
|
||||
"NameOwnerChanged",
|
||||
@@ -124,29 +198,26 @@ func (t *DBusResolvedResolver) Start() error {
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) Close() error {
|
||||
t.updateAccess.Lock()
|
||||
updateCancel := t.updateCancel
|
||||
t.updateCancel = nil
|
||||
t.updateAccess.Unlock()
|
||||
if updateCancel != nil {
|
||||
updateCancel()
|
||||
}
|
||||
t.updateRunAccess.Lock()
|
||||
t.closed = true
|
||||
serverSet := t.savedServerSet.Swap(nil)
|
||||
t.updateRunAccess.Unlock()
|
||||
var closeErr error
|
||||
t.closeOnce.Do(func() {
|
||||
t.updateAccess.Lock()
|
||||
updateCancel := t.updateCancel
|
||||
t.updateCancel = nil
|
||||
t.updateAccess.Unlock()
|
||||
if updateCancel != nil {
|
||||
updateCancel()
|
||||
}
|
||||
t.updateRunAccess.Lock()
|
||||
t.closed = true
|
||||
serverSet := t.savedServerSet.Swap(nil)
|
||||
t.updateRunAccess.Unlock()
|
||||
if serverSet != nil {
|
||||
closeErr = serverSet.Close()
|
||||
}
|
||||
if t.interfaceCallback != nil {
|
||||
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
|
||||
}
|
||||
if t.systemBus != nil {
|
||||
_ = t.systemBus.Close()
|
||||
}
|
||||
})
|
||||
if serverSet != nil {
|
||||
closeErr = serverSet.serverScope.Close()
|
||||
}
|
||||
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
|
||||
if t.networkCallback != nil {
|
||||
t.networkMonitor.UnregisterCallback(t.networkCallback)
|
||||
}
|
||||
_ = t.systemBus.Close()
|
||||
return closeErr
|
||||
}
|
||||
|
||||
@@ -155,10 +226,12 @@ func (t *DBusResolvedResolver) Reset() {
|
||||
if serverSet == nil {
|
||||
return
|
||||
}
|
||||
for _, server := range serverSet.servers {
|
||||
server.primaryTransport.Reset()
|
||||
if server.fallbackTransport != nil {
|
||||
server.fallbackTransport.Reset()
|
||||
for _, scope := range serverSet.scopes {
|
||||
for _, server := range scope.servers {
|
||||
server.primaryTransport.Reset()
|
||||
if server.fallbackTransport != nil {
|
||||
server.fallbackTransport.Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,40 +244,20 @@ func (t *DBusResolvedResolver) Environment() []string {
|
||||
return serverSet.signature
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
serverSet := t.savedServerSet.Load()
|
||||
if serverSet == nil {
|
||||
var err error
|
||||
serverSet, err = t.checkResolved(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
previousServerSet := t.savedServerSet.Swap(serverSet)
|
||||
if previousServerSet != nil {
|
||||
_ = previousServerSet.Close()
|
||||
}
|
||||
}
|
||||
response, err := t.exchangeServerSet(ctx, message, serverSet)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
t.updateStatus(t.ctx)
|
||||
refreshedServerSet := t.savedServerSet.Load()
|
||||
if refreshedServerSet == nil || refreshedServerSet == serverSet {
|
||||
return nil, err
|
||||
}
|
||||
return t.exchangeServerSet(ctx, message, refreshedServerSet)
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
|
||||
serverSet := t.savedServerSet.Load()
|
||||
if serverSet == nil {
|
||||
go func() {
|
||||
callback(t.Exchange(ctx, message))
|
||||
err := t.updateStatus(t.ctx)
|
||||
if err != nil {
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
t.exchangeServerSet(ctx, message, t.savedServerSet.Load(), callback)
|
||||
}()
|
||||
return
|
||||
}
|
||||
t.exchangeServerSetAsync(ctx, message, serverSet, func(response *mDNS.Msg, err error) {
|
||||
t.exchangeServerSet(ctx, message, serverSet, func(response *mDNS.Msg, err error) {
|
||||
if err == nil {
|
||||
callback(response, nil)
|
||||
return
|
||||
@@ -216,29 +269,114 @@ func (t *DBusResolvedResolver) ExchangeAsync(ctx context.Context, message *mDNS.
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
t.exchangeServerSetAsync(ctx, message, refreshedServerSet, callback)
|
||||
t.exchangeServerSet(ctx, message, refreshedServerSet, callback)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) exchangeServerSetAsync(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet, callback func(response *mDNS.Msg, err error)) {
|
||||
if len(serverSet.servers) == 0 {
|
||||
callback(nil, E.New("link has no DNS servers configured"))
|
||||
func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet, callback func(response *mDNS.Msg, err error)) {
|
||||
if serverSet == nil {
|
||||
callback(nil, os.ErrClosed)
|
||||
return
|
||||
}
|
||||
serverExchangers := make([]dnsTransport.AsyncExchanger, 0, len(serverSet.servers))
|
||||
for _, server := range serverSet.servers {
|
||||
scopes := serverSet.selectScopes(message.Question[0].Name)
|
||||
if len(scopes) == 0 {
|
||||
callback(nil, E.New("no appropriate name servers or networks for name found"))
|
||||
return
|
||||
}
|
||||
scopeExchangers := common.Map(scopes, func(scope *resolvedScope) dnsTransport.AsyncExchanger {
|
||||
return func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) {
|
||||
scope.exchange(exchangeCtx, message, exchangeCallback)
|
||||
}
|
||||
})
|
||||
dnsTransport.ExchangeParallel(ctx, scopeExchangers, acceptResolvedScopeResponse, callback)
|
||||
}
|
||||
|
||||
func (s *resolvedScope) exchange(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
|
||||
serverCount := uint32(len(s.servers))
|
||||
currentServer := s.currentServer.Load()
|
||||
serverExchangers := make([]dnsTransport.AsyncExchanger, 0, serverCount)
|
||||
for i := range serverCount {
|
||||
serverIndex := (currentServer + i) % serverCount
|
||||
server := s.servers[serverIndex]
|
||||
serverExchangers = append(serverExchangers, func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) {
|
||||
server.primaryTransport.ExchangeAsync(exchangeCtx, message, func(response *mDNS.Msg, exchangeErr error) {
|
||||
if exchangeErr != nil && server.fallbackTransport != nil {
|
||||
server.fallbackTransport.ExchangeAsync(exchangeCtx, message, exchangeCallback)
|
||||
return
|
||||
server.exchange(exchangeCtx, message, func(response *mDNS.Msg, err error) {
|
||||
if exchangeCtx.Err() == nil && !acceptResolvedServerResponse(response, err) {
|
||||
s.currentServer.CompareAndSwap(serverIndex, (serverIndex+1)%serverCount)
|
||||
}
|
||||
exchangeCallback(response, exchangeErr)
|
||||
exchangeCallback(response, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
dnsTransport.ExchangeSequential(ctx, serverExchangers, nil, callback)
|
||||
dnsTransport.ExchangeSequential(ctx, serverExchangers, acceptResolvedServerResponse, callback)
|
||||
}
|
||||
|
||||
func (s *resolvedServer) exchange(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
|
||||
exchangeResolvedTransport(ctx, s.primaryTransport, message, func(response *mDNS.Msg, err error) {
|
||||
if err != nil && s.fallbackTransport != nil {
|
||||
exchangeResolvedTransport(ctx, s.fallbackTransport, message, callback)
|
||||
return
|
||||
}
|
||||
callback(response, err)
|
||||
})
|
||||
}
|
||||
|
||||
func exchangeResolvedTransport(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
|
||||
exchangeCtx, cancel := context.WithTimeout(ctx, resolvedServerTimeout)
|
||||
transport.ExchangeAsync(exchangeCtx, message, func(response *mDNS.Msg, err error) {
|
||||
cancel()
|
||||
callback(response, err)
|
||||
})
|
||||
}
|
||||
|
||||
// systemd-resolved starts a query on every scope at the best match level at once
|
||||
// (resolved-dns-query.c dns_query_go_scopes) and completes it with the first positive answer
|
||||
// (dns_query_ready). Within a scope, SERVFAIL and timeouts move the scope to its next server, which
|
||||
// later queries start from (resolved-dns-transaction.c dns_transaction_retry).
|
||||
func acceptResolvedServerResponse(response *mDNS.Msg, err error) bool {
|
||||
return err == nil && response.Rcode != mDNS.RcodeServerFailure
|
||||
}
|
||||
|
||||
func acceptResolvedScopeResponse(response *mDNS.Msg, err error) bool {
|
||||
return err == nil && response.Rcode == mDNS.RcodeSuccess
|
||||
}
|
||||
|
||||
func (s *resolvedServerSet) selectScopes(name string) []*resolvedScope {
|
||||
var (
|
||||
bestMatch = resolvedScopeNoMatch
|
||||
selectedScopes []*resolvedScope
|
||||
)
|
||||
for _, scope := range s.scopes {
|
||||
match := scope.match(name)
|
||||
if match == resolvedScopeNoMatch || match < bestMatch {
|
||||
continue
|
||||
}
|
||||
if match > bestMatch {
|
||||
bestMatch = match
|
||||
selectedScopes = selectedScopes[:0]
|
||||
}
|
||||
selectedScopes = append(selectedScopes, scope)
|
||||
}
|
||||
return selectedScopes
|
||||
}
|
||||
|
||||
func (s *resolvedScope) match(name string) int {
|
||||
bestLabels := -1
|
||||
for _, domain := range s.domains {
|
||||
if mDNS.IsSubDomain(domain, name) {
|
||||
bestLabels = max(bestLabels, mDNS.CountLabel(domain))
|
||||
}
|
||||
}
|
||||
if bestLabels >= 0 {
|
||||
return bestLabels
|
||||
}
|
||||
if !s.defaultRoute {
|
||||
return resolvedScopeNoMatch
|
||||
}
|
||||
if s.fallback {
|
||||
return resolvedScopeLastResort
|
||||
}
|
||||
return resolvedScopeMaybe
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) loopUpdateStatus() {
|
||||
@@ -275,26 +413,39 @@ func (t *DBusResolvedResolver) postUpdateStatus() {
|
||||
}
|
||||
go func() {
|
||||
defer updateCancel()
|
||||
timer := time.NewTimer(resolvedUpdateDelay)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-updateContext.Done():
|
||||
return
|
||||
}
|
||||
t.updateStatus(updateContext)
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) updateStatus(ctx context.Context) {
|
||||
func (t *DBusResolvedResolver) updateStatus(ctx context.Context) error {
|
||||
t.updateRunAccess.Lock()
|
||||
defer t.updateRunAccess.Unlock()
|
||||
if t.closed || ctx.Err() != nil {
|
||||
return
|
||||
if t.closed {
|
||||
return os.ErrClosed
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
serverSet, err := t.checkResolved(ctx)
|
||||
if t.closed || ctx.Err() != nil {
|
||||
if serverSet != nil {
|
||||
_ = serverSet.Close()
|
||||
_ = serverSet.serverScope.Close()
|
||||
}
|
||||
return
|
||||
if t.closed {
|
||||
return os.ErrClosed
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
oldServerSet := t.savedServerSet.Swap(serverSet)
|
||||
if oldServerSet != nil {
|
||||
_ = oldServerSet.Close()
|
||||
_ = oldServerSet.serverScope.Close()
|
||||
}
|
||||
if err != nil {
|
||||
var dbusErr dbus.Error
|
||||
@@ -304,34 +455,16 @@ func (t *DBusResolvedResolver) updateStatus(ctx context.Context) {
|
||||
if oldServerSet != nil {
|
||||
t.logger.Debug("systemd-resolved service is gone")
|
||||
}
|
||||
return
|
||||
return err
|
||||
} else if oldServerSet == nil {
|
||||
t.logger.Debug("using systemd-resolved service as resolver")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) {
|
||||
if serverSet == nil || len(serverSet.servers) == 0 {
|
||||
return nil, E.New("link has no DNS servers configured")
|
||||
}
|
||||
var lastError error
|
||||
for _, server := range serverSet.servers {
|
||||
response, err := server.primaryTransport.Exchange(ctx, message)
|
||||
if err != nil && server.fallbackTransport != nil {
|
||||
response, err = server.fallbackTransport.Exchange(ctx, message)
|
||||
}
|
||||
if err != nil {
|
||||
lastError = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
return nil, lastError
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) {
|
||||
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
|
||||
err := dbusObject.(*dbus.Object).CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Err
|
||||
managerObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
|
||||
err := managerObject.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -339,102 +472,173 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
|
||||
if defaultInterface == nil {
|
||||
return nil, E.New("missing default interface")
|
||||
}
|
||||
call := dbusObject.(*dbus.Object).CallWithContext(
|
||||
ctx,
|
||||
"org.freedesktop.resolve1.Manager.GetLink",
|
||||
0,
|
||||
int32(defaultInterface.Index),
|
||||
)
|
||||
if call.Err != nil {
|
||||
return nil, call.Err
|
||||
}
|
||||
var linkPath dbus.ObjectPath
|
||||
err = call.Store(&linkPath)
|
||||
managerProperties, err := loadResolvedProperties(ctx, managerObject, "org.freedesktop.resolve1.Manager")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
linkObject := t.systemBus.Object("org.freedesktop.resolve1", linkPath)
|
||||
if linkObject == nil {
|
||||
return nil, E.New("missing link object for default interface")
|
||||
}
|
||||
dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject)
|
||||
servers, err := loadResolvedManagerServers(managerProperties, "DNS")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ctx.Err()
|
||||
var managerDomains []resolvedManagerDomain
|
||||
err = storeResolvedProperty(managerProperties, "Domains", &managerDomains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
linkDNSEx, err := loadResolvedLinkDNSEx(linkObject)
|
||||
var dnsOverTLSMode string
|
||||
err = storeResolvedProperty(managerProperties, "DNSOverTLS", &dnsOverTLSMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ctx.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
globalScope := resolvedScopeSpecification{
|
||||
interfaceName: defaultInterface.Name,
|
||||
dnsOverTLSMode: dnsOverTLSMode,
|
||||
defaultRoute: true,
|
||||
}
|
||||
linkDNS, err := loadResolvedLinkDNS(linkObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var linkScopes []resolvedScopeSpecification
|
||||
linkScopeIndexes := make(map[int32]int)
|
||||
for _, server := range servers {
|
||||
if server.interfaceIndex == 0 {
|
||||
serverSpecification, loaded := buildResolvedServerSpecification(globalScope.interfaceName, server)
|
||||
if loaded {
|
||||
globalScope.servers = append(globalScope.servers, serverSpecification)
|
||||
}
|
||||
continue
|
||||
}
|
||||
scopeIndex, loaded := linkScopeIndexes[server.interfaceIndex]
|
||||
if !loaded {
|
||||
linkScope, linkLoaded, linkErr := t.loadResolvedLinkScope(ctx, managerObject, server.interfaceIndex)
|
||||
if linkErr != nil {
|
||||
return nil, linkErr
|
||||
}
|
||||
if !linkLoaded {
|
||||
linkScopeIndexes[server.interfaceIndex] = -1
|
||||
continue
|
||||
}
|
||||
scopeIndex = len(linkScopes)
|
||||
linkScopeIndexes[server.interfaceIndex] = scopeIndex
|
||||
linkScopes = append(linkScopes, linkScope)
|
||||
} else if scopeIndex < 0 {
|
||||
continue
|
||||
}
|
||||
serverSpecification, loaded := buildResolvedServerSpecification(linkScopes[scopeIndex].interfaceName, server)
|
||||
if loaded {
|
||||
linkScopes[scopeIndex].servers = append(linkScopes[scopeIndex].servers, serverSpecification)
|
||||
}
|
||||
}
|
||||
if len(linkDNSEx) == 0 && len(linkDNS) == 0 {
|
||||
for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() {
|
||||
if inbound.Type() == C.TypeTun {
|
||||
return nil, E.New("No appropriate name servers or networks for name found")
|
||||
for _, domain := range managerDomains {
|
||||
domainName := mDNS.Fqdn(domain.Domain)
|
||||
if domain.InterfaceIndex == 0 {
|
||||
globalScope.domains = append(globalScope.domains, domainName)
|
||||
continue
|
||||
}
|
||||
scopeIndex, loaded := linkScopeIndexes[domain.InterfaceIndex]
|
||||
if loaded && scopeIndex >= 0 {
|
||||
linkScopes[scopeIndex].domains = append(linkScopes[scopeIndex].domains, domainName)
|
||||
}
|
||||
}
|
||||
if len(globalScope.servers) == 0 && !slices.ContainsFunc(linkScopes, func(it resolvedScopeSpecification) bool {
|
||||
return it.defaultRoute && len(it.servers) > 0
|
||||
}) {
|
||||
fallbackServers, fallbackErr := loadResolvedManagerServers(managerProperties, "FallbackDNS")
|
||||
if fallbackErr != nil {
|
||||
return nil, fallbackErr
|
||||
}
|
||||
for _, server := range fallbackServers {
|
||||
serverSpecification, loaded := buildResolvedServerSpecification(globalScope.interfaceName, server)
|
||||
if loaded {
|
||||
globalScope.servers = append(globalScope.servers, serverSpecification)
|
||||
}
|
||||
}
|
||||
return nil, E.New("link has no DNS servers configured")
|
||||
globalScope.fallback = true
|
||||
}
|
||||
scopeSpecifications := common.Filter(append([]resolvedScopeSpecification{globalScope}, linkScopes...), func(it resolvedScopeSpecification) bool {
|
||||
return len(it.servers) > 0
|
||||
})
|
||||
if len(scopeSpecifications) == 0 {
|
||||
return nil, E.New("no DNS servers configured")
|
||||
}
|
||||
serverSet := &resolvedServerSet{
|
||||
serverScope: adapter.NewScope(t.ctx, t.logger),
|
||||
}
|
||||
for _, scopeSpecification := range scopeSpecifications {
|
||||
scope, createErr := t.createResolvedScope(serverSet.serverScope, scopeSpecification)
|
||||
if createErr != nil {
|
||||
_ = serverSet.serverScope.Close()
|
||||
return nil, createErr
|
||||
}
|
||||
serverSet.scopes = append(serverSet.scopes, scope)
|
||||
for _, serverSpecification := range scopeSpecification.servers {
|
||||
serverSet.signature = append(serverSet.signature, scopeSpecification.interfaceName+"/"+M.SocksaddrFrom(serverSpecification.address, serverSpecification.port).String())
|
||||
}
|
||||
}
|
||||
return serverSet, nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) loadResolvedLinkScope(ctx context.Context, managerObject dbus.BusObject, interfaceIndex int32) (resolvedScopeSpecification, bool, error) {
|
||||
linkInterface, err := t.interfaceFinder.ByIndex(int(interfaceIndex))
|
||||
if err != nil || slices.Contains(t.interfaceMonitor.MyInterfaces(), linkInterface.Name) {
|
||||
return resolvedScopeSpecification{}, false, nil
|
||||
}
|
||||
var linkPath dbus.ObjectPath
|
||||
err = managerObject.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.GetLink", 0, interfaceIndex).Store(&linkPath)
|
||||
if err != nil {
|
||||
return resolvedScopeSpecification{}, false, err
|
||||
}
|
||||
linkProperties, err := loadResolvedProperties(ctx, t.systemBus.Object("org.freedesktop.resolve1", linkPath), "org.freedesktop.resolve1.Link")
|
||||
if err != nil {
|
||||
return resolvedScopeSpecification{}, false, err
|
||||
}
|
||||
var scopesMask uint64
|
||||
err = storeResolvedProperty(linkProperties, "ScopesMask", &scopesMask)
|
||||
if err != nil {
|
||||
return resolvedScopeSpecification{}, false, err
|
||||
}
|
||||
if scopesMask&resolvedScopesMaskDNS == 0 {
|
||||
return resolvedScopeSpecification{}, false, nil
|
||||
}
|
||||
linkScope := resolvedScopeSpecification{
|
||||
interfaceName: linkInterface.Name,
|
||||
}
|
||||
err = storeResolvedProperty(linkProperties, "DNSOverTLS", &linkScope.dnsOverTLSMode)
|
||||
if err != nil {
|
||||
return resolvedScopeSpecification{}, false, err
|
||||
}
|
||||
err = storeResolvedProperty(linkProperties, "DefaultRoute", &linkScope.defaultRoute)
|
||||
if err != nil {
|
||||
return resolvedScopeSpecification{}, false, err
|
||||
}
|
||||
return linkScope, true, nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) createResolvedScope(serverScope *adapter.Scope, scopeSpecification resolvedScopeSpecification) (*resolvedScope, error) {
|
||||
scope := &resolvedScope{
|
||||
domains: scopeSpecification.domains,
|
||||
defaultRoute: scopeSpecification.defaultRoute,
|
||||
fallback: scopeSpecification.fallback,
|
||||
}
|
||||
serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
|
||||
AbstractDialerOptions: option.AbstractDialerOptions{
|
||||
BindInterface: defaultInterface.Name,
|
||||
BindInterface: scopeSpecification.interfaceName,
|
||||
UDPFragmentDefault: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var serverSpecifications []resolvedServerSpecification
|
||||
if len(linkDNSEx) > 0 {
|
||||
for _, entry := range linkDNSEx {
|
||||
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name)
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
serverSpecifications = append(serverSpecifications, serverSpecification)
|
||||
}
|
||||
} else {
|
||||
for _, entry := range linkDNS {
|
||||
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "")
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
serverSpecifications = append(serverSpecifications, serverSpecification)
|
||||
}
|
||||
}
|
||||
if len(serverSpecifications) == 0 {
|
||||
return nil, E.New("no valid DNS servers on link")
|
||||
}
|
||||
serverSet := &resolvedServerSet{
|
||||
servers: make([]resolvedServer, 0, len(serverSpecifications)),
|
||||
signature: common.Map(serverSpecifications, func(it resolvedServerSpecification) string {
|
||||
return M.SocksaddrFrom(it.address, it.port).String()
|
||||
}),
|
||||
}
|
||||
for _, serverSpecification := range serverSpecifications {
|
||||
server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification)
|
||||
for _, serverSpecification := range scopeSpecification.servers {
|
||||
server, createErr := t.createResolvedServer(serverScope, serverDialer, scopeSpecification.dnsOverTLSMode, serverSpecification)
|
||||
if createErr != nil {
|
||||
_ = serverSet.Close()
|
||||
return nil, createErr
|
||||
}
|
||||
serverSet.servers = append(serverSet.servers, server)
|
||||
scope.servers = append(scope.servers, server)
|
||||
}
|
||||
return serverSet, nil
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) {
|
||||
func (t *DBusResolvedResolver) createResolvedServer(serverScope *adapter.Scope, serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) {
|
||||
if dnsOverTLSMode == "yes" {
|
||||
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
|
||||
primaryTransport, err := t.createResolvedTransport(serverScope, serverDialer, serverSpecification, true)
|
||||
if err != nil {
|
||||
return resolvedServer{}, err
|
||||
}
|
||||
@@ -443,13 +647,12 @@ func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOv
|
||||
}, nil
|
||||
}
|
||||
if dnsOverTLSMode == "opportunistic" {
|
||||
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
|
||||
primaryTransport, err := t.createResolvedTransport(serverScope, serverDialer, serverSpecification, true)
|
||||
if err != nil {
|
||||
return resolvedServer{}, err
|
||||
}
|
||||
fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
|
||||
fallbackTransport, err := t.createResolvedTransport(serverScope, serverDialer, serverSpecification, false)
|
||||
if err != nil {
|
||||
_ = primaryTransport.Close()
|
||||
return resolvedServer{}, err
|
||||
}
|
||||
return resolvedServer{
|
||||
@@ -457,7 +660,7 @@ func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOv
|
||||
fallbackTransport: fallbackTransport,
|
||||
}, nil
|
||||
}
|
||||
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
|
||||
primaryTransport, err := t.createResolvedTransport(serverScope, serverDialer, serverSpecification, false)
|
||||
if err != nil {
|
||||
return resolvedServer{}, err
|
||||
}
|
||||
@@ -466,7 +669,7 @@ func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOv
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) {
|
||||
func (t *DBusResolvedResolver) createResolvedTransport(serverScope *adapter.Scope, serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) {
|
||||
serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS))
|
||||
if useTLS {
|
||||
tlsAddress := serverSpecification.address
|
||||
@@ -485,35 +688,22 @@ func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, se
|
||||
return nil, err
|
||||
}
|
||||
serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig)
|
||||
err = serverTransport.Start(adapter.StartStateStart)
|
||||
err = serverTransport.Start(adapter.StartStateStart, serverScope)
|
||||
if err != nil {
|
||||
_ = serverTransport.Close()
|
||||
return nil, err
|
||||
}
|
||||
return serverTransport, nil
|
||||
}
|
||||
serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress)
|
||||
err := serverTransport.Start(adapter.StartStateStart)
|
||||
err := serverTransport.Start(adapter.StartStateStart, serverScope)
|
||||
if err != nil {
|
||||
_ = serverTransport.Close()
|
||||
return nil, err
|
||||
}
|
||||
return serverTransport, nil
|
||||
}
|
||||
|
||||
func (s *resolvedServerSet) Close() error {
|
||||
var errors []error
|
||||
for _, server := range s.servers {
|
||||
errors = append(errors, server.primaryTransport.Close())
|
||||
if server.fallbackTransport != nil {
|
||||
errors = append(errors, server.fallbackTransport.Close())
|
||||
}
|
||||
}
|
||||
return E.Errors(errors...)
|
||||
}
|
||||
|
||||
func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) {
|
||||
address, loaded := netip.AddrFromSlice(rawAddress)
|
||||
func buildResolvedServerSpecification(interfaceName string, server resolvedManagerServer) (resolvedServerSpecification, bool) {
|
||||
address, loaded := netip.AddrFromSlice(server.address)
|
||||
if !loaded {
|
||||
return resolvedServerSpecification{}, false
|
||||
}
|
||||
@@ -522,8 +712,8 @@ func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, p
|
||||
}
|
||||
return resolvedServerSpecification{
|
||||
address: address,
|
||||
port: port,
|
||||
serverName: serverName,
|
||||
port: server.port,
|
||||
serverName: server.serverName,
|
||||
}, true
|
||||
}
|
||||
|
||||
@@ -537,57 +727,55 @@ func resolvedServerPort(port uint16, useTLS bool) uint16 {
|
||||
return 53
|
||||
}
|
||||
|
||||
func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) {
|
||||
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
|
||||
func loadResolvedProperties(ctx context.Context, object dbus.BusObject, interfaceName string) (map[string]dbus.Variant, error) {
|
||||
var properties map[string]dbus.Variant
|
||||
err := object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.GetAll", 0, interfaceName).Store(&properties)
|
||||
if err != nil {
|
||||
if isResolvedUnknownPropertyError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
return nil, E.Cause(err, "load ", interfaceName, " properties")
|
||||
}
|
||||
var linkDNS []resolved.LinkDNS
|
||||
err = dnsProperty.Store(&linkDNS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return linkDNS, nil
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) {
|
||||
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx")
|
||||
if err != nil {
|
||||
if isResolvedUnknownPropertyError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
func storeResolvedProperty(properties map[string]dbus.Variant, name string, value any) error {
|
||||
property, loaded := properties[name]
|
||||
if !loaded {
|
||||
return nil
|
||||
}
|
||||
var linkDNSEx []resolved.LinkDNSEx
|
||||
err = dnsProperty.Store(&linkDNSEx)
|
||||
err := property.Store(value)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse resolved property ", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadResolvedManagerServers(properties map[string]dbus.Variant, name string) ([]resolvedManagerServer, error) {
|
||||
_, loaded := properties[name+"Ex"]
|
||||
if loaded {
|
||||
var serversEx []resolvedManagerDNSEx
|
||||
err := storeResolvedProperty(properties, name+"Ex", &serversEx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return common.Map(serversEx, func(it resolvedManagerDNSEx) resolvedManagerServer {
|
||||
return resolvedManagerServer{
|
||||
interfaceIndex: it.InterfaceIndex,
|
||||
address: it.Address,
|
||||
port: it.Port,
|
||||
serverName: it.Name,
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
var servers []resolvedManagerDNS
|
||||
err := storeResolvedProperty(properties, name, &servers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return linkDNSEx, nil
|
||||
}
|
||||
|
||||
func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) {
|
||||
dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS")
|
||||
if err != nil {
|
||||
if isResolvedUnknownPropertyError(err) {
|
||||
return "", nil
|
||||
return common.Map(servers, func(it resolvedManagerDNS) resolvedManagerServer {
|
||||
return resolvedManagerServer{
|
||||
interfaceIndex: it.InterfaceIndex,
|
||||
address: it.Address,
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
var dnsOverTLSMode string
|
||||
err = dnsOverTLSProperty.Store(&dnsOverTLSMode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dnsOverTLSMode, nil
|
||||
}
|
||||
|
||||
func isResolvedUnknownPropertyError(err error) bool {
|
||||
var dbusError dbus.Error
|
||||
return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty"
|
||||
}), nil
|
||||
}
|
||||
|
||||
func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user