Merge tag 'v1.14.1'

This commit is contained in:
Shtorm
2026-09-20 22:37:07 +03:00
100 changed files with 2522 additions and 1029 deletions
+1 -1
View File
@@ -1 +1 @@
45832ab074849607406baa3e3a2c4660274602ed
0d28acc44093df24b2526dea3d6ffefd6b0a54f0
+28
View File
@@ -0,0 +1,28 @@
--- a/src/internal/cpu/cpu_arm64_ios.go
+++ b/src/internal/cpu/cpu_arm64_ios.go
@@ -0,0 +1,14 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build arm64 && ios
+
+package cpu
+
+func osInit() {
+ ARM64.HasAES = true
+ ARM64.HasPMULL = true
+ ARM64.HasSHA1 = true
+ ARM64.HasSHA2 = true
+}
--- a/src/internal/cpu/cpu_arm64_other.go
+++ b/src/internal/cpu/cpu_arm64_other.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build arm64 && !linux && !freebsd && !android && (!darwin || ios) && !openbsd
+//go:build arm64 && !linux && !freebsd && !android && !darwin && !openbsd
package cpu
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
# Go's internal/cpu never detects ARM64 features on GOOS=ios, so crypto/aes,
# AES-GCM and crypto/sha256 use their generic implementations on iOS and tvOS.
# See https://github.com/SagerNet/sing-box/issues/4486
#
# Only the ARMv8.0 cryptographic extensions are asserted; ARMv8.1 atomics and
# SHA-512 are absent on the A8/A9 devices still supported by the deployment targets.
#
# Remove once the Go release used by the Apple library build includes the fix.
export GOTOOLCHAIN=local
GOROOT="$(go env GOROOT)"
PATCH_FILE="$(cd "$(dirname "$0")" && pwd)/go_ios_cpu_features.patch"
cd "$GOROOT"
if [[ -f src/internal/cpu/cpu_arm64_ios.go ]]; then
echo "already patched"
else
patch --verbose -p1 < "$PATCH_FILE"
fi
CPU_FILES="$(GOOS=ios GOARCH=arm64 CGO_ENABLED=0 go list -f '{{.GoFiles}}' internal/cpu)"
echo "internal/cpu files for ios/arm64: $CPU_FILES"
case "$CPU_FILES" in
*cpu_arm64_ios.go*) ;;
*)
echo "patch is not effective" >&2
exit 1
;;
esac
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
VERSION="1.26.7"
VERSION="1.26.8"
PATCH_COMMITS=(
"f080b0c6346eb690c0dc82497b35925f385b35ac"
"2d9c12887c342fb9051d231aa5388743cb7e9cb6"
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
VERSION="1.26.7"
VERSION="1.26.8"
PATCH_COMMITS=(
"a4ae550aa148b04c9d4890e98bee63aede5c4b53"
"95b851f661584711faa8115b3234a461044f4510"
+23 -18
View File
@@ -54,7 +54,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.7
go-version: 1.26.8
cache: false
- name: Cache Go modules
uses: actions/cache/restore@v4
@@ -145,7 +145,7 @@ jobs:
if: ${{ ! matrix.legacy_win7 }}
uses: actions/setup-go@v5
with:
go-version: 1.26.7
go-version: 1.26.8
cache: false
- name: Cache Go modules
uses: actions/cache/restore@v4
@@ -160,7 +160,7 @@ jobs:
with:
path: |
~/go/go_win7
key: go_win7_1267
key: go_win7_1268
- name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
env:
@@ -488,7 +488,7 @@ jobs:
if: ${{ ! matrix.legacy_osx }}
uses: actions/setup-go@v5
with:
go-version: 1.26.7
go-version: 1.26.8
cache: false
- name: Cache Go modules
uses: actions/cache/restore@v4
@@ -503,7 +503,7 @@ jobs:
with:
path: |
~/go/go_osx
key: go_osx_1267
key: go_osx_1268
- name: Setup Go for macOS 10.13
if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true'
env:
@@ -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: |-
+1 -1
View File
@@ -55,7 +55,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.7
go-version: 1.26.8
- name: Clone cronet-go
if: matrix.naive
run: |
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.7
go-version: 1.26.8
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -72,7 +72,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.7
go-version: 1.26.8
- name: Clone cronet-go
if: matrix.naive
run: |
-9
View File
@@ -8,18 +8,9 @@ import (
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/varbin"
)
type ClashServer interface {
LifecycleService
Mode() string
ModeList() []string
SetMode(mode string)
AddModeUpdateHook(hook *observable.Subscriber[struct{}])
}
type URLTestHistory struct {
Time time.Time `json:"time"`
Delay uint16 `json:"delay"`
+5 -1
View File
@@ -93,7 +93,11 @@ func JudgeFlow(router Router, inbound string, inboundType string, network uint8,
case PreMatchDrop:
return tun.FlowVerdict{Action: tun.ActionDrop}
case PreMatchBypass:
return tun.FlowVerdict{Action: tun.ActionBypass}
port, isPort := result.Outbound.(tun.Port)
if !isPort {
return tun.FlowVerdict{Action: tun.ActionBypass}
}
return tun.FlowVerdict{Action: tun.ActionBypass, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker}
case PreMatchHijackDNS:
return tun.FlowVerdict{Action: tun.ActionHijackDNS}
default:
+12 -7
View File
@@ -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
Submodule clients/desktop updated: cebee0d527...fb1175ca3b
+1 -1
View File
@@ -39,5 +39,5 @@ func ReadTagVersion() (badversion.Version, error) {
}
func TestFlightVersion(version badversion.Version) string {
return F.ToString(version.Major, ".", version.Minor, ".10")
return F.ToString(version.Major, ".", version.Minor, ".1000")
}
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/service"
"github.com/spf13/cobra"
)
@@ -30,7 +31,7 @@ func check() error {
if err != nil {
return err
}
ctx, cancel := context.WithCancel(globalCtx)
ctx, cancel := context.WithCancel(service.ExtendContext(globalCtx))
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
+2 -1
View File
@@ -19,6 +19,7 @@ import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/service"
"github.com/spf13/cobra"
)
@@ -136,7 +137,7 @@ func create(options option.Options) (*box.Box, context.CancelFunc, error) {
}
options.Log.DisableColor = true
}
ctx, cancel := context.WithCancel(globalCtx)
ctx, cancel := context.WithCancel(service.ExtendContext(globalCtx))
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"github.com/sagernet/sing-box"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/spf13/cobra"
)
@@ -30,7 +31,7 @@ func createPreStartedClient() (*box.Box, error) {
return nil, err
}
}
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
instance, err := box.New(box.Options{Context: service.ExtendContext(globalCtx), Options: options})
if err != nil {
return nil, E.Cause(err, "create service")
}
+27 -16
View File
@@ -22,7 +22,11 @@ import (
"github.com/sagernet/sing/contrab/maphash"
)
const pathProc = "/proc"
const (
pathProc = "/proc"
processPathsAllUsers = ^uint32(0)
)
var _ Searcher = (*linuxSearcher)(nil)
@@ -101,6 +105,8 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
}
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
source = netip.AddrPortFrom(source.Addr().Unmap(), source.Port())
destination = netip.AddrPortFrom(destination.Addr().Unmap(), destination.Port())
family, protocol, err := socketDiagSettings(network, source)
if err != nil {
return 0, 0, err
@@ -118,28 +124,33 @@ func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.Addr
return 0, 0, err
}
}
return querySocketDiagOnce(family, protocol, source)
return dumpSocketDiag(family, protocol, source, destination)
}
// The socket keeps the uid it was created with, while /proc reflects the
// current uid of the process, so a socket created before a privilege drop
// only appears under a scan of all users.
func (s *linuxSearcher) findProcessPath(targetInode, uid uint32) (string, error) {
if cached, ok := s.processPathCache.Get(uid); ok {
if processPath, found := cached.entries[targetInode]; found {
for _, scanUID := range []uint32{uid, processPathsAllUsers} {
if cached, ok := s.processPathCache.Get(scanUID); ok {
if processPath, found := cached.entries[targetInode]; found {
return processPath, nil
}
}
processPaths, err := buildProcessPaths(scanUID)
if err != nil {
return "", err
}
s.processPathCache.Add(scanUID, &uidProcessPaths{entries: processPaths})
processPath, found := processPaths[targetInode]
if found {
return processPath, nil
}
}
processPaths, err := buildProcessPathsByUID(uid)
if err != nil {
return "", err
}
s.processPathCache.Add(uid, &uidProcessPaths{entries: processPaths})
processPath, found := processPaths[targetInode]
if !found {
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
}
return processPath, nil
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
}
func buildProcessPathsByUID(uid uint32) (map[uint32]string, error) {
func buildProcessPaths(uid uint32) (map[uint32]string, error) {
files, err := os.ReadDir(pathProc)
if err != nil {
return nil, err
@@ -157,7 +168,7 @@ func buildProcessPathsByUID(uid uint32) (map[uint32]string, error) {
}
return nil, err
}
if info.Sys().(*syscall.Stat_t).Uid != uid {
if uid != processPathsAllUsers && info.Sys().(*syscall.Stat_t).Uid != uid {
continue
}
processPath := filepath.Join(pathProc, file.Name())
+123 -27
View File
@@ -9,6 +9,7 @@ import (
"os"
"sync"
"syscall"
"time"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
@@ -19,6 +20,7 @@ const (
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
socketDiagResponseMinSize = 72
socketDiagByFamily = 20
socketDiagTimeout = 100 * time.Millisecond
)
type socketDiagConn struct {
@@ -28,6 +30,13 @@ type socketDiagConn struct {
fd int
}
type socketDiagEntry struct {
source netip.AddrPort
destination netip.AddrPort
uid uint32
inode uint32
}
func socketDiagConnIndex(family, protocol uint8) int {
index := 0
if protocol == syscall.IPPROTO_UDP {
@@ -83,15 +92,6 @@ func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort
return 0, 0, err
}
func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) {
fd, err := openSocketDiag()
if err != nil {
return 0, 0, E.Cause(err, "dial netlink")
}
defer syscall.Close(fd)
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
}
func (c *socketDiagConn) ensureOpenLocked() error {
if c.fd != -1 {
return nil
@@ -109,12 +109,12 @@ func openSocketDiag() (int, error) {
if err != nil {
return -1, err
}
timeout := &syscall.Timeval{Usec: 100}
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil {
timeout := syscall.NsecToTimeval(socketDiagTimeout.Nanoseconds())
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &timeout); err != nil {
syscall.Close(fd)
return -1, err
}
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil {
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &timeout); err != nil {
syscall.Close(fd)
return -1, err
}
@@ -196,36 +196,132 @@ func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) {
if err != nil {
return 0, 0, E.Cause(err, "parse netlink message")
}
return unpackSocketDiagMessages(messages)
}
func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) {
for _, message := range messages {
switch message.Header.Type {
case syscall.NLMSG_DONE:
continue
case syscall.NLMSG_ERROR:
err = unpackSocketDiagError(&message)
if err != nil {
return 0, 0, err
}
case socketDiagByFamily:
inode, uid = unpackSocketDiagResponse(&message)
if inode != 0 || uid != 0 {
return inode, uid, nil
entry, valid := unpackSocketDiagEntry(&message)
if valid && (entry.inode != 0 || entry.uid != 0) {
return entry.inode, entry.uid, nil
}
}
}
return 0, 0, ErrNotFound
}
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
if len(msg.Data) < socketDiagResponseMinSize {
return 0, 0
// The dump only filters by port (inet_diag_dump_icsk, udp_dump), so every socket
// in the namespace with the requested local port is returned regardless of address.
// A dual-stack socket carrying a v4-mapped address is only listed under AF_INET6.
func dumpSocketDiag(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
families := []uint8{family}
if family == syscall.AF_INET {
families = append(families, syscall.AF_INET6)
}
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
return inode, uid
for _, dumpFamily := range families {
inode, uid, err = dumpSocketDiagFamily(dumpFamily, protocol, source, destination)
if err == nil || !errors.Is(err, ErrNotFound) {
return inode, uid, err
}
}
return 0, 0, ErrNotFound
}
func dumpSocketDiagFamily(family, protocol uint8, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
fd, err := openSocketDiag()
if err != nil {
return 0, 0, E.Cause(err, "dial netlink")
}
defer syscall.Close(fd)
_, err = syscall.Write(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
if err != nil {
return 0, 0, E.Cause(err, "write netlink request")
}
var (
localMatch socketDiagEntry
hasLocalMatch bool
wildcardMatch socketDiagEntry
hasWildcardMatch bool
buffer = make([]byte, 64<<10)
n int
messages []syscall.NetlinkMessage
)
for {
n, err = syscall.Read(fd, buffer)
if err != nil {
return 0, 0, E.Cause(err, "read netlink response")
}
messages, err = syscall.ParseNetlinkMessage(buffer[:n])
if err != nil {
return 0, 0, E.Cause(err, "parse netlink message")
}
if len(messages) == 0 {
return 0, 0, E.New("empty netlink response")
}
for _, message := range messages {
switch message.Header.Type {
case syscall.NLMSG_DONE:
if hasLocalMatch {
return localMatch.inode, localMatch.uid, nil
}
if hasWildcardMatch {
return wildcardMatch.inode, wildcardMatch.uid, nil
}
return 0, 0, ErrNotFound
case syscall.NLMSG_ERROR:
err = unpackSocketDiagError(&message)
if err != nil {
return 0, 0, err
}
case socketDiagByFamily:
entry, valid := unpackSocketDiagEntry(&message)
if !valid || (entry.inode == 0 && entry.uid == 0) || entry.source.Port() != source.Port() {
continue
}
if entry.source.Addr() == source.Addr() && (!destination.IsValid() || entry.destination == destination) {
return entry.inode, entry.uid, nil
}
if protocol != syscall.IPPROTO_UDP {
continue
}
if !hasLocalMatch && entry.source.Addr() == source.Addr() {
hasLocalMatch = true
localMatch = entry
}
if !hasWildcardMatch && entry.source.Addr().IsUnspecified() {
hasWildcardMatch = true
wildcardMatch = entry
}
}
}
}
}
func unpackSocketDiagEntry(msg *syscall.NetlinkMessage) (socketDiagEntry, bool) {
if len(msg.Data) < socketDiagResponseMinSize {
return socketDiagEntry{}, false
}
data := msg.Data
var sourceAddr, destinationAddr netip.Addr
switch data[0] {
case syscall.AF_INET:
sourceAddr = netip.AddrFrom4([4]byte(data[8:12]))
destinationAddr = netip.AddrFrom4([4]byte(data[24:28]))
case syscall.AF_INET6:
sourceAddr = netip.AddrFrom16([16]byte(data[8:24])).Unmap()
destinationAddr = netip.AddrFrom16([16]byte(data[24:40])).Unmap()
default:
return socketDiagEntry{}, false
}
return socketDiagEntry{
source: netip.AddrPortFrom(sourceAddr, binary.BigEndian.Uint16(data[4:6])),
destination: netip.AddrPortFrom(destinationAddr, binary.BigEndian.Uint16(data[6:8])),
uid: binary.NativeEndian.Uint32(data[64:68]),
inode: binary.NativeEndian.Uint32(data[68:72]),
}, true
}
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error {
+108
View File
@@ -0,0 +1,108 @@
//go:build linux
package process
import (
"context"
"net"
"net/netip"
"os"
"syscall"
"testing"
"github.com/sagernet/sing-box/log"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/stretchr/testify/require"
)
func socketInode(t *testing.T, conn syscall.Conn) uint32 {
rawConn, err := conn.SyscallConn()
require.NoError(t, err)
var inode uint32
err = rawConn.Control(func(fd uintptr) {
var stat syscall.Stat_t
require.NoError(t, syscall.Fstat(int(fd), &stat))
inode = uint32(stat.Ino)
})
require.NoError(t, err)
return inode
}
func TestDumpSocketDiagTCP(t *testing.T) {
t.Parallel()
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()
first, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2)}}).Dial("tcp", listener.Addr().String())
require.NoError(t, err)
defer first.Close()
second, err := (&net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(M.AddrPortFromNet(first.LocalAddr()).Port())}}).Dial("tcp", listener.Addr().String())
require.NoError(t, err)
defer second.Close()
destination := M.AddrPortFromNet(listener.Addr())
for _, conn := range []net.Conn{second, first} {
inode, uid, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), destination)
require.NoError(t, err)
require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode)
require.Equal(t, uint32(os.Getuid()), uid)
inode, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{})
require.NoError(t, err)
require.Equal(t, socketInode(t, conn.(syscall.Conn)), inode)
}
_, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_TCP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(first.LocalAddr()).Port()), destination)
require.ErrorIs(t, err, ErrNotFound)
}
func TestDumpSocketDiagUDP(t *testing.T) {
t.Parallel()
first, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 2)})
require.NoError(t, err)
defer first.Close()
port := M.AddrPortFromNet(first.LocalAddr()).Port()
second, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 3), Port: int(port)})
require.NoError(t, err)
defer second.Close()
for _, conn := range []*net.UDPConn{second, first} {
inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, M.AddrPortFromNet(conn.LocalAddr()), netip.AddrPort{})
require.NoError(t, err)
require.Equal(t, socketInode(t, conn), inode)
}
_, _, err = dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), port), netip.AddrPort{})
require.ErrorIs(t, err, ErrNotFound)
wildcard, err := net.ListenUDP("udp4", &net.UDPAddr{})
require.NoError(t, err)
defer wildcard.Close()
inode, _, err := dumpSocketDiag(syscall.AF_INET, syscall.IPPROTO_UDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 4}), M.AddrPortFromNet(wildcard.LocalAddr()).Port()), netip.AddrPort{})
require.NoError(t, err)
require.Equal(t, socketInode(t, wildcard), inode)
}
func TestLinuxSearcherFindProcessInfo(t *testing.T) {
t.Parallel()
searcher, err := NewSearcher(Config{Logger: log.NewNOPFactory().NewLogger("test")})
require.NoError(t, err)
defer searcher.Close()
executable, err := os.Executable()
require.NoError(t, err)
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()
tcpConn, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err)
defer tcpConn.Close()
info, err := searcher.FindProcessInfo(context.Background(), N.NetworkTCP, M.AddrPortFromNet(tcpConn.LocalAddr()), M.AddrPortFromNet(listener.Addr()))
require.NoError(t, err)
require.Equal(t, executable, info.ProcessPath)
require.Equal(t, int32(os.Getuid()), info.UserId)
udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{})
require.NoError(t, err)
defer udpConn.Close()
info, err = searcher.FindProcessInfo(context.Background(), N.NetworkUDP, netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), M.AddrPortFromNet(udpConn.LocalAddr()).Port()), netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 53))
require.NoError(t, err)
require.Equal(t, executable, info.ProcessPath)
}
+3 -5
View File
@@ -3,8 +3,6 @@ package daemon
import (
"context"
"time"
"github.com/sagernet/sing-box/log"
)
const defaultAttachedLogMaxLines = 3000
@@ -20,9 +18,9 @@ func NewAttachedService(ctx context.Context) *StartedService {
s.serviceStatus = &ServiceStatus{Status: ServiceStatus_STARTED}
s.startedAt = time.Now()
instance.urlTestHistoryStorage.AddUpdateHook(s.urlTestSubscriber)
if instance.clashServer != nil {
instance.clashServer.AddModeUpdateHook(s.clashModeSubscriber)
if instance.clashMode != nil {
instance.clashMode.AddUpdateHook(s.clashModeSubscriber)
}
instance.logFactory.(log.ObservableFactory).AttachPlatformWriter(s)
instance.logFactory.AttachPlatformWriter(s)
return s
}
+5 -3
View File
@@ -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),
+39
View File
@@ -0,0 +1,39 @@
package daemon
import (
"github.com/sagernet/sing-box/log"
)
type logRing struct {
entries []*log.Entry
maxLines int
start int
}
func (r *logRing) push(entry *log.Entry) {
if r.maxLines <= 0 {
return
}
if len(r.entries) < r.maxLines {
r.entries = append(r.entries, entry)
return
}
r.entries[r.start] = entry
r.start++
if r.start == len(r.entries) {
r.start = 0
}
}
func (r *logRing) array() []*log.Entry {
result := make([]*log.Entry, 0, len(r.entries))
result = append(result, r.entries[r.start:]...)
result = append(result, r.entries[:r.start]...)
return result
}
func (r *logRing) reset() {
clear(r.entries)
r.entries = r.entries[:0]
r.start = 0
}
+6 -7
View File
@@ -6,7 +6,6 @@ import (
"unsafe"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing/common/memory"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -18,20 +17,20 @@ var _ ManagedServiceServer = (*ManagedService)(nil)
type ManagedService struct {
handler ManagedHandler
debug bool
oomReporter oomkiller.OOMReporter
oomRecorder *oomkiller.Recorder
}
type ManagedServiceOptions struct {
Handler ManagedHandler
Debug bool
OOMReporter oomkiller.OOMReporter
OOMRecorder *oomkiller.Recorder
}
func NewManagedService(options ManagedServiceOptions) *ManagedService {
return &ManagedService{
handler: options.Handler,
debug: options.Debug,
oomReporter: options.OOMReporter,
oomRecorder: options.OOMRecorder,
}
}
@@ -87,10 +86,10 @@ func (s *ManagedService) TriggerDebugCrash(ctx context.Context, request *DebugCr
}
func (s *ManagedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
if s.oomReporter == nil {
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
if s.oomRecorder == nil {
return nil, status.Error(codes.Unavailable, "OOM recorder not available")
}
return &emptypb.Empty{}, s.oomReporter.WriteReport(memory.Total())
return &emptypb.Empty{}, s.oomRecorder.WriteReport()
}
func (s *ManagedService) mustEmbedUnimplementedManagedServiceServer() {
+18 -27
View File
@@ -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 {
+17
View File
@@ -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 {
+5 -1
View File
@@ -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
View File
@@ -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**:
-1
View File
@@ -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 |
-1
View File
@@ -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 |
+3 -6
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
+22
View File
@@ -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 选项已废弃,且可以被证书提供者替代。
@@ -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")
}
}
+21
View File
@@ -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
}
+5 -8
View File
@@ -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
}
+6 -3
View File
@@ -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()
}
+2 -58
View File
@@ -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
}
+3 -3
View File
@@ -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)
}
+36 -99
View File
@@ -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) {
+109
View File
@@ -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)
+62
View File
@@ -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
}
+19 -4
View File
@@ -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
}
+12
View File
@@ -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{})
+63 -201
View File
@@ -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)
}
+1
View File
@@ -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 (
+2 -1
View File
@@ -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)
}
},
}
+1
View File
@@ -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)
+40 -40
View File
@@ -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
+70 -70
View File
@@ -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=
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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"`
+5 -1
View File
@@ -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
}
+2 -2
View File
@@ -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)
}
}
+2
View File
@@ -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() {
+2 -2
View File
@@ -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")
}
+3 -3
View File
@@ -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")
}
+2
View File
@@ -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() {
+15 -3
View File
@@ -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{
+2 -2
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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 {
+22
View File
@@ -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
}
+9 -9
View File
@@ -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
+7 -6
View File
@@ -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 {
+26 -19
View File
@@ -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
}
+5 -1
View File
@@ -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 {
+26
View File
@@ -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()
}
+25
View File
@@ -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()
}
+17 -1
View File
@@ -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() {
+141
View File
@@ -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()
}
+559
View File
@@ -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)
}
+25 -33
View File
@@ -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()
}
}
+11 -55
View File
@@ -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)
}
}
}
+2 -10
View File
@@ -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
}
+35 -7
View File
@@ -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 -1
View File
@@ -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()
+3
View File
@@ -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"`
}
+51 -28
View File
@@ -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()
+3 -3
View File
@@ -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))
}
}
+3 -3
View File
@@ -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",
+2 -2
View File
@@ -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()
}
}