mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-23 00:16:54 +00:00
Merge tag 'v1.14.1'
This commit is contained in:
@@ -1 +1 @@
|
||||
45832ab074849607406baa3e3a2c4660274602ed
|
||||
0d28acc44093df24b2526dea3d6ffefd6b0a54f0
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
--- a/src/internal/cpu/cpu_arm64_ios.go
|
||||
+++ b/src/internal/cpu/cpu_arm64_ios.go
|
||||
@@ -0,0 +1,14 @@
|
||||
+// Copyright 2020 The Go Authors. All rights reserved.
|
||||
+// Use of this source code is governed by a BSD-style
|
||||
+// license that can be found in the LICENSE file.
|
||||
+
|
||||
+//go:build arm64 && ios
|
||||
+
|
||||
+package cpu
|
||||
+
|
||||
+func osInit() {
|
||||
+ ARM64.HasAES = true
|
||||
+ ARM64.HasPMULL = true
|
||||
+ ARM64.HasSHA1 = true
|
||||
+ ARM64.HasSHA2 = true
|
||||
+}
|
||||
--- a/src/internal/cpu/cpu_arm64_other.go
|
||||
+++ b/src/internal/cpu/cpu_arm64_other.go
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
-//go:build arm64 && !linux && !freebsd && !android && (!darwin || ios) && !openbsd
|
||||
+//go:build arm64 && !linux && !freebsd && !android && !darwin && !openbsd
|
||||
|
||||
package cpu
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Go's internal/cpu never detects ARM64 features on GOOS=ios, so crypto/aes,
|
||||
# AES-GCM and crypto/sha256 use their generic implementations on iOS and tvOS.
|
||||
# See https://github.com/SagerNet/sing-box/issues/4486
|
||||
#
|
||||
# Only the ARMv8.0 cryptographic extensions are asserted; ARMv8.1 atomics and
|
||||
# SHA-512 are absent on the A8/A9 devices still supported by the deployment targets.
|
||||
#
|
||||
# Remove once the Go release used by the Apple library build includes the fix.
|
||||
|
||||
export GOTOOLCHAIN=local
|
||||
|
||||
GOROOT="$(go env GOROOT)"
|
||||
PATCH_FILE="$(cd "$(dirname "$0")" && pwd)/go_ios_cpu_features.patch"
|
||||
|
||||
cd "$GOROOT"
|
||||
if [[ -f src/internal/cpu/cpu_arm64_ios.go ]]; then
|
||||
echo "already patched"
|
||||
else
|
||||
patch --verbose -p1 < "$PATCH_FILE"
|
||||
fi
|
||||
|
||||
CPU_FILES="$(GOOS=ios GOARCH=arm64 CGO_ENABLED=0 go list -f '{{.GoFiles}}' internal/cpu)"
|
||||
echo "internal/cpu files for ios/arm64: $CPU_FILES"
|
||||
case "$CPU_FILES" in
|
||||
*cpu_arm64_ios.go*) ;;
|
||||
*)
|
||||
echo "patch is not effective" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.26.7"
|
||||
VERSION="1.26.8"
|
||||
PATCH_COMMITS=(
|
||||
"f080b0c6346eb690c0dc82497b35925f385b35ac"
|
||||
"2d9c12887c342fb9051d231aa5388743cb7e9cb6"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.26.7"
|
||||
VERSION="1.26.8"
|
||||
PATCH_COMMITS=(
|
||||
"a4ae550aa148b04c9d4890e98bee63aede5c4b53"
|
||||
"95b851f661584711faa8115b3234a461044f4510"
|
||||
|
||||
+23
-18
@@ -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:
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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: |-
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.7
|
||||
go-version: 1.26.8
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
|
||||
@@ -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"`
|
||||
|
||||
+5
-1
@@ -93,7 +93,11 @@ func JudgeFlow(router Router, inbound string, inboundType string, network uint8,
|
||||
case PreMatchDrop:
|
||||
return tun.FlowVerdict{Action: tun.ActionDrop}
|
||||
case PreMatchBypass:
|
||||
return tun.FlowVerdict{Action: tun.ActionBypass}
|
||||
port, isPort := result.Outbound.(tun.Port)
|
||||
if !isPort {
|
||||
return tun.FlowVerdict{Action: tun.ActionBypass}
|
||||
}
|
||||
return tun.FlowVerdict{Action: tun.ActionBypass, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker}
|
||||
case PreMatchHijackDNS:
|
||||
return tun.FlowVerdict{Action: tun.ActionHijackDNS}
|
||||
default:
|
||||
|
||||
@@ -28,6 +28,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"
|
||||
@@ -169,7 +170,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 +193,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,
|
||||
@@ -260,11 +261,18 @@ 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)
|
||||
}
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
@@ -460,13 +468,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 {
|
||||
|
||||
Submodule clients/android deleted from eb87216961
Submodule clients/apple deleted from c19945f65b
+1
-1
Submodule clients/desktop updated: cebee0d527...fb1175ca3b
@@ -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")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -30,7 +31,7 @@ func check() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
ctx, cancel := context.WithCancel(service.ExtendContext(globalCtx))
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -136,7 +137,7 @@ func create(options option.Options) (*box.Box, context.CancelFunc, error) {
|
||||
}
|
||||
options.Log.DisableColor = true
|
||||
}
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
ctx, cancel := context.WithCancel(service.ExtendContext(globalCtx))
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/sagernet/sing-box"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -30,7 +31,7 @@ func createPreStartedClient() (*box.Box, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
|
||||
instance, err := box.New(box.Options{Context: service.ExtendContext(globalCtx), Options: options})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create service")
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ import (
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
)
|
||||
|
||||
const pathProc = "/proc"
|
||||
const (
|
||||
pathProc = "/proc"
|
||||
|
||||
processPathsAllUsers = ^uint32(0)
|
||||
)
|
||||
|
||||
var _ Searcher = (*linuxSearcher)(nil)
|
||||
|
||||
@@ -101,6 +105,8 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
source = netip.AddrPortFrom(source.Addr().Unmap(), source.Port())
|
||||
destination = netip.AddrPortFrom(destination.Addr().Unmap(), destination.Port())
|
||||
family, protocol, err := socketDiagSettings(network, source)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
@@ -118,28 +124,33 @@ func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.Addr
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
return querySocketDiagOnce(family, protocol, source)
|
||||
return dumpSocketDiag(family, protocol, source, destination)
|
||||
}
|
||||
|
||||
// The socket keeps the uid it was created with, while /proc reflects the
|
||||
// current uid of the process, so a socket created before a privilege drop
|
||||
// only appears under a scan of all users.
|
||||
func (s *linuxSearcher) findProcessPath(targetInode, uid uint32) (string, error) {
|
||||
if cached, ok := s.processPathCache.Get(uid); ok {
|
||||
if processPath, found := cached.entries[targetInode]; found {
|
||||
for _, scanUID := range []uint32{uid, processPathsAllUsers} {
|
||||
if cached, ok := s.processPathCache.Get(scanUID); ok {
|
||||
if processPath, found := cached.entries[targetInode]; found {
|
||||
return processPath, nil
|
||||
}
|
||||
}
|
||||
processPaths, err := buildProcessPaths(scanUID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.processPathCache.Add(scanUID, &uidProcessPaths{entries: processPaths})
|
||||
processPath, found := processPaths[targetInode]
|
||||
if found {
|
||||
return processPath, nil
|
||||
}
|
||||
}
|
||||
processPaths, err := buildProcessPathsByUID(uid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.processPathCache.Add(uid, &uidProcessPaths{entries: processPaths})
|
||||
processPath, found := processPaths[targetInode]
|
||||
if !found {
|
||||
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
|
||||
}
|
||||
return processPath, nil
|
||||
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
|
||||
}
|
||||
|
||||
func buildProcessPathsByUID(uid uint32) (map[uint32]string, error) {
|
||||
func buildProcessPaths(uid uint32) (map[uint32]string, error) {
|
||||
files, err := os.ReadDir(pathProc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -157,7 +168,7 @@ func buildProcessPathsByUID(uid uint32) (map[uint32]string, error) {
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
if uid != processPathsAllUsers && info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
continue
|
||||
}
|
||||
processPath := filepath.Join(pathProc, file.Name())
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -19,6 +20,7 @@ const (
|
||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
|
||||
socketDiagResponseMinSize = 72
|
||||
socketDiagByFamily = 20
|
||||
socketDiagTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
type socketDiagConn struct {
|
||||
@@ -28,6 +30,13 @@ type socketDiagConn struct {
|
||||
fd int
|
||||
}
|
||||
|
||||
type socketDiagEntry struct {
|
||||
source netip.AddrPort
|
||||
destination netip.AddrPort
|
||||
uid uint32
|
||||
inode uint32
|
||||
}
|
||||
|
||||
func socketDiagConnIndex(family, protocol uint8) int {
|
||||
index := 0
|
||||
if protocol == syscall.IPPROTO_UDP {
|
||||
@@ -83,15 +92,6 @@ func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) {
|
||||
fd, err := openSocketDiag()
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "dial netlink")
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
|
||||
}
|
||||
|
||||
func (c *socketDiagConn) ensureOpenLocked() error {
|
||||
if c.fd != -1 {
|
||||
return nil
|
||||
@@ -109,12 +109,12 @@ func openSocketDiag() (int, error) {
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
timeout := &syscall.Timeval{Usec: 100}
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil {
|
||||
timeout := syscall.NsecToTimeval(socketDiagTimeout.Nanoseconds())
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &timeout); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil {
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &timeout); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
@@ -196,36 +196,132 @@ func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) {
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "parse netlink message")
|
||||
}
|
||||
return unpackSocketDiagMessages(messages)
|
||||
}
|
||||
|
||||
func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) {
|
||||
for _, message := range messages {
|
||||
switch message.Header.Type {
|
||||
case syscall.NLMSG_DONE:
|
||||
continue
|
||||
case syscall.NLMSG_ERROR:
|
||||
err = unpackSocketDiagError(&message)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
case socketDiagByFamily:
|
||||
inode, uid = unpackSocketDiagResponse(&message)
|
||||
if inode != 0 || uid != 0 {
|
||||
return inode, uid, nil
|
||||
entry, valid := unpackSocketDiagEntry(&message)
|
||||
if valid && (entry.inode != 0 || entry.uid != 0) {
|
||||
return entry.inode, entry.uid, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
}
|
||||
|
||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||
if len(msg.Data) < socketDiagResponseMinSize {
|
||||
return 0, 0
|
||||
// The dump only filters by port (inet_diag_dump_icsk, udp_dump), so every socket
|
||||
// in the namespace with the requested local port is returned regardless of address.
|
||||
// A dual-stack socket carrying a v4-mapped address is only listed under AF_INET6.
|
||||
func dumpSocketDiag(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
families := []uint8{family}
|
||||
if family == syscall.AF_INET {
|
||||
families = append(families, syscall.AF_INET6)
|
||||
}
|
||||
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
|
||||
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
|
||||
return inode, uid
|
||||
for _, dumpFamily := range families {
|
||||
inode, uid, err = dumpSocketDiagFamily(dumpFamily, protocol, source, destination)
|
||||
if err == nil || !errors.Is(err, ErrNotFound) {
|
||||
return inode, uid, err
|
||||
}
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
}
|
||||
|
||||
func dumpSocketDiagFamily(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
fd, err := openSocketDiag()
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "dial netlink")
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
_, err = syscall.Write(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "write netlink request")
|
||||
}
|
||||
var (
|
||||
localMatch socketDiagEntry
|
||||
hasLocalMatch bool
|
||||
wildcardMatch socketDiagEntry
|
||||
hasWildcardMatch bool
|
||||
buffer = make([]byte, 64<<10)
|
||||
n int
|
||||
messages []syscall.NetlinkMessage
|
||||
)
|
||||
for {
|
||||
n, err = syscall.Read(fd, buffer)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "read netlink response")
|
||||
}
|
||||
messages, err = syscall.ParseNetlinkMessage(buffer[:n])
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "parse netlink message")
|
||||
}
|
||||
if len(messages) == 0 {
|
||||
return 0, 0, E.New("empty netlink response")
|
||||
}
|
||||
for _, message := range messages {
|
||||
switch message.Header.Type {
|
||||
case syscall.NLMSG_DONE:
|
||||
if hasLocalMatch {
|
||||
return localMatch.inode, localMatch.uid, nil
|
||||
}
|
||||
if hasWildcardMatch {
|
||||
return wildcardMatch.inode, wildcardMatch.uid, nil
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
case syscall.NLMSG_ERROR:
|
||||
err = unpackSocketDiagError(&message)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
case socketDiagByFamily:
|
||||
entry, valid := unpackSocketDiagEntry(&message)
|
||||
if !valid || (entry.inode == 0 && entry.uid == 0) || entry.source.Port() != source.Port() {
|
||||
continue
|
||||
}
|
||||
if entry.source.Addr() == source.Addr() && (!destination.IsValid() || entry.destination == destination) {
|
||||
return entry.inode, entry.uid, nil
|
||||
}
|
||||
if protocol != syscall.IPPROTO_UDP {
|
||||
continue
|
||||
}
|
||||
if !hasLocalMatch && entry.source.Addr() == source.Addr() {
|
||||
hasLocalMatch = true
|
||||
localMatch = entry
|
||||
}
|
||||
if !hasWildcardMatch && entry.source.Addr().IsUnspecified() {
|
||||
hasWildcardMatch = true
|
||||
wildcardMatch = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unpackSocketDiagEntry(msg *syscall.NetlinkMessage) (socketDiagEntry, bool) {
|
||||
if len(msg.Data) < socketDiagResponseMinSize {
|
||||
return socketDiagEntry{}, false
|
||||
}
|
||||
data := msg.Data
|
||||
var sourceAddr, destinationAddr netip.Addr
|
||||
switch data[0] {
|
||||
case syscall.AF_INET:
|
||||
sourceAddr = netip.AddrFrom4([4]byte(data[8:12]))
|
||||
destinationAddr = netip.AddrFrom4([4]byte(data[24:28]))
|
||||
case syscall.AF_INET6:
|
||||
sourceAddr = netip.AddrFrom16([16]byte(data[8:24])).Unmap()
|
||||
destinationAddr = netip.AddrFrom16([16]byte(data[24:40])).Unmap()
|
||||
default:
|
||||
return socketDiagEntry{}, false
|
||||
}
|
||||
return socketDiagEntry{
|
||||
source: netip.AddrPortFrom(sourceAddr, binary.BigEndian.Uint16(data[4:6])),
|
||||
destination: netip.AddrPortFrom(destinationAddr, binary.BigEndian.Uint16(data[6:8])),
|
||||
uid: binary.NativeEndian.Uint32(data[64:68]),
|
||||
inode: binary.NativeEndian.Uint32(data[68:72]),
|
||||
}, true
|
||||
}
|
||||
|
||||
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//go:build linux
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func socketInode(t *testing.T, conn syscall.Conn) uint32 {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
require.NoError(t, err)
|
||||
var inode uint32
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
var stat syscall.Stat_t
|
||||
require.NoError(t, syscall.Fstat(int(fd), &stat))
|
||||
inode = uint32(stat.Ino)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return inode
|
||||
}
|
||||
|
||||
func TestDumpSocketDiagTCP(t *testing.T) {
|
||||
t.Parallel()
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
first, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2)}}).Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer first.Close()
|
||||
second, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(M.AddrPortFromNet(first.LocalAddr()).Port())}}).Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer second.Close()
|
||||
destination := M.AddrPortFromNet(listener.Addr())
|
||||
for _, conn := range []net.Conn{second, first} {
|
||||
inode, uid, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), destination)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode)
|
||||
require.Equal(t, uint32(os.Getuid()), uid)
|
||||
inode, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode)
|
||||
}
|
||||
_, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(first.LocalAddr()).Port()), destination)
|
||||
require.ErrorIs(t, err, ErrNotFound)
|
||||
}
|
||||
|
||||
func TestDumpSocketDiagUDP(t *testing.T) {
|
||||
t.Parallel()
|
||||
first, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 2)})
|
||||
require.NoError(t, err)
|
||||
defer first.Close()
|
||||
port := M.AddrPortFromNet(first.LocalAddr()).Port()
|
||||
second, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(port)})
|
||||
require.NoError(t, err)
|
||||
defer second.Close()
|
||||
for _, conn := range []*net.UDPConn{second, first} {
|
||||
inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, socketInode(t, conn), inode)
|
||||
}
|
||||
_, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), port), netip.AddrPort{})
|
||||
require.ErrorIs(t, err, ErrNotFound)
|
||||
|
||||
wildcard, err := net.ListenUDP("udp4", &net.UDPAddr{})
|
||||
require.NoError(t, err)
|
||||
defer wildcard.Close()
|
||||
inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(wildcard.LocalAddr()).Port()), netip.AddrPort{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, socketInode(t, wildcard), inode)
|
||||
}
|
||||
|
||||
func TestLinuxSearcherFindProcessInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
searcher, err := NewSearcher(Config{Logger: log.NewNOPFactory().NewLogger("test")})
|
||||
require.NoError(t, err)
|
||||
defer searcher.Close()
|
||||
executable, err := os.Executable()
|
||||
require.NoError(t, err)
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
tcpConn, err := net.Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer tcpConn.Close()
|
||||
info, err := searcher.FindProcessInfo(context.Background(), N.NetworkTCP, M.AddrPortFromNet(tcpConn.LocalAddr()), M.AddrPortFromNet(listener.Addr()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, executable, info.ProcessPath)
|
||||
require.Equal(t, int32(os.Getuid()), info.UserId)
|
||||
|
||||
udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{})
|
||||
require.NoError(t, err)
|
||||
defer udpConn.Close()
|
||||
info, err = searcher.FindProcessInfo(context.Background(), N.NetworkUDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), M.AddrPortFromNet(udpConn.LocalAddr()).Port()), netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 53))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, executable, info.ProcessPath)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+5
-3
@@ -9,6 +9,7 @@ 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"
|
||||
@@ -25,7 +26,7 @@ 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
|
||||
@@ -38,6 +39,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
|
||||
@@ -134,7 +136,7 @@ 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.cacheFile = service.FromContext[adapter.CacheFile](ctx)
|
||||
@@ -149,7 +151,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),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
type logRing struct {
|
||||
entries []*log.Entry
|
||||
maxLines int
|
||||
start int
|
||||
}
|
||||
|
||||
func (r *logRing) push(entry *log.Entry) {
|
||||
if r.maxLines <= 0 {
|
||||
return
|
||||
}
|
||||
if len(r.entries) < r.maxLines {
|
||||
r.entries = append(r.entries, entry)
|
||||
return
|
||||
}
|
||||
r.entries[r.start] = entry
|
||||
r.start++
|
||||
if r.start == len(r.entries) {
|
||||
r.start = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (r *logRing) array() []*log.Entry {
|
||||
result := make([]*log.Entry, 0, len(r.entries))
|
||||
result = append(result, r.entries[r.start:]...)
|
||||
result = append(result, r.entries[:r.start]...)
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *logRing) reset() {
|
||||
clear(r.entries)
|
||||
r.entries = r.entries[:0]
|
||||
r.start = 0
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -18,20 +17,20 @@ var _ ManagedServiceServer = (*ManagedService)(nil)
|
||||
type ManagedService struct {
|
||||
handler ManagedHandler
|
||||
debug bool
|
||||
oomReporter oomkiller.OOMReporter
|
||||
oomRecorder *oomkiller.Recorder
|
||||
}
|
||||
|
||||
type ManagedServiceOptions struct {
|
||||
Handler ManagedHandler
|
||||
Debug bool
|
||||
OOMReporter oomkiller.OOMReporter
|
||||
OOMRecorder *oomkiller.Recorder
|
||||
}
|
||||
|
||||
func NewManagedService(options ManagedServiceOptions) *ManagedService {
|
||||
return &ManagedService{
|
||||
handler: options.Handler,
|
||||
debug: options.Debug,
|
||||
oomReporter: options.OOMReporter,
|
||||
oomRecorder: options.OOMRecorder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,10 +86,10 @@ func (s *ManagedService) TriggerDebugCrash(ctx context.Context, request *DebugCr
|
||||
}
|
||||
|
||||
func (s *ManagedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
if s.oomReporter == nil {
|
||||
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
|
||||
if s.oomRecorder == nil {
|
||||
return nil, status.Error(codes.Unavailable, "OOM recorder not available")
|
||||
}
|
||||
return &emptypb.Empty{}, s.oomReporter.WriteReport(memory.Total())
|
||||
return &emptypb.Empty{}, s.oomRecorder.WriteReport()
|
||||
}
|
||||
|
||||
func (s *ManagedService) mustEmbedUnimplementedManagedServiceServer() {
|
||||
|
||||
+18
-27
@@ -23,7 +23,6 @@ import (
|
||||
"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 +46,6 @@ type StartedService struct {
|
||||
// platform adapter.PlatformInterface
|
||||
handler PlatformHandler
|
||||
debug bool
|
||||
logMaxLines int
|
||||
oomKillerEnabled bool
|
||||
oomKillerDisabled bool
|
||||
oomMemoryLimit uint64
|
||||
@@ -64,7 +62,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 +97,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 +138,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)
|
||||
}
|
||||
@@ -277,8 +275,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 +376,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 +629,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 +659,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 +694,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 +2086,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 +2097,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 {
|
||||
|
||||
@@ -1073,6 +1073,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 +1348,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 {
|
||||
|
||||
@@ -109,7 +109,11 @@ func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
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")
|
||||
}
|
||||
t.interfaceCallback = interfaceMonitor.RegisterCallback(t.interfaceUpdated)
|
||||
}
|
||||
go func() {
|
||||
err := t.fetch()
|
||||
|
||||
+5
-1
@@ -2,6 +2,10 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.14.1
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0
|
||||
|
||||
Important changes since 1.13:
|
||||
@@ -54,7 +58,7 @@ Important changes since 1.13:
|
||||
* Update gVisor to 20260727.0
|
||||
* Update Tailscale to v1.102.1
|
||||
* Update uTLS to v1.8.7
|
||||
* Update NaiveProxy to v150.0.7871.63-1
|
||||
* Update NaiveProxy to v150.0.7871.63-2
|
||||
|
||||
**1**:
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
|
||||
| `inet6_route_address` | :material-check: | / |
|
||||
| `inet4_route_exclude_address` | :material-check: | / |
|
||||
| `inet6_route_exclude_address` | :material-check: | / |
|
||||
| `endpoint_independent_nat` | :material-check: | / |
|
||||
| `stack` | :material-check: | / |
|
||||
| `include_interface` | :material-close: | No permission |
|
||||
| `exclude_interface` | :material-close: | No permission |
|
||||
|
||||
@@ -27,7 +27,6 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension
|
||||
| `inet6_route_address` | :material-check: | / |
|
||||
| `inet4_route_exclude_address` | :material-check: | / |
|
||||
| `inet6_route_exclude_address` | :material-check: | / |
|
||||
| `endpoint_independent_nat` | :material-check: | / |
|
||||
| `stack` | :material-check: | / |
|
||||
| `include_interface` | :material-close:️ | Not implemented |
|
||||
| `exclude_interface` | :material-close:️ | Not implemented |
|
||||
|
||||
@@ -550,13 +550,10 @@ Exclude custom routes when `auto_route` is enabled.
|
||||
|
||||
#### endpoint_independent_nat
|
||||
|
||||
!!! info ""
|
||||
This option has had no effect since sing-box 1.11.0 and can be removed from the configuration.
|
||||
|
||||
This item is only available on the gvisor stack, other stacks are endpoint-independent NAT by default.
|
||||
|
||||
Enable endpoint-independent NAT.
|
||||
|
||||
Performance may degrade slightly, so it is not recommended to enable on when it is not needed.
|
||||
Since sing-box 1.14.0, use [UDP NAT fields](/configuration/shared/udp-nat/)
|
||||
to customize the mapping and filtering behavior.
|
||||
|
||||
#### stack
|
||||
|
||||
|
||||
@@ -534,9 +534,9 @@ sing-box DNS 模块,等价于一条
|
||||
|
||||
#### endpoint_independent_nat
|
||||
|
||||
启用独立于端点的 NAT。
|
||||
此选项自 sing-box 1.11.0 起不再生效,可从配置中移除。
|
||||
|
||||
性能可能会略有下降,所以不建议在不需要的时候开启。
|
||||
自 sing-box 1.14.0 起,可使用 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)自定义映射和过滤行为。
|
||||
|
||||
#### stack
|
||||
|
||||
|
||||
+12
-1
@@ -9,11 +9,22 @@ icon: material/arrange-bring-forward
|
||||
Apple platform clients migrated to a new Apple developer account, so the macOS standalone client
|
||||
is a new application, and profiles and settings are not inherited.
|
||||
|
||||
Before starting sing-box 1.14.0-rc.2 or later, they can be migrated using the following command:
|
||||
Before starting sing-box 1.14.0-rc.2 or later, they can be migrated using the following commands:
|
||||
|
||||
```bash
|
||||
mv ~/Library/Group\ Containers/287TTNZF8L.io.nekohasekai.sfavt \
|
||||
~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt
|
||||
xattr -c ~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt
|
||||
rm ~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt/.com.apple.containermanagerd.metadata.plist
|
||||
```
|
||||
|
||||
If you have already migrated using an earlier version of this command and a permission prompt
|
||||
appears at startup, run the following commands and restart the application:
|
||||
|
||||
```bash
|
||||
xattr -c ~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt
|
||||
rm ~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt/.com.apple.containermanagerd.metadata.plist
|
||||
tccutil reset All io.nekohasekai.sfamt.standalone
|
||||
```
|
||||
|
||||
### Migrate inline ACME to certificate provider
|
||||
|
||||
@@ -4,6 +4,28 @@ icon: material/arrange-bring-forward
|
||||
|
||||
## 1.14.0
|
||||
|
||||
### 迁移 macOS standalone 客户端数据
|
||||
|
||||
Apple 平台客户端已迁移至新的 Apple 开发者账户,因此 macOS standalone 客户端是一个新应用,
|
||||
配置文件与设置不会被继承。
|
||||
|
||||
在启动 sing-box 1.14.0-rc.2 或更高版本之前,可使用以下命令迁移:
|
||||
|
||||
```bash
|
||||
mv ~/Library/Group\ Containers/287TTNZF8L.io.nekohasekai.sfavt \
|
||||
~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt
|
||||
xattr -c ~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt
|
||||
rm ~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt/.com.apple.containermanagerd.metadata.plist
|
||||
```
|
||||
|
||||
如果您已使用此命令的早期版本迁移,且启动时出现权限弹窗,请执行以下命令并重新启动应用:
|
||||
|
||||
```bash
|
||||
xattr -c ~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt
|
||||
rm ~/Library/Group\ Containers/P8XK3KHB48.io.nekohasekai.sfamt/.com.apple.containermanagerd.metadata.plist
|
||||
tccutil reset All io.nekohasekai.sfamt.standalone
|
||||
```
|
||||
|
||||
### 迁移内联 ACME 到证书提供者
|
||||
|
||||
TLS 中的内联 ACME 选项已废弃,且可以被证书提供者替代。
|
||||
|
||||
+34
-8
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -17,11 +18,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
policyKitService = "org.freedesktop.PolicyKit1"
|
||||
policyKitAuthorityPath = dbus.ObjectPath("/org/freedesktop/PolicyKit1/Authority")
|
||||
policyKitAuthorityInterface = "org.freedesktop.PolicyKit1.Authority"
|
||||
policyKitTakeOverAction = "io.nekohasekai.sfl.take-over-service"
|
||||
policyKitAllowUserInteraction = uint32(1)
|
||||
policyKitService = "org.freedesktop.PolicyKit1"
|
||||
policyKitAuthorityPath = dbus.ObjectPath("/org/freedesktop/PolicyKit1/Authority")
|
||||
policyKitAuthorityInterface = "org.freedesktop.PolicyKit1.Authority"
|
||||
policyKitTakeOverAction = "io.nekohasekai.sfl.take-over-service"
|
||||
policyKitSetInsecureModeAction = "io.nekohasekai.sfl.set-insecure-mode"
|
||||
policyKitAllowUserInteraction = uint32(1)
|
||||
)
|
||||
|
||||
type policyKitSubject struct {
|
||||
@@ -36,6 +38,27 @@ type policyKitAuthorizationResult struct {
|
||||
}
|
||||
|
||||
func authorizeTakeOver(ctx context.Context, identity peerIdentity) error {
|
||||
return checkPolicyKitAuthorization(ctx, identity, policyKitTakeOverAction)
|
||||
}
|
||||
|
||||
func authorizeSetInsecureMode(ctx context.Context, identity peerIdentity, enabled bool) error {
|
||||
if enabled {
|
||||
return checkPolicyKitAuthorization(ctx, identity, policyKitSetInsecureModeAction)
|
||||
}
|
||||
ownerUserID, err := loadOwner()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return status.Error(codes.PermissionDenied, "the service has no owner")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if ownerUserID != identity.UserID {
|
||||
return status.Error(codes.PermissionDenied, "the service is owned by another user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPolicyKitAuthorization(ctx context.Context, identity peerIdentity, action string) error {
|
||||
if listenAddress != "" {
|
||||
return nil
|
||||
}
|
||||
@@ -72,7 +95,7 @@ func authorizeTakeOver(ctx context.Context, identity peerIdentity) error {
|
||||
0,
|
||||
resultChannel,
|
||||
subject,
|
||||
policyKitTakeOverAction,
|
||||
action,
|
||||
map[string]string{},
|
||||
policyKitAllowUserInteraction,
|
||||
cancellationID,
|
||||
@@ -88,7 +111,10 @@ func authorizeTakeOver(ctx context.Context, identity peerIdentity) error {
|
||||
return E.Cause(err, "read PolicyKit authorization result")
|
||||
}
|
||||
if !result.Authorized {
|
||||
return status.Error(codes.PermissionDenied, "take over authorization was denied")
|
||||
if result.Challenge {
|
||||
return status.Error(codes.Unauthenticated, "no authentication agent is available")
|
||||
}
|
||||
return status.Error(codes.PermissionDenied, "authorization was denied")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
@@ -97,6 +123,6 @@ func authorizeTakeOver(ctx context.Context, identity peerIdentity) error {
|
||||
0,
|
||||
cancellationID,
|
||||
).Err
|
||||
return status.Error(codes.Canceled, "take over authorization was canceled")
|
||||
return status.Error(codes.Canceled, "authorization was canceled")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func authorizeTakeOver(ctx context.Context, identity peerIdentity) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func authorizeSetInsecureMode(ctx context.Context, identity peerIdentity, enabled bool) error {
|
||||
if enabled {
|
||||
return status.Error(codes.PermissionDenied, "enabling insecure mode requires an elevated service command")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import "context"
|
||||
|
||||
func authorizeTakeOver(ctx context.Context, identity peerIdentity) error {
|
||||
return nil
|
||||
}
|
||||
@@ -247,26 +247,23 @@ func (s *desktopService) SetInsecureModeEnabled(ctx context.Context, request *Se
|
||||
if !insecureModeAvailable() {
|
||||
return nil, status.Error(codes.FailedPrecondition, "insecure mode is not available on this platform")
|
||||
}
|
||||
if request.Enabled {
|
||||
return nil, status.Error(codes.PermissionDenied, "enabling insecure mode requires an elevated service command")
|
||||
err = authorizeSetInsecureMode(ctx, identity, request.Enabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.daemon.lifecycleAccess.Lock()
|
||||
defer s.daemon.lifecycleAccess.Unlock()
|
||||
if s.daemon.closed {
|
||||
return nil, os.ErrClosed
|
||||
}
|
||||
err = authorizeDisableInsecureMode(identity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wasEnabled := s.daemon.insecureModeEnabled()
|
||||
err = updateDaemonSettings(workingDirectory, func(settings *daemonSettings) {
|
||||
settings.InsecureModeEnabled = false
|
||||
settings.InsecureModeEnabled = request.Enabled
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wasEnabled && s.daemon.startedService.Instance() != nil {
|
||||
if wasEnabled && !request.Enabled && s.daemon.startedService.Instance() != nil {
|
||||
var ownerUserID string
|
||||
ownerUserID, err = loadOwner()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func authorizeDisableInsecureMode(identity peerIdentity) error {
|
||||
ownerUserID, err := loadOwner()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return status.Error(codes.PermissionDenied, "the service has no owner")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if ownerUserID != identity.UserID {
|
||||
return status.Error(codes.PermissionDenied, "the service is owned by another user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
func authorizeDisableInsecureMode(identity peerIdentity) error {
|
||||
return nil
|
||||
}
|
||||
@@ -33,6 +33,7 @@ type Daemon struct {
|
||||
logger log.ContextLogger
|
||||
startedService *daemon.StartedService
|
||||
powerManager *powerreport.Manager
|
||||
oomRecorder *oomkiller.Recorder
|
||||
server *grpc.Server
|
||||
runtimeWorkingDirectory string
|
||||
lifecycleAccess sync.Mutex
|
||||
@@ -63,14 +64,15 @@ func newDaemon() (*Daemon, error) {
|
||||
Context: ctx,
|
||||
LogMaxLines: 3000,
|
||||
})
|
||||
reporter := libbox.NewOOMReporter(d.startedService)
|
||||
service.MustRegister[oomkiller.OOMReporter](ctx, reporter)
|
||||
d.oomRecorder = oomkiller.NewRecorder(libbox.OOMRecorderOptions(d.startedService))
|
||||
service.MustRegister[*oomkiller.Recorder](ctx, d.oomRecorder)
|
||||
d.oomRecorder.Start()
|
||||
d.powerManager = powerreport.NewManager()
|
||||
service.MustRegister[*powerreport.Manager](ctx, d.powerManager)
|
||||
managedService := daemon.NewManagedService(daemon.ManagedServiceOptions{
|
||||
Handler: &managedHandler{d},
|
||||
Debug: debugEnabled,
|
||||
OOMReporter: reporter,
|
||||
OOMRecorder: d.oomRecorder,
|
||||
})
|
||||
authorizer := newAuthorizer(d)
|
||||
serverOptions := []grpc.ServerOption{
|
||||
@@ -289,6 +291,7 @@ func (d *Daemon) Close() {
|
||||
}
|
||||
_ = d.startedService.CloseService()
|
||||
d.startedService.Close()
|
||||
_ = d.oomRecorder.Close()
|
||||
if d.platform != nil {
|
||||
_ = d.platform.Close()
|
||||
}
|
||||
|
||||
@@ -3,16 +3,13 @@ package experimental
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"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"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
type ClashServerConstructor = func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error)
|
||||
type ClashServerConstructor = func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error)
|
||||
|
||||
var clashServerConstructor ClashServerConstructor
|
||||
|
||||
@@ -20,62 +17,9 @@ func RegisterClashServerConstructor(constructor ClashServerConstructor) {
|
||||
clashServerConstructor = constructor
|
||||
}
|
||||
|
||||
func NewClashServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
|
||||
func NewClashServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error) {
|
||||
if clashServerConstructor == nil {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
return clashServerConstructor(ctx, logFactory, options)
|
||||
}
|
||||
|
||||
func CalculateClashModeList(options option.Options) []string {
|
||||
var clashModes []string
|
||||
clashModes = append(clashModes, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...)
|
||||
clashModes = append(clashModes, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...)
|
||||
clashModes = common.FilterNotDefault(common.Uniq(clashModes))
|
||||
predefinedOrder := []string{
|
||||
"Rule", "Global", "Direct",
|
||||
}
|
||||
var newClashModes []string
|
||||
for _, mode := range clashModes {
|
||||
if !common.Contains(predefinedOrder, mode) {
|
||||
newClashModes = append(newClashModes, mode)
|
||||
}
|
||||
}
|
||||
sort.Strings(newClashModes)
|
||||
for _, mode := range predefinedOrder {
|
||||
if common.Contains(clashModes, mode) {
|
||||
newClashModes = append(newClashModes, mode)
|
||||
}
|
||||
}
|
||||
return newClashModes
|
||||
}
|
||||
|
||||
func extraClashModeFromRule(rules []option.Rule) []string {
|
||||
var clashMode []string
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if rule.DefaultOptions.ClashMode != "" {
|
||||
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...)
|
||||
}
|
||||
}
|
||||
return clashMode
|
||||
}
|
||||
|
||||
func extraClashModeFromDNSRule(rules []option.DNSRule) []string {
|
||||
var clashMode []string
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if rule.DefaultOptions.ClashMode != "" {
|
||||
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...)
|
||||
}
|
||||
}
|
||||
return clashMode
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ func getConfigs(server *Server, logFactory log.Factory) func(w http.ResponseWrit
|
||||
logLevel = log.LevelError
|
||||
}
|
||||
render.JSON(w, r, &configSchema{
|
||||
Mode: server.mode,
|
||||
ModeList: server.modeList,
|
||||
Mode: server.clashMode.Mode(),
|
||||
ModeList: server.clashMode.ModeList(),
|
||||
BindAddress: "*",
|
||||
LogLevel: log.FormatLevel(logLevel),
|
||||
})
|
||||
@@ -60,7 +60,7 @@ func patchConfigs(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if newConfig.Mode != "" {
|
||||
server.SetMode(newConfig.Mode)
|
||||
server.clashMode.SetMode(newConfig.Mode)
|
||||
}
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -19,12 +18,12 @@ import (
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/clashmode"
|
||||
"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/json"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
"github.com/sagernet/ws"
|
||||
@@ -38,7 +37,7 @@ func init() {
|
||||
experimental.RegisterClashServerConstructor(NewServer)
|
||||
}
|
||||
|
||||
var _ adapter.ClashServer = (*Server)(nil)
|
||||
var _ adapter.LifecycleService = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
ctx context.Context
|
||||
@@ -52,20 +51,16 @@ type Server struct {
|
||||
httpServer *http.Server
|
||||
trafficManager *trafficcontrol.Manager
|
||||
urlTestHistory *urltest.HistoryStorage
|
||||
clashMode *clashmode.Manager
|
||||
logDebug bool
|
||||
|
||||
mode string
|
||||
modeList []string
|
||||
modeUpdateAccess sync.Mutex
|
||||
modeUpdateHooks []*observable.Subscriber[struct{}]
|
||||
|
||||
externalController bool
|
||||
externalUI string
|
||||
externalUIDownloadURL string
|
||||
externalUIDownloadDetour string
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
|
||||
func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error) {
|
||||
trafficManager := service.PtrFromContext[trafficcontrol.Manager](ctx)
|
||||
if trafficManager == nil {
|
||||
return nil, E.New("missing traffic manager")
|
||||
@@ -74,6 +69,10 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
if urlTestHistory == nil {
|
||||
return nil, E.New("missing URL test history storage")
|
||||
}
|
||||
clashMode := service.PtrFromContext[clashmode.Manager](ctx)
|
||||
if clashMode == nil {
|
||||
return nil, E.New("missing clash mode manager")
|
||||
}
|
||||
chiRouter := chi.NewRouter()
|
||||
s := &Server{
|
||||
ctx: ctx,
|
||||
@@ -90,20 +89,12 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
},
|
||||
trafficManager: trafficManager,
|
||||
urlTestHistory: urlTestHistory,
|
||||
clashMode: clashMode,
|
||||
logDebug: logFactory.Level() >= log.LevelDebug,
|
||||
modeList: options.ModeList,
|
||||
externalController: options.ExternalController != "",
|
||||
externalUIDownloadURL: options.ExternalUIDownloadURL,
|
||||
externalUIDownloadDetour: options.ExternalUIDownloadDetour,
|
||||
}
|
||||
defaultMode := "Rule"
|
||||
if options.DefaultMode != "" {
|
||||
defaultMode = options.DefaultMode
|
||||
}
|
||||
if !common.Contains(s.modeList, defaultMode) {
|
||||
s.modeList = append([]string{defaultMode}, s.modeList...)
|
||||
}
|
||||
s.mode = defaultMode
|
||||
//goland:noinspection GoDeprecation
|
||||
//nolint:staticcheck
|
||||
if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" {
|
||||
@@ -159,45 +150,34 @@ func (s *Server) Name() string {
|
||||
}
|
||||
|
||||
func (s *Server) Start(stage adapter.StartStage) error {
|
||||
switch stage {
|
||||
case adapter.StartStateStart:
|
||||
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
|
||||
if cacheFile != nil {
|
||||
mode := cacheFile.LoadMode()
|
||||
if common.Any(s.modeList, func(it string) bool {
|
||||
return strings.EqualFold(it, mode)
|
||||
}) {
|
||||
s.mode = mode
|
||||
}
|
||||
}
|
||||
case adapter.StartStateStarted:
|
||||
if s.externalController {
|
||||
s.checkAndDownloadExternalUI()
|
||||
var (
|
||||
listener net.Listener
|
||||
err error
|
||||
)
|
||||
for range 3 {
|
||||
listener, err = net.Listen("tcp", s.httpServer.Addr)
|
||||
if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return E.Cause(err, "external controller listen error")
|
||||
}
|
||||
s.logger.Notice("restful api listening at ", listener.Addr())
|
||||
go func() {
|
||||
err = s.httpServer.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Error("external controller serve error: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if stage != adapter.StartStateStarted {
|
||||
return nil
|
||||
}
|
||||
if s.externalController {
|
||||
s.checkAndDownloadExternalUI()
|
||||
var (
|
||||
listener net.Listener
|
||||
err error
|
||||
)
|
||||
for range 3 {
|
||||
listener, err = net.Listen("tcp", s.httpServer.Addr)
|
||||
if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return E.Cause(err, "external controller listen error")
|
||||
}
|
||||
s.logger.Notice("restful api listening at ", listener.Addr())
|
||||
go func() {
|
||||
err = s.httpServer.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Error("external controller serve error: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -207,49 +187,6 @@ func (s *Server) Close() error {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Server) Mode() string {
|
||||
return s.mode
|
||||
}
|
||||
|
||||
func (s *Server) ModeList() []string {
|
||||
return s.modeList
|
||||
}
|
||||
|
||||
func (s *Server) AddModeUpdateHook(hook *observable.Subscriber[struct{}]) {
|
||||
s.modeUpdateAccess.Lock()
|
||||
defer s.modeUpdateAccess.Unlock()
|
||||
s.modeUpdateHooks = append(s.modeUpdateHooks, hook)
|
||||
}
|
||||
|
||||
func (s *Server) SetMode(newMode string) {
|
||||
if !common.Contains(s.modeList, newMode) {
|
||||
newMode = common.Find(s.modeList, func(it string) bool {
|
||||
return strings.EqualFold(it, newMode)
|
||||
})
|
||||
}
|
||||
if !common.Contains(s.modeList, newMode) {
|
||||
return
|
||||
}
|
||||
if newMode == s.mode {
|
||||
return
|
||||
}
|
||||
s.mode = newMode
|
||||
s.modeUpdateAccess.Lock()
|
||||
for _, hook := range s.modeUpdateHooks {
|
||||
hook.Emit(struct{}{})
|
||||
}
|
||||
s.modeUpdateAccess.Unlock()
|
||||
s.dnsRouter.ClearCache()
|
||||
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
|
||||
if cacheFile != nil {
|
||||
err := cacheFile.StoreMode(newMode)
|
||||
if err != nil {
|
||||
s.logger.Error(E.Cause(err, "save mode"))
|
||||
}
|
||||
}
|
||||
s.logger.Notice("updated mode: ", newMode)
|
||||
}
|
||||
|
||||
func authentication(serverSecret string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package clashmode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
ctx context.Context
|
||||
logger log.Logger
|
||||
dnsRouter adapter.DNSRouter
|
||||
mode string
|
||||
modeList []string
|
||||
updateAccess sync.Mutex
|
||||
updateHooks []*observable.Subscriber[struct{}]
|
||||
}
|
||||
|
||||
func NewManager(ctx context.Context, logger log.Logger, defaultMode string, modeList []string) *Manager {
|
||||
if defaultMode == "" {
|
||||
defaultMode = "Rule"
|
||||
}
|
||||
if !common.Contains(modeList, defaultMode) {
|
||||
modeList = append([]string{defaultMode}, modeList...)
|
||||
}
|
||||
return &Manager{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
mode: defaultMode,
|
||||
modeList: modeList,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Name() string {
|
||||
return "clash mode manager"
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
cacheFile := service.FromContext[adapter.CacheFile](m.ctx)
|
||||
if cacheFile != nil {
|
||||
mode := cacheFile.LoadMode()
|
||||
if common.Any(m.modeList, func(it string) bool {
|
||||
return strings.EqualFold(it, mode)
|
||||
}) {
|
||||
m.mode = mode
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Mode() string {
|
||||
return m.mode
|
||||
}
|
||||
|
||||
func (m *Manager) ModeList() []string {
|
||||
return m.modeList
|
||||
}
|
||||
|
||||
func (m *Manager) AddUpdateHook(hook *observable.Subscriber[struct{}]) {
|
||||
m.updateAccess.Lock()
|
||||
defer m.updateAccess.Unlock()
|
||||
m.updateHooks = append(m.updateHooks, hook)
|
||||
}
|
||||
|
||||
func (m *Manager) SetMode(newMode string) {
|
||||
if !common.Contains(m.modeList, newMode) {
|
||||
newMode = common.Find(m.modeList, func(it string) bool {
|
||||
return strings.EqualFold(it, newMode)
|
||||
})
|
||||
}
|
||||
if !common.Contains(m.modeList, newMode) {
|
||||
return
|
||||
}
|
||||
if newMode == m.mode {
|
||||
return
|
||||
}
|
||||
m.mode = newMode
|
||||
m.updateAccess.Lock()
|
||||
for _, hook := range m.updateHooks {
|
||||
hook.Emit(struct{}{})
|
||||
}
|
||||
m.updateAccess.Unlock()
|
||||
m.dnsRouter.ClearCache()
|
||||
cacheFile := service.FromContext[adapter.CacheFile](m.ctx)
|
||||
if cacheFile != nil {
|
||||
err := cacheFile.StoreMode(newMode)
|
||||
if err != nil {
|
||||
m.logger.Error(E.Cause(err, "save mode"))
|
||||
}
|
||||
}
|
||||
m.logger.Notice("updated mode: ", newMode)
|
||||
}
|
||||
|
||||
var _ adapter.LifecycleService = (*Manager)(nil)
|
||||
@@ -0,0 +1,62 @@
|
||||
package clashmode
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
func CalculateModeList(options option.Options) []string {
|
||||
var clashModes []string
|
||||
clashModes = append(clashModes, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...)
|
||||
clashModes = append(clashModes, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...)
|
||||
clashModes = common.FilterNotDefault(common.Uniq(clashModes))
|
||||
predefinedOrder := []string{
|
||||
"Rule", "Global", "Direct",
|
||||
}
|
||||
var newClashModes []string
|
||||
for _, mode := range clashModes {
|
||||
if !common.Contains(predefinedOrder, mode) {
|
||||
newClashModes = append(newClashModes, mode)
|
||||
}
|
||||
}
|
||||
sort.Strings(newClashModes)
|
||||
for _, mode := range predefinedOrder {
|
||||
if common.Contains(clashModes, mode) {
|
||||
newClashModes = append(newClashModes, mode)
|
||||
}
|
||||
}
|
||||
return newClashModes
|
||||
}
|
||||
|
||||
func extraClashModeFromRule(rules []option.Rule) []string {
|
||||
var clashMode []string
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if rule.DefaultOptions.ClashMode != "" {
|
||||
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...)
|
||||
}
|
||||
}
|
||||
return clashMode
|
||||
}
|
||||
|
||||
func extraClashModeFromDNSRule(rules []option.DNSRule) []string {
|
||||
var clashMode []string
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
if rule.DefaultOptions.ClashMode != "" {
|
||||
clashMode = append(clashMode, rule.DefaultOptions.ClashMode)
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...)
|
||||
}
|
||||
}
|
||||
return clashMode
|
||||
}
|
||||
@@ -36,6 +36,7 @@ type CommandServer struct {
|
||||
platformInterface PlatformInterface
|
||||
platformWrapper *platformInterfaceWrapper
|
||||
powerManager *powerreport.Manager
|
||||
oomRecorder *oomkiller.Recorder
|
||||
grpcServer *grpc.Server
|
||||
listener net.Listener
|
||||
endPauseTimer *time.Timer
|
||||
@@ -83,12 +84,14 @@ func NewCommandServer(handler CommandServerHandler, platformInterface PlatformIn
|
||||
// GroupID: sGroupID,
|
||||
// SystemProxyEnabled: false,
|
||||
})
|
||||
reporter := &oomReporter{startedService: server.StartedService}
|
||||
service.MustRegister[oomkiller.OOMReporter](ctx, reporter)
|
||||
oomRecorder := oomkiller.NewRecorder(OOMRecorderOptions(server.StartedService))
|
||||
service.MustRegister[*oomkiller.Recorder](ctx, oomRecorder)
|
||||
oomRecorder.Start()
|
||||
server.oomRecorder = oomRecorder
|
||||
server.managedService = daemon.NewManagedService(daemon.ManagedServiceOptions{
|
||||
Handler: (*platformHandler)(server),
|
||||
Debug: sDebug,
|
||||
OOMReporter: reporter,
|
||||
OOMRecorder: oomRecorder,
|
||||
})
|
||||
if sPowerReportEnabled {
|
||||
err := powerManager.Start(PowerReportOptions(server.StartedService))
|
||||
@@ -190,11 +193,15 @@ func (s *CommandServer) Start() error {
|
||||
}
|
||||
|
||||
func (s *CommandServer) Close() {
|
||||
if s.endPauseTimer != nil {
|
||||
s.endPauseTimer.Stop()
|
||||
}
|
||||
if s.grpcServer != nil {
|
||||
s.grpcServer.Stop()
|
||||
}
|
||||
common.Close(s.listener)
|
||||
s.StartedService.Close()
|
||||
s.oomRecorder.Close()
|
||||
s.powerManager.Close()
|
||||
}
|
||||
|
||||
@@ -260,13 +267,21 @@ func (s *CommandServer) Pause() {
|
||||
instance.PauseManager().DevicePause()
|
||||
if C.IsIos {
|
||||
if s.endPauseTimer == nil {
|
||||
s.endPauseTimer = time.AfterFunc(time.Minute, instance.PauseManager().DeviceWake)
|
||||
s.endPauseTimer = time.AfterFunc(time.Minute, s.endDevicePause)
|
||||
} else {
|
||||
s.endPauseTimer.Reset(time.Minute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CommandServer) endDevicePause() {
|
||||
instance := s.StartedService.Instance()
|
||||
if instance == nil || instance.PauseManager() == nil {
|
||||
return
|
||||
}
|
||||
instance.PauseManager().DeviceWake()
|
||||
}
|
||||
|
||||
func (s *CommandServer) Wake() {
|
||||
recorder := s.powerManager.Recorder()
|
||||
if recorder != nil {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -52,28 +51,27 @@ type labelMap struct {
|
||||
labelSet
|
||||
}
|
||||
|
||||
func WriteFile(destPath string, name string) (string, error) {
|
||||
func WriteFile(filePath string, name string) error {
|
||||
writer, ok := profileWriters[name]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unsupported profile %q", name)
|
||||
return fmt.Errorf("unsupported profile %q", name)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(destPath, name+".pb")
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := writer(file); err != nil {
|
||||
err = writer(file)
|
||||
if err != nil {
|
||||
_ = os.Remove(filePath)
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
_ = os.Remove(filePath)
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
return filePath, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var profileWriters = map[string]func(io.Writer) error{
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
//go:build badlinkname
|
||||
|
||||
package runtimeinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Mirrors runtime.g of the pinned Go toolchain up to goid; the offsets of gopc and startpc are
|
||||
// discovered by probeGoroutine.
|
||||
|
||||
type runtimeG struct {
|
||||
stackLo uintptr
|
||||
stackHi uintptr
|
||||
_ [2]uintptr
|
||||
_ [3]unsafe.Pointer
|
||||
_ [6]uintptr
|
||||
_ [4]uintptr
|
||||
_ unsafe.Pointer
|
||||
atomicstatus uint32
|
||||
_ uint32
|
||||
goid uint64
|
||||
}
|
||||
|
||||
//go:linkname allgs runtime.allgs
|
||||
var allgs []*runtimeG
|
||||
|
||||
const probeScanBytes = 512
|
||||
|
||||
var (
|
||||
gopcOffset uintptr
|
||||
startpcOffset uintptr
|
||||
probeDone chan bool
|
||||
)
|
||||
|
||||
func (gp *runtimeG) word(offset uintptr) uintptr {
|
||||
return *(*uintptr)(unsafe.Add(unsafe.Pointer(gp), offset))
|
||||
}
|
||||
|
||||
func (gp *runtimeG) gopc() uintptr {
|
||||
return gp.word(gopcOffset)
|
||||
}
|
||||
|
||||
func (gp *runtimeG) startpc() uintptr {
|
||||
return gp.word(startpcOffset)
|
||||
}
|
||||
|
||||
const (
|
||||
statusDead = 6
|
||||
statusScan = 0x1000
|
||||
)
|
||||
|
||||
var statusNames = map[uint32]string{
|
||||
0: "idle",
|
||||
1: "runnable",
|
||||
2: "running",
|
||||
3: "syscall",
|
||||
4: "waiting",
|
||||
6: "dead",
|
||||
8: "copystack",
|
||||
9: "preempted",
|
||||
}
|
||||
|
||||
var (
|
||||
layoutOnce sync.Once
|
||||
layoutVerified bool
|
||||
)
|
||||
|
||||
func probeLayout() {
|
||||
probeDone = make(chan bool, 1)
|
||||
go probeGoroutine()
|
||||
layoutVerified = <-probeDone
|
||||
}
|
||||
|
||||
func probeGoroutine() {
|
||||
id := currentGoroutineID()
|
||||
var self *runtimeG
|
||||
for _, gp := range allgs {
|
||||
if gp.goid == id {
|
||||
self = gp
|
||||
break
|
||||
}
|
||||
}
|
||||
if self == nil {
|
||||
probeDone <- false
|
||||
return
|
||||
}
|
||||
creatorName := functionName(reflect.ValueOf(probeLayout).Pointer())
|
||||
selfName := functionName(reflect.ValueOf(probeGoroutine).Pointer())
|
||||
goidOffset := unsafe.Offsetof(self.goid)
|
||||
for offset := goidOffset + 8; offset < goidOffset+probeScanBytes; offset += 8 {
|
||||
name := functionName(self.word(offset))
|
||||
if gopcOffset == 0 && name == creatorName {
|
||||
gopcOffset = offset
|
||||
} else if startpcOffset == 0 && name == selfName {
|
||||
startpcOffset = offset
|
||||
}
|
||||
}
|
||||
probeDone <- gopcOffset != 0 && startpcOffset != 0
|
||||
}
|
||||
|
||||
func functionName(pc uintptr) string {
|
||||
function := runtime.FuncForPC(pc)
|
||||
if function == nil {
|
||||
return "unknown"
|
||||
}
|
||||
return function.Name()
|
||||
}
|
||||
|
||||
func currentGoroutineID() uint64 {
|
||||
var stackBuffer [64]byte
|
||||
n := runtime.Stack(stackBuffer[:], false)
|
||||
fields := bytes.Fields(stackBuffer[:n])
|
||||
if len(fields) < 2 {
|
||||
return 0
|
||||
}
|
||||
id, _ := strconv.ParseUint(string(fields[1]), 10, 64)
|
||||
return id
|
||||
}
|
||||
|
||||
func collectGoroutines() *GoroutineReport {
|
||||
layoutOnce.Do(probeLayout)
|
||||
if !layoutVerified {
|
||||
return nil
|
||||
}
|
||||
report := &GoroutineReport{ByStatus: make(map[string]int)}
|
||||
groups := make(map[string]*GoroutineGroup)
|
||||
for _, gp := range allgs {
|
||||
status := atomic.LoadUint32(&gp.atomicstatus) &^ statusScan
|
||||
stackSize := uint64(gp.stackHi - gp.stackLo)
|
||||
if status == statusDead {
|
||||
report.Dead++
|
||||
if gp.stackLo != 0 {
|
||||
report.DeadStackBytes += stackSize
|
||||
}
|
||||
continue
|
||||
}
|
||||
report.Total++
|
||||
report.StackBytes += stackSize
|
||||
statusName, known := statusNames[status]
|
||||
if !known {
|
||||
statusName = strconv.Itoa(int(status))
|
||||
}
|
||||
report.ByStatus[statusName]++
|
||||
name := functionName(gp.startpc())
|
||||
group, found := groups[name]
|
||||
if !found {
|
||||
group = &GoroutineGroup{Function: name, CreatedBy: functionName(gp.gopc()), MinStackBytes: stackSize}
|
||||
groups[name] = group
|
||||
}
|
||||
group.Count++
|
||||
group.StackBytes += stackSize
|
||||
group.MaxStackBytes = max(group.MaxStackBytes, stackSize)
|
||||
group.MinStackBytes = min(group.MinStackBytes, stackSize)
|
||||
}
|
||||
report.ByFunction = make([]GoroutineGroup, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
report.ByFunction = append(report.ByFunction, *group)
|
||||
}
|
||||
slices.SortFunc(report.ByFunction, func(a, b GoroutineGroup) int {
|
||||
return cmp.Compare(b.StackBytes, a.StackBytes)
|
||||
})
|
||||
return report
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !badlinkname
|
||||
|
||||
package runtimeinfo
|
||||
|
||||
func collectGoroutines() *GoroutineReport {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//go:build badlinkname
|
||||
|
||||
package runtimeinfo
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
)
|
||||
|
||||
// Mirrors sync.Pool, sync.poolLocal and sync.poolChainElt of the pinned Go toolchain.
|
||||
|
||||
type syncPool struct {
|
||||
local unsafe.Pointer
|
||||
localSize uintptr
|
||||
victim unsafe.Pointer
|
||||
victimSize uintptr
|
||||
_ func() any
|
||||
}
|
||||
|
||||
type poolLocal struct {
|
||||
private [2]unsafe.Pointer
|
||||
sharedHead unsafe.Pointer
|
||||
_ unsafe.Pointer
|
||||
_ [128 - 4*unsafe.Sizeof(uintptr(0))]byte
|
||||
}
|
||||
|
||||
type poolChainElt struct {
|
||||
headTail uint64
|
||||
_ []unsafe.Pointer
|
||||
_ unsafe.Pointer
|
||||
prev unsafe.Pointer
|
||||
}
|
||||
|
||||
func collectBufferPools() []PoolReport {
|
||||
allocator := reflect.ValueOf(buf.DefaultAllocator)
|
||||
if allocator.Kind() != reflect.Pointer {
|
||||
return nil
|
||||
}
|
||||
pools := allocator.Elem().FieldByName("buffers")
|
||||
if !pools.IsValid() || pools.Kind() != reflect.Array || pools.Type().Elem() != reflect.TypeFor[sync.Pool]() {
|
||||
return nil
|
||||
}
|
||||
if unsafe.Sizeof(syncPool{}) != unsafe.Sizeof(sync.Pool{}) || unsafe.Sizeof(poolLocal{}) != 128 {
|
||||
return nil
|
||||
}
|
||||
result := make([]PoolReport, 0, pools.Len())
|
||||
for i := range pools.Len() {
|
||||
pool := (*syncPool)(pools.Index(i).Addr().UnsafePointer())
|
||||
size := min(1<<(6+i), buf.MaxPooledBufferSize)
|
||||
report := PoolReport{
|
||||
Size: size,
|
||||
Cached: countPoolLocals(pool.local, pool.localSize),
|
||||
Victim: countPoolLocals(pool.victim, pool.victimSize),
|
||||
}
|
||||
report.Bytes = uint64(report.Cached+report.Victim) * uint64(size)
|
||||
result = append(result, report)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func countPoolLocals(locals unsafe.Pointer, count uintptr) int {
|
||||
if locals == nil {
|
||||
return 0
|
||||
}
|
||||
var total int
|
||||
for i := range count {
|
||||
local := (*poolLocal)(unsafe.Add(locals, i*unsafe.Sizeof(poolLocal{})))
|
||||
if local.private[0] != nil {
|
||||
total++
|
||||
}
|
||||
for element := (*poolChainElt)(atomic.LoadPointer(&local.sharedHead)); element != nil; element = (*poolChainElt)(atomic.LoadPointer(&element.prev)) {
|
||||
headTail := atomic.LoadUint64(&element.headTail)
|
||||
total += int(uint32(headTail>>32) - uint32(headTail))
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !badlinkname
|
||||
|
||||
package runtimeinfo
|
||||
|
||||
func collectBufferPools() []PoolReport {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package runtimeinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"runtime/metrics"
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
Metrics map[string]uint64 `json:"metrics"`
|
||||
Goroutines *GoroutineReport `json:"goroutines,omitempty"`
|
||||
Pools []PoolReport `json:"bufferPools,omitempty"`
|
||||
}
|
||||
|
||||
type GoroutineReport struct {
|
||||
Total int `json:"total"`
|
||||
StackBytes uint64 `json:"stackBytes"`
|
||||
Dead int `json:"dead"`
|
||||
DeadStackBytes uint64 `json:"deadStackBytes"`
|
||||
ByStatus map[string]int `json:"byStatus"`
|
||||
ByFunction []GoroutineGroup `json:"byFunction"`
|
||||
}
|
||||
|
||||
type GoroutineGroup struct {
|
||||
Function string `json:"function"`
|
||||
CreatedBy string `json:"createdBy,omitempty"`
|
||||
Count int `json:"count"`
|
||||
StackBytes uint64 `json:"stackBytes"`
|
||||
MaxStackBytes uint64 `json:"maxStackBytes"`
|
||||
MinStackBytes uint64 `json:"minStackBytes"`
|
||||
}
|
||||
|
||||
type PoolReport struct {
|
||||
Size int `json:"size"`
|
||||
Cached int `json:"cached"`
|
||||
Victim int `json:"victim"`
|
||||
Bytes uint64 `json:"bytes"`
|
||||
}
|
||||
|
||||
func Collect() Report {
|
||||
return Report{
|
||||
Metrics: collectMetrics(),
|
||||
Goroutines: collectGoroutines(),
|
||||
Pools: collectBufferPools(),
|
||||
}
|
||||
}
|
||||
|
||||
func WriteFile(path string) error {
|
||||
content, err := json.MarshalIndent(Collect(), "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, content, 0o666)
|
||||
}
|
||||
|
||||
func collectMetrics() map[string]uint64 {
|
||||
descriptions := metrics.All()
|
||||
samples := make([]metrics.Sample, 0, len(descriptions))
|
||||
for _, description := range descriptions {
|
||||
if description.Kind != metrics.KindUint64 {
|
||||
continue
|
||||
}
|
||||
samples = append(samples, metrics.Sample{Name: description.Name})
|
||||
}
|
||||
metrics.Read(samples)
|
||||
result := make(map[string]uint64, len(samples))
|
||||
for _, sample := range samples {
|
||||
if sample.Value.Kind() != metrics.KindUint64 {
|
||||
continue
|
||||
}
|
||||
result[sample.Name] = sample.Value.Uint64()
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -56,6 +56,18 @@ func (m *platformDefaultInterfaceMonitor) UnregisterCallback(element *list.Eleme
|
||||
m.callbacks.Remove(element)
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) UpdateNetworkPath(networkPath string) {
|
||||
m.logger.Debug("updated network path: ", networkPath)
|
||||
if m.powerManager == nil {
|
||||
return
|
||||
}
|
||||
recorder := m.powerManager.Recorder()
|
||||
if recorder == nil {
|
||||
return
|
||||
}
|
||||
recorder.UpdateNetworkPath(networkPath)
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) {
|
||||
if sFixAndroidStack {
|
||||
done := make(chan struct{})
|
||||
|
||||
@@ -5,21 +5,18 @@ package libbox
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/trafficcontrol"
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/internal/oomprofile"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/internal/runtimeinfo"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
)
|
||||
|
||||
var oomReportProfiles = []string{
|
||||
@@ -34,157 +31,66 @@ var oomReportProfiles = []string{
|
||||
type oomReportMetadata struct {
|
||||
reportMetadata
|
||||
RecordedAt string `json:"recordedAt"`
|
||||
EndedAt string `json:"endedAt,omitempty"`
|
||||
MemoryLimit string `json:"memoryLimit,omitempty"`
|
||||
MemoryUsage string `json:"memoryUsage"`
|
||||
AvailableMemory string `json:"availableMemory,omitempty"`
|
||||
// Heap
|
||||
HeapAlloc string `json:"heapAlloc,omitempty"`
|
||||
HeapObjects uint64 `json:"heapObjects,omitempty,string"`
|
||||
HeapInuse string `json:"heapInuse,omitempty"`
|
||||
HeapIdle string `json:"heapIdle,omitempty"`
|
||||
HeapReleased string `json:"heapReleased,omitempty"`
|
||||
HeapSys string `json:"heapSys,omitempty"`
|
||||
// Stack
|
||||
StackInuse string `json:"stackInuse,omitempty"`
|
||||
StackSys string `json:"stackSys,omitempty"`
|
||||
// Runtime metadata
|
||||
MSpanInuse string `json:"mSpanInuse,omitempty"`
|
||||
MSpanSys string `json:"mSpanSys,omitempty"`
|
||||
MCacheSys string `json:"mCacheSys,omitempty"`
|
||||
BuckHashSys string `json:"buckHashSys,omitempty"`
|
||||
GCSys string `json:"gcSys,omitempty"`
|
||||
OtherSys string `json:"otherSys,omitempty"`
|
||||
Sys string `json:"sys,omitempty"`
|
||||
// GC & runtime
|
||||
TotalAlloc string `json:"totalAlloc,omitempty"`
|
||||
NumGC uint32 `json:"numGC,omitempty,string"`
|
||||
NumGoroutine int `json:"numGoroutine,omitempty,string"`
|
||||
NextGC string `json:"nextGC,omitempty"`
|
||||
LastGC string `json:"lastGC,omitempty"`
|
||||
Snapshots int `json:"snapshots,omitempty,string"`
|
||||
}
|
||||
|
||||
type oomReporter struct {
|
||||
startedService *daemon.StartedService
|
||||
func OOMRecorderOptions(startedService *daemon.StartedService) oomkiller.RecorderOptions {
|
||||
return oomkiller.RecorderOptions{
|
||||
BasePath: sWorkingPath,
|
||||
Logger: log.StdLogger(),
|
||||
AcceptDraft: acceptOOMDraft,
|
||||
MetadataCallback: func(status oomkiller.ReportStatus) any {
|
||||
metadata := oomReportMetadata{
|
||||
reportMetadata: baseReportMetadata(),
|
||||
RecordedAt: status.RecordedAt.UTC().Format(time.RFC3339),
|
||||
MemoryUsage: byteformats.FormatMemoryBytes(status.PeakMemory),
|
||||
Snapshots: status.Snapshots,
|
||||
}
|
||||
metadata.StartedAt = status.StartedAt.UTC().Format(time.RFC3339)
|
||||
if !status.EndedAt.IsZero() {
|
||||
metadata.EndedAt = status.EndedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if status.MemoryLimit > 0 {
|
||||
metadata.MemoryLimit = byteformats.FormatMemoryBytes(status.MemoryLimit)
|
||||
}
|
||||
if status.AvailableKnown {
|
||||
metadata.AvailableMemory = byteformats.FormatMemoryBytes(status.MinAvailable)
|
||||
}
|
||||
return metadata
|
||||
},
|
||||
OwnerCallback: chownReport,
|
||||
LogCallback: func() []byte {
|
||||
return formatLogEntries(startedService.SavedLog())
|
||||
},
|
||||
SnapshotCallback: func(directory string, prefix string) {
|
||||
for _, name := range oomReportProfiles {
|
||||
writeOOMProfile(filepath.Join(directory, prefix+"."+name+".pb"), name)
|
||||
}
|
||||
runtimeInfoPath := filepath.Join(directory, prefix+".runtime.json")
|
||||
err := runtimeinfo.WriteFile(runtimeInfoPath)
|
||||
if err == nil {
|
||||
chownReport(runtimeInfoPath)
|
||||
}
|
||||
copyConfigSnapshot(directory)
|
||||
content := oomConnectionsContent(startedService)
|
||||
if content != nil {
|
||||
writeReportFile(directory, prefix+".connections.json", content)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var _ oomkiller.OOMReporter = (*oomReporter)(nil)
|
||||
|
||||
func NewOOMReporter(startedService *daemon.StartedService) oomkiller.OOMReporter {
|
||||
return &oomReporter{startedService: startedService}
|
||||
}
|
||||
|
||||
func (r *oomReporter) WriteReport(memoryUsage uint64) error {
|
||||
draftPath := filepath.Join(sWorkingPath, "oom_draft")
|
||||
draftInfo, err := os.Stat(draftPath)
|
||||
func acceptOOMDraft(metadataContent []byte) bool {
|
||||
var draftMetadata reportMetadata
|
||||
err := json.Unmarshal(metadataContent, &draftMetadata)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
draftInfo = nil
|
||||
return false
|
||||
}
|
||||
reportsDir := filepath.Join(sWorkingPath, "oom_reports")
|
||||
err = os.MkdirAll(reportsDir, 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chownReport(reportsDir)
|
||||
|
||||
destPath, err := nextAvailableReportPath(reportsDir, time.Now().UTC())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = r.writeSnapshot(destPath, memoryUsage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return discardDraftIfCurrent(draftPath, draftInfo)
|
||||
}
|
||||
|
||||
func (r *oomReporter) WriteDraft(memoryUsage uint64) error {
|
||||
draftPath := filepath.Join(sWorkingPath, "oom_draft")
|
||||
os.RemoveAll(draftPath)
|
||||
return r.writeSnapshot(draftPath, memoryUsage)
|
||||
}
|
||||
|
||||
func (r *oomReporter) DiscardDraft() error {
|
||||
draftPath := filepath.Join(sWorkingPath, "oom_draft")
|
||||
return os.RemoveAll(draftPath)
|
||||
}
|
||||
|
||||
func discardDraftIfCurrent(draftPath string, draftInfo os.FileInfo) error {
|
||||
if draftInfo == nil {
|
||||
return nil
|
||||
}
|
||||
currentInfo, err := os.Stat(draftPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !os.SameFile(draftInfo, currentInfo) {
|
||||
return nil
|
||||
}
|
||||
return os.RemoveAll(draftPath)
|
||||
}
|
||||
|
||||
func (r *oomReporter) writeSnapshot(destPath string, memoryUsage uint64) error {
|
||||
now := time.Now().UTC()
|
||||
err := os.MkdirAll(destPath, 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chownReport(destPath)
|
||||
|
||||
for _, name := range oomReportProfiles {
|
||||
writeOOMProfile(destPath, name)
|
||||
}
|
||||
|
||||
writeReportFile(destPath, "cmdline", []byte(strings.Join(os.Args, "\000")))
|
||||
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
metadata := oomReportMetadata{
|
||||
reportMetadata: baseReportMetadata(),
|
||||
RecordedAt: now.Format(time.RFC3339),
|
||||
MemoryUsage: byteformats.FormatMemoryBytes(memoryUsage),
|
||||
// Heap
|
||||
HeapAlloc: byteformats.FormatMemoryBytes(memStats.HeapAlloc),
|
||||
HeapObjects: memStats.HeapObjects,
|
||||
HeapInuse: byteformats.FormatMemoryBytes(memStats.HeapInuse),
|
||||
HeapIdle: byteformats.FormatMemoryBytes(memStats.HeapIdle),
|
||||
HeapReleased: byteformats.FormatMemoryBytes(memStats.HeapReleased),
|
||||
HeapSys: byteformats.FormatMemoryBytes(memStats.HeapSys),
|
||||
// Stack
|
||||
StackInuse: byteformats.FormatMemoryBytes(memStats.StackInuse),
|
||||
StackSys: byteformats.FormatMemoryBytes(memStats.StackSys),
|
||||
// Runtime metadata
|
||||
MSpanInuse: byteformats.FormatMemoryBytes(memStats.MSpanInuse),
|
||||
MSpanSys: byteformats.FormatMemoryBytes(memStats.MSpanSys),
|
||||
MCacheSys: byteformats.FormatMemoryBytes(memStats.MCacheSys),
|
||||
BuckHashSys: byteformats.FormatMemoryBytes(memStats.BuckHashSys),
|
||||
GCSys: byteformats.FormatMemoryBytes(memStats.GCSys),
|
||||
OtherSys: byteformats.FormatMemoryBytes(memStats.OtherSys),
|
||||
Sys: byteformats.FormatMemoryBytes(memStats.Sys),
|
||||
// GC & runtime
|
||||
TotalAlloc: byteformats.FormatMemoryBytes(memStats.TotalAlloc),
|
||||
NumGC: memStats.NumGC,
|
||||
NumGoroutine: runtime.NumGoroutine(),
|
||||
NextGC: byteformats.FormatMemoryBytes(memStats.NextGC),
|
||||
}
|
||||
if memStats.LastGC > 0 {
|
||||
metadata.LastGC = time.Unix(0, int64(memStats.LastGC)).UTC().Format(time.RFC3339)
|
||||
}
|
||||
availableMemory := memory.Available()
|
||||
if availableMemory > 0 {
|
||||
metadata.AvailableMemory = byteformats.FormatMemoryBytes(availableMemory)
|
||||
}
|
||||
writeReportMetadata(destPath, metadata)
|
||||
copyConfigSnapshot(destPath)
|
||||
writeOOMLog(destPath, r.startedService.SavedLog())
|
||||
r.writeOOMConnections(destPath)
|
||||
|
||||
return nil
|
||||
return draftMetadata.AppVersion == sAppVersion && draftMetadata.AppMarketingVersion == sAppMarketingVersion
|
||||
}
|
||||
|
||||
type oomConnectionsInfo struct {
|
||||
@@ -213,14 +119,14 @@ type oomConnectionInfo struct {
|
||||
Download string `json:"download,omitempty"`
|
||||
}
|
||||
|
||||
func (r *oomReporter) writeOOMConnections(destPath string) {
|
||||
instance := r.startedService.Instance()
|
||||
func oomConnectionsContent(startedService *daemon.StartedService) []byte {
|
||||
instance := startedService.Instance()
|
||||
if instance == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
trafficManager := instance.TrafficManager()
|
||||
if trafficManager == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
connections := trafficManager.Connections()
|
||||
sort.Slice(connections, func(i, j int) bool {
|
||||
@@ -235,9 +141,9 @@ func (r *oomReporter) writeOOMConnections(destPath string) {
|
||||
}
|
||||
data, err := json.MarshalIndent(info, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
writeReportFile(destPath, "connections.json", data)
|
||||
return data
|
||||
}
|
||||
|
||||
func buildOOMConnections(connections []*trafficcontrol.TrackerMetadata) []oomConnectionInfo {
|
||||
@@ -314,14 +220,6 @@ func formatLogEntries(entries []*log.Entry) []byte {
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func writeOOMLog(destPath string, entries []*log.Entry) {
|
||||
content := formatLogEntries(entries)
|
||||
if content == nil {
|
||||
return
|
||||
}
|
||||
writeReportFile(destPath, "go.log", content)
|
||||
}
|
||||
|
||||
func writeWithoutColors(buffer *bytes.Buffer, message string) {
|
||||
start := 0
|
||||
for index := 0; index < len(message); {
|
||||
@@ -343,54 +241,18 @@ func writeWithoutColors(buffer *bytes.Buffer, message string) {
|
||||
buffer.WriteString(message[start:])
|
||||
}
|
||||
|
||||
func writeOOMProfile(destPath string, name string) {
|
||||
filePath, err := oomprofile.WriteFile(destPath, name)
|
||||
func writeOOMProfile(filePath string, name string) {
|
||||
err := oomprofile.WriteFile(filePath, name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
chownReport(filePath)
|
||||
}
|
||||
|
||||
func promoteOOMDraftAt(workingPath string) {
|
||||
draftPath := filepath.Join(workingPath, "oom_draft")
|
||||
info, err := os.Stat(draftPath)
|
||||
if err != nil || !info.IsDir() {
|
||||
return
|
||||
}
|
||||
metadataContent, err := os.ReadFile(filepath.Join(draftPath, "metadata.json"))
|
||||
if err != nil {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
}
|
||||
var draftMetadata reportMetadata
|
||||
err = json.Unmarshal(metadataContent, &draftMetadata)
|
||||
if err != nil || draftMetadata.AppVersion != sAppVersion || draftMetadata.AppMarketingVersion != sAppMarketingVersion {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
}
|
||||
reportsDir := filepath.Join(workingPath, "oom_reports")
|
||||
initReportDir(reportsDir)
|
||||
destPath, err := nextAvailableReportPath(reportsDir, info.ModTime().UTC())
|
||||
if err != nil {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
}
|
||||
err = os.Rename(draftPath, destPath)
|
||||
if err != nil {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
}
|
||||
chownReport(destPath)
|
||||
}
|
||||
|
||||
func promoteOOMDraft() {
|
||||
promoteOOMDraftAt(sWorkingPath)
|
||||
}
|
||||
|
||||
func PromoteOOMDraft() {
|
||||
promoteOOMDraft()
|
||||
oomkiller.PromoteDraft(sWorkingPath, acceptOOMDraft)
|
||||
}
|
||||
|
||||
func PromoteOOMDraftAt(workingPath string) {
|
||||
promoteOOMDraftAt(workingPath)
|
||||
oomkiller.PromoteDraft(workingPath, acceptOOMDraft)
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ func (c *ConnectionOwner) AndroidPackageNames() StringIterator {
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
UpdateDefaultInterface(interfaceName string, interfaceIndex int32, isExpensive bool, isConstrained bool)
|
||||
UpdateNetworkPath(networkPath string)
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
@@ -29,7 +30,7 @@ func PowerReportOptions(startedService *daemon.StartedService) powerreport.Optio
|
||||
},
|
||||
ProfileCallback: func(path string) {
|
||||
for _, name := range oomReportProfiles {
|
||||
writeOOMProfile(path, name)
|
||||
writeOOMProfile(filepath.Join(path, name+".pb"), name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ func ReloadSetupOptions(options *SetupOptions) {
|
||||
if sOOMKillerEnabled {
|
||||
if sOOMMemoryLimit == 0 && C.IsIos {
|
||||
sOOMMemoryLimit = oomkiller.DefaultAppleNetworkExtensionMemoryLimit
|
||||
debug.SetGCPercent(oomkiller.DefaultAppleNetworkExtensionGCPercent)
|
||||
}
|
||||
if sOOMMemoryLimit > 0 {
|
||||
debug.SetMemoryLimit(sOOMMemoryLimit * 4 / 5)
|
||||
|
||||
@@ -50,8 +50,8 @@ require (
|
||||
github.com/sagernet/asc-go v0.0.0-20260827085112-8f8226245b0d
|
||||
github.com/sagernet/bbolt v0.0.0-20260823094646-e24805439c9c
|
||||
github.com/sagernet/cors v1.2.1
|
||||
github.com/sagernet/cronet-go v0.0.0-20260831031307-45832ab07484
|
||||
github.com/sagernet/cronet-go/all v0.0.0-20260831031307-45832ab07484
|
||||
github.com/sagernet/cronet-go v0.0.0-20260912104727-0d28acc44093
|
||||
github.com/sagernet/cronet-go/all v0.0.0-20260912104727-0d28acc44093
|
||||
github.com/sagernet/fswatch v0.1.2
|
||||
github.com/sagernet/gliderssh v0.3.4-0.20260531100337-2194faca5648
|
||||
github.com/sagernet/gomobile v0.1.12
|
||||
@@ -59,22 +59,22 @@ require (
|
||||
github.com/sagernet/netlink v0.0.0-20260814022025-64455d367bbf
|
||||
github.com/sagernet/nftables v0.3.0-mod.4
|
||||
github.com/sagernet/quic-go v0.61.0-sing-box-mod.7
|
||||
github.com/sagernet/sing v0.9.0-beta.4
|
||||
github.com/sagernet/sing v0.9.4
|
||||
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3
|
||||
github.com/sagernet/sing-mux v0.3.5
|
||||
github.com/sagernet/sing-mux v0.3.6
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260729104525-103eb5fe5eb6
|
||||
github.com/sagernet/sing-quic v0.7.0-beta.4
|
||||
github.com/sagernet/sing-quic v0.7.0
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1
|
||||
github.com/sagernet/sing-snell v0.0.0-20260829071736-20f2eaec77c3
|
||||
github.com/sagernet/sing-tun v0.9.0-beta.4
|
||||
github.com/sagernet/sing-tun v0.9.3
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
|
||||
github.com/sagernet/sing-vmess v0.2.8
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1
|
||||
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.4
|
||||
github.com/sagernet/wireguard-go v0.0.5-extended-1.6.1
|
||||
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.5
|
||||
github.com/sagernet/wireguard-go v0.0.6-extended-1.6.1
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.0.2
|
||||
github.com/shtorm-7/workerpool v0.5.0
|
||||
@@ -211,35 +211,35 @@ require (
|
||||
github.com/pires/go-proxyproto v0.11.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260831030607-f80ef37265e5 // indirect
|
||||
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260912104006-c10c03c318db // indirect
|
||||
github.com/shtorm-7/rmux v1.0.1 // indirect
|
||||
github.com/smallstep/pkcs7 v0.1.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
@@ -270,9 +270,9 @@ require (
|
||||
zombiezen.com/go/capnproto2 v2.18.2+incompatible // indirect
|
||||
)
|
||||
|
||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.5-extended-1.6.1
|
||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.6-extended-1.6.1
|
||||
|
||||
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.102.1-sing-box-1.14-mod.4-extended-1.0.3
|
||||
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.102.1-sing-box-1.14-mod.5-extended-1.0.3
|
||||
|
||||
replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0
|
||||
|
||||
|
||||
@@ -395,68 +395,68 @@ github.com/sagernet/bbolt v0.0.0-20260823094646-e24805439c9c h1:JdEDkgiwsDxiZb4+
|
||||
github.com/sagernet/bbolt v0.0.0-20260823094646-e24805439c9c/go.mod h1:7/qFPFH5fqK5XQeY8EA1Qpaly08Av77p38Zt50ieQww=
|
||||
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
|
||||
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
|
||||
github.com/sagernet/cronet-go v0.0.0-20260831031307-45832ab07484 h1:m9mp6XyetfRKGwhJ3n4/48nIV4BK8wtY/ox31o2uXbo=
|
||||
github.com/sagernet/cronet-go v0.0.0-20260831031307-45832ab07484/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw=
|
||||
github.com/sagernet/cronet-go/all v0.0.0-20260831031307-45832ab07484 h1:K0ZZ6A1l6++yAmdlBmk6/uFWB9u3wiiH06hJdRK3760=
|
||||
github.com/sagernet/cronet-go/all v0.0.0-20260831031307-45832ab07484/go.mod h1:A+ICEP6DN5hO2aIXKuQ9PedwwngwRQUSul2QO2oNS4w=
|
||||
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260831030607-f80ef37265e5 h1:YB3mwkD8kJgUv1Pyt9ULE53s6ZgPoqUZk0UPsPlU91g=
|
||||
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
|
||||
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260831030607-f80ef37265e5 h1:1fk5mfJVwhfBa1I8q5e8NLqxpuPC3kZEijf+fzjzUAc=
|
||||
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM=
|
||||
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260831030607-f80ef37265e5 h1:Iyp6sZ1NqKdEe8BTOg0jF9PAlB+jz6Mr+MYc8hqxl+4=
|
||||
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260831030607-f80ef37265e5/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc=
|
||||
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260831030607-f80ef37265e5 h1:u0r5kq5fjvOhrlA1rpqNhhwXP9OdNk7xWy8qH2NfPc4=
|
||||
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ=
|
||||
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260831030607-f80ef37265e5 h1:Qevs2qJ3OzSxWpeDouT+VPI/RwhR8S35WdaWB0wGwUk=
|
||||
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs=
|
||||
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260831030607-f80ef37265e5 h1:ab6tfSgqfsiKvxrfZNLGHwvrB5xG66YJa8N3Xy/3gHw=
|
||||
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0=
|
||||
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260831030607-f80ef37265e5 h1:DECxuNWiZf/SI+dHOyux4zIQSpX7Rx44ZgRqee3L79A=
|
||||
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260831030607-f80ef37265e5/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0=
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260831030607-f80ef37265e5 h1:8vMHErGz1Ut/CDIlFGBFYxrQtss7fG232e7yPleZUcY=
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4=
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260831030607-f80ef37265e5 h1:i41nGgyv2O7ytxhxQ8YZ++VrY2m6bPplqK/v/kUCRr4=
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260831030607-f80ef37265e5/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo=
|
||||
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260831030607-f80ef37265e5 h1:u1xfank4+oUEhSWRRcKhK2vvykLFoh+IFHjPw4EV6K8=
|
||||
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ=
|
||||
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260831030607-f80ef37265e5 h1:6vWlr+2OvV0vIzkg/R7ny+7TnGZ45WBDK+IDm7pAbMk=
|
||||
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260831030607-f80ef37265e5/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU=
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260831030607-f80ef37265e5 h1:aE/JycRNXzewRfR801aCoWNvuwaCylQqHSBSNI+QT+g=
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI=
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260831030607-f80ef37265e5 h1:EjTX6AErgO9R7/geSuydPmI8zo2rjA0+HOhAM8DQN0M=
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260831030607-f80ef37265e5/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260831030607-f80ef37265e5 h1:e7mwyBfKSUeTjD+W9HTMCVFmVcXUzqq9AyJGXB8ViAw=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260831030607-f80ef37265e5/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260831030607-f80ef37265e5 h1:B+qkPThRPgPAhtLF/r9DoYAfic++/MjBSZ+trl7LPNk=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260831030607-f80ef37265e5 h1:FKfBZP4GPVgT8oECoY2BtYHi//Y0Y/rt3z1I76d6thg=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260831030607-f80ef37265e5/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260831030607-f80ef37265e5 h1:DffAWw6kqf37at38oABCgjqnFjjDk85RYiud7gqN62M=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260831030607-f80ef37265e5/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow=
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260831030607-f80ef37265e5 h1:x0wmFKQPGwD2Yv/OTmGDbNTEPwtc414NHqjegf1Uiw8=
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk=
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260831030607-f80ef37265e5 h1:ugUChFe1eVrSOTH6uBlfoIbGtyH3czlcHZ1Rvamjq28=
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260831030607-f80ef37265e5/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E=
|
||||
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260831030607-f80ef37265e5 h1:ii/FMyTM8Gpltwx93Q0c/YAnxwKMJ6Dhan6ZNs6p42g=
|
||||
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260831030607-f80ef37265e5/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8=
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260831030607-f80ef37265e5 h1:27omzPf1U+CkXgmL9N1YhJ2frJCQsiDNEhTIaGS+Xzg=
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260831030607-f80ef37265e5/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w=
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260831030607-f80ef37265e5 h1:SdF9F+Dq0gKBD6STdx4ScdsyaSJkIjkU6V7gkX2VLHQ=
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260831030607-f80ef37265e5/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0=
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260831030607-f80ef37265e5 h1:Pl3AjNnOXQuiynGrxBBTLi86RLYK7r7E+cnBVMks+iw=
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs=
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260831030607-f80ef37265e5 h1:dUyfzwer6H6AKycGoQOjdsdToGC4oqHv8peBK9q3OcY=
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260831030607-f80ef37265e5/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc=
|
||||
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260831030607-f80ef37265e5 h1:JJnON/3i+p9mfuLEZ8FOmynL43/LWmA81eu5V6eI1VM=
|
||||
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260831030607-f80ef37265e5/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4=
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260831030607-f80ef37265e5 h1:zgmvvztHMkS2poND9yjmpoBY0Rm56KVhX6+5+FU6wz0=
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc=
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260831030607-f80ef37265e5 h1:XSgRX0Pk77QZOhiUSBvuMpeW64FFMcFM0+aZQczxeX8=
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260831030607-f80ef37265e5/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc=
|
||||
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260831030607-f80ef37265e5 h1:lcNNmqbPlt4m7bBkD9xs6Nvm9R3GHjyhefnwLJrbLGQ=
|
||||
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8=
|
||||
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260831030607-f80ef37265e5 h1:gx9BY7k0JwB7ugdsp+zmY4vdNTIO9g+BkNhSBpNw7+k=
|
||||
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260831030607-f80ef37265e5/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw=
|
||||
github.com/sagernet/cronet-go v0.0.0-20260912104727-0d28acc44093 h1:CsvsLVIli53L6nzgEpyRjjWnRRhTds4D3eh5sEnF5Sw=
|
||||
github.com/sagernet/cronet-go v0.0.0-20260912104727-0d28acc44093/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw=
|
||||
github.com/sagernet/cronet-go/all v0.0.0-20260912104727-0d28acc44093 h1:1lRUqrLcBz0Q0r/xvyiXS5hZwH5pOq6Lwb4dqbSqJfs=
|
||||
github.com/sagernet/cronet-go/all v0.0.0-20260912104727-0d28acc44093/go.mod h1:bvm9T9FdwHYYfWdZkLoZ4Qr9Dce0TTQ7ngXlbGZnnJk=
|
||||
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260912104006-c10c03c318db h1:o0NCKT9gZsOLJK5s5wXW5A5EaFCnF5En6SApmHwUMJo=
|
||||
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260912104006-c10c03c318db/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
|
||||
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260912104006-c10c03c318db h1:75fzfzaz/j1mKsqNE01KTMMP3i/9FgmPlwO2BIV8TpU=
|
||||
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM=
|
||||
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260912104006-c10c03c318db h1:tz6Tm3qqxPKp9QMFDUxbVzu5NgYQZq2hY9CGQ+sTmII=
|
||||
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260912104006-c10c03c318db/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc=
|
||||
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260912104006-c10c03c318db h1:OO+rUNHeZja1m0n/bBfkWtK2NQkNN9iROP0CRqw9aL8=
|
||||
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ=
|
||||
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260912104006-c10c03c318db h1:78W68WJdZjQDhkp4tcFkLcXBH4W8jlCBK1eFcBKKZwI=
|
||||
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs=
|
||||
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260912104006-c10c03c318db h1:qVLzw+rWqzD82/CUwvxfSohUj6aSD9zRAX7WtKiSNeQ=
|
||||
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0=
|
||||
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260912104006-c10c03c318db h1:EoaFEVn+GVZw2jKXgK+73BdpUZKJ85PGUdbAyxy72pk=
|
||||
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260912104006-c10c03c318db/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0=
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260912104006-c10c03c318db h1:rtv8UspnSr157Hp9k6Pj0IV224GvANVx+XTTd2/CjdY=
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4=
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260912104006-c10c03c318db h1:TwBUmUKDuQzA5nIshSzVE0tCKK1GpEfy54E1h3o62t8=
|
||||
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260912104006-c10c03c318db/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo=
|
||||
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260912104006-c10c03c318db h1:T2yA2pXtVKeZniYbp8bfcNTigauAl3EO3AO1UwVXoGE=
|
||||
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260912104006-c10c03c318db/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ=
|
||||
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260912104006-c10c03c318db h1:w5BfjtEy9mjMiXtmKfB2uBGeRG4pwbuQ6ouTflNnWyU=
|
||||
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260912104006-c10c03c318db/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU=
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260912104006-c10c03c318db h1:yNdO+HC5OkgKgwqVYd49FRVt5A2jfxsyrSKrf2uM0aM=
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI=
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260912104006-c10c03c318db h1:jJ1WycEj07LsIhtZdgXSiDa9qMkDUvBtr/B8WQm5utQ=
|
||||
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260912104006-c10c03c318db/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260912104006-c10c03c318db h1:HPBv0BqbHC8ktpn1rSMeoREDPumyFFpJHZsUCwzwLqY=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260912104006-c10c03c318db/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260912104006-c10c03c318db h1:7mJdu1gjC45rEkiIGtdCuEs3v1Oj2IXBnSh9uIgZEmE=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260912104006-c10c03c318db h1:YCBQCFTjlkRkkCXV+ScjN/URW9Mx58f6Dv34a8JRtGw=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260912104006-c10c03c318db/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260912104006-c10c03c318db h1:RIp7sKxgUGMidhmi7oVhX8DgnG6UjKk7kvxWvWfKvbY=
|
||||
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260912104006-c10c03c318db/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow=
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260912104006-c10c03c318db h1:lElglqZOqg3g7/L8uzJKS6yPNgIxbEu4/vVTzlYE7W8=
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk=
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260912104006-c10c03c318db h1:4HvprIkwKcrPYuyQaH5tdIeh5KVBgkauL6o0VqpCkSo=
|
||||
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260912104006-c10c03c318db/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E=
|
||||
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260912104006-c10c03c318db h1:rWrNWeyy563mTk3KZas3ore7JPq7M+l8r0XV9lAjc0c=
|
||||
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260912104006-c10c03c318db/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8=
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260912104006-c10c03c318db h1:YADxQVD/X9yyV3i/xS3T/lWhpVLslIIl+e+srwpT9p0=
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260912104006-c10c03c318db/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w=
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260912104006-c10c03c318db h1:2hgFBooF0jPOGXKcuJgMA4h1aSOULyAI6OgubZBoD7g=
|
||||
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260912104006-c10c03c318db/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0=
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260912104006-c10c03c318db h1:fFv7izOOsC/HQCDAwTtPjIlayo8znNlAnPqppDb08Jo=
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs=
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260912104006-c10c03c318db h1:meCf6CbZqWpV5ITTjawLJzZfFrx6thSgs7a9PZyqdTM=
|
||||
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260912104006-c10c03c318db/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc=
|
||||
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260912104006-c10c03c318db h1:7bOFA2F/WApDRUamwGM5OThrY5H3+UZpW0yQLlukE8Y=
|
||||
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260912104006-c10c03c318db/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4=
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260912104006-c10c03c318db h1:9HjQtfjpo5PTr5AMBWgyTVnWI/zQOTDVQ1agftOOoGo=
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc=
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260912104006-c10c03c318db h1:9OUQzCcoCvleQVLb3xW3AGB+eKGxPmldK1gg61xh7T8=
|
||||
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260912104006-c10c03c318db/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc=
|
||||
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260912104006-c10c03c318db h1:fKs7KaruaDhStaiYpaqMQXDCmAc4YmNLI/pBkPYvwH8=
|
||||
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8=
|
||||
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260912104006-c10c03c318db h1:K7/F1mWxmf5yUDxSSk+6pRk+yfCoWsqIgt2LsjRpP8w=
|
||||
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260912104006-c10c03c318db/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw=
|
||||
github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk=
|
||||
github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880=
|
||||
github.com/sagernet/gliderssh v0.3.4-0.20260531100337-2194faca5648 h1:IWVjKBARzVjdmH0VUaeTBOBli1qkwKmTG4XfbkpSS20=
|
||||
@@ -477,8 +477,8 @@ github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df h1:ogyC7
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260729104525-103eb5fe5eb6 h1:ZAvhkor0prJ8KEX9EmTEH+gJ1WlA3ffjS8GZT6KKq9c=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260729104525-103eb5fe5eb6/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw=
|
||||
github.com/sagernet/sing-quic v0.7.0-beta.4 h1:frzDiZqUbT0yLVaVfgaLtoVQ/zGa3xIsDTHP+aSM/W8=
|
||||
github.com/sagernet/sing-quic v0.7.0-beta.4/go.mod h1:rJFeVxbEej2iuyeCJ3drKW+p5rOgtLIW3XhZ6ty7Fig=
|
||||
github.com/sagernet/sing-quic v0.7.0 h1:dgbJdRYsa9DXbgjchnuVf2xhAvjY4/jylWdNNVZWmRU=
|
||||
github.com/sagernet/sing-quic v0.7.0/go.mod h1:rJFeVxbEej2iuyeCJ3drKW+p5rOgtLIW3XhZ6ty7Fig=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
||||
@@ -487,8 +487,8 @@ github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkG
|
||||
github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-snell v0.0.0-20260829071736-20f2eaec77c3 h1:wjEw0lmCSC+oqh7GD4rbMB/dk7+hZ2VopUj4xM5/Jrc=
|
||||
github.com/sagernet/sing-snell v0.0.0-20260829071736-20f2eaec77c3/go.mod h1:et8Lws4f5QbOrY65DmjevHGup3mijJkhswkto6cwciM=
|
||||
github.com/sagernet/sing-tun v0.9.0-beta.4 h1:gIIZU4HevhtTQubZiOdMD0/RnECD6csl2kG6666KYmQ=
|
||||
github.com/sagernet/sing-tun v0.9.0-beta.4/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0=
|
||||
github.com/sagernet/sing-tun v0.9.3 h1:fCXbGezGMUXi00X0MmG3iX5QR91sPApZwzfr0MAVHv4=
|
||||
github.com/sagernet/sing-tun v0.9.3/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0=
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca h1:5wA+IE0Fq1CGVLOgSpm0gKKZ03HzugcJ2JyhmDqeX6A=
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca/go.mod h1:ADAZZU85MnM91XNhd2TdZRTaIbA7RjTZuQ1KCHcUbNg=
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
|
||||
@@ -511,10 +511,10 @@ github.com/shtorm-7/sing-mux v0.3.5-extended-1.0.0 h1:AfCNqpQlrdWnEd64wXN+cmoHlP
|
||||
github.com/shtorm-7/sing-mux v0.3.5-extended-1.0.0/go.mod h1:1qHy7cnbDQSY2OWL7CybproWC0t5Ia/DIuNHAzB6ZCA=
|
||||
github.com/shtorm-7/sing-vmess v0.2.8-extended-1.0.0 h1:OjvqZOhYSi9eIJGYC0iPDPqvgo/asujvhAGkFzbZ5/Y=
|
||||
github.com/shtorm-7/sing-vmess v0.2.8-extended-1.0.0/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
|
||||
github.com/shtorm-7/tailscale v1.102.1-sing-box-1.14-mod.4-extended-1.0.3 h1:AG4leaIK/VByNXDCPtxtBjLc1orPAtBBNcfw+rebPno=
|
||||
github.com/shtorm-7/tailscale v1.102.1-sing-box-1.14-mod.4-extended-1.0.3/go.mod h1:+1R9TnmCDbxb7h7ZOBKxi5UI6aWf+polOO/fc0WB/4o=
|
||||
github.com/shtorm-7/wireguard-go v0.0.5-extended-1.6.1 h1:lLd8c5SNbm01idBlaSc18Urn4i4/ME/Umu8BeQLJ/io=
|
||||
github.com/shtorm-7/wireguard-go v0.0.5-extended-1.6.1/go.mod h1:1VZmkU7yPAZVE0XKufUqqAhEY1RnWUcyZLmry85y+7A=
|
||||
github.com/shtorm-7/tailscale v1.102.1-sing-box-1.14-mod.5-extended-1.0.3 h1:cji5tRY667BMIwlzHvuIf51J2H/c8WPZYoOFNDtbbsA=
|
||||
github.com/shtorm-7/tailscale v1.102.1-sing-box-1.14-mod.5-extended-1.0.3/go.mod h1:+1R9TnmCDbxb7h7ZOBKxi5UI6aWf+polOO/fc0WB/4o=
|
||||
github.com/shtorm-7/wireguard-go v0.0.6-extended-1.6.1 h1:aPKSNxyu5VGqSLvDKoNRP/lnSCxS2sRKuzxDBWX99wo=
|
||||
github.com/shtorm-7/wireguard-go v0.0.6-extended-1.6.1/go.mod h1:1VZmkU7yPAZVE0XKufUqqAhEY1RnWUcyZLmry85y+7A=
|
||||
github.com/shtorm-7/workerpool v0.5.0 h1:NPZuNgyH0EUm4aQsTL09xR1iV+7GCFw6jX9Z4aAVp2s=
|
||||
github.com/shtorm-7/workerpool v0.5.0/go.mod h1:NI0pUZgmGu0BdKO9j3mct1DNZmgXbyTS9foorljdH6E=
|
||||
github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU=
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
experimental.RegisterClashServerConstructor(func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
|
||||
experimental.RegisterClashServerConstructor(func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error) {
|
||||
return nil, E.New(`clash api is not included in this build, rebuild with -tags with_clash_api`)
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,12 +17,12 @@ type Factory interface {
|
||||
SetLevel(level Level)
|
||||
Logger() ContextLogger
|
||||
NewLogger(tag string) ContextLogger
|
||||
AttachPlatformWriter(writer PlatformWriter)
|
||||
}
|
||||
|
||||
type ObservableFactory interface {
|
||||
Factory
|
||||
observable.Observable[Entry]
|
||||
AttachPlatformWriter(writer PlatformWriter)
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
|
||||
+128
-116
@@ -6,8 +6,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
@@ -20,159 +18,173 @@ type Formatter struct {
|
||||
DisableLineBreak bool
|
||||
}
|
||||
|
||||
func (f Formatter) Format(ctx context.Context, level Level, tag string, message string, timestamp time.Time) string {
|
||||
levelString := strings.ToUpper(FormatLevel(level))
|
||||
if !f.DisableColors {
|
||||
var (
|
||||
levelLabels [LevelTrace + 1]string
|
||||
coloredLevelLabels [LevelTrace + 1]string
|
||||
)
|
||||
|
||||
func init() {
|
||||
for level := LevelPanic; level <= LevelTrace; level++ {
|
||||
label := strings.ToUpper(FormatLevel(level))
|
||||
levelLabels[level] = label
|
||||
switch level {
|
||||
case LevelDebug, LevelTrace:
|
||||
levelString = aurora.White(levelString).String()
|
||||
coloredLevelLabels[level] = aurora.White(label).String()
|
||||
case LevelInfo:
|
||||
levelString = aurora.Cyan(levelString).String()
|
||||
coloredLevelLabels[level] = aurora.Cyan(label).String()
|
||||
case LevelNotice:
|
||||
levelString = aurora.Green(levelString).String()
|
||||
coloredLevelLabels[level] = aurora.Green(label).String()
|
||||
case LevelWarn:
|
||||
levelString = aurora.Yellow(levelString).String()
|
||||
coloredLevelLabels[level] = aurora.Yellow(label).String()
|
||||
case LevelError, LevelFatal, LevelPanic:
|
||||
levelString = aurora.Red(levelString).String()
|
||||
coloredLevelLabels[level] = aurora.Red(label).String()
|
||||
}
|
||||
}
|
||||
if tag != "" {
|
||||
message = tag + ": " + message
|
||||
}
|
||||
}
|
||||
|
||||
func (f Formatter) Format(ctx context.Context, level Level, tag string, message string, timestamp time.Time) string {
|
||||
var id ID
|
||||
var hasId bool
|
||||
if ctx != nil {
|
||||
id, hasId = IDFromContext(ctx)
|
||||
}
|
||||
var builder strings.Builder
|
||||
builder.Grow(len(tag) + len(message) + 64)
|
||||
f.writePrefix(&builder, level, timestamp)
|
||||
if hasId {
|
||||
activeDuration := FormatDuration(time.Since(id.CreatedAt))
|
||||
if !f.DisableColors {
|
||||
var color aurora.Color
|
||||
color = aurora.Color(uint8(id.ID))
|
||||
color %= 215
|
||||
row := uint(color / 36)
|
||||
column := uint(color % 36)
|
||||
|
||||
var r, g, b float32
|
||||
r = float32(row * 51)
|
||||
g = float32(column / 6 * 51)
|
||||
b = float32((column % 6) * 51)
|
||||
luma := 0.2126*r + 0.7152*g + 0.0722*b
|
||||
if luma < 60 {
|
||||
row = 5 - row
|
||||
column = 35 - column
|
||||
color = aurora.Color(row*36 + column)
|
||||
}
|
||||
color += 16
|
||||
color = color << 16
|
||||
color |= 1 << 14
|
||||
message = F.ToString("[", aurora.Colorize(id.ID, color).String(), " ", activeDuration, "] ", message)
|
||||
} else {
|
||||
message = F.ToString("[", id.ID, " ", activeDuration, "] ", message)
|
||||
}
|
||||
f.writeIdPrefix(&builder, id)
|
||||
}
|
||||
switch {
|
||||
case f.DisableTimestamp:
|
||||
message = levelString + " " + message
|
||||
case f.FullTimestamp:
|
||||
message = timestamp.Format(f.TimestampFormat) + " " + levelString + " " + message
|
||||
default:
|
||||
message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message
|
||||
if tag != "" {
|
||||
builder.WriteString(tag)
|
||||
builder.WriteString(": ")
|
||||
}
|
||||
if f.DisableLineBreak {
|
||||
if message[len(message)-1] == '\n' {
|
||||
message = message[:len(message)-1]
|
||||
}
|
||||
builder.WriteString(strings.TrimSuffix(message, "\n"))
|
||||
} else {
|
||||
if message[len(message)-1] != '\n' {
|
||||
message += "\n"
|
||||
builder.WriteString(message)
|
||||
if !strings.HasSuffix(message, "\n") {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
return message
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string, message string, timestamp time.Time) (string, string) {
|
||||
levelString := strings.ToUpper(FormatLevel(level))
|
||||
if !f.DisableColors {
|
||||
switch level {
|
||||
case LevelDebug, LevelTrace:
|
||||
levelString = aurora.White(levelString).String()
|
||||
case LevelInfo:
|
||||
levelString = aurora.Cyan(levelString).String()
|
||||
case LevelNotice:
|
||||
levelString = aurora.Green(levelString).String()
|
||||
case LevelWarn:
|
||||
levelString = aurora.Yellow(levelString).String()
|
||||
case LevelError, LevelFatal, LevelPanic:
|
||||
levelString = aurora.Red(levelString).String()
|
||||
}
|
||||
}
|
||||
if tag != "" {
|
||||
message = tag + ": " + message
|
||||
}
|
||||
messageSimple := message
|
||||
func (f Formatter) FormatSimple(ctx context.Context, tag string, message string) string {
|
||||
var id ID
|
||||
var hasId bool
|
||||
if ctx != nil {
|
||||
id, hasId = IDFromContext(ctx)
|
||||
}
|
||||
if !hasId && tag == "" {
|
||||
return message
|
||||
}
|
||||
var builder strings.Builder
|
||||
builder.Grow(len(tag) + len(message) + 32)
|
||||
if hasId {
|
||||
activeDuration := FormatDuration(time.Since(id.CreatedAt))
|
||||
if !f.DisableColors {
|
||||
var color aurora.Color
|
||||
color = aurora.Color(uint8(id.ID))
|
||||
color %= 215
|
||||
row := uint(color / 36)
|
||||
column := uint(color % 36)
|
||||
|
||||
var r, g, b float32
|
||||
r = float32(row * 51)
|
||||
g = float32(column / 6 * 51)
|
||||
b = float32((column % 6) * 51)
|
||||
luma := 0.2126*r + 0.7152*g + 0.0722*b
|
||||
if luma < 60 {
|
||||
row = 5 - row
|
||||
column = 35 - column
|
||||
color = aurora.Color(row*36 + column)
|
||||
}
|
||||
color += 16
|
||||
color = color << 16
|
||||
color |= 1 << 14
|
||||
message = F.ToString("[", aurora.Colorize(id.ID, color).String(), " ", activeDuration, "] ", message)
|
||||
} else {
|
||||
message = F.ToString("[", id.ID, " ", activeDuration, "] ", message)
|
||||
}
|
||||
messageSimple = F.ToString("[", id.ID, " ", activeDuration, "] ", messageSimple)
|
||||
builder.WriteByte('[')
|
||||
writeUint(&builder, uint64(id.ID))
|
||||
builder.WriteByte(' ')
|
||||
writeDuration(&builder, time.Since(id.CreatedAt))
|
||||
builder.WriteString("] ")
|
||||
}
|
||||
if tag != "" {
|
||||
builder.WriteString(tag)
|
||||
builder.WriteString(": ")
|
||||
}
|
||||
builder.WriteString(message)
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (f Formatter) writePrefix(builder *strings.Builder, level Level, timestamp time.Time) {
|
||||
var levelString string
|
||||
if int(level) >= len(levelLabels) {
|
||||
levelString = "UNKNOWN"
|
||||
} else if f.DisableColors {
|
||||
levelString = levelLabels[level]
|
||||
} else {
|
||||
levelString = coloredLevelLabels[level]
|
||||
}
|
||||
switch {
|
||||
case f.DisableTimestamp:
|
||||
message = levelString + " " + message
|
||||
builder.WriteString(levelString)
|
||||
builder.WriteByte(' ')
|
||||
case f.FullTimestamp:
|
||||
message = timestamp.Format(f.TimestampFormat) + " " + levelString + " " + message
|
||||
var timeBuffer [64]byte
|
||||
builder.Write(timestamp.AppendFormat(timeBuffer[:0], f.TimestampFormat))
|
||||
builder.WriteByte(' ')
|
||||
builder.WriteString(levelString)
|
||||
builder.WriteByte(' ')
|
||||
default:
|
||||
message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message
|
||||
builder.WriteString(levelString)
|
||||
builder.WriteByte('[')
|
||||
seconds := strconv.AppendInt(make([]byte, 0, 20), int64(timestamp.Sub(f.BaseTime)/time.Second), 10)
|
||||
for pad := 4 - len(seconds); pad > 0; pad-- {
|
||||
builder.WriteByte('0')
|
||||
}
|
||||
builder.Write(seconds)
|
||||
builder.WriteString("] ")
|
||||
}
|
||||
if message[len(message)-1] != '\n' {
|
||||
message += "\n"
|
||||
}
|
||||
return message, messageSimple
|
||||
}
|
||||
|
||||
func xd(value int, x int) string {
|
||||
message := strconv.Itoa(value)
|
||||
for len(message) < x {
|
||||
message = "0" + message
|
||||
func (f Formatter) writeIdPrefix(builder *strings.Builder, id ID) {
|
||||
builder.WriteByte('[')
|
||||
if f.DisableColors {
|
||||
writeUint(builder, uint64(id.ID))
|
||||
} else {
|
||||
builder.WriteString("\x1b[38;5;")
|
||||
writeUint(builder, uint64(colorForID(id.ID)))
|
||||
builder.WriteByte('m')
|
||||
writeUint(builder, uint64(id.ID))
|
||||
builder.WriteString("\x1b[0m")
|
||||
}
|
||||
builder.WriteByte(' ')
|
||||
writeDuration(builder, time.Since(id.CreatedAt))
|
||||
builder.WriteString("] ")
|
||||
}
|
||||
|
||||
func colorForID(value uint32) uint8 {
|
||||
color := uint8(value) % 215
|
||||
row := uint(color / 36)
|
||||
column := uint(color % 36)
|
||||
r := float32(row * 51)
|
||||
g := float32(column / 6 * 51)
|
||||
b := float32((column % 6) * 51)
|
||||
luma := 0.2126*r + 0.7152*g + 0.0722*b
|
||||
if luma < 60 {
|
||||
row = 5 - row
|
||||
column = 35 - column
|
||||
color = uint8(row*36 + column)
|
||||
}
|
||||
return color + 16
|
||||
}
|
||||
|
||||
func writeUint(builder *strings.Builder, value uint64) {
|
||||
builder.Write(strconv.AppendUint(make([]byte, 0, 20), value, 10))
|
||||
}
|
||||
|
||||
func writeInt(builder *strings.Builder, value int64) {
|
||||
builder.Write(strconv.AppendInt(make([]byte, 0, 20), value, 10))
|
||||
}
|
||||
|
||||
func writeDuration(builder *strings.Builder, duration time.Duration) {
|
||||
if duration < time.Second {
|
||||
writeInt(builder, duration.Milliseconds())
|
||||
builder.WriteString("ms")
|
||||
} else if duration < time.Minute {
|
||||
writeInt(builder, int64(duration.Seconds()))
|
||||
builder.WriteByte('.')
|
||||
writeInt(builder, int64(duration.Seconds()*100)%100)
|
||||
builder.WriteByte('s')
|
||||
} else {
|
||||
writeInt(builder, int64(duration.Minutes()))
|
||||
builder.WriteByte('m')
|
||||
writeInt(builder, int64(duration.Seconds())%60)
|
||||
builder.WriteByte('s')
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func FormatDuration(duration time.Duration) string {
|
||||
if duration < time.Second {
|
||||
return F.ToString(duration.Milliseconds(), "ms")
|
||||
} else if duration < time.Minute {
|
||||
return F.ToString(int64(duration.Seconds()), ".", int64(duration.Seconds()*100)%100, "s")
|
||||
} else {
|
||||
return F.ToString(int64(duration.Minutes()), "m", int64(duration.Seconds())%60, "s")
|
||||
}
|
||||
var builder strings.Builder
|
||||
writeDuration(&builder, duration)
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
+7
-13
@@ -24,6 +24,7 @@ type defaultFactory struct {
|
||||
file *os.File
|
||||
filePath string
|
||||
platformWriters atomic.Pointer[[]PlatformWriter]
|
||||
needConsole bool
|
||||
needObservable bool
|
||||
level Level
|
||||
subscriber *observable.Subscriber[Entry]
|
||||
@@ -58,6 +59,7 @@ func NewDefaultFactory(
|
||||
},
|
||||
writer: writer,
|
||||
filePath: filePath,
|
||||
needConsole: writer != io.Discard || filePath != "",
|
||||
needObservable: needObservable,
|
||||
level: LevelTrace,
|
||||
subscriber: observable.NewSubscriber[Entry](128),
|
||||
@@ -81,6 +83,7 @@ func (f *defaultFactory) Start() error {
|
||||
f.writer = logFile
|
||||
f.file = logFile
|
||||
}
|
||||
f.needConsole = f.writer != io.Discard
|
||||
}
|
||||
if f.needObservable {
|
||||
f.observer = observable.NewObserver[Entry](f.subscriber, 64)
|
||||
@@ -144,19 +147,7 @@ func (f *defaultFactory) UnSubscribe(sub observable.Subscription[Entry]) {
|
||||
}
|
||||
|
||||
func (f *defaultFactory) output(ctx context.Context, level Level, tag string, message string, timestamp time.Time) {
|
||||
if f.needObservable {
|
||||
formatted, formattedSimple := f.formatter.FormatWithSimple(ctx, level, tag, message, timestamp)
|
||||
if level <= f.level {
|
||||
if level == LevelPanic {
|
||||
panic(formatted)
|
||||
}
|
||||
f.writer.Write([]byte(formatted))
|
||||
if level == LevelFatal {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
f.subscriber.Emit(Entry{level, formattedSimple})
|
||||
} else if level <= f.level {
|
||||
if level <= f.level && (f.needConsole || level == LevelPanic || level == LevelFatal) {
|
||||
formatted := f.formatter.Format(ctx, level, tag, message, timestamp)
|
||||
if level == LevelPanic {
|
||||
panic(formatted)
|
||||
@@ -166,6 +157,9 @@ func (f *defaultFactory) output(ctx context.Context, level Level, tag string, me
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if f.needObservable {
|
||||
f.subscriber.Emit(Entry{level, f.formatter.FormatSimple(ctx, tag, message)})
|
||||
}
|
||||
platformWriters := f.loadPlatformWriters()
|
||||
if len(platformWriters) > 0 {
|
||||
platformMessage := f.platformFormatter.Format(ctx, level, tag, message, timestamp)
|
||||
|
||||
@@ -30,7 +30,6 @@ type ClashAPIOptions struct {
|
||||
ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty" reference:"outbound"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
DefaultMode string `json:"default_mode,omitempty"`
|
||||
ModeList []string `json:"-"`
|
||||
AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"`
|
||||
AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"`
|
||||
|
||||
|
||||
+5
-5
@@ -12,11 +12,11 @@ import (
|
||||
)
|
||||
|
||||
type HTTP2Options struct {
|
||||
IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"`
|
||||
KeepAlivePeriod badoption.Duration `json:"keep_alive_period,omitempty"`
|
||||
StreamReceiveWindow byteformats.MemoryBytes `json:"stream_receive_window,omitempty"`
|
||||
ConnectionReceiveWindow byteformats.MemoryBytes `json:"connection_receive_window,omitempty"`
|
||||
MaxConcurrentStreams int `json:"max_concurrent_streams,omitempty"`
|
||||
IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"`
|
||||
KeepAlivePeriod badoption.Duration `json:"keep_alive_period,omitempty"`
|
||||
StreamReceiveWindow *byteformats.MemoryBytes `json:"stream_receive_window,omitempty"`
|
||||
ConnectionReceiveWindow *byteformats.MemoryBytes `json:"connection_receive_window,omitempty"`
|
||||
MaxConcurrentStreams int `json:"max_concurrent_streams,omitempty"`
|
||||
}
|
||||
|
||||
type QUICOptions struct {
|
||||
|
||||
+4
-4
@@ -25,8 +25,8 @@ type OpenVPNClientEndpointOptions struct {
|
||||
Servers []OpenVPNRemoteOptions `json:"servers,omitempty"`
|
||||
RemoteRandom bool `json:"remote_random,omitempty"`
|
||||
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
|
||||
PeerAddress badoption.Addr `json:"peer_address,omitempty"`
|
||||
PeerAddressIPv6 badoption.Addr `json:"peer_address_ipv6,omitempty"`
|
||||
PeerAddress *badoption.Addr `json:"peer_address,omitempty"`
|
||||
PeerAddressIPv6 *badoption.Addr `json:"peer_address_ipv6,omitempty"`
|
||||
Topology string `json:"topology,omitempty" enum:"net30,p2p,subnet"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
@@ -81,8 +81,8 @@ type OpenVPNServerEndpointOptions struct {
|
||||
RemotePort uint16 `json:"remote_port,omitempty"`
|
||||
MaxClients int `json:"max_clients,omitempty"`
|
||||
Address badoption.Listable[netip.Prefix] `json:"address"`
|
||||
PeerAddress badoption.Addr `json:"peer_address,omitempty"`
|
||||
PeerAddressIPv6 badoption.Addr `json:"peer_address_ipv6,omitempty"`
|
||||
PeerAddress *badoption.Addr `json:"peer_address,omitempty"`
|
||||
PeerAddressIPv6 *badoption.Addr `json:"peer_address_ipv6,omitempty"`
|
||||
Topology string `json:"topology,omitempty" enum:"net30,p2p,subnet"`
|
||||
DuplicateCN bool `json:"duplicate_cn,omitempty"`
|
||||
Users []auth.User `json:"users,omitempty"`
|
||||
|
||||
@@ -99,7 +99,11 @@ func (h *Outbound) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
|
||||
func (h *Outbound) fetchMyAddresses() {
|
||||
myInterfaceNames := h.network.InterfaceMonitor().MyInterfaces()
|
||||
interfaceMonitor := h.network.InterfaceMonitor()
|
||||
if interfaceMonitor == nil {
|
||||
return
|
||||
}
|
||||
myInterfaceNames := interfaceMonitor.MyInterfaces()
|
||||
if len(myInterfaceNames) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||
if outboundHandler, isHandler := selected.(adapter.ConnectionHandler); isHandler {
|
||||
outboundHandler.NewConnection(ctx, conn, metadata, onClose)
|
||||
} else {
|
||||
s.connection.NewConnection(ctx, selected, conn, metadata, onClose)
|
||||
s.connection.NewConnection(ctx, s, conn, metadata, onClose)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, m
|
||||
if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandler); isHandler {
|
||||
outboundHandler.NewPacketConnection(ctx, conn, metadata, onClose)
|
||||
} else {
|
||||
s.connection.NewPacketConnection(ctx, selected, conn, metadata, onClose)
|
||||
s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/http"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
@@ -152,6 +153,7 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata
|
||||
func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
|
||||
user, loaded := auth.UserFromContext[string](ctx)
|
||||
if !loaded {
|
||||
if !metadata.Destination.IsValid() {
|
||||
|
||||
@@ -405,11 +405,11 @@ func buildClientDataChannelOptions(options option.OpenVPNClientEndpointOptions)
|
||||
}
|
||||
|
||||
func buildClientTunnelOptions(options option.OpenVPNClientEndpointOptions, requirePeerAddress bool) (ovpn.ClientTunnelOptions, error) {
|
||||
vpnGateway := netip.Addr(options.PeerAddress)
|
||||
vpnGateway := options.PeerAddress.Build(netip.Addr{})
|
||||
if vpnGateway.IsValid() && !vpnGateway.Is4() {
|
||||
return ovpn.ClientTunnelOptions{}, E.New("`peer_address` must be an IPv4 address")
|
||||
}
|
||||
vpnGatewayIPv6 := netip.Addr(options.PeerAddressIPv6)
|
||||
vpnGatewayIPv6 := options.PeerAddressIPv6.Build(netip.Addr{})
|
||||
if vpnGatewayIPv6.IsValid() && !vpnGatewayIPv6.Is6() {
|
||||
return ovpn.ClientTunnelOptions{}, E.New("`peer_address_ipv6` must be an IPv6 address")
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ func buildServerOptions(options option.OpenVPNServerEndpointOptions) (ovpn.Serve
|
||||
if options.TLS == nil {
|
||||
return ovpn.ServerOptions{}, E.New("missing `tls` options")
|
||||
}
|
||||
if len(options.StaticKey) > 0 || options.StaticKeyPath != "" || options.KeyDirection != "" || options.Cipher != "" || options.Remote != "" || options.RemotePort != 0 || netip.Addr(options.PeerAddress).IsValid() || netip.Addr(options.PeerAddressIPv6).IsValid() {
|
||||
if len(options.StaticKey) > 0 || options.StaticKeyPath != "" || options.KeyDirection != "" || options.Cipher != "" || options.Remote != "" || options.RemotePort != 0 || options.PeerAddress.Build(netip.Addr{}).IsValid() || options.PeerAddressIPv6.Build(netip.Addr{}).IsValid() {
|
||||
return ovpn.ServerOptions{}, E.New("static-key server options require `mode: static_key`")
|
||||
}
|
||||
tlsOptions, keyDirection, err := buildServerTLSOptions(*options.TLS)
|
||||
@@ -367,11 +367,11 @@ func buildStaticKeyServerOptions(options option.OpenVPNServerEndpointOptions, pr
|
||||
if err != nil {
|
||||
return ovpn.ServerOptions{}, err
|
||||
}
|
||||
vpnGateway := netip.Addr(options.PeerAddress)
|
||||
vpnGateway := options.PeerAddress.Build(netip.Addr{})
|
||||
if vpnGateway.IsValid() && !vpnGateway.Is4() {
|
||||
return ovpn.ServerOptions{}, E.New("`peer_address` must be an IPv4 address")
|
||||
}
|
||||
vpnGatewayIPv6 := netip.Addr(options.PeerAddressIPv6)
|
||||
vpnGatewayIPv6 := options.PeerAddressIPv6.Build(netip.Addr{})
|
||||
if vpnGatewayIPv6.IsValid() && !vpnGatewayIPv6.Is6() {
|
||||
return ovpn.ServerOptions{}, E.New("`peer_address_ipv6` must be an IPv6 address")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
)
|
||||
@@ -103,6 +104,7 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata
|
||||
func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
|
||||
user, loaded := auth.UserFromContext[string](ctx)
|
||||
if !loaded {
|
||||
if !metadata.Destination.IsValid() {
|
||||
|
||||
@@ -129,7 +129,7 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
|
||||
}
|
||||
myResolvers = append(myResolvers, myResolver)
|
||||
}
|
||||
routes[domain.WithTrailingDot()] = myResolvers
|
||||
routes[mDNS.CanonicalName(domain.WithTrailingDot())] = myResolvers
|
||||
}
|
||||
hosts := make(map[string][]netip.Addr)
|
||||
for domain, addresses := range dnsConfig.Hosts {
|
||||
@@ -275,14 +275,25 @@ func (t *DNSTransport) PreferredDomain(domain string) bool {
|
||||
if t.acceptSearchDomain && len(searchDomains) > 0 && mDNS.CountLabel(domain) == 1 {
|
||||
return true
|
||||
}
|
||||
canonicalDomain := mDNS.CanonicalName(domain)
|
||||
for suffix := range routes {
|
||||
if mDNS.IsSubDomain(suffix, domain) {
|
||||
if matchDomainSuffix(canonicalDomain, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchDomainSuffix(domain string, suffix string) bool {
|
||||
if suffix == "." || suffix == "" {
|
||||
return true
|
||||
}
|
||||
if !strings.HasSuffix(domain, suffix) {
|
||||
return false
|
||||
}
|
||||
return len(domain) == len(suffix) || domain[len(domain)-len(suffix)-1] == '.'
|
||||
}
|
||||
|
||||
func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
done := make(chan struct{})
|
||||
var (
|
||||
@@ -375,8 +386,9 @@ func (t *DNSTransport) exchangeOnce(ctx context.Context, message *mDNS.Msg, allo
|
||||
}
|
||||
return
|
||||
}
|
||||
canonicalName := mDNS.CanonicalName(question.Name)
|
||||
for domainSuffix, transports := range routes {
|
||||
if mDNS.IsSubDomain(domainSuffix, question.Name) {
|
||||
if matchDomainSuffix(canonicalName, domainSuffix) {
|
||||
if len(transports) == 0 {
|
||||
callback(&mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
|
||||
@@ -953,7 +953,7 @@ func (t *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain stri
|
||||
}
|
||||
}
|
||||
for _, suffix := range t.routeSuffixes.Load() {
|
||||
if mDNS.IsSubDomain(suffix, domain) {
|
||||
if matchDomainSuffix(domain, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1000,7 +1000,7 @@ func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCf
|
||||
}
|
||||
routeSuffixes := make([]string, 0, len(dnsCfg.Routes))
|
||||
for fqdn := range dnsCfg.Routes {
|
||||
routeSuffixes = append(routeSuffixes, fqdn.WithoutTrailingDot())
|
||||
routeSuffixes = append(routeSuffixes, strings.ToLower(fqdn.WithoutTrailingDot()))
|
||||
}
|
||||
t.routeDomains.Store(routeDomains)
|
||||
t.routeSuffixes.Store(routeSuffixes)
|
||||
|
||||
+1
-1
@@ -291,7 +291,7 @@ func (r *NetworkManager) InterfaceFinder() control.InterfaceFinder {
|
||||
}
|
||||
|
||||
func (r *NetworkManager) UpdateInterfaces() error {
|
||||
defer r.updateNetworkEnvironment()
|
||||
defer r.postUpdateNetworkEnvironment()
|
||||
if r.platformInterface == nil || !r.platformInterface.UsePlatformNetworkInterfaces() {
|
||||
return r.interfaceFinder.Update()
|
||||
} else {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func systemGateways(interfaceIndex int) []netip.Addr {
|
||||
rib, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0)
|
||||
rib, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBType(unix.NET_RT_FLAGS), unix.RTF_GATEWAY)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
+13
-2
@@ -404,9 +404,20 @@ func (r *Router) PreMatch(metadata adapter.InboundContext, firstPacket []byte) a
|
||||
}
|
||||
return adapter.PreMatchResult{Action: adapter.PreMatchBypass}
|
||||
}
|
||||
return r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound)
|
||||
if metadata.Destination.IsDomain() || metadata.Destination != packetDestination {
|
||||
return r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound)
|
||||
}
|
||||
result := r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound)
|
||||
if result.Action != adapter.PreMatchFlow {
|
||||
return adapter.PreMatchResult{Action: adapter.PreMatchBypass}
|
||||
}
|
||||
result.Action = adapter.PreMatchBypass
|
||||
return result
|
||||
case *R.RuleActionReject:
|
||||
rejectErr := action.Error(r.ctx)
|
||||
if rejectErr == nil && metadata.Network == N.NetworkICMP {
|
||||
return continueResult
|
||||
}
|
||||
if errors.Is(rejectErr, R.ErrDrop) {
|
||||
return adapter.PreMatchResult{Action: adapter.PreMatchDrop}
|
||||
}
|
||||
@@ -524,9 +535,9 @@ func (r *Router) preMatchFlow(ctx context.Context, metadata *adapter.InboundCont
|
||||
} else if metadata.Destination != packetDestination {
|
||||
result.Destination = metadata.Destination.AddrPort()
|
||||
}
|
||||
r.logger.InfoContext(ctx, "pre-match: forward ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString(), " via outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
metadataCopy := *metadata
|
||||
result.NewTracker = func() tun.FlowTracker {
|
||||
r.logger.InfoContext(ctx, "pre-match: forward ", metadataCopy.Network, " connection from ", metadataCopy.Source.AddrString(), " to ", metadataCopy.Destination.AddrString(), " via outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
flowTrackers := make([]tun.FlowTracker, 0, len(r.trackers)+1)
|
||||
flowTrackers = append(flowTrackers, newFlowLogger(ctx, r.logger, metadataCopy, outbound))
|
||||
for _, tracker := range r.trackers {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package rule
|
||||
|
||||
import "github.com/sagernet/sing-box/adapter"
|
||||
|
||||
type ruleMatchState uint8
|
||||
|
||||
const (
|
||||
@@ -24,3 +26,23 @@ func (g ruleGroupMatch) mergeWith(other ruleGroupMatch) ruleGroupMatch {
|
||||
satisfied: g.satisfied | other.satisfied,
|
||||
}
|
||||
}
|
||||
|
||||
type ruleMatchSnapshot struct {
|
||||
ipCidrMatchSource bool
|
||||
ipCidrAcceptEmpty bool
|
||||
deferredIPCIDRMatchGroups uint8
|
||||
}
|
||||
|
||||
func snapshotRuleMatch(metadata *adapter.InboundContext) ruleMatchSnapshot {
|
||||
return ruleMatchSnapshot{
|
||||
ipCidrMatchSource: metadata.IPCIDRMatchSource,
|
||||
ipCidrAcceptEmpty: metadata.IPCIDRAcceptEmpty,
|
||||
deferredIPCIDRMatchGroups: metadata.DeferredIPCIDRMatchGroups,
|
||||
}
|
||||
}
|
||||
|
||||
func (s ruleMatchSnapshot) restore(metadata *adapter.InboundContext) {
|
||||
metadata.IPCIDRMatchSource = s.ipCidrMatchSource
|
||||
metadata.IPCIDRAcceptEmpty = s.ipCidrAcceptEmpty
|
||||
metadata.DeferredIPCIDRMatchGroups = s.deferredIPCIDRMatchGroups
|
||||
}
|
||||
|
||||
@@ -206,32 +206,32 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
|
||||
matched bool
|
||||
deferredGroups uint8
|
||||
)
|
||||
snapshot := snapshotRuleMatch(metadata)
|
||||
if r.mode == C.LogicalTypeAnd {
|
||||
matched = true
|
||||
for _, rule := range r.rules {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleCache()
|
||||
if !rule.Match(&nestedMetadata) {
|
||||
metadata.ResetRuleCache()
|
||||
if !rule.Match(metadata) {
|
||||
matched = false
|
||||
deferredGroups = 0
|
||||
break
|
||||
}
|
||||
deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups
|
||||
deferredGroups |= metadata.DeferredIPCIDRMatchGroups
|
||||
}
|
||||
} else {
|
||||
for _, rule := range r.rules {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleCache()
|
||||
if rule.Match(&nestedMetadata) {
|
||||
metadata.ResetRuleCache()
|
||||
if rule.Match(metadata) {
|
||||
matched = true
|
||||
if nestedMetadata.DeferredIPCIDRMatchGroups == 0 {
|
||||
if metadata.DeferredIPCIDRMatchGroups == 0 {
|
||||
deferredGroups = 0
|
||||
break
|
||||
}
|
||||
deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups
|
||||
deferredGroups |= metadata.DeferredIPCIDRMatchGroups
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot.restore(metadata)
|
||||
if matched {
|
||||
metadata.DeferredIPCIDRMatchGroups |= deferredGroups
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ func NewDefaultInterfaceAddressItem(networkManager adapter.NetworkManager, inter
|
||||
}
|
||||
|
||||
func (r *DefaultInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.interfaceMonitor == nil {
|
||||
return false
|
||||
}
|
||||
defaultInterface := r.interfaceMonitor.DefaultInterface()
|
||||
if defaultInterface == nil {
|
||||
return false
|
||||
|
||||
@@ -5,15 +5,16 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/experimental/clashmode"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*ClashModeItem)(nil)
|
||||
|
||||
type ClashModeItem struct {
|
||||
ctx context.Context
|
||||
clashServer adapter.ClashServer
|
||||
mode string
|
||||
ctx context.Context
|
||||
clashMode *clashmode.Manager
|
||||
mode string
|
||||
}
|
||||
|
||||
func NewClashModeItem(ctx context.Context, mode string) *ClashModeItem {
|
||||
@@ -24,15 +25,15 @@ func NewClashModeItem(ctx context.Context, mode string) *ClashModeItem {
|
||||
}
|
||||
|
||||
func (r *ClashModeItem) Start() error {
|
||||
r.clashServer = service.FromContext[adapter.ClashServer](r.ctx)
|
||||
r.clashMode = service.PtrFromContext[clashmode.Manager](r.ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ClashModeItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.clashServer == nil {
|
||||
if r.clashMode == nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(r.clashServer.Mode(), r.mode)
|
||||
return strings.EqualFold(r.clashMode.Mode(), r.mode)
|
||||
}
|
||||
|
||||
func (r *ClashModeItem) String() string {
|
||||
|
||||
@@ -52,12 +52,15 @@ func (r *RuleSetItem) Close() error {
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
|
||||
snapshot := snapshotRuleMatch(metadata)
|
||||
for _, ruleSet := range r.setList {
|
||||
nestedMetadata := r.nestedMetadata(metadata)
|
||||
if ruleSet.Match(&nestedMetadata) {
|
||||
r.prepareNestedMatch(metadata)
|
||||
if ruleSet.Match(metadata) {
|
||||
snapshot.restore(metadata)
|
||||
return true
|
||||
}
|
||||
}
|
||||
snapshot.restore(metadata)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -67,17 +70,19 @@ func (r *RuleSetItem) matchWithOuterGroups(metadata *adapter.InboundContext, out
|
||||
matched bool
|
||||
deferredGroups uint8
|
||||
)
|
||||
snapshot := snapshotRuleMatch(metadata)
|
||||
for _, ruleSet := range r.setList {
|
||||
nestedMetadata := r.nestedMetadata(metadata)
|
||||
r.prepareNestedMatch(metadata)
|
||||
if provider, isProvider := ruleSet.(mergeableRuleProvider); isProvider {
|
||||
branch := provider.mergeableRule()
|
||||
if branch != nil {
|
||||
branchGroups, branchMatched := branch.evaluateForMerge(&nestedMetadata)
|
||||
branchGroups, branchMatched := branch.evaluateForMerge(metadata)
|
||||
if branchMatched {
|
||||
merged := outerGroups.mergeWith(branchGroups)
|
||||
if merged.done() {
|
||||
branchDeferredGroups := nestedMetadata.DeferredIPCIDRMatchGroups &^ uint8(merged.satisfied)
|
||||
branchDeferredGroups := metadata.DeferredIPCIDRMatchGroups &^ uint8(merged.satisfied)
|
||||
if branchDeferredGroups == 0 {
|
||||
snapshot.restore(metadata)
|
||||
metadata.DeferredIPCIDRMatchGroups &^= uint8(merged.satisfied)
|
||||
return true
|
||||
}
|
||||
@@ -88,26 +93,26 @@ func (r *RuleSetItem) matchWithOuterGroups(metadata *adapter.InboundContext, out
|
||||
continue
|
||||
}
|
||||
}
|
||||
if outerDone && ruleSet.Match(&nestedMetadata) {
|
||||
if nestedMetadata.DeferredIPCIDRMatchGroups == 0 {
|
||||
if outerDone && ruleSet.Match(metadata) {
|
||||
if metadata.DeferredIPCIDRMatchGroups == 0 {
|
||||
snapshot.restore(metadata)
|
||||
return true
|
||||
}
|
||||
matched = true
|
||||
deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups
|
||||
deferredGroups |= metadata.DeferredIPCIDRMatchGroups
|
||||
}
|
||||
}
|
||||
snapshot.restore(metadata)
|
||||
if matched {
|
||||
metadata.DeferredIPCIDRMatchGroups |= deferredGroups
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) nestedMetadata(metadata *adapter.InboundContext) adapter.InboundContext {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleMatchCache()
|
||||
nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource
|
||||
nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
|
||||
return nestedMetadata
|
||||
func (r *RuleSetItem) prepareNestedMatch(metadata *adapter.InboundContext) {
|
||||
metadata.ResetRuleMatchCache()
|
||||
metadata.IPCIDRMatchSource = r.ipCidrMatchSource
|
||||
metadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
|
||||
}
|
||||
|
||||
type mergeableRuleProvider interface {
|
||||
@@ -130,17 +135,19 @@ func matchAnyHeadlessRule(rules []adapter.HeadlessRule, metadata *adapter.Inboun
|
||||
matched bool
|
||||
deferredGroups uint8
|
||||
)
|
||||
snapshot := snapshotRuleMatch(metadata)
|
||||
for _, rule := range rules {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleMatchCache()
|
||||
if rule.Match(&nestedMetadata) {
|
||||
if nestedMetadata.DeferredIPCIDRMatchGroups == 0 {
|
||||
metadata.ResetRuleMatchCache()
|
||||
if rule.Match(metadata) {
|
||||
if metadata.DeferredIPCIDRMatchGroups == 0 {
|
||||
snapshot.restore(metadata)
|
||||
return true
|
||||
}
|
||||
matched = true
|
||||
deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups
|
||||
deferredGroups |= metadata.DeferredIPCIDRMatchGroups
|
||||
}
|
||||
}
|
||||
snapshot.restore(metadata)
|
||||
if matched {
|
||||
metadata.DeferredIPCIDRMatchGroups |= deferredGroups
|
||||
}
|
||||
|
||||
@@ -40,7 +40,11 @@ func NewNetworkInterfaceAddressItem(networkManager adapter.NetworkManager, inter
|
||||
|
||||
func (r *NetworkInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool {
|
||||
interfaces := r.networkManager.NetworkInterfaces()
|
||||
myInterfaces := r.networkManager.InterfaceMonitor().MyInterfaces()
|
||||
var myInterfaces []string
|
||||
interfaceMonitor := r.networkManager.InterfaceMonitor()
|
||||
if interfaceMonitor != nil {
|
||||
myInterfaces = interfaceMonitor.MyInterfaces()
|
||||
}
|
||||
match:
|
||||
for ifType, addresses := range r.interfaceAddresses {
|
||||
for _, networkInterface := range interfaces {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//go:build !windows
|
||||
|
||||
package oomkiller
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func lockDraft(path string) (*os.File, error) {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func unlockDraft(file *os.File) {
|
||||
syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
|
||||
file.Close()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package oomkiller
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func lockDraft(path string) (*os.File, error) {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = windows.LockFileEx(windows.Handle(file.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK|windows.LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &windows.Overlapped{})
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func unlockDraft(file *os.File) {
|
||||
windows.UnlockFileEx(windows.Handle(file.Fd()), 0, 1, 0, &windows.Overlapped{})
|
||||
file.Close()
|
||||
}
|
||||
@@ -10,7 +10,10 @@ import (
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
const DefaultAppleNetworkExtensionMemoryLimit = 50 * 1024 * 1024
|
||||
const (
|
||||
DefaultAppleNetworkExtensionMemoryLimit = 50 * 1024 * 1024
|
||||
DefaultAppleNetworkExtensionGCPercent = 50
|
||||
)
|
||||
|
||||
type policyMode uint8
|
||||
|
||||
@@ -25,6 +28,19 @@ func (m policyMode) hasTimerMode() bool {
|
||||
return m != policyModeNone
|
||||
}
|
||||
|
||||
func (m policyMode) String() string {
|
||||
switch m {
|
||||
case policyModeMemoryLimit:
|
||||
return "memory_limit"
|
||||
case policyModeAvailable:
|
||||
return "available"
|
||||
case policyModeNetworkExtension:
|
||||
return "network_extension"
|
||||
default:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
|
||||
func resolvePolicyMode(ctx context.Context, options option.OOMKillerServiceOptions) (uint64, policyMode) {
|
||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||
if C.IsIos && platformInterface != nil && platformInterface.UnderNetworkExtension() {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package oomkiller
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func PromoteDraft(basePath string, accept func(metadataContent []byte) bool) {
|
||||
draftPath := filepath.Join(basePath, DraftDirectoryName)
|
||||
info, err := os.Stat(draftPath)
|
||||
if err != nil || !info.IsDir() {
|
||||
return
|
||||
}
|
||||
lockPath := filepath.Join(draftPath, lockFileName)
|
||||
lock, err := lockDraft(lockPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
os.Remove(lockPath)
|
||||
unlockDraft(lock)
|
||||
if !draftNotable(draftPath) {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
}
|
||||
if accept != nil {
|
||||
metadataContent, readErr := os.ReadFile(filepath.Join(draftPath, metadataFileName))
|
||||
if readErr != nil || !accept(metadataContent) {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
promoteDirectory(draftPath, filepath.Join(basePath, ReportsDirectoryName))
|
||||
}
|
||||
|
||||
func draftNotable(draftPath string) bool {
|
||||
file, err := os.Open(filepath.Join(draftPath, eventsFileName))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Buffer(nil, 1<<20)
|
||||
for scanner.Scan() {
|
||||
var event struct {
|
||||
Type string `json:"t"`
|
||||
}
|
||||
err = json.Unmarshal(scanner.Bytes(), &event)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
switch event.Type {
|
||||
case eventTypePressure, eventTypeReset, eventTypeSnapshot:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func promoteDirectory(draftPath string, reportsPath string) {
|
||||
info, err := os.Stat(draftPath)
|
||||
if err != nil || !info.IsDir() {
|
||||
return
|
||||
}
|
||||
err = os.MkdirAll(reportsPath, 0o777)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
destPath, err := nextAvailableReportPath(reportsPath, info.ModTime().UTC())
|
||||
if err != nil {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
}
|
||||
err = os.Rename(draftPath, destPath)
|
||||
if err != nil {
|
||||
os.RemoveAll(draftPath)
|
||||
}
|
||||
}
|
||||
|
||||
func nextAvailableReportPath(reportsDir string, timestamp time.Time) (string, error) {
|
||||
destName := timestamp.Format("2006-01-02T15-04-05")
|
||||
destPath := filepath.Join(reportsDir, destName)
|
||||
_, err := os.Stat(destPath)
|
||||
if os.IsNotExist(err) {
|
||||
return destPath, nil
|
||||
}
|
||||
for i := 1; i <= 1000; i++ {
|
||||
suffixedPath := filepath.Join(reportsDir, destName+"-"+strconv.Itoa(i))
|
||||
_, err = os.Stat(suffixedPath)
|
||||
if os.IsNotExist(err) {
|
||||
return suffixedPath, nil
|
||||
}
|
||||
}
|
||||
return "", E.New("no available report path for ", destName)
|
||||
}
|
||||
|
||||
func copyDirectory(sourcePath string, destPath string) error {
|
||||
entries, err := os.ReadDir(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(destPath, 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
err = copyFile(filepath.Join(sourcePath, entry.Name()), filepath.Join(destPath, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(sourcePath string, destPath string) error {
|
||||
source, err := os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
dest, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(dest, source)
|
||||
if err != nil {
|
||||
dest.Close()
|
||||
return err
|
||||
}
|
||||
return dest.Close()
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
package oomkiller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/metrics"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
)
|
||||
|
||||
const (
|
||||
DraftDirectoryName = "oom_draft"
|
||||
ReportsDirectoryName = "oom_reports"
|
||||
|
||||
timelineFileName = "timeline.jsonl"
|
||||
eventsFileName = "events.jsonl"
|
||||
metadataFileName = "metadata.json"
|
||||
logFileName = "go.log"
|
||||
lockFileName = ".lock"
|
||||
|
||||
normalSampleInterval = time.Minute
|
||||
pressureSampleInterval = time.Second
|
||||
denseSampleWindow = 2 * time.Minute
|
||||
|
||||
pressureSnapshotMinInterval = time.Hour
|
||||
resetSnapshotMinInterval = 10 * time.Minute
|
||||
maxAutomaticSnapshots = 8
|
||||
)
|
||||
|
||||
const (
|
||||
SnapshotReasonPressure = "pressure"
|
||||
SnapshotReasonReset = "reset"
|
||||
SnapshotReasonManual = "manual"
|
||||
|
||||
resetReasonThreshold = "threshold"
|
||||
resetReasonRate = "rate"
|
||||
|
||||
eventTypeStart = "start"
|
||||
eventTypeStop = "stop"
|
||||
eventTypePressure = "pressure"
|
||||
eventTypeState = "state"
|
||||
eventTypeReset = "reset"
|
||||
eventTypeSnapshot = "snapshot"
|
||||
eventTypeReport = "report"
|
||||
)
|
||||
|
||||
type RecorderOptions struct {
|
||||
BasePath string
|
||||
Logger logger.Logger
|
||||
AcceptDraft func(metadataContent []byte) bool
|
||||
MetadataCallback func(status ReportStatus) any
|
||||
OwnerCallback func(path string)
|
||||
LogCallback func() []byte
|
||||
SnapshotCallback func(directory string, prefix string)
|
||||
}
|
||||
|
||||
type ReportStatus struct {
|
||||
StartedAt time.Time
|
||||
RecordedAt time.Time
|
||||
EndedAt time.Time
|
||||
MemoryLimit uint64
|
||||
PeakMemory uint64
|
||||
MinAvailable uint64
|
||||
AvailableKnown bool
|
||||
Snapshots int
|
||||
}
|
||||
|
||||
type Recorder struct {
|
||||
basePath string
|
||||
draftPath string
|
||||
logger logger.Logger
|
||||
acceptDraft func(metadataContent []byte) bool
|
||||
metadataCallback func(status ReportStatus) any
|
||||
ownerCallback func(path string)
|
||||
logCallback func() []byte
|
||||
snapshotCallback func(directory string, prefix string)
|
||||
|
||||
access sync.Mutex
|
||||
started bool
|
||||
closed bool
|
||||
draftCreated bool
|
||||
draftLock *os.File
|
||||
notable bool
|
||||
status ReportStatus
|
||||
lastRowAt time.Time
|
||||
lastRowState pressureState
|
||||
hasRow bool
|
||||
denseUntil time.Time
|
||||
previousGCCycles uint64
|
||||
metricsSamples []metrics.Sample
|
||||
|
||||
snapshotAccess sync.Mutex
|
||||
snapshotSequence int
|
||||
automaticSnapshots int
|
||||
lastSnapshotAt map[string]time.Time
|
||||
}
|
||||
|
||||
type timelineRow struct {
|
||||
At string `json:"at"`
|
||||
State string `json:"state"`
|
||||
MemoryBytes uint64 `json:"memoryBytes"`
|
||||
AvailableBytes uint64 `json:"availableBytes,omitempty"`
|
||||
GoMemoryBytes uint64 `json:"goMemoryBytes,omitempty"`
|
||||
GoHeapLiveBytes uint64 `json:"goHeapLiveBytes,omitempty"`
|
||||
GoStackBytes uint64 `json:"goStackBytes,omitempty"`
|
||||
Goroutines uint64 `json:"goroutines,omitempty"`
|
||||
GCCycles uint64 `json:"gcCycles,omitempty"`
|
||||
Connections int `json:"connections,omitempty"`
|
||||
}
|
||||
|
||||
type eventRecord struct {
|
||||
Type string `json:"t"`
|
||||
At string `json:"at"`
|
||||
Policy string `json:"policy,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
MemoryBytes uint64 `json:"memoryBytes,omitempty"`
|
||||
AvailableBytes uint64 `json:"availableBytes,omitempty"`
|
||||
MemoryAfterBytes uint64 `json:"memoryAfterBytes,omitempty"`
|
||||
MemoryLimit uint64 `json:"memoryLimit,omitempty"`
|
||||
TriggerBytes uint64 `json:"triggerBytes,omitempty"`
|
||||
ArmedBytes uint64 `json:"armedBytes,omitempty"`
|
||||
ResumeBytes uint64 `json:"resumeBytes,omitempty"`
|
||||
ReportOnly bool `json:"reportOnly,omitempty"`
|
||||
Connections int `json:"connections,omitempty"`
|
||||
Sequence int `json:"seq,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
Runtime *runtimeStats `json:"runtime,omitempty"`
|
||||
}
|
||||
|
||||
type runtimeStats struct {
|
||||
HeapAlloc uint64 `json:"heapAlloc"`
|
||||
HeapObjects uint64 `json:"heapObjects"`
|
||||
HeapInuse uint64 `json:"heapInuse"`
|
||||
HeapIdle uint64 `json:"heapIdle"`
|
||||
HeapReleased uint64 `json:"heapReleased"`
|
||||
HeapSys uint64 `json:"heapSys"`
|
||||
StackInuse uint64 `json:"stackInuse"`
|
||||
StackSys uint64 `json:"stackSys"`
|
||||
Sys uint64 `json:"sys"`
|
||||
TotalAlloc uint64 `json:"totalAlloc"`
|
||||
NumGC uint32 `json:"numGC"`
|
||||
NumGoroutine int `json:"numGoroutine"`
|
||||
NextGC uint64 `json:"nextGC"`
|
||||
LastGC string `json:"lastGC,omitempty"`
|
||||
}
|
||||
|
||||
func NewRecorder(options RecorderOptions) *Recorder {
|
||||
recorderLogger := options.Logger
|
||||
if recorderLogger == nil {
|
||||
recorderLogger = logger.NOP()
|
||||
}
|
||||
return &Recorder{
|
||||
basePath: options.BasePath,
|
||||
draftPath: filepath.Join(options.BasePath, DraftDirectoryName),
|
||||
logger: recorderLogger,
|
||||
acceptDraft: options.AcceptDraft,
|
||||
metadataCallback: options.MetadataCallback,
|
||||
ownerCallback: options.OwnerCallback,
|
||||
logCallback: options.LogCallback,
|
||||
snapshotCallback: options.SnapshotCallback,
|
||||
metricsSamples: []metrics.Sample{
|
||||
{Name: "/memory/classes/total:bytes"},
|
||||
{Name: "/gc/heap/live:bytes"},
|
||||
{Name: "/memory/classes/heap/stacks:bytes"},
|
||||
{Name: "/sched/goroutines:goroutines"},
|
||||
{Name: "/gc/cycles/total:gc-cycles"},
|
||||
},
|
||||
lastSnapshotAt: make(map[string]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) Start() {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
if r.started {
|
||||
return
|
||||
}
|
||||
PromoteDraft(r.basePath, r.acceptDraft)
|
||||
r.started = true
|
||||
r.status.StartedAt = time.Now()
|
||||
}
|
||||
|
||||
func (r *Recorder) Close() error {
|
||||
r.snapshotAccess.Lock()
|
||||
defer r.snapshotAccess.Unlock()
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
if !r.started || r.closed {
|
||||
return nil
|
||||
}
|
||||
r.closed = true
|
||||
if !r.draftCreated {
|
||||
return nil
|
||||
}
|
||||
if !r.notable {
|
||||
r.releaseDraftLocked()
|
||||
return os.RemoveAll(r.draftPath)
|
||||
}
|
||||
r.status.EndedAt = time.Now()
|
||||
r.writeLogLocked()
|
||||
r.writeMetadataLocked()
|
||||
r.releaseDraftLocked()
|
||||
promoteDirectory(r.draftPath, filepath.Join(r.basePath, ReportsDirectoryName))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Recorder) releaseDraftLocked() {
|
||||
if r.draftLock == nil {
|
||||
return
|
||||
}
|
||||
os.Remove(r.draftLock.Name())
|
||||
unlockDraft(r.draftLock)
|
||||
r.draftLock = nil
|
||||
}
|
||||
|
||||
func (r *Recorder) WriteReport() error {
|
||||
sample := memorySample{usage: memory.Total()}
|
||||
if memory.AvailableAvailable() {
|
||||
sample.availableKnown = true
|
||||
sample.available = memory.Available()
|
||||
}
|
||||
err := r.snapshot(SnapshotReasonManual, sample, true)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write snapshot")
|
||||
}
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
reportsDir := filepath.Join(r.basePath, ReportsDirectoryName)
|
||||
err = os.MkdirAll(reportsDir, 0o777)
|
||||
if err != nil {
|
||||
return E.Cause(err, "create reports directory")
|
||||
}
|
||||
r.chown(reportsDir)
|
||||
destPath, err := nextAvailableReportPath(reportsDir, time.Now().UTC())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.appendEventLocked(eventRecord{Type: eventTypeReport, MemoryBytes: sample.usage, AvailableBytes: sample.available})
|
||||
err = copyDirectory(r.draftPath, destPath)
|
||||
if err != nil {
|
||||
os.RemoveAll(destPath)
|
||||
return E.Cause(err, "copy draft to ", destPath)
|
||||
}
|
||||
r.chownTree(destPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Recorder) instanceStarted(config timerConfig, thresholds pressureThresholds) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
r.status.MemoryLimit = config.memoryLimit
|
||||
r.appendEventLocked(eventRecord{
|
||||
Type: eventTypeStart,
|
||||
Policy: config.policyMode.String(),
|
||||
MemoryLimit: config.memoryLimit,
|
||||
TriggerBytes: thresholds.trigger,
|
||||
ArmedBytes: thresholds.armed,
|
||||
ResumeBytes: thresholds.resume,
|
||||
ReportOnly: config.killerDisabled,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Recorder) instanceStopped() {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
r.appendEventLocked(eventRecord{Type: eventTypeStop})
|
||||
}
|
||||
|
||||
func (r *Recorder) sample(sample memorySample, state pressureState, connections int) {
|
||||
now := time.Now()
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
r.observeLocked(sample)
|
||||
if r.hasRow && state == r.lastRowState {
|
||||
interval := normalSampleInterval
|
||||
if now.Before(r.denseUntil) {
|
||||
interval = pressureSampleInterval
|
||||
}
|
||||
if now.Sub(r.lastRowAt) < interval {
|
||||
return
|
||||
}
|
||||
}
|
||||
err := r.ensureDraftLocked()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
metrics.Read(r.metricsSamples)
|
||||
gcCycles := r.metricsSamples[4].Value.Uint64()
|
||||
row := timelineRow{
|
||||
At: now.UTC().Format(time.RFC3339),
|
||||
State: state.String(),
|
||||
MemoryBytes: sample.usage,
|
||||
AvailableBytes: sample.available,
|
||||
GoMemoryBytes: r.metricsSamples[0].Value.Uint64(),
|
||||
GoHeapLiveBytes: r.metricsSamples[1].Value.Uint64(),
|
||||
GoStackBytes: r.metricsSamples[2].Value.Uint64(),
|
||||
Goroutines: r.metricsSamples[3].Value.Uint64(),
|
||||
Connections: connections,
|
||||
}
|
||||
if r.hasRow {
|
||||
row.GCCycles = gcCycles - r.previousGCCycles
|
||||
}
|
||||
r.previousGCCycles = gcCycles
|
||||
r.hasRow = true
|
||||
r.lastRowAt = now
|
||||
r.lastRowState = state
|
||||
timelinePath := filepath.Join(r.draftPath, timelineFileName)
|
||||
err = appendRecord(timelinePath, row)
|
||||
if err != nil {
|
||||
r.logger.Error(E.Cause(err, "OOM report: write timeline"))
|
||||
return
|
||||
}
|
||||
r.chown(timelinePath)
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func (r *Recorder) recordPressure(sample memorySample) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
r.notable = true
|
||||
r.observeLocked(sample)
|
||||
r.denseUntil = time.Now().Add(denseSampleWindow)
|
||||
r.appendEventLocked(eventRecord{Type: eventTypePressure, MemoryBytes: sample.usage, AvailableBytes: sample.available})
|
||||
}
|
||||
|
||||
func (r *Recorder) recordStateChange(state pressureState, sample memorySample) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
r.observeLocked(sample)
|
||||
r.denseUntil = time.Now().Add(denseSampleWindow)
|
||||
r.appendEventLocked(eventRecord{Type: eventTypeState, State: state.String(), MemoryBytes: sample.usage, AvailableBytes: sample.available})
|
||||
}
|
||||
|
||||
func (r *Recorder) recordReset(reason string, before memorySample, after memorySample, connections int, reportOnly bool) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
r.notable = true
|
||||
r.observeLocked(before)
|
||||
r.denseUntil = time.Now().Add(denseSampleWindow)
|
||||
r.appendEventLocked(eventRecord{
|
||||
Type: eventTypeReset,
|
||||
Reason: reason,
|
||||
MemoryBytes: before.usage,
|
||||
AvailableBytes: before.available,
|
||||
MemoryAfterBytes: after.usage,
|
||||
Connections: connections,
|
||||
ReportOnly: reportOnly,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Recorder) snapshot(reason string, sample memorySample, force bool) error {
|
||||
r.snapshotAccess.Lock()
|
||||
defer r.snapshotAccess.Unlock()
|
||||
now := time.Now()
|
||||
if !force {
|
||||
if r.automaticSnapshots >= maxAutomaticSnapshots {
|
||||
return nil
|
||||
}
|
||||
var minInterval time.Duration
|
||||
switch reason {
|
||||
case SnapshotReasonPressure:
|
||||
minInterval = pressureSnapshotMinInterval
|
||||
case SnapshotReasonReset:
|
||||
minInterval = resetSnapshotMinInterval
|
||||
}
|
||||
lastAt, found := r.lastSnapshotAt[reason]
|
||||
if found && now.Sub(lastAt) < minInterval {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
r.access.Lock()
|
||||
if r.closed {
|
||||
r.access.Unlock()
|
||||
return E.New("OOM recorder closed")
|
||||
}
|
||||
err := r.ensureDraftLocked()
|
||||
r.access.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.snapshotSequence++
|
||||
sequence := r.snapshotSequence
|
||||
if !force {
|
||||
r.automaticSnapshots++
|
||||
}
|
||||
r.lastSnapshotAt[reason] = now
|
||||
prefix := "snapshot-" + strconv.Itoa(sequence) + "-" + reason
|
||||
if r.snapshotCallback != nil {
|
||||
r.snapshotCallback(r.draftPath, prefix)
|
||||
}
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
stats := &runtimeStats{
|
||||
HeapAlloc: memStats.HeapAlloc,
|
||||
HeapObjects: memStats.HeapObjects,
|
||||
HeapInuse: memStats.HeapInuse,
|
||||
HeapIdle: memStats.HeapIdle,
|
||||
HeapReleased: memStats.HeapReleased,
|
||||
HeapSys: memStats.HeapSys,
|
||||
StackInuse: memStats.StackInuse,
|
||||
StackSys: memStats.StackSys,
|
||||
Sys: memStats.Sys,
|
||||
TotalAlloc: memStats.TotalAlloc,
|
||||
NumGC: memStats.NumGC,
|
||||
NumGoroutine: runtime.NumGoroutine(),
|
||||
NextGC: memStats.NextGC,
|
||||
}
|
||||
if memStats.LastGC > 0 {
|
||||
stats.LastGC = time.Unix(0, int64(memStats.LastGC)).UTC().Format(time.RFC3339)
|
||||
}
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
r.notable = true
|
||||
r.status.Snapshots = sequence
|
||||
r.observeLocked(sample)
|
||||
r.writeLogLocked()
|
||||
r.appendEventLocked(eventRecord{
|
||||
Type: eventTypeSnapshot,
|
||||
Reason: reason,
|
||||
Sequence: sequence,
|
||||
Prefix: prefix,
|
||||
MemoryBytes: sample.usage,
|
||||
AvailableBytes: sample.available,
|
||||
Runtime: stats,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Recorder) observeLocked(sample memorySample) {
|
||||
if sample.usage > r.status.PeakMemory {
|
||||
r.status.PeakMemory = sample.usage
|
||||
}
|
||||
if sample.availableKnown && (!r.status.AvailableKnown || sample.available < r.status.MinAvailable) {
|
||||
r.status.AvailableKnown = true
|
||||
r.status.MinAvailable = sample.available
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) ensureDraftLocked() error {
|
||||
if r.draftCreated {
|
||||
_, err := os.Stat(r.draftPath)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
r.logger.Error("OOM report: draft directory lost, recreating")
|
||||
r.releaseDraftLocked()
|
||||
r.draftCreated = false
|
||||
r.hasRow = false
|
||||
}
|
||||
if !r.started {
|
||||
return E.New("OOM recorder not started")
|
||||
}
|
||||
if r.closed {
|
||||
return E.New("OOM recorder closed")
|
||||
}
|
||||
err := os.MkdirAll(r.draftPath, 0o777)
|
||||
if err != nil {
|
||||
r.logger.Error(E.Cause(err, "OOM report: create draft directory"))
|
||||
return E.Cause(err, "create draft directory ", r.draftPath)
|
||||
}
|
||||
r.chown(r.draftPath)
|
||||
lockPath := filepath.Join(r.draftPath, lockFileName)
|
||||
r.draftLock, err = lockDraft(lockPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "lock draft directory ", r.draftPath)
|
||||
}
|
||||
r.chown(lockPath)
|
||||
r.draftCreated = true
|
||||
r.writeMetadataLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Recorder) appendEventLocked(event eventRecord) {
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
err := r.ensureDraftLocked()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
event.At = time.Now().UTC().Format(time.RFC3339)
|
||||
eventsPath := filepath.Join(r.draftPath, eventsFileName)
|
||||
err = appendRecord(eventsPath, event)
|
||||
if err != nil {
|
||||
r.logger.Error(E.Cause(err, "OOM report: write events"))
|
||||
} else {
|
||||
r.chown(eventsPath)
|
||||
}
|
||||
r.writeMetadataLocked()
|
||||
}
|
||||
|
||||
func (r *Recorder) writeMetadataLocked() {
|
||||
if r.metadataCallback == nil {
|
||||
return
|
||||
}
|
||||
r.status.RecordedAt = time.Now()
|
||||
content, err := json.Marshal(r.metadataCallback(r.status))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r.writeFile(filepath.Join(r.draftPath, metadataFileName), content)
|
||||
}
|
||||
|
||||
func (r *Recorder) writeLogLocked() {
|
||||
if r.logCallback == nil {
|
||||
return
|
||||
}
|
||||
content := r.logCallback()
|
||||
if len(content) == 0 {
|
||||
return
|
||||
}
|
||||
r.writeFile(filepath.Join(r.draftPath, logFileName), content)
|
||||
}
|
||||
|
||||
func (r *Recorder) writeFile(path string, content []byte) {
|
||||
err := os.WriteFile(path, content, 0o666)
|
||||
if err != nil {
|
||||
r.logger.Error(E.Cause(err, "OOM report: write ", filepath.Base(path)))
|
||||
return
|
||||
}
|
||||
r.chown(path)
|
||||
}
|
||||
|
||||
func (r *Recorder) chown(path string) {
|
||||
if r.ownerCallback != nil {
|
||||
r.ownerCallback(path)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) chownTree(directory string) {
|
||||
r.chown(directory)
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
r.chown(filepath.Join(directory, entry.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
func appendRecord(path string, record any) error {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return json.NewEncoder(file).Encode(record)
|
||||
}
|
||||
@@ -2,39 +2,29 @@ package oomkiller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
boxConstant "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/service"
|
||||
)
|
||||
|
||||
type OOMReporter interface {
|
||||
WriteReport(memoryUsage uint64) error
|
||||
WriteDraft(memoryUsage uint64) error
|
||||
DiscardDraft() error
|
||||
}
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
network adapter.NetworkManager
|
||||
timerConfig timerConfig
|
||||
adaptiveTimer *adaptiveTimer
|
||||
lastReportTime atomic.Int64
|
||||
//nolint:unused // touched only on darwin && cgo via writeOOMDraft/discardOOMDraft.
|
||||
lastDraftTime atomic.Int64
|
||||
//nolint:unused // touched only on darwin && cgo via writeOOMDraft/discardOOMDraft.
|
||||
draftCancelled atomic.Bool
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
network adapter.NetworkManager
|
||||
connections adapter.ConnectionManager
|
||||
recorder *Recorder
|
||||
timerConfig timerConfig
|
||||
adaptiveTimer *adaptiveTimer
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) {
|
||||
@@ -48,27 +38,29 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
network: service.FromContext[adapter.NetworkManager](ctx),
|
||||
connections: service.FromContext[adapter.ConnectionManager](ctx),
|
||||
recorder: service.FromContext[*Recorder](ctx),
|
||||
timerConfig: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) writeOOMReport(memoryUsage uint64) {
|
||||
now := time.Now().Unix()
|
||||
lastReport := s.lastReportTime.Load()
|
||||
if now-lastReport < 3600 {
|
||||
return
|
||||
func (s *Service) startTimer() error {
|
||||
if !s.timerConfig.policyMode.hasTimerMode() {
|
||||
return E.New("memory pressure monitoring is not available on this platform without memory_limit")
|
||||
}
|
||||
if !s.lastReportTime.CompareAndSwap(lastReport, now) {
|
||||
return
|
||||
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.network, s.connections, s.recorder, s.timerConfig)
|
||||
if s.recorder != nil {
|
||||
s.recorder.instanceStarted(s.timerConfig, s.adaptiveTimer.limitThresholds)
|
||||
}
|
||||
reporter := service.FromContext[OOMReporter](s.ctx)
|
||||
if reporter == nil {
|
||||
return
|
||||
s.adaptiveTimer.start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) stopTimer() {
|
||||
if s.adaptiveTimer != nil {
|
||||
s.adaptiveTimer.stop()
|
||||
}
|
||||
err := reporter.WriteReport(memoryUsage)
|
||||
if err != nil {
|
||||
s.logger.Warn("failed to write OOM report: ", err)
|
||||
} else {
|
||||
s.logger.Info("OOM report saved")
|
||||
if s.recorder != nil {
|
||||
s.recorder.instanceStopped()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,16 +34,11 @@ import "C"
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
const oomDraftMinInterval = time.Hour
|
||||
|
||||
var (
|
||||
globalAccess sync.Mutex
|
||||
globalServices []*Service
|
||||
@@ -53,8 +48,11 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
err := s.startTimer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.timerConfig.policyMode == policyModeNetworkExtension {
|
||||
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.network, s.timerConfig, nil)
|
||||
globalAccess.Lock()
|
||||
isFirst := len(globalServices) == 0
|
||||
globalServices = append(globalServices, s)
|
||||
@@ -62,20 +60,12 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if isFirst {
|
||||
C.startMemoryPressureMonitor()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !s.timerConfig.policyMode.hasTimerMode() {
|
||||
return E.New("memory pressure monitoring is not available on this platform without memory_limit")
|
||||
}
|
||||
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.network, s.timerConfig, s.writeOOMReport)
|
||||
s.adaptiveTimer.start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
if s.adaptiveTimer != nil {
|
||||
s.adaptiveTimer.stop()
|
||||
}
|
||||
s.stopTimer()
|
||||
if s.timerConfig.policyMode == policyModeNetworkExtension {
|
||||
globalAccess.Lock()
|
||||
for i, svc := range globalServices {
|
||||
@@ -89,7 +79,6 @@ func (s *Service) Close() error {
|
||||
if isLast {
|
||||
C.stopMemoryPressureMonitor()
|
||||
}
|
||||
s.discardOOMDraft()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -106,45 +95,12 @@ func goMemoryPressureCallback(status C.ulong) {
|
||||
sample := readMemorySample(policyModeNetworkExtension)
|
||||
for _, s := range services {
|
||||
s.logger.Warn("memory pressure: critical, usage: ", byteformats.FormatMemoryBytes(sample.usage))
|
||||
s.writeOOMDraft(sample.usage)
|
||||
if s.recorder != nil {
|
||||
s.recorder.recordPressure(sample)
|
||||
}
|
||||
s.adaptiveTimer.notifyPressure()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) writeOOMDraft(memoryUsage uint64) {
|
||||
if s.draftCancelled.Load() {
|
||||
return
|
||||
}
|
||||
now := time.Now().UnixNano()
|
||||
lastDraft := s.lastDraftTime.Load()
|
||||
if time.Duration(now-lastDraft) < oomDraftMinInterval {
|
||||
return
|
||||
}
|
||||
s.lastDraftTime.Store(now)
|
||||
reporter := service.FromContext[OOMReporter](s.ctx)
|
||||
if reporter == nil {
|
||||
return
|
||||
}
|
||||
err := reporter.WriteDraft(memoryUsage)
|
||||
if s.draftCancelled.Load() {
|
||||
reporter.DiscardDraft()
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("failed to write OOM draft: ", err)
|
||||
} else {
|
||||
s.logger.Warn("OOM draft saved")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) discardOOMDraft() {
|
||||
s.draftCancelled.Store(true)
|
||||
reporter := service.FromContext[OOMReporter](s.ctx)
|
||||
if reporter == nil {
|
||||
return
|
||||
}
|
||||
err := reporter.DiscardDraft()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to discard OOM draft: ", err)
|
||||
if s.recorder != nil {
|
||||
s.recorder.snapshot(SnapshotReasonPressure, sample, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,16 @@ package oomkiller
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
if !s.timerConfig.policyMode.hasTimerMode() {
|
||||
return E.New("memory pressure monitoring is not available on this platform without memory_limit")
|
||||
}
|
||||
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.network, s.timerConfig, s.writeOOMReport)
|
||||
s.adaptiveTimer.start()
|
||||
return nil
|
||||
return s.startTimer()
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
if s.adaptiveTimer != nil {
|
||||
s.adaptiveTimer.stop()
|
||||
}
|
||||
s.stopTimer()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -102,7 +102,8 @@ type adaptiveTimer struct {
|
||||
timerConfig
|
||||
logger log.ContextLogger
|
||||
network adapter.NetworkManager
|
||||
onTriggered func(uint64)
|
||||
connections adapter.ConnectionManager
|
||||
recorder *Recorder
|
||||
limitThresholds pressureThresholds
|
||||
|
||||
access sync.Mutex
|
||||
@@ -115,12 +116,13 @@ type adaptiveTimer struct {
|
||||
pressureBaselineTime time.Time
|
||||
}
|
||||
|
||||
func newAdaptiveTimer(logger log.ContextLogger, network adapter.NetworkManager, config timerConfig, onTriggered func(uint64)) *adaptiveTimer {
|
||||
func newAdaptiveTimer(logger log.ContextLogger, network adapter.NetworkManager, connections adapter.ConnectionManager, recorder *Recorder, config timerConfig) *adaptiveTimer {
|
||||
t := &adaptiveTimer{
|
||||
timerConfig: config,
|
||||
logger: logger,
|
||||
network: network,
|
||||
onTriggered: onTriggered,
|
||||
connections: connections,
|
||||
recorder: recorder,
|
||||
}
|
||||
if config.policyMode == policyModeMemoryLimit || config.policyMode == policyModeNetworkExtension {
|
||||
t.limitThresholds = computeLimitThresholds(config.memoryLimit, config.safetyMargin)
|
||||
@@ -192,14 +194,24 @@ func (t *adaptiveTimer) poll() {
|
||||
}
|
||||
}
|
||||
}
|
||||
state := t.state
|
||||
t.access.Unlock()
|
||||
var connections int
|
||||
if t.connections != nil {
|
||||
connections = t.connections.Count()
|
||||
}
|
||||
if t.recorder != nil {
|
||||
t.recorder.sample(sample, state, connections)
|
||||
if state != previousState {
|
||||
t.recorder.recordStateChange(state, sample)
|
||||
}
|
||||
}
|
||||
if !triggered {
|
||||
return
|
||||
}
|
||||
if t.onTriggered != nil {
|
||||
t.onTriggered(sample.usage)
|
||||
}
|
||||
var reason string
|
||||
if rateTriggered {
|
||||
reason = resetReasonRate
|
||||
if t.killerDisabled {
|
||||
t.logger.Warn("memory growth rate critical (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample))
|
||||
} else {
|
||||
@@ -207,6 +219,7 @@ func (t *adaptiveTimer) poll() {
|
||||
t.network.ResetNetwork(context.Background())
|
||||
}
|
||||
} else {
|
||||
reason = resetReasonThreshold
|
||||
if t.killerDisabled {
|
||||
t.logger.Warn("memory threshold reached (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample))
|
||||
} else {
|
||||
@@ -216,6 +229,10 @@ func (t *adaptiveTimer) poll() {
|
||||
}
|
||||
badCleanup()
|
||||
runtimeDebug.FreeOSMemory()
|
||||
if t.recorder != nil {
|
||||
t.recorder.recordReset(reason, sample, readMemorySample(t.policyMode), connections, t.killerDisabled)
|
||||
t.recorder.snapshot(SnapshotReasonReset, sample, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *adaptiveTimer) nextState(sample memorySample) pressureState {
|
||||
@@ -320,9 +337,20 @@ func readMemorySample(mode policyMode) memorySample {
|
||||
sample := memorySample{
|
||||
usage: memory.Total(),
|
||||
}
|
||||
if mode == policyModeAvailable {
|
||||
if mode == policyModeAvailable || mode == policyModeNetworkExtension {
|
||||
sample.availableKnown = true
|
||||
sample.available = memory.Available()
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func (s pressureState) String() string {
|
||||
switch s {
|
||||
case pressureStateArmed:
|
||||
return "armed"
|
||||
case pressureStateTriggered:
|
||||
return "triggered"
|
||||
default:
|
||||
return "normal"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ package oomkiller
|
||||
import runtimeDebug "runtime/debug"
|
||||
|
||||
func (t *adaptiveTimer) notifyPressure() {
|
||||
badCleanup()
|
||||
runtimeDebug.FreeOSMemory()
|
||||
t.access.Lock()
|
||||
t.startLocked()
|
||||
if t.timer == nil {
|
||||
t.access.Unlock()
|
||||
return
|
||||
}
|
||||
t.forceMinInterval = true
|
||||
t.pendingPressureBaseline = true
|
||||
t.access.Unlock()
|
||||
|
||||
@@ -65,6 +65,7 @@ type timelineRow struct {
|
||||
ConnectionsOpened uint64 `json:"connectionsOpened,omitempty"`
|
||||
InterfacePackets map[string]uint64 `json:"interfacePackets,omitempty"`
|
||||
NetworkType string `json:"network,omitempty"`
|
||||
NetworkPathUpdates uint64 `json:"pathUpdates,omitempty"`
|
||||
}
|
||||
|
||||
type qosBreakdown struct {
|
||||
@@ -80,6 +81,7 @@ type qosBreakdown struct {
|
||||
const (
|
||||
eventTypeBreak = "break"
|
||||
eventTypeNetwork = "network"
|
||||
eventTypePath = "path"
|
||||
)
|
||||
|
||||
type eventRecord struct {
|
||||
@@ -89,6 +91,7 @@ type eventRecord struct {
|
||||
Direction string `json:"direction,omitempty"`
|
||||
Size int `json:"size,omitempty"`
|
||||
NetworkType string `json:"network,omitempty"`
|
||||
NetworkPath string `json:"path,omitempty"`
|
||||
By *Attribution `json:"by,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -70,11 +70,13 @@ type Recorder struct {
|
||||
pendingBreak atomic.Pointer[breakRecord]
|
||||
notify chan struct{}
|
||||
|
||||
dnsQueries atomic.Uint64
|
||||
connectionsOpened atomic.Uint64
|
||||
dnsQueries atomic.Uint64
|
||||
connectionsOpened atomic.Uint64
|
||||
networkPathUpdates atomic.Uint64
|
||||
|
||||
access sync.Mutex
|
||||
networkType string
|
||||
networkPath string
|
||||
rows []timelineRow
|
||||
events []eventRecord
|
||||
previous previousSample
|
||||
@@ -96,15 +98,16 @@ type breakRecord struct {
|
||||
}
|
||||
|
||||
type previousSample struct {
|
||||
at time.Time
|
||||
usage systemUsage
|
||||
gcSeconds float64
|
||||
gcCycles uint64
|
||||
absoluteTime int64
|
||||
continuousTime int64
|
||||
interfaces map[string]interfaceCounters
|
||||
dnsQueries uint64
|
||||
connectionsOpened uint64
|
||||
at time.Time
|
||||
usage systemUsage
|
||||
gcSeconds float64
|
||||
gcCycles uint64
|
||||
absoluteTime int64
|
||||
continuousTime int64
|
||||
interfaces map[string]interfaceCounters
|
||||
dnsQueries uint64
|
||||
connectionsOpened uint64
|
||||
networkPathUpdates uint64
|
||||
}
|
||||
|
||||
func NewRecorder(options Options) *Recorder {
|
||||
@@ -285,6 +288,24 @@ func (r *Recorder) UpdateNetworkType(networkType string) {
|
||||
r.notifyWorker()
|
||||
}
|
||||
|
||||
func (r *Recorder) UpdateNetworkPath(description string) {
|
||||
r.networkPathUpdates.Add(1)
|
||||
now := time.Now()
|
||||
r.access.Lock()
|
||||
if r.closed || r.networkPath == description {
|
||||
r.access.Unlock()
|
||||
return
|
||||
}
|
||||
r.networkPath = description
|
||||
r.events = append(r.events, eventRecord{
|
||||
Type: eventTypePath,
|
||||
At: now.UTC().Format(time.RFC3339),
|
||||
NetworkPath: description,
|
||||
})
|
||||
r.access.Unlock()
|
||||
r.notifyWorker()
|
||||
}
|
||||
|
||||
func (r *Recorder) notifyWorker() {
|
||||
select {
|
||||
case r.notify <- struct{}{}:
|
||||
@@ -345,13 +366,14 @@ func (r *Recorder) consumeBreakLocked() {
|
||||
func (r *Recorder) resetPreviousLocked(now time.Time) {
|
||||
metrics.Read(r.metricsSamples)
|
||||
r.previous = previousSample{
|
||||
at: now,
|
||||
usage: readSystemUsage(),
|
||||
gcSeconds: r.metricsSamples[0].Value.Float64(),
|
||||
gcCycles: r.metricsSamples[2].Value.Uint64(),
|
||||
interfaces: readInterfaceCounters(),
|
||||
dnsQueries: r.dnsQueries.Load(),
|
||||
connectionsOpened: r.connectionsOpened.Load(),
|
||||
at: now,
|
||||
usage: readSystemUsage(),
|
||||
gcSeconds: r.metricsSamples[0].Value.Float64(),
|
||||
gcCycles: r.metricsSamples[2].Value.Uint64(),
|
||||
interfaces: readInterfaceCounters(),
|
||||
dnsQueries: r.dnsQueries.Load(),
|
||||
connectionsOpened: r.connectionsOpened.Load(),
|
||||
networkPathUpdates: r.networkPathUpdates.Load(),
|
||||
}
|
||||
r.previous.absoluteTime, r.previous.continuousTime = readClocks()
|
||||
}
|
||||
@@ -361,16 +383,17 @@ func (r *Recorder) sampleLocked(now time.Time) {
|
||||
r.resetPreviousLocked(now)
|
||||
current := &r.previous
|
||||
row := timelineRow{
|
||||
From: previous.at.UTC().Format(time.RFC3339),
|
||||
To: now.UTC().Format(time.RFC3339),
|
||||
CPUGCMS: int64((current.gcSeconds - previous.gcSeconds) * 1000),
|
||||
Goroutines: r.metricsSamples[1].Value.Uint64(),
|
||||
GCCycles: current.gcCycles - previous.gcCycles,
|
||||
GoMemoryBytes: r.metricsSamples[3].Value.Uint64(),
|
||||
GoHeapLiveBytes: r.metricsSamples[4].Value.Uint64(),
|
||||
DNSQueries: current.dnsQueries - previous.dnsQueries,
|
||||
ConnectionsOpened: current.connectionsOpened - previous.connectionsOpened,
|
||||
NetworkType: r.networkType,
|
||||
From: previous.at.UTC().Format(time.RFC3339),
|
||||
To: now.UTC().Format(time.RFC3339),
|
||||
CPUGCMS: int64((current.gcSeconds - previous.gcSeconds) * 1000),
|
||||
Goroutines: r.metricsSamples[1].Value.Uint64(),
|
||||
GCCycles: current.gcCycles - previous.gcCycles,
|
||||
GoMemoryBytes: r.metricsSamples[3].Value.Uint64(),
|
||||
GoHeapLiveBytes: r.metricsSamples[4].Value.Uint64(),
|
||||
DNSQueries: current.dnsQueries - previous.dnsQueries,
|
||||
ConnectionsOpened: current.connectionsOpened - previous.connectionsOpened,
|
||||
NetworkType: r.networkType,
|
||||
NetworkPathUpdates: current.networkPathUpdates - previous.networkPathUpdates,
|
||||
}
|
||||
if memory.TotalAvailable() {
|
||||
row.MemoryBytes = memory.Total()
|
||||
|
||||
@@ -119,9 +119,9 @@ func (t *resolve1Manager) createMetadata(sender dbus.Sender) adapter.InboundCont
|
||||
if err == nil {
|
||||
processInfo.ProcessPath = processPath
|
||||
} else {
|
||||
processPath, err = os.Readlink(F.ToString("/proc/", senderPid, "/comm"))
|
||||
if err == nil {
|
||||
processInfo.ProcessPath = processPath
|
||||
processName, readErr := os.ReadFile(F.ToString("/proc/", senderPid, "/comm"))
|
||||
if readErr == nil {
|
||||
processInfo.ProcessPath = strings.TrimSpace(string(processName))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestOpenVPNStaticKeyClientDataPath(t *testing.T) {
|
||||
Mode: ovpn.ModeStaticKey,
|
||||
Network: N.NetworkTCP,
|
||||
Address: []netip.Prefix{netip.MustParsePrefix(clientTunnelAddress + "/30")},
|
||||
PeerAddress: badoption.Addr(netip.MustParseAddr(peerTunnelAddress)),
|
||||
PeerAddress: common.Ptr(badoption.Addr(netip.MustParseAddr(peerTunnelAddress))),
|
||||
Topology: "p2p",
|
||||
StaticKeyPath: staticKeyPath,
|
||||
KeyDirection: "client",
|
||||
@@ -286,7 +286,7 @@ func runOpenVPNStaticKeySelfToSelf(t *testing.T, protocol string) {
|
||||
RemotePort: clientOpenVPNPort,
|
||||
MaxClients: 1,
|
||||
Address: []netip.Prefix{netip.MustParsePrefix(serverTunnelAddress + "/30")},
|
||||
PeerAddress: badoption.Addr(netip.MustParseAddr(clientTunnelAddress)),
|
||||
PeerAddress: common.Ptr(badoption.Addr(netip.MustParseAddr(clientTunnelAddress))),
|
||||
Topology: "p2p",
|
||||
StaticKeyPath: staticKeyPath,
|
||||
KeyDirection: "server",
|
||||
@@ -302,7 +302,7 @@ func runOpenVPNStaticKeySelfToSelf(t *testing.T, protocol string) {
|
||||
Mode: ovpn.ModeStaticKey,
|
||||
Network: protocol,
|
||||
Address: []netip.Prefix{netip.MustParsePrefix(clientTunnelAddress + "/30")},
|
||||
PeerAddress: badoption.Addr(netip.MustParseAddr(serverTunnelAddress)),
|
||||
PeerAddress: common.Ptr(badoption.Addr(netip.MustParseAddr(serverTunnelAddress))),
|
||||
Topology: "p2p",
|
||||
StaticKeyPath: staticKeyPath,
|
||||
KeyDirection: "client",
|
||||
|
||||
@@ -333,9 +333,9 @@ func (e *Endpoint) BindUpdate() error {
|
||||
|
||||
func (e *Endpoint) onPauseUpdated(event int) {
|
||||
switch event {
|
||||
case pause.EventDevicePaused, pause.EventNetworkPause:
|
||||
case pause.EventNetworkPause:
|
||||
e.device.Down()
|
||||
case pause.EventDeviceWake, pause.EventNetworkWake:
|
||||
case pause.EventNetworkWake:
|
||||
e.device.Up()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user