Compare commits

...
Author SHA1 Message Date
Shtorm d9442d14b1 Update mtg-multi 2026-10-19 17:34:19 +03:00
Shtorm 7dd51eb244 Merge branch 'extended' of https://github.com/shtorm-7/sing-box into extended 2026-10-19 17:10:47 +03:00
Shtorm f269dee097 fix: Wait for trusttunnel client connection and add disable_path_mtu_discovery 2026-10-19 17:09:48 +03:00
Shtorm 60094a12b4 fix: Use domain resolver in mieru outbound 2026-10-19 17:07:19 +03:00
Shtorm dee34e0300 masque: Add L4 proxy mode and handshake timeout 2026-10-19 17:05:38 +03:00
Shtorm b2bb7bf347 Update wireguard-go 2026-10-19 17:03:01 +03:00
Shtorm 5be93998c6 Update sudoku 2026-10-19 16:43:45 +03:00
Shtorm 364c6a6e6a xhttp: Port upstream Xray fixes and config checks 2026-10-19 15:20:03 +03:00
Shtorm cb165a381c fix: Synchronize provider outbounds and update tickers 2026-10-19 14:47:55 +03:00
Shtorm 6b5ee55ba2 fix: Report fork component references to reference manager 2026-10-19 14:37:32 +03:00
Shtorm 2283c95513 Merge tag 'v1.14.3' 2026-10-19 01:05:55 +03:00
世界 7054cac512 Bump version 2026-10-09 12:36:50 +08:00
世界 78d44d52d9 Reset network on DNS server changes 2026-10-09 12:36:03 +08:00
世界 1a773a425a Exclude USB/IP from legacy macOS build 2026-10-09 12:36:03 +08:00
世界 194ebd18b6 tun: Fix inconsistent DNS mode behavior without auto_route 2026-10-09 12:36:03 +08:00
世界 80c1171410 tailscale: Fix SSH auth banners never sent 2026-10-09 12:36:03 +08:00
世界 67fb235ee8 Add roothide package to iOS jailbreak release 2026-10-09 12:36:03 +08:00
世界 02537831e1 Move auto-redirect and bridge index allocation out of constructors 2026-10-09 12:36:03 +08:00
世界 f9fcb8964f Fix usage and cache files overwritten when loading fails 2026-10-09 12:36:03 +08:00
世界 a691a44023 Refactor lifecycle to scoped cleanup 2026-10-09 12:36:03 +08:00
世界 a364ff4794 Remove unimplemented hot reload from managers 2026-10-09 12:36:03 +08:00
世界 bff5acf292 Handle connected UDP EOF on BSD 2026-10-09 12:36:03 +08:00
世界 52f9bad034 Fix protocol input validation 2026-10-09 12:36:03 +08:00
世界 a0d775ea66 Fix zero UDP checksum 2026-10-09 12:36:03 +08:00
世界 79fc394d67 documentation: Update Apple App Store link 2026-10-09 12:36:03 +08:00
世界 f95aa6ac11 Update dependencies 2026-10-09 12:36:02 +08:00
世界 6e3c86f518 Use screen state to end device pause on iOS 2026-10-09 12:36:02 +08:00
世界 7a3d4e4a8e Add goroutine dump and hang trigger to libbox 2026-10-02 18:00:09 +08:00
世界 351ce12f73 Record reloads in reports and dump heap on reset 2026-10-02 18:00:09 +08:00
世界 85c4a530b7 Add platform metadata to reports 2026-10-02 18:00:09 +08:00
世界 cdea9e87f1 Log network path only when changed 2026-10-02 18:00:09 +08:00
世界 21ba30b8c3 Keep idle connections across short device sleeps 2026-10-02 18:00:09 +08:00
世界 07130365d1 Improve idle connection management 2026-10-02 18:00:09 +08:00
世界 eb7deeae8a Improve power report attribution and sampling 2026-10-02 18:00:09 +08:00
世界 fd9a076d25 Close idle connections of unreferenced outbounds and DNS servers 2026-10-02 18:00:09 +08:00
embogomolovand世界 d2cf9747d7 Fix interrupt group holding lock while closing connections 2026-10-02 18:00:09 +08:00
世界 d41e1745e6 Fix UDP fragmentation 2026-10-02 18:00:09 +08:00
世界 0efa644942 Fix tun GSO check 2026-10-02 18:00:09 +08:00
世界 021e0877a3 Cleanup documentations 2026-10-02 18:00:09 +08:00
世界 ddbf34639a Fix local DNS server ignoring systemd-resolved global DNS servers 2026-10-02 18:00:08 +08:00
ShtormandGitHub 5956780ba3 Merge pull request #159 from mihail-jdanov/extended
provider: Fix startup failure when restoring an empty subscription cache
2026-09-30 18:49:13 +02:00
Mikhail Zhdanov d75ac4ea21 provider: Fix startup failure when restoring an empty subscription cache
Decode cached subscription data as serialized options, allowing an empty
outbound list produced by include/exclude filters. Empty providers now
use the existing Compatible group fallback instead of failing startup.

Share outbound filtering and application between cache restoration and
subscription updates.
2026-09-30 19:56:01 +07:00
世界 4537a1ac00 Fix system TUN read loop stopping on write errors 2026-09-25 19:28:45 +08:00
世界 af6e64c3b6 Bump version 2026-09-24 18:52:34 +08:00
世界 e6a1a3073e Fix hysteria port hopping connecting to another resolved server address 2026-09-24 17:23:36 +08:00
世界 83aec4222f Fix missing handshake success report for hijack-dns 2026-09-24 17:23:36 +08:00
世界 17c52d03b3 Fix hysteria2 realm STUN resolution ignoring default domain resolver 2026-09-24 17:23:36 +08:00
世界 111ecb3483 Fix network reset on every startup 2026-09-24 17:23:36 +08:00
世界 6b6c20a248 Fix resolved service 2026-09-24 17:23:36 +08:00
世界 4abede8ceb Fix network reset 2026-09-24 17:23:36 +08:00
世界 0d91c45479 Fix file manager treating Windows absolute paths as relative 2026-09-22 09:51:17 +08:00
世界 ca7216a381 Fix UDP domain destination lost in outbound groups 2026-09-21 23:38:34 +08:00
世界 19f57607a9 Fix HTTP sniffer storing IP literal hosts as domain 2026-09-21 22:56:08 +08:00
世界 886422d2e0 Fix FakeIP reset failing on missing buckets 2026-09-21 22:00:03 +08:00
世界 1ea3bb4563 Fix OOM report generated on clean shutdown 2026-09-21 12:30:41 +08:00
世界 78afbb2e62 Respect DNS query timeouts during deduplication 2026-09-21 12:26:53 +08:00
Shtorm 6cfc2a86f8 wireguard: Add random_trailers and disable_cookies amnezia options 2026-09-21 02:52:33 +03:00
Shtorm 55faa763f9 build: Add openvpn, openconnect, cloudflared and usbip build tags to goreleaser 2026-09-20 23:18:07 +03:00
Shtorm 466eb0245a Merge tag 'v1.14.1' 2026-09-20 22:37:07 +03:00
Shtorm aae5f7ff02 reality: Add support_x25519mlkem768 option 2026-09-20 22:01:43 +03:00
Shtorm 311d9b51eb call: Update vk, telemost, dion and wbstream implementations 2026-09-20 21:57:51 +03:00
Shtorm 197cf99b73 transport: Reuse force_close helper in xhttp and grpclite 2026-09-20 21:41:03 +03:00
世界 aab74fea51 Add libbox API to detect tun inbounds 2026-09-20 22:24:38 +08:00
世界 f5e8cdae19 Fix gRPC transport half-close 2026-09-20 22:24:38 +08:00
世界 60f0a0369b Fix half-close not propagating through connection wrappers 2026-09-20 22:24:38 +08:00
世界 6a7f11fca3 Fix JSON comment scanner panic on truncated input 2026-09-20 22:24:38 +08:00
世界 afb0b30558 Fix traffic counters on interrupted splice copies 2026-09-20 22:24:38 +08:00
世界 bea01fe80a Fix HTTP proxy panic when upstream responds early 2026-09-20 22:24:38 +08:00
世界 fbb74b13e6 Fix early handshake not sent through outbound groups 2026-09-20 22:24:38 +08:00
世界 7b1d7e1c85 Fix tls_fragment treating empty write as first packet 2026-09-20 22:24:38 +08:00
世界 3f0958bf58 Fix read loops spinning on persistent errors 2026-09-20 22:24:38 +08:00
世界 18b0317018 Fix HTTP/2 stream errors leaking into nested readers and transport data races 2026-09-20 22:24:38 +08:00
世界 9be5fcf4e1 Fix dial contexts cancelled while connections are still in use 2026-09-20 22:24:38 +08:00
世界 9cf7a7a28a Use xcode-27 runner image for Apple clients 2026-09-20 22:24:38 +08:00
世界 a1f948c330 Fix implicit default DNS server and outbound skipping initialize stage 2026-09-20 22:24:38 +08:00
世界 cd568a563b Fix command client cancellation during connection 2026-09-20 22:24:38 +08:00
世界 96b004bab1 Fix initial WireGuard handshake for domain peers 2026-09-20 22:24:38 +08:00
世界 5ae597bfba documentation: Fix default TLS min_version 2026-09-20 22:24:38 +08:00
世界 c0b8fe8b57 Fix API dashboard using implicit default HTTP client 2026-09-20 22:24:38 +08:00
世界 f5f4995c32 Fix systemd daemon reload after package installation 2026-09-20 22:24:38 +08:00
世界 d7cc756eba Fix TestFlight publish being skipped on stable 2026-09-20 22:24:38 +08:00
世界 9fa1890aea Add App Store review submission command 2026-09-20 22:24:38 +08:00
世界 394285b78b Fix TestFlight publish picking builds of other versions 2026-09-20 22:24:38 +08:00
世界 491813c360 Bind network reset dispatch to manager lifecycle 2026-09-20 22:24:38 +08:00
世界 474aa5b5d9 Fix crash on corrupted cache file 2026-09-20 22:24:38 +08:00
Shtorm 6439d67043 xhttp: Update xhttp core 2026-09-19 14:32:34 +03:00
Shtorm e63f3b6be2 parser: Fix nil match panic in v2rayTransportWsPath 2026-09-19 14:13:33 +03:00
Shtorm 2c379103f3 masque: Use custom QUIC transport with configurable connection ID length 2026-09-19 14:02:12 +03:00
世界 1ac1a339cb Bump version 2026-09-15 01:21:37 +08:00
世界 dc3b490149 Fix Apple DerivedData cache shared across branches 2026-09-15 01:21:36 +08:00
世界 6c87a0c044 Fix Tailscale netmap expiry timer keeping closed endpoint alive 2026-09-15 00:21:10 +08:00
世界 b5d5338e52 Fix memory pressure callback restarting stopped OOM killer timer 2026-09-15 00:21:10 +08:00
世界 73a4196cb3 Fix closed connection errors not recognized on Windows 2026-09-14 23:18:42 +08:00
世界 9713a546e6 Update Go to 1.26.8 2026-09-14 23:18:10 +08:00
世界 54e5c497bb Wake the current instance after device pause on iOS 2026-09-14 23:18:09 +08:00
世界 c827ec8303 Read process name from comm in resolved fallback 2026-09-14 23:18:09 +08:00
世界 b7eb49bb87 Fix cronet-go 2026-09-12 18:52:34 +08:00
Bruce WayneandGitHub 378f151c75 documentation: Fix endpoint_independent_nat 2026-09-10 16:47:09 +08:00
世界 8d0500eded Fix auto redirect pre-match and L3 forwarding 2026-09-10 14:59:10 +08:00
世界 70d0ba7279 Fix network monitor spinning after netlink receive overrun on Linux 2026-09-09 12:46:59 +08:00
世界 6d1fc214c1 Authorize enabling insecure mode with PolicyKit on Linux 2026-09-08 23:05:12 +08:00
世界 046c46eacb Fix config check sharing service registry with the running instance 2026-09-08 19:01:17 +08:00
世界 89ca622e99 Fix WireGuard endpoint stuck after network change on macOS 2026-09-08 18:45:52 +08:00
世界 f909997082 Fix bypass with outbound not bypassing in pre-match 2026-09-08 16:30:59 +08:00
世界 6364d9a5f1 Fix WireGuard endpoint stopping on device sleep 2026-09-08 15:19:58 +08:00
世界 d6ec014ca4 Fix omitempty for JSON struct fields
Claude-Session: https://claude.ai/code/session_01NxBrtZjwwaJC8uxCNbgmKu
2026-09-08 15:03:09 +08:00
世界 b2a5ac3f60 Fix Tailscale endpoint not binding IPv6 sockets on Windows 2026-09-08 14:48:44 +08:00
世界 515a73e4ee Fix selector not interrupting routed connections 2026-09-08 13:19:30 +08:00
世界 0c041aa7f6 Fix direct inbound UDP on 32-bit Linux before 5.1 2026-09-06 16:25:12 +08:00
世界 0d7fae13bc Fix process search for socks UDP associate 2026-09-06 15:54:54 +08:00
世界 0265f31f84 Fix Linux process path lookup after privilege drop 2026-09-06 15:51:35 +08:00
世界 4ef3432c0d Fix Linux process search matching the wrong socket 2026-09-06 15:51:35 +08:00
世界 288411b0b9 Fix crash when interface monitor is unavailable 2026-09-06 14:56:27 +08:00
Shtorm 6a6a1e0a31 Merge branch 'extended' of https://github.com/shtorm-7/sing-box into extended 2026-09-05 12:57:15 +03:00
Shtorm 217f77643f provider: Rename StoreProviders to StoreSubscriptions 2026-09-05 10:12:55 +03:00
Shtorm 7afda58818 provider: Add override_dialer_options option 2026-09-05 09:23:55 +03:00
世界 a25ad8ce52 Fix h2mux with Go 1.27 2026-09-05 13:49:28 +08:00
Shtorm 64605fc47a protocol: Forward lifecycle Start stage to nested outbounds in vpn, failover and limiters 2026-09-05 04:30:03 +03:00
Shtorm 08703c9035 Update wireguard-go 2026-09-05 04:26:58 +03:00
世界 94ae161956 dns: Reject resolver discovery queries 2026-09-03 15:13:51 +08:00
世界 5c7467aaba oomkiller: Suppress unused lint for recordPressure on non-darwin platforms 2026-09-03 13:30:52 +08:00
世界 e4a19a2a7c Enable ARM64 cryptographic extensions in iOS library builds
Go's internal/cpu never detects ARM64 features on GOOS=ios, so AES, AES-GCM
and SHA-256 use their generic implementations on iOS and tvOS. Patch the
Go source in the Apple library build to assert the ARMv8.0 cryptographic
extensions, as Go already does for macOS.

See https://github.com/SagerNet/sing-box/issues/4486
2026-09-03 13:30:52 +08:00
世界 4bc15be97c tun: Fix auto_redirect pre-match skipped for forwarded traffic 2026-09-02 23:05:36 +08:00
世界 ad8d053985 Update dependencies 2026-09-02 22:22:32 +08:00
世界 21b8fcacc6 Record memory usage continuously in OOM report 2026-09-02 20:01:03 +08:00
世界 0944cd9d7b Reduce cache initial allocation 2026-09-02 18:45:51 +08:00
世界 bdd86c1fef Reduce logging allocations 2026-09-01 22:39:35 +08:00
世界 a556d49491 Separate Clash mode management from Clash API server 2026-09-01 22:39:21 +08:00
世界 6c4589c933 Record platform network path in power report 2026-09-01 21:43:43 +08:00
世界 b807ff30d0 Reduce rule matching memory allocations 2026-09-01 21:32:13 +08:00
世界 3e185071eb Reduce network environment update overhead 2026-08-31 20:55:53 +08:00
世界 650ef881c8 Use patch version 1000 for TestFlight 2026-08-31 19:07:48 +08:00
世界 f0a2c348f6 documentation: Fix macOS standalone client data migration 2026-08-31 15:35:35 +08:00
世界 df34f5068b documentation: Fix NaiveProxy version in changelog 2026-08-31 12:13:08 +08:00
500 changed files with 19693 additions and 10583 deletions
+1 -1
View File
@@ -1 +1 @@
45832ab074849607406baa3e3a2c4660274602ed
0d28acc44093df24b2526dea3d6ffefd6b0a54f0
+28
View File
@@ -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
+34
View File
@@ -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
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
VERSION="1.26.7"
VERSION="1.26.8"
PATCH_COMMITS=(
"f080b0c6346eb690c0dc82497b35925f385b35ac"
"2d9c12887c342fb9051d231aa5388743cb7e9cb6"
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
VERSION="1.26.7"
VERSION="1.26.8"
PATCH_COMMITS=(
"a4ae550aa148b04c9d4890e98bee63aede5c4b53"
"95b851f661584711faa8115b3234a461044f4510"
+29 -23
View File
@@ -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
+1 -1
View File
@@ -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: |
+2 -2
View File
@@ -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: |
+58
View File
@@ -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
+7 -4
View File
@@ -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 ./... && \
+7 -96
View File
@@ -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
-1
View File
@@ -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
}
-23
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+7 -91
View File
@@ -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
View File
@@ -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() {
-52
View File
@@ -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()
}
+9
View File
@@ -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
View File
@@ -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
}
}
}
+1
View File
@@ -10,6 +10,7 @@ import (
)
type Provider interface {
Lifecycle
Type() string
Tag() string
Outbounds() []Outbound
+39 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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:
-1
View File
@@ -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
}
+7 -91
View File
@@ -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
+14
View File
@@ -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
}
+5
View File
@@ -22,3 +22,8 @@ type V2RayClientTransport interface {
DialContext(ctx context.Context) (net.Conn, error)
Close() error
}
type V2RayMultiplexClientTransport interface {
V2RayClientTransport
MultiplexEnabled() bool
}
+139 -127
View File
@@ -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
Submodule clients/desktop updated: cebee0d527...50f5038491
+425 -235
View File
@@ -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
}
}
+1 -1
View File
@@ -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")
}
+47
View File
@@ -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
}
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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")
}
+4
View File
@@ -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
View File
@@ -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) {
+4 -3
View File
@@ -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"
+29 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+53
View File
@@ -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
}
@@ -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
@@ -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()
}
@@ -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
+13 -9
View File
@@ -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)
},
+52 -31
View File
@@ -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)
+6 -4
View File
@@ -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 {
+29 -5
View File
@@ -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
}
+6 -20
View File
@@ -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 {
+9 -11
View File
@@ -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
View File
@@ -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:]
+3 -3
View File
@@ -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
}
+17 -7
View File
@@ -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()
+3 -1
View File
@@ -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 {
+2 -2
View File
@@ -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)
}
+10 -16
View File
@@ -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 != "" {
+1 -5
View File
@@ -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
}
+5 -3
View File
@@ -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)
}
+27 -16
View File
@@ -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())
+123 -27
View File
@@ -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 {
+108
View File
@@ -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)
}
+1 -1
View File
@@ -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
}
+4 -1
View File
@@ -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
View File
@@ -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 {
+23 -19
View File
@@ -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
}
+1 -1
View File
@@ -254,5 +254,5 @@ func (c *realityConnWrapper) ReaderReplaceable() bool {
}
func (c *realityConnWrapper) WriterReplaceable() bool {
return true
return false
}
+10 -3
View File
@@ -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
}
+1 -1
View File
@@ -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
}()
+7 -13
View File
@@ -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
}
+13
View File
@@ -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)
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !with_quic
package urltest
import "context"
func contextWithQUICKeepSession(ctx context.Context) context.Context {
return ctx
}
+7 -1
View File
@@ -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
}
+5
View File
@@ -0,0 +1,5 @@
//go:build !(ios && tvos)
package constant
const IsTvOS = false
+5
View File
@@ -0,0 +1,5 @@
//go:build ios && tvos
package constant
const IsTvOS = true
+3 -5
View File
@@ -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
View File
@@ -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
}
+39
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+31
View File
@@ -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
View File
@@ -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
}
+64
View File
@@ -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 := &parallelExchange{
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"))
+12 -7
View File
@@ -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
}
+1 -5
View File
@@ -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
}
+1 -5
View File
@@ -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
View File
@@ -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()
+4
View File
@@ -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()
}
+18 -12
View File
@@ -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 {
-1
View File
@@ -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))
}
+416 -228
View File
@@ -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