From df34f5068b961fe3390a61eb3e773ad9bf4d98e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 31 Aug 2026 12:13:08 +0800 Subject: [PATCH 01/41] documentation: Fix NaiveProxy version in changelog --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index d5c335be..3474f88a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -54,7 +54,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**: From f0a2c348f611caf4a0a965e4b0081d7672417050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 31 Aug 2026 15:35:35 +0800 Subject: [PATCH 02/41] documentation: Fix macOS standalone client data migration --- docs/migration.md | 13 ++++++++++++- docs/migration.zh.md | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/migration.md b/docs/migration.md index 3b006288..8a78869f 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -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 diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 4d003e1e..a710b17c 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -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 选项已废弃,且可以被证书提供者替代。 From 650ef881c8fb216259e4ebcfbd74234554c39612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 31 Aug 2026 19:07:48 +0800 Subject: [PATCH 03/41] Use patch version 1000 for TestFlight --- cmd/internal/build_shared/tag.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/internal/build_shared/tag.go b/cmd/internal/build_shared/tag.go index a4971a23..b0287abd 100644 --- a/cmd/internal/build_shared/tag.go +++ b/cmd/internal/build_shared/tag.go @@ -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") } From 3e185071eb966395c8a559726876b17e9eca47b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 31 Aug 2026 20:55:53 +0800 Subject: [PATCH 04/41] Reduce network environment update overhead --- route/network.go | 2 +- route/network_environment_darwin.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/route/network.go b/route/network.go index 07de8145..1ee735cc 100644 --- a/route/network.go +++ b/route/network.go @@ -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 { diff --git a/route/network_environment_darwin.go b/route/network_environment_darwin.go index 92633878..ad84d3a2 100644 --- a/route/network_environment_darwin.go +++ b/route/network_environment_darwin.go @@ -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 } From b807ff30d0bfc08993be962ed7771b0433a5045d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 1 Sep 2026 21:32:13 +0800 Subject: [PATCH 05/41] Reduce rule matching memory allocations --- protocol/tailscale/dns_transport.go | 18 ++++++++++-- protocol/tailscale/endpoint.go | 4 +-- route/rule/match_state.go | 22 ++++++++++++++ route/rule/rule_abstract.go | 18 ++++++------ route/rule/rule_item_rule_set.go | 45 +++++++++++++++++------------ 5 files changed, 74 insertions(+), 33 deletions(-) diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index d91b498f..b48aa823 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -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{ diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 4cd0ecb9..f46a4869 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -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) diff --git a/route/rule/match_state.go b/route/rule/match_state.go index f7f937f8..e6b4fd84 100644 --- a/route/rule/match_state.go +++ b/route/rule/match_state.go @@ -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 +} diff --git a/route/rule/rule_abstract.go b/route/rule/rule_abstract.go index f21df788..cb7d5060 100644 --- a/route/rule/rule_abstract.go +++ b/route/rule/rule_abstract.go @@ -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 } diff --git a/route/rule/rule_item_rule_set.go b/route/rule/rule_item_rule_set.go index eb7c74e1..256183e2 100644 --- a/route/rule/rule_item_rule_set.go +++ b/route/rule/rule_item_rule_set.go @@ -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 } From 6c4589c9338d530a637a12c664ef135ab8f2829a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 1 Sep 2026 21:43:43 +0800 Subject: [PATCH 06/41] Record platform network path in power report --- experimental/libbox/monitor.go | 12 +++++ experimental/libbox/platform.go | 1 + service/powerreport/record.go | 3 ++ service/powerreport/recorder.go | 79 +++++++++++++++++++++------------ 4 files changed, 67 insertions(+), 28 deletions(-) diff --git a/experimental/libbox/monitor.go b/experimental/libbox/monitor.go index 49f7702a..273b1275 100644 --- a/experimental/libbox/monitor.go +++ b/experimental/libbox/monitor.go @@ -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{}) diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index 3cf24489..108cc209 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -89,6 +89,7 @@ func (c *ConnectionOwner) AndroidPackageNames() StringIterator { type InterfaceUpdateListener interface { UpdateDefaultInterface(interfaceName string, interfaceIndex int32, isExpensive bool, isConstrained bool) + UpdateNetworkPath(networkPath string) } const ( diff --git a/service/powerreport/record.go b/service/powerreport/record.go index 730f97c7..d0e3220c 100644 --- a/service/powerreport/record.go +++ b/service/powerreport/record.go @@ -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"` } diff --git a/service/powerreport/recorder.go b/service/powerreport/recorder.go index 26fe4a32..0b903a8d 100644 --- a/service/powerreport/recorder.go +++ b/service/powerreport/recorder.go @@ -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() From a556d49491c23db6117aea241a778e2c3a9e498f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 1 Sep 2026 22:39:21 +0800 Subject: [PATCH 07/41] Separate Clash mode management from Clash API server --- adapter/experimental.go | 9 -- box.go | 19 ++-- daemon/attached_service.go | 8 +- daemon/instance.go | 7 +- daemon/started_service.go | 24 ++--- experimental/clashapi.go | 60 +------------ experimental/clashapi/configs.go | 6 +- experimental/clashapi/server.go | 135 ++++++++-------------------- experimental/clashmode/manager.go | 109 ++++++++++++++++++++++ experimental/clashmode/mode_list.go | 62 +++++++++++++ include/clashapi_stub.go | 2 +- log/factory.go | 2 +- option/experimental.go | 1 - route/rule/rule_item_clash_mode.go | 13 +-- 14 files changed, 252 insertions(+), 205 deletions(-) create mode 100644 experimental/clashmode/manager.go create mode 100644 experimental/clashmode/mode_list.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 493d8027..d5a1e5bb 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -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"` diff --git a/box.go b/box.go index f06a1176..7bbedd59 100644 --- a/box.go +++ b/box.go @@ -27,6 +27,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" @@ -157,7 +158,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 != "" { @@ -177,7 +178,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, @@ -243,11 +244,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 @@ -420,13 +428,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 { diff --git a/daemon/attached_service.go b/daemon/attached_service.go index b726ac2c..43c6b10a 100644 --- a/daemon/attached_service.go +++ b/daemon/attached_service.go @@ -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 } diff --git a/daemon/instance.go b/daemon/instance.go index 44736e25..495b2b99 100644 --- a/daemon/instance.go +++ b/daemon/instance.go @@ -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 @@ -130,7 +131,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) @@ -145,7 +146,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), diff --git a/daemon/started_service.go b/daemon/started_service.go index 0ca8e4bb..c9f8f884 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -277,8 +277,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 @@ -635,14 +635,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 +665,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 +700,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 } diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 4ad07c8b..2472f040 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -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 -} diff --git a/experimental/clashapi/configs.go b/experimental/clashapi/configs.go index 8ae1d258..35a1d4ba 100644 --- a/experimental/clashapi/configs.go +++ b/experimental/clashapi/configs.go @@ -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) } diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 8de33d6a..b9222ad5 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -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 @@ -51,20 +50,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") @@ -73,6 +68,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, @@ -88,20 +87,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 != "" { @@ -157,45 +148,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.Info("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.Info("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 } @@ -205,49 +185,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.Info("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) { diff --git a/experimental/clashmode/manager.go b/experimental/clashmode/manager.go new file mode 100644 index 00000000..6491d754 --- /dev/null +++ b/experimental/clashmode/manager.go @@ -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.Info("updated mode: ", newMode) +} + +var _ adapter.LifecycleService = (*Manager)(nil) diff --git a/experimental/clashmode/mode_list.go b/experimental/clashmode/mode_list.go new file mode 100644 index 00000000..e0c283e1 --- /dev/null +++ b/experimental/clashmode/mode_list.go @@ -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 +} diff --git a/include/clashapi_stub.go b/include/clashapi_stub.go index e7d5304f..53f78148 100644 --- a/include/clashapi_stub.go +++ b/include/clashapi_stub.go @@ -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`) }) } diff --git a/log/factory.go b/log/factory.go index b73fb236..9c1921a3 100644 --- a/log/factory.go +++ b/log/factory.go @@ -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 { diff --git a/option/experimental.go b/option/experimental.go index 3259e6a8..5cf6f51b 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -26,7 +26,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"` diff --git a/route/rule/rule_item_clash_mode.go b/route/rule/rule_item_clash_mode.go index fe2347a0..799ae06d 100644 --- a/route/rule/rule_item_clash_mode.go +++ b/route/rule/rule_item_clash_mode.go @@ -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 { From bdd86c1fef249f670b98fab5a2fe65d1499512c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 1 Sep 2026 22:39:35 +0800 Subject: [PATCH 08/41] Reduce logging allocations --- daemon/log_ring.go | 39 +++++++ daemon/started_service.go | 21 +--- log/format.go | 240 ++++++++++++++++++++------------------ log/observable.go | 20 ++-- 4 files changed, 179 insertions(+), 141 deletions(-) create mode 100644 daemon/log_ring.go diff --git a/daemon/log_ring.go b/daemon/log_ring.go new file mode 100644 index 00000000..f59e9cc2 --- /dev/null +++ b/daemon/log_ring.go @@ -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 +} diff --git a/daemon/started_service.go b/daemon/started_service.go index c9f8f884..5e8c7998 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -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) } @@ -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 { @@ -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 { diff --git a/log/format.go b/log/format.go index 6f4347b1..4ffa9e54 100644 --- a/log/format.go +++ b/log/format.go @@ -6,8 +6,6 @@ import ( "strings" "time" - F "github.com/sagernet/sing/common/format" - "github.com/logrusorgru/aurora" ) @@ -20,155 +18,171 @@ 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 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 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() } diff --git a/log/observable.go b/log/observable.go index a11d4f73..ab50e56b 100644 --- a/log/observable.go +++ b/log/observable.go @@ -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) From 0944cd9d7b5aabe8cb1c52999978d812ff501f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 2 Sep 2026 18:45:51 +0800 Subject: [PATCH 09/41] Reduce cache initial allocation --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c3e941d5..915c9f15 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ 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.0-beta.4.0.20260902104404-02ea509ed96a github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.5 github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df diff --git a/go.sum b/go.sum index b6de9499..8b42c27c 100644 --- a/go.sum +++ b/go.sum @@ -318,8 +318,8 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 h1:sW2O+DoNF+my1PGlSZ3zG2N+fdxLR3DC0l4sXCM0lBE= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E= -github.com/sagernet/sing v0.9.0-beta.4 h1:DYoEzb3FeCZ9bA66hJmL0NFDXckZpa4oYF/VpbFJEkE= -github.com/sagernet/sing v0.9.0-beta.4/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= +github.com/sagernet/sing v0.9.0-beta.4.0.20260902104404-02ea509ed96a h1:uQfvnufKH6xVdkMppQMxiIZEQaXCFutYfAwwgX8IDao= +github.com/sagernet/sing v0.9.0-beta.4.0.20260902104404-02ea509ed96a/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= From 21b8fcacc67af41c26ff797a8b085808e8b3bfa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 2 Sep 2026 18:46:38 +0800 Subject: [PATCH 10/41] Record memory usage continuously in OOM report --- daemon/managed_service.go | 13 +- experimental/boxdd/server.go | 9 +- experimental/libbox/command_server.go | 10 +- .../libbox/internal/oomprofile/oomprofile.go | 22 +- .../runtimeinfo/goroutine_badlinkname.go | 172 ++++++ .../internal/runtimeinfo/goroutine_stub.go | 7 + .../internal/runtimeinfo/pool_badlinkname.go | 81 +++ .../libbox/internal/runtimeinfo/pool_stub.go | 7 + .../internal/runtimeinfo/runtimeinfo.go | 74 +++ experimental/libbox/oom_report.go | 264 ++------- experimental/libbox/power_report.go | 3 +- experimental/libbox/setup.go | 1 + service/oomkiller/lock_unix.go | 26 + service/oomkiller/lock_windows.go | 25 + service/oomkiller/policy.go | 18 +- service/oomkiller/promote.go | 141 +++++ service/oomkiller/recorder.go | 558 ++++++++++++++++++ service/oomkiller/service.go | 58 +- service/oomkiller/service_darwin.go | 66 +-- service/oomkiller/service_stub.go | 12 +- service/oomkiller/timer.go | 42 +- service/oomkiller/timer_darwin.go | 1 + 22 files changed, 1277 insertions(+), 333 deletions(-) create mode 100644 experimental/libbox/internal/runtimeinfo/goroutine_badlinkname.go create mode 100644 experimental/libbox/internal/runtimeinfo/goroutine_stub.go create mode 100644 experimental/libbox/internal/runtimeinfo/pool_badlinkname.go create mode 100644 experimental/libbox/internal/runtimeinfo/pool_stub.go create mode 100644 experimental/libbox/internal/runtimeinfo/runtimeinfo.go create mode 100644 service/oomkiller/lock_unix.go create mode 100644 service/oomkiller/lock_windows.go create mode 100644 service/oomkiller/promote.go create mode 100644 service/oomkiller/recorder.go diff --git a/daemon/managed_service.go b/daemon/managed_service.go index 14c2bd57..dfc986cb 100644 --- a/daemon/managed_service.go +++ b/daemon/managed_service.go @@ -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() { diff --git a/experimental/boxdd/server.go b/experimental/boxdd/server.go index 583a25b5..8e9f6b16 100644 --- a/experimental/boxdd/server.go +++ b/experimental/boxdd/server.go @@ -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() } diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index 2391b74e..c75c4ccd 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -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)) @@ -195,6 +198,7 @@ func (s *CommandServer) Close() { } common.Close(s.listener) s.StartedService.Close() + s.oomRecorder.Close() s.powerManager.Close() } diff --git a/experimental/libbox/internal/oomprofile/oomprofile.go b/experimental/libbox/internal/oomprofile/oomprofile.go index 0728af8f..5d355337 100644 --- a/experimental/libbox/internal/oomprofile/oomprofile.go +++ b/experimental/libbox/internal/oomprofile/oomprofile.go @@ -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{ diff --git a/experimental/libbox/internal/runtimeinfo/goroutine_badlinkname.go b/experimental/libbox/internal/runtimeinfo/goroutine_badlinkname.go new file mode 100644 index 00000000..90b06054 --- /dev/null +++ b/experimental/libbox/internal/runtimeinfo/goroutine_badlinkname.go @@ -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 +} diff --git a/experimental/libbox/internal/runtimeinfo/goroutine_stub.go b/experimental/libbox/internal/runtimeinfo/goroutine_stub.go new file mode 100644 index 00000000..e76ef398 --- /dev/null +++ b/experimental/libbox/internal/runtimeinfo/goroutine_stub.go @@ -0,0 +1,7 @@ +//go:build !badlinkname + +package runtimeinfo + +func collectGoroutines() *GoroutineReport { + return nil +} diff --git a/experimental/libbox/internal/runtimeinfo/pool_badlinkname.go b/experimental/libbox/internal/runtimeinfo/pool_badlinkname.go new file mode 100644 index 00000000..3d4b9867 --- /dev/null +++ b/experimental/libbox/internal/runtimeinfo/pool_badlinkname.go @@ -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 +} diff --git a/experimental/libbox/internal/runtimeinfo/pool_stub.go b/experimental/libbox/internal/runtimeinfo/pool_stub.go new file mode 100644 index 00000000..f5db1c6d --- /dev/null +++ b/experimental/libbox/internal/runtimeinfo/pool_stub.go @@ -0,0 +1,7 @@ +//go:build !badlinkname + +package runtimeinfo + +func collectBufferPools() []PoolReport { + return nil +} diff --git a/experimental/libbox/internal/runtimeinfo/runtimeinfo.go b/experimental/libbox/internal/runtimeinfo/runtimeinfo.go new file mode 100644 index 00000000..b3a9b187 --- /dev/null +++ b/experimental/libbox/internal/runtimeinfo/runtimeinfo.go @@ -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 +} diff --git a/experimental/libbox/oom_report.go b/experimental/libbox/oom_report.go index d9bbbdd5..6cccdb46 100644 --- a/experimental/libbox/oom_report.go +++ b/experimental/libbox/oom_report.go @@ -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) } diff --git a/experimental/libbox/power_report.go b/experimental/libbox/power_report.go index d78c72b1..29648cf6 100644 --- a/experimental/libbox/power_report.go +++ b/experimental/libbox/power_report.go @@ -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) } }, } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 9c3390ad..a78cba54 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -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) diff --git a/service/oomkiller/lock_unix.go b/service/oomkiller/lock_unix.go new file mode 100644 index 00000000..677b20ff --- /dev/null +++ b/service/oomkiller/lock_unix.go @@ -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() +} diff --git a/service/oomkiller/lock_windows.go b/service/oomkiller/lock_windows.go new file mode 100644 index 00000000..7a2767fe --- /dev/null +++ b/service/oomkiller/lock_windows.go @@ -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() +} diff --git a/service/oomkiller/policy.go b/service/oomkiller/policy.go index aa744301..31c4b790 100644 --- a/service/oomkiller/policy.go +++ b/service/oomkiller/policy.go @@ -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() { diff --git a/service/oomkiller/promote.go b/service/oomkiller/promote.go new file mode 100644 index 00000000..6069624a --- /dev/null +++ b/service/oomkiller/promote.go @@ -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() +} diff --git a/service/oomkiller/recorder.go b/service/oomkiller/recorder.go new file mode 100644 index 00000000..a8a089a9 --- /dev/null +++ b/service/oomkiller/recorder.go @@ -0,0 +1,558 @@ +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) +} + +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) +} diff --git a/service/oomkiller/service.go b/service/oomkiller/service.go index d2bef722..7ce51616 100644 --- a/service/oomkiller/service.go +++ b/service/oomkiller/service.go @@ -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() } } diff --git a/service/oomkiller/service_darwin.go b/service/oomkiller/service_darwin.go index 166ddc7d..a3814cde 100644 --- a/service/oomkiller/service_darwin.go +++ b/service/oomkiller/service_darwin.go @@ -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) + } } } diff --git a/service/oomkiller/service_stub.go b/service/oomkiller/service_stub.go index 81ebbf03..f75fe8d5 100644 --- a/service/oomkiller/service_stub.go +++ b/service/oomkiller/service_stub.go @@ -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 } diff --git a/service/oomkiller/timer.go b/service/oomkiller/timer.go index 1f38956f..5dfdd7b3 100644 --- a/service/oomkiller/timer.go +++ b/service/oomkiller/timer.go @@ -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" + } +} diff --git a/service/oomkiller/timer_darwin.go b/service/oomkiller/timer_darwin.go index f73ab28f..7cb1993e 100644 --- a/service/oomkiller/timer_darwin.go +++ b/service/oomkiller/timer_darwin.go @@ -5,6 +5,7 @@ package oomkiller import runtimeDebug "runtime/debug" func (t *adaptiveTimer) notifyPressure() { + badCleanup() runtimeDebug.FreeOSMemory() t.access.Lock() t.startLocked() From ad8d0539850ecc60fb4b08dc0be5875d0de7cfaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 2 Sep 2026 22:22:32 +0800 Subject: [PATCH 11/41] Update dependencies --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 915c9f15..f2483a74 100644 --- a/go.mod +++ b/go.mod @@ -45,22 +45,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.0.20260902104404-02ea509ed96a + github.com/sagernet/sing v0.9.0 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.5 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.0 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-0.20260823125007-8bd032a91a30 + github.com/sagernet/wireguard-go v0.0.5 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.12.0 diff --git a/go.sum b/go.sum index 8b42c27c..cd7dc182 100644 --- a/go.sum +++ b/go.sum @@ -318,8 +318,8 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 h1:sW2O+DoNF+my1PGlSZ3zG2N+fdxLR3DC0l4sXCM0lBE= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E= -github.com/sagernet/sing v0.9.0-beta.4.0.20260902104404-02ea509ed96a h1:uQfvnufKH6xVdkMppQMxiIZEQaXCFutYfAwwgX8IDao= -github.com/sagernet/sing v0.9.0-beta.4.0.20260902104404-02ea509ed96a/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= +github.com/sagernet/sing v0.9.0 h1:NQvJxtYEl+2uIh/Bkxf5cqAZfZuFCQKQe2z85Pbfxag= +github.com/sagernet/sing v0.9.0/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= @@ -328,8 +328,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= @@ -338,18 +338,18 @@ 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.0 h1:02AbhT1Ne3O2T/L4ZZmYGBW6k3VI9fssMq9LnGEzseQ= +github.com/sagernet/sing-tun v0.9.0/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/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= -github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= +github.com/sagernet/sing-vmess v0.2.8 h1:xd5nnDOMlC76RgrLksS4jlk3eMt3c3CvQY3NsjWPWeI= +github.com/sagernet/sing-vmess v0.2.8/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.4 h1:b8rLu1OcWPBEtfwPmCI+jOHrMcVEnZODY63/lirNSYc= github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.4/go.mod h1:WLUSOPmTcf7VN9gLCe01qUSIvD+/cKC177neENyZPkI= -github.com/sagernet/wireguard-go v0.0.5-0.20260823125007-8bd032a91a30 h1:Z9QAr893OvR9qDHvxYTsYZhpvqwcc0LWbJ4ECg/eBgg= -github.com/sagernet/wireguard-go v0.0.5-0.20260823125007-8bd032a91a30/go.mod h1:er10sELpmzLXq7S7Pbc1Zsbyapcr+/gxNAHKTo6fzVA= +github.com/sagernet/wireguard-go v0.0.5 h1:FLoxIfX8bcedFqn3Yfop7tJNQV1NeMNRZ/9LQLQdKvU= +github.com/sagernet/wireguard-go v0.0.5/go.mod h1:er10sELpmzLXq7S7Pbc1Zsbyapcr+/gxNAHKTo6fzVA= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= From 4bc15be97c25fa34453dbeab553f2a0c29a75539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 2 Sep 2026 23:05:36 +0800 Subject: [PATCH 12/41] tun: Fix auto_redirect pre-match skipped for forwarded traffic --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f2483a74..979259e4 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( 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 + github.com/sagernet/sing-tun v0.9.1-0.20260902150428-1bd9bb8b83e8 github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca github.com/sagernet/sing-vmess v0.2.8 github.com/sagernet/smux v1.5.50-sing-box-mod.1 diff --git a/go.sum b/go.sum index cd7dc182..2e7e94d4 100644 --- a/go.sum +++ b/go.sum @@ -338,8 +338,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 h1:02AbhT1Ne3O2T/L4ZZmYGBW6k3VI9fssMq9LnGEzseQ= -github.com/sagernet/sing-tun v0.9.0/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0= +github.com/sagernet/sing-tun v0.9.1-0.20260902150428-1bd9bb8b83e8 h1:2BKKMS9mkNV+rgWospnRbyFYDapYFEgq/1RJnp9gNtE= +github.com/sagernet/sing-tun v0.9.1-0.20260902150428-1bd9bb8b83e8/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/sing-vmess v0.2.8 h1:xd5nnDOMlC76RgrLksS4jlk3eMt3c3CvQY3NsjWPWeI= From e4a19a2a7cd20aa59da93c6170cee573d008768d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 3 Sep 2026 13:28:39 +0800 Subject: [PATCH 13/41] Enable ARM64 cryptographic extensions in iOS library builds Go's internal/cpu never detects ARM64 features on GOOS=ios, so AES, AES-GCM and SHA-256 use their generic implementations on iOS and tvOS. Patch the Go source in the Apple library build to assert the ARMv8.0 cryptographic extensions, as Go already does for macOS. See https://github.com/SagerNet/sing-box/issues/4486 --- .github/go_ios_cpu_features.patch | 28 +++++++++++++++++++++++++ .github/patch_go_for_ios.sh | 34 +++++++++++++++++++++++++++++++ .github/workflows/build.yml | 5 +++++ 3 files changed, 67 insertions(+) create mode 100644 .github/go_ios_cpu_features.patch create mode 100755 .github/patch_go_for_ios.sh diff --git a/.github/go_ios_cpu_features.patch b/.github/go_ios_cpu_features.patch new file mode 100644 index 00000000..704d2d76 --- /dev/null +++ b/.github/go_ios_cpu_features.patch @@ -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 + diff --git a/.github/patch_go_for_ios.sh b/.github/patch_go_for_ios.sh new file mode 100755 index 00000000..6105dd50 --- /dev/null +++ b/.github/patch_go_for_ios.sh @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b30ac60d..fe7c43a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1246,6 +1246,9 @@ jobs: with: go-version: 1.26.7 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 @@ -1265,6 +1268,8 @@ jobs: 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" From 5c7467aaba485b3d9014195bd4feb684afd06654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 3 Sep 2026 13:30:52 +0800 Subject: [PATCH 14/41] oomkiller: Suppress unused lint for recordPressure on non-darwin platforms --- service/oomkiller/recorder.go | 1 + 1 file changed, 1 insertion(+) diff --git a/service/oomkiller/recorder.go b/service/oomkiller/recorder.go index a8a089a9..c8805ef8 100644 --- a/service/oomkiller/recorder.go +++ b/service/oomkiller/recorder.go @@ -324,6 +324,7 @@ func (r *Recorder) sample(sample memorySample, state pressureState, connections r.chown(timelinePath) } +//nolint:unused func (r *Recorder) recordPressure(sample memorySample) { r.access.Lock() defer r.access.Unlock() From 94ae1619564fe73b01475f104c090ce18a1679db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 3 Sep 2026 15:13:51 +0800 Subject: [PATCH 15/41] dns: Reject resolver discovery queries --- dns/router.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dns/router.go b/dns/router.go index 46355c3f..e7c0e964 100644 --- a/dns/router.go +++ b/dns/router.go @@ -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 { From a25ad8ce5256a2e05faef57198679b0feeaf96ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 5 Sep 2026 13:45:13 +0800 Subject: [PATCH 16/41] Fix h2mux with Go 1.27 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 979259e4..f5f9f35f 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 github.com/sagernet/sing v0.9.0 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 diff --git a/go.sum b/go.sum index 2e7e94d4..e919d0d2 100644 --- a/go.sum +++ b/go.sum @@ -322,8 +322,8 @@ github.com/sagernet/sing v0.9.0 h1:NQvJxtYEl+2uIh/Bkxf5cqAZfZuFCQKQe2z85Pbfxag= github.com/sagernet/sing v0.9.0/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= -github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= -github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= +github.com/sagernet/sing-mux v0.3.6 h1:ix+CPhZsR9XIAmEVLIUdz+itDzXs1T6iKO8eF/XOVOQ= +github.com/sagernet/sing-mux v0.3.6/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df h1:ogyC7KPTWueN7kXFhKSdHeS5/t/zBjQgAOpWjqnrMos= 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= From 288411b0b9044c11a00a8ab478000e3ec1133101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Sep 2026 14:53:49 +0800 Subject: [PATCH 17/41] Fix crash when interface monitor is unavailable --- dns/transport/dhcp/dhcp.go | 6 +++++- protocol/direct/outbound.go | 6 +++++- route/rule/rule_default_interface_address.go | 3 +++ route/rule/rule_network_interface_address.go | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/dns/transport/dhcp/dhcp.go b/dns/transport/dhcp/dhcp.go index c1ab2ffa..0f0aa102 100644 --- a/dns/transport/dhcp/dhcp.go +++ b/dns/transport/dhcp/dhcp.go @@ -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() diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index 1ca35af0..020b3d62 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -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 } diff --git a/route/rule/rule_default_interface_address.go b/route/rule/rule_default_interface_address.go index 2d7fdebe..611cb0dc 100644 --- a/route/rule/rule_default_interface_address.go +++ b/route/rule/rule_default_interface_address.go @@ -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 diff --git a/route/rule/rule_network_interface_address.go b/route/rule/rule_network_interface_address.go index e331d622..d20e6781 100644 --- a/route/rule/rule_network_interface_address.go +++ b/route/rule/rule_network_interface_address.go @@ -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 { From 4ef3432c0d00a1b653d1e8ea244debc8258dad83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Sep 2026 15:51:35 +0800 Subject: [PATCH 18/41] Fix Linux process search matching the wrong socket --- common/process/searcher_linux.go | 4 +- common/process/socket_diag_linux.go | 150 +++++++++++++++++++---- common/process/socket_diag_linux_test.go | 108 ++++++++++++++++ 3 files changed, 234 insertions(+), 28 deletions(-) create mode 100644 common/process/socket_diag_linux_test.go diff --git a/common/process/searcher_linux.go b/common/process/searcher_linux.go index d226c0e4..573899c2 100644 --- a/common/process/searcher_linux.go +++ b/common/process/searcher_linux.go @@ -101,6 +101,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,7 +120,7 @@ func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.Addr return 0, 0, err } } - return querySocketDiagOnce(family, protocol, source) + return dumpSocketDiag(family, protocol, source, destination) } func (s *linuxSearcher) findProcessPath(targetInode, uid uint32) (string, error) { diff --git a/common/process/socket_diag_linux.go b/common/process/socket_diag_linux.go index 0adb3d13..b4769b85 100644 --- a/common/process/socket_diag_linux.go +++ b/common/process/socket_diag_linux.go @@ -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 { diff --git a/common/process/socket_diag_linux_test.go b/common/process/socket_diag_linux_test.go new file mode 100644 index 00000000..1aec6f20 --- /dev/null +++ b/common/process/socket_diag_linux_test.go @@ -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) +} From 0265f31f8402de79faff14bf1bbbb0ab7fc3079c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Sep 2026 15:51:35 +0800 Subject: [PATCH 19/41] Fix Linux process path lookup after privilege drop --- common/process/searcher_linux.go | 39 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/common/process/searcher_linux.go b/common/process/searcher_linux.go index 573899c2..570e23d3 100644 --- a/common/process/searcher_linux.go +++ b/common/process/searcher_linux.go @@ -22,7 +22,11 @@ import ( "github.com/sagernet/sing/contrab/maphash" ) -const pathProc = "/proc" +const ( + pathProc = "/proc" + + processPathsAllUsers = ^uint32(0) +) var _ Searcher = (*linuxSearcher)(nil) @@ -123,25 +127,30 @@ func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.Addr 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 @@ -159,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()) From 0d7fae13bc64376615aff48ebe10ea35109cef19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Sep 2026 15:54:54 +0800 Subject: [PATCH 20/41] Fix process search for socks UDP associate --- go.mod | 2 +- go.sum | 4 ++-- protocol/mixed/inbound.go | 2 ++ protocol/socks/inbound.go | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f5f9f35f..0b0d226e 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ 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 + github.com/sagernet/sing v0.9.1 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.6 github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df diff --git a/go.sum b/go.sum index e919d0d2..d9cbe87e 100644 --- a/go.sum +++ b/go.sum @@ -318,8 +318,8 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 h1:sW2O+DoNF+my1PGlSZ3zG2N+fdxLR3DC0l4sXCM0lBE= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E= -github.com/sagernet/sing v0.9.0 h1:NQvJxtYEl+2uIh/Bkxf5cqAZfZuFCQKQe2z85Pbfxag= -github.com/sagernet/sing v0.9.0/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= +github.com/sagernet/sing v0.9.1 h1:DGIEmmCdB0fpPUPJ/E7gjqEFZccgfjlqxPZHm8KldK0= +github.com/sagernet/sing v0.9.1/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= github.com/sagernet/sing-mux v0.3.6 h1:ix+CPhZsR9XIAmEVLIUdz+itDzXs1T6iKO8eF/XOVOQ= diff --git a/protocol/mixed/inbound.go b/protocol/mixed/inbound.go index d3531947..c6cd08cb 100644 --- a/protocol/mixed/inbound.go +++ b/protocol/mixed/inbound.go @@ -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" @@ -148,6 +149,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() { diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go index 0f570b51..c498efc4 100644 --- a/protocol/socks/inbound.go +++ b/protocol/socks/inbound.go @@ -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" ) @@ -99,6 +100,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() { From 0c041aa7f66e862eacc0655904ea5e9af9b5ee20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 6 Sep 2026 16:25:12 +0800 Subject: [PATCH 21/41] Fix direct inbound UDP on 32-bit Linux before 5.1 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0b0d226e..3629d5df 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ 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.1 + github.com/sagernet/sing v0.9.2 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.6 github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df diff --git a/go.sum b/go.sum index d9cbe87e..7beea7ee 100644 --- a/go.sum +++ b/go.sum @@ -318,8 +318,8 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 h1:sW2O+DoNF+my1PGlSZ3zG2N+fdxLR3DC0l4sXCM0lBE= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E= -github.com/sagernet/sing v0.9.1 h1:DGIEmmCdB0fpPUPJ/E7gjqEFZccgfjlqxPZHm8KldK0= -github.com/sagernet/sing v0.9.1/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= +github.com/sagernet/sing v0.9.2 h1:HnNoCn7nZoPd7iDMXIKR6zJfM2Lq2a2Fi4SL4PMkXS4= +github.com/sagernet/sing v0.9.2/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= github.com/sagernet/sing-mux v0.3.6 h1:ix+CPhZsR9XIAmEVLIUdz+itDzXs1T6iKO8eF/XOVOQ= From 515a73e4eeb5961b473abae53867595f1afda257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Sep 2026 13:19:30 +0800 Subject: [PATCH 22/41] Fix selector not interrupting routed connections --- protocol/group/selector.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol/group/selector.go b/protocol/group/selector.go index 0542d073..3d1a8bf4 100644 --- a/protocol/group/selector.go +++ b/protocol/group/selector.go @@ -166,7 +166,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) } } @@ -176,7 +176,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) } } From b2a5ac3f606965c8a8bb6a50932c06e71bf4ba27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Sep 2026 13:38:49 +0800 Subject: [PATCH 23/41] Fix Tailscale endpoint not binding IPv6 sockets on Windows --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3629d5df..83ad4096 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ 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.2 + github.com/sagernet/sing v0.9.3 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.6 github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df diff --git a/go.sum b/go.sum index 7beea7ee..a084d642 100644 --- a/go.sum +++ b/go.sum @@ -318,8 +318,8 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 h1:sW2O+DoNF+my1PGlSZ3zG2N+fdxLR3DC0l4sXCM0lBE= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E= -github.com/sagernet/sing v0.9.2 h1:HnNoCn7nZoPd7iDMXIKR6zJfM2Lq2a2Fi4SL4PMkXS4= -github.com/sagernet/sing v0.9.2/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= +github.com/sagernet/sing v0.9.3 h1:66QWoMcD24KrQ4PqiaNNWApogJaf6LoozcW2RPtJ7KE= +github.com/sagernet/sing v0.9.3/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= github.com/sagernet/sing-mux v0.3.6 h1:ix+CPhZsR9XIAmEVLIUdz+itDzXs1T6iKO8eF/XOVOQ= From d6ec014ca49ed974824c061f4032d2f6fd54a254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Sep 2026 15:01:09 +0800 Subject: [PATCH 24/41] Fix omitempty for JSON struct fields Claude-Session: https://claude.ai/code/session_01NxBrtZjwwaJC8uxCNbgmKu --- option/http.go | 10 +++++----- option/openvpn.go | 8 ++++---- protocol/openvpn/client.go | 4 ++-- protocol/openvpn/server.go | 6 +++--- test/openvpn_test.go | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/option/http.go b/option/http.go index 4c6da9de..c51b2643 100644 --- a/option/http.go +++ b/option/http.go @@ -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 { diff --git a/option/openvpn.go b/option/openvpn.go index 7a12a4f0..d62aaf98 100644 --- a/option/openvpn.go +++ b/option/openvpn.go @@ -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"` diff --git a/protocol/openvpn/client.go b/protocol/openvpn/client.go index 42715ef6..03f8e07a 100644 --- a/protocol/openvpn/client.go +++ b/protocol/openvpn/client.go @@ -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") } diff --git a/protocol/openvpn/server.go b/protocol/openvpn/server.go index 9c4ee269..cd8ccc66 100644 --- a/protocol/openvpn/server.go +++ b/protocol/openvpn/server.go @@ -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") } diff --git a/test/openvpn_test.go b/test/openvpn_test.go index 80f27e58..950db0cb 100644 --- a/test/openvpn_test.go +++ b/test/openvpn_test.go @@ -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", From 6364d9a5f12443b06f3919f8afbf22b24ec67010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Sep 2026 15:19:58 +0800 Subject: [PATCH 25/41] Fix WireGuard endpoint stopping on device sleep --- transport/wireguard/endpoint.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index 45228443..20edc21a 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -300,9 +300,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() } } From f90999708232e6dfc10bceeac7c08c19e597c454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Sep 2026 16:27:29 +0800 Subject: [PATCH 26/41] Fix bypass with outbound not bypassing in pre-match --- adapter/router.go | 6 +++++- go.mod | 2 +- go.sum | 4 ++-- route/route.go | 12 ++++++++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index 1de092b1..be626741 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -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: diff --git a/go.mod b/go.mod index 83ad4096..a513fce2 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( 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.1-0.20260902150428-1bd9bb8b83e8 + github.com/sagernet/sing-tun v0.9.1 github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca github.com/sagernet/sing-vmess v0.2.8 github.com/sagernet/smux v1.5.50-sing-box-mod.1 diff --git a/go.sum b/go.sum index a084d642..dcd489bf 100644 --- a/go.sum +++ b/go.sum @@ -338,8 +338,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.1-0.20260902150428-1bd9bb8b83e8 h1:2BKKMS9mkNV+rgWospnRbyFYDapYFEgq/1RJnp9gNtE= -github.com/sagernet/sing-tun v0.9.1-0.20260902150428-1bd9bb8b83e8/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0= +github.com/sagernet/sing-tun v0.9.1 h1:eyz5ae+vTlLXMCfeKiNureMihMaP8QAABoHuplQj31A= +github.com/sagernet/sing-tun v0.9.1/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/sing-vmess v0.2.8 h1:xd5nnDOMlC76RgrLksS4jlk3eMt3c3CvQY3NsjWPWeI= diff --git a/route/route.go b/route/route.go index 93a00225..fc353cdb 100644 --- a/route/route.go +++ b/route/route.go @@ -392,7 +392,15 @@ 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 errors.Is(rejectErr, R.ErrDrop) { @@ -512,9 +520,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 { From 89ca622e99c5618b39adb638477b593c9e8cf708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Sep 2026 18:45:52 +0800 Subject: [PATCH 27/41] Fix WireGuard endpoint stuck after network change on macOS --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a513fce2..d4051f85 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( 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 + github.com/sagernet/wireguard-go v0.0.6 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.12.0 diff --git a/go.sum b/go.sum index dcd489bf..fe687857 100644 --- a/go.sum +++ b/go.sum @@ -348,8 +348,8 @@ github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1h github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.4 h1:b8rLu1OcWPBEtfwPmCI+jOHrMcVEnZODY63/lirNSYc= github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.4/go.mod h1:WLUSOPmTcf7VN9gLCe01qUSIvD+/cKC177neENyZPkI= -github.com/sagernet/wireguard-go v0.0.5 h1:FLoxIfX8bcedFqn3Yfop7tJNQV1NeMNRZ/9LQLQdKvU= -github.com/sagernet/wireguard-go v0.0.5/go.mod h1:er10sELpmzLXq7S7Pbc1Zsbyapcr+/gxNAHKTo6fzVA= +github.com/sagernet/wireguard-go v0.0.6 h1:zAjWSzufYThDsyLHxPLyyR+XdpBMlFDWD7WxJAvynSw= +github.com/sagernet/wireguard-go v0.0.6/go.mod h1:er10sELpmzLXq7S7Pbc1Zsbyapcr+/gxNAHKTo6fzVA= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= From 046c46eacb92336152c4c8b2c115f66dcff9a70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Sep 2026 19:01:17 +0800 Subject: [PATCH 28/41] Fix config check sharing service registry with the running instance --- cmd/sing-box/cmd_check.go | 3 ++- cmd/sing-box/cmd_run.go | 3 ++- cmd/sing-box/cmd_tools.go | 3 ++- daemon/instance.go | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/sing-box/cmd_check.go b/cmd/sing-box/cmd_check.go index 29a39081..5bd1093f 100644 --- a/cmd/sing-box/cmd_check.go +++ b/cmd/sing-box/cmd_check.go @@ -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, diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index 2037a979..fecd99a0 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -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, diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index 55e5b458..3c60a49a 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -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") } diff --git a/daemon/instance.go b/daemon/instance.go index 495b2b99..5e7502ce 100644 --- a/daemon/instance.go +++ b/daemon/instance.go @@ -39,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 From 6d1fc214c16bd4c45510012a898b7fa82f045862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Sep 2026 22:30:33 +0800 Subject: [PATCH 29/41] Authorize enabling insecure mode with PolicyKit on Linux --- ...e_takeover_linux.go => authorize_linux.go} | 42 +++++++++++++++---- experimental/boxdd/authorize_stub.go | 21 ++++++++++ experimental/boxdd/authorize_takeover_stub.go | 9 ---- experimental/boxdd/desktop_service.go | 13 +++--- .../boxdd/insecure_mode_authorize_linux.go | 24 ----------- .../boxdd/insecure_mode_authorize_stub.go | 7 ---- 6 files changed, 60 insertions(+), 56 deletions(-) rename experimental/boxdd/{authorize_takeover_linux.go => authorize_linux.go} (62%) create mode 100644 experimental/boxdd/authorize_stub.go delete mode 100644 experimental/boxdd/authorize_takeover_stub.go delete mode 100644 experimental/boxdd/insecure_mode_authorize_linux.go delete mode 100644 experimental/boxdd/insecure_mode_authorize_stub.go diff --git a/experimental/boxdd/authorize_takeover_linux.go b/experimental/boxdd/authorize_linux.go similarity index 62% rename from experimental/boxdd/authorize_takeover_linux.go rename to experimental/boxdd/authorize_linux.go index 8fd1912c..9cdf0430 100644 --- a/experimental/boxdd/authorize_takeover_linux.go +++ b/experimental/boxdd/authorize_linux.go @@ -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") } } diff --git a/experimental/boxdd/authorize_stub.go b/experimental/boxdd/authorize_stub.go new file mode 100644 index 00000000..7c4cf241 --- /dev/null +++ b/experimental/boxdd/authorize_stub.go @@ -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 +} diff --git a/experimental/boxdd/authorize_takeover_stub.go b/experimental/boxdd/authorize_takeover_stub.go deleted file mode 100644 index c62b68d5..00000000 --- a/experimental/boxdd/authorize_takeover_stub.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !linux - -package main - -import "context" - -func authorizeTakeOver(ctx context.Context, identity peerIdentity) error { - return nil -} diff --git a/experimental/boxdd/desktop_service.go b/experimental/boxdd/desktop_service.go index e9422fec..1942f9fb 100644 --- a/experimental/boxdd/desktop_service.go +++ b/experimental/boxdd/desktop_service.go @@ -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 { diff --git a/experimental/boxdd/insecure_mode_authorize_linux.go b/experimental/boxdd/insecure_mode_authorize_linux.go deleted file mode 100644 index 35769a0c..00000000 --- a/experimental/boxdd/insecure_mode_authorize_linux.go +++ /dev/null @@ -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 -} diff --git a/experimental/boxdd/insecure_mode_authorize_stub.go b/experimental/boxdd/insecure_mode_authorize_stub.go deleted file mode 100644 index 8040cbea..00000000 --- a/experimental/boxdd/insecure_mode_authorize_stub.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !linux - -package main - -func authorizeDisableInsecureMode(identity peerIdentity) error { - return nil -} From 70d0ba72799f34f1999a7ac56d20a7e43723849f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 9 Sep 2026 12:46:59 +0800 Subject: [PATCH 30/41] Fix network monitor spinning after netlink receive overrun on Linux --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d4051f85..dc7f7cd6 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( 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.1 + github.com/sagernet/sing-tun v0.9.2 github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca github.com/sagernet/sing-vmess v0.2.8 github.com/sagernet/smux v1.5.50-sing-box-mod.1 diff --git a/go.sum b/go.sum index fe687857..4db2d772 100644 --- a/go.sum +++ b/go.sum @@ -338,8 +338,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.1 h1:eyz5ae+vTlLXMCfeKiNureMihMaP8QAABoHuplQj31A= -github.com/sagernet/sing-tun v0.9.1/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0= +github.com/sagernet/sing-tun v0.9.2 h1:xTg0qISVo6pIw7kEDlqQsP9jUhw7tLkfrOgX5gZkBL8= +github.com/sagernet/sing-tun v0.9.2/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/sing-vmess v0.2.8 h1:xd5nnDOMlC76RgrLksS4jlk3eMt3c3CvQY3NsjWPWeI= From 8d0500eded81f787104171d58f6bb4aae7e2d7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 10 Sep 2026 13:47:21 +0800 Subject: [PATCH 31/41] Fix auto redirect pre-match and L3 forwarding --- go.mod | 2 +- go.sum | 4 ++-- route/route.go | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dc7f7cd6..fb5c6a32 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( 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.2 + 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 github.com/sagernet/smux v1.5.50-sing-box-mod.1 diff --git a/go.sum b/go.sum index 4db2d772..6d2ac5fd 100644 --- a/go.sum +++ b/go.sum @@ -338,8 +338,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.2 h1:xTg0qISVo6pIw7kEDlqQsP9jUhw7tLkfrOgX5gZkBL8= -github.com/sagernet/sing-tun v0.9.2/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/sing-vmess v0.2.8 h1:xd5nnDOMlC76RgrLksS4jlk3eMt3c3CvQY3NsjWPWeI= diff --git a/route/route.go b/route/route.go index fc353cdb..62f1c865 100644 --- a/route/route.go +++ b/route/route.go @@ -403,6 +403,9 @@ func (r *Router) PreMatch(metadata adapter.InboundContext, firstPacket []byte) a 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} } From 378f151c75fac502813cd5235c8bd444996cfd05 Mon Sep 17 00:00:00 2001 From: Bruce Wayne Date: Thu, 10 Sep 2026 16:47:09 +0800 Subject: [PATCH 32/41] documentation: Fix endpoint_independent_nat --- docs/clients/android/features.md | 1 - docs/clients/apple/features.md | 1 - docs/configuration/inbound/tun.md | 9 +++------ docs/configuration/inbound/tun.zh.md | 4 ++-- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/clients/android/features.md b/docs/clients/android/features.md index b76a6418..da1fa6f3 100644 --- a/docs/clients/android/features.md +++ b/docs/clients/android/features.md @@ -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 | diff --git a/docs/clients/apple/features.md b/docs/clients/apple/features.md index 67f0a84b..027cb603 100644 --- a/docs/clients/apple/features.md +++ b/docs/clients/apple/features.md @@ -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 | diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index eef22028..ec8829ea 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -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 diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index b1f226af..726d34e8 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -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 From b7eb49bb87903ff494df732a55b8e1cf137f7559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 12 Sep 2026 18:52:34 +0800 Subject: [PATCH 33/41] Fix cronet-go --- .github/CRONET_GO_VERSION | 2 +- go.mod | 62 +++++++++---------- go.sum | 124 +++++++++++++++++++------------------- 3 files changed, 94 insertions(+), 94 deletions(-) diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index 95a68fba..ff9f0d16 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -45832ab074849607406baa3e3a2c4660274602ed +0d28acc44093df24b2526dea3d6ffefd6b0a54f0 diff --git a/go.mod b/go.mod index fb5c6a32..c2fac649 100644 --- a/go.mod +++ b/go.mod @@ -36,8 +36,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 @@ -140,35 +140,35 @@ require ( github.com/pires/go-proxyproto v0.8.1 // 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/smallstep/pkcs7 v0.1.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect diff --git a/go.sum b/go.sum index 6d2ac5fd..ecd68999 100644 --- a/go.sum +++ b/go.sum @@ -242,68 +242,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= From c827ec83035545a74a0df521185d43d0a0876d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 4 Sep 2026 22:55:44 +0800 Subject: [PATCH 34/41] Read process name from comm in resolved fallback --- service/resolved/resolve1.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/resolved/resolve1.go b/service/resolved/resolve1.go index 6b347060..dadc975b 100644 --- a/service/resolved/resolve1.go +++ b/service/resolved/resolve1.go @@ -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)) } } From 54e5c497bb45ef758e8420d4b7b5b44563b0741d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 5 Sep 2026 16:53:14 +0800 Subject: [PATCH 35/41] Wake the current instance after device pause on iOS --- experimental/libbox/command_server.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index c75c4ccd..5f62d03d 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -193,6 +193,9 @@ func (s *CommandServer) Start() error { } func (s *CommandServer) Close() { + if s.endPauseTimer != nil { + s.endPauseTimer.Stop() + } if s.grpcServer != nil { s.grpcServer.Stop() } @@ -264,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 { From 9713a546e63e87ede22c26d19055944f445aa59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 13 Sep 2026 20:53:30 +0800 Subject: [PATCH 36/41] Update Go to 1.26.8 --- .github/setup_go_for_macos1013.sh | 2 +- .github/setup_go_for_windows7.sh | 2 +- .github/workflows/build.yml | 30 +++++++++++++++--------------- .github/workflows/docker.yml | 2 +- .github/workflows/linux.yml | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/setup_go_for_macos1013.sh b/.github/setup_go_for_macos1013.sh index b2668c89..b37543fd 100755 --- a/.github/setup_go_for_macos1013.sh +++ b/.github/setup_go_for_macos1013.sh @@ -2,7 +2,7 @@ set -euo pipefail -VERSION="1.26.7" +VERSION="1.26.8" PATCH_COMMITS=( "f080b0c6346eb690c0dc82497b35925f385b35ac" "2d9c12887c342fb9051d231aa5388743cb7e9cb6" diff --git a/.github/setup_go_for_windows7.sh b/.github/setup_go_for_windows7.sh index 1256c69a..f3065a34 100755 --- a/.github/setup_go_for_windows7.sh +++ b/.github/setup_go_for_windows7.sh @@ -2,7 +2,7 @@ set -euo pipefail -VERSION="1.26.7" +VERSION="1.26.8" PATCH_COMMITS=( "a4ae550aa148b04c9d4890e98bee63aede5c4b53" "95b851f661584711faa8115b3234a461044f4510" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe7c43a7..e42b097b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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,7 +1244,7 @@ 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') @@ -1261,8 +1261,8 @@ 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 @@ -1331,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 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index c5e1778b..d525f4f0 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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: | diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index fd2ba7b3..985c787d 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -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: | From 73a4196cb323737ccfb9700741775fe35e1437c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 14 Sep 2026 23:18:42 +0800 Subject: [PATCH 37/41] Fix closed connection errors not recognized on Windows --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c2fac649..ffe890d2 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ 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.3 + 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.6 github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df diff --git a/go.sum b/go.sum index ecd68999..a7ef96de 100644 --- a/go.sum +++ b/go.sum @@ -318,8 +318,8 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 h1:sW2O+DoNF+my1PGlSZ3zG2N+fdxLR3DC0l4sXCM0lBE= github.com/sagernet/quic-go v0.61.0-sing-box-mod.7/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E= -github.com/sagernet/sing v0.9.3 h1:66QWoMcD24KrQ4PqiaNNWApogJaf6LoozcW2RPtJ7KE= -github.com/sagernet/sing v0.9.3/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= +github.com/sagernet/sing v0.9.4 h1:nIdH+Yvy4NzTezB+c3z/9of1jLRggnrtMtb3uMOIaNM= +github.com/sagernet/sing v0.9.4/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= github.com/sagernet/sing-mux v0.3.6 h1:ix+CPhZsR9XIAmEVLIUdz+itDzXs1T6iKO8eF/XOVOQ= From b5d5338e525154be9ef2eabfa0ec2fd230d517a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 15 Sep 2026 00:18:43 +0800 Subject: [PATCH 38/41] Fix memory pressure callback restarting stopped OOM killer timer --- service/oomkiller/timer_darwin.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/service/oomkiller/timer_darwin.go b/service/oomkiller/timer_darwin.go index 7cb1993e..7cac34bc 100644 --- a/service/oomkiller/timer_darwin.go +++ b/service/oomkiller/timer_darwin.go @@ -8,7 +8,10 @@ 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() From 6c87a0c04423b177ae83e453ce1749e07144d452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 15 Sep 2026 00:18:43 +0800 Subject: [PATCH 39/41] Fix Tailscale netmap expiry timer keeping closed endpoint alive --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ffe890d2..5f09cf8f 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca 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/tailscale v1.102.1-sing-box-1.14-mod.5 github.com/sagernet/wireguard-go v0.0.6 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index a7ef96de..669bcba5 100644 --- a/go.sum +++ b/go.sum @@ -346,8 +346,8 @@ github.com/sagernet/sing-vmess v0.2.8 h1:xd5nnDOMlC76RgrLksS4jlk3eMt3c3CvQY3NsjW github.com/sagernet/sing-vmess v0.2.8/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= -github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.4 h1:b8rLu1OcWPBEtfwPmCI+jOHrMcVEnZODY63/lirNSYc= -github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.4/go.mod h1:WLUSOPmTcf7VN9gLCe01qUSIvD+/cKC177neENyZPkI= +github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.5 h1:mGLWVgj2Pl2NlgE1+sEUIDIMB42JdWEPwYmxTnVWg44= +github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.5/go.mod h1:WLUSOPmTcf7VN9gLCe01qUSIvD+/cKC177neENyZPkI= github.com/sagernet/wireguard-go v0.0.6 h1:zAjWSzufYThDsyLHxPLyyR+XdpBMlFDWD7WxJAvynSw= github.com/sagernet/wireguard-go v0.0.6/go.mod h1:er10sELpmzLXq7S7Pbc1Zsbyapcr+/gxNAHKTo6fzVA= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= From dc3b49014914f64e6cce21cdc39683e53008f72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 15 Sep 2026 01:21:36 +0800 Subject: [PATCH 40/41] Fix Apple DerivedData cache shared across branches --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e42b097b..049399e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1375,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: |- From 1ac1a339cb1223e9c70eae14c44411c75033c02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 14 Sep 2026 23:29:15 +0800 Subject: [PATCH 41/41] Bump version --- clients/android | 2 +- clients/apple | 2 +- clients/desktop | 2 +- docs/changelog.md | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/clients/android b/clients/android index af610983..a3668ae6 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit af61098358a8141dea71f232b7eaebf4ccee8868 +Subproject commit a3668ae6e4bbcb3ceff8461d0cac55d79edf504f diff --git a/clients/apple b/clients/apple index 008f73fc..2b1763a8 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit 008f73fc6d576aced976659d6dc52a9a12365c24 +Subproject commit 2b1763a80f2c1dee1ab3ac62d84dbda7dc5178f4 diff --git a/clients/desktop b/clients/desktop index cebee0d5..fb1175ca 160000 --- a/clients/desktop +++ b/clients/desktop @@ -1 +1 @@ -Subproject commit cebee0d527c4e5d5500f971553628e0dfa8bae0f +Subproject commit fb1175ca3b00bfc676c4b4b2b25f5eacb6a0c7ae diff --git a/docs/changelog.md b/docs/changelog.md index 3474f88a..35bfd648 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.14.1 + +* Fixes and improvements + #### 1.14.0 Important changes since 1.13: