diff --git a/adapter/inbound.go b/adapter/inbound.go index 42f0825c..ae1067d9 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -54,6 +54,11 @@ type InboundContext struct { User string Outbound string + // power report + + RouteRule string + RouteOutbound string + // sniffer Protocol string @@ -173,6 +178,17 @@ func DNSResponseAddresses(response *dns.Msg) []netip.Addr { type inboundContextKey struct{} +type dnsTransportTagKey struct{} + +func ContextWithDNSTransportTag(ctx context.Context, transportTag string) context.Context { + return context.WithValue(ctx, (*dnsTransportTagKey)(nil), transportTag) +} + +func DNSTransportTagFromContext(ctx context.Context) (string, bool) { + transportTag, loaded := ctx.Value((*dnsTransportTagKey)(nil)).(string) + return transportTag, loaded +} + func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context { return context.WithValue(ctx, (*inboundContextKey)(nil), inboundContext) } diff --git a/common/dialer/default.go b/common/dialer/default.go index dbe869bd..0bffbf1c 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -5,6 +5,7 @@ import ( "errors" "net" "net/netip" + "slices" "syscall" "time" @@ -42,6 +43,8 @@ type DefaultDialer struct { connectionManager adapter.ConnectionManager networkManager adapter.NetworkManager powerManager *powerreport.Manager + outboundManager adapter.OutboundManager + dnsTransportManager adapter.DNSTransportManager networkStrategy *C.NetworkStrategy defaultNetworkStrategy bool networkType []C.InterfaceType @@ -235,6 +238,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial connectionManager: connectionManager, networkManager: networkManager, powerManager: service.FromContext[*powerreport.Manager](ctx), + outboundManager: service.FromContext[adapter.OutboundManager](ctx), + dnsTransportManager: service.FromContext[adapter.DNSTransportManager](ctx), networkStrategy: networkStrategy, defaultNetworkStrategy: defaultNetworkStrategy, networkType: networkType, @@ -422,7 +427,7 @@ func (d *DefaultDialer) trackConn(ctx context.Context, destination M.Socksaddr, recorder := d.powerManager.Recorder() if recorder != nil { recorder.CountConnectionOpened() - attribution := dialAttribution(ctx, destination) + attribution := d.dialAttribution(ctx, destination) conn = bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) { recorder.Touch(powerreport.DirectionInbound, int(n), attribution) }}, []N.CountFunc{func(n int64) { @@ -444,7 +449,7 @@ func (d *DefaultDialer) trackPacketConn(ctx context.Context, destination M.Socks recorder := d.powerManager.Recorder() if recorder != nil { recorder.CountConnectionOpened() - attribution := dialAttribution(ctx, destination) + attribution := d.dialAttribution(ctx, destination) conn = bufio.NewNetPacketConn(bufio.NewCounterPacketConn(bufio.NewPacketConn(conn), []N.CountFunc{func(n int64) { recorder.Touch(powerreport.DirectionInbound, int(n), attribution) }}, []N.CountFunc{func(n int64) { @@ -455,24 +460,80 @@ func (d *DefaultDialer) trackPacketConn(ctx context.Context, destination M.Socks return conn, nil } -func dialAttribution(ctx context.Context, destination M.Socksaddr) *powerreport.Attribution { +func (d *DefaultDialer) dialAttribution(ctx context.Context, destination M.Socksaddr) *powerreport.Attribution { + attribution := &powerreport.Attribution{} + dnsTransportTag, hasDNSTransport := adapter.DNSTransportTagFromContext(ctx) + if hasDNSTransport { + attribution.DNS = dnsTransportTag + if d.dnsTransportManager != nil { + transport, loaded := d.dnsTransportManager.Transport(dnsTransportTag) + if loaded { + attribution.DNSType = transport.Type() + } + } + } metadata := adapter.ContextFrom(ctx) if metadata == nil { - return &powerreport.Attribution{Destination: destination.String()} + attribution.Destination = destination.String() + return attribution } - attribution := &powerreport.Attribution{ - Domain: metadata.Domain, - Outbound: metadata.Outbound, + attribution.Inbound = metadata.Inbound + attribution.InboundType = metadata.InboundType + attribution.Network = metadata.Network + if metadata.Source.IsValid() { + attribution.Source = metadata.Source.String() } - if metadata.Inbound != "" { - attribution.Inbound = metadata.InboundType + "/" + metadata.Inbound - } else { - attribution.Inbound = metadata.InboundType + attribution.Domain = metadata.Domain + attribution.Protocol = metadata.Protocol + attribution.User = metadata.User + if metadata.ProcessInfo != nil { + attribution.Process = &powerreport.ProcessAttribution{ + ProcessID: metadata.ProcessInfo.ProcessID, + UserID: metadata.ProcessInfo.UserId, + UserName: metadata.ProcessInfo.UserName, + ProcessPath: metadata.ProcessInfo.ProcessPath, + PackageNames: metadata.ProcessInfo.AndroidPackageNames, + } + } + attribution.Rule = metadata.RouteRule + attribution.Outbound = metadata.Outbound + if d.outboundManager != nil { + if metadata.Outbound != "" { + outbound, loaded := d.outboundManager.Outbound(metadata.Outbound) + if loaded { + attribution.OutboundType = outbound.Type() + } + } + if metadata.RouteOutbound != "" { + attribution.Chain = d.outboundChain(metadata.RouteOutbound) + } } if metadata.Destination.IsValid() { attribution.Destination = metadata.Destination.String() + if metadata.Destination != destination { + attribution.Server = destination.String() + } } else { attribution.Destination = destination.String() } return attribution } + +func (d *DefaultDialer) outboundChain(head string) []string { + var chain []string + next := head + for { + detour, loaded := d.outboundManager.Outbound(next) + if !loaded { + break + } + chain = append(chain, next) + outboundGroup, isGroup := detour.(adapter.OutboundGroup) + if !isGroup { + break + } + next = outboundGroup.Now() + } + slices.Reverse(chain) + return chain +} diff --git a/dns/client.go b/dns/client.go index 60a2e46e..d99aed0e 100644 --- a/dns/client.go +++ b/dns/client.go @@ -321,12 +321,12 @@ func (c *Client) beginExchange(ctx context.Context, transport adapter.DNSTranspo } } - contextTransport, transportTagLoaded := transportTagFromContext(ctx) + contextTransport, transportTagLoaded := adapter.DNSTransportTagFromContext(ctx) if transportTagLoaded && transport.Tag() == contextTransport { operation.release() return nil, nil, exchangeDone, E.New("DNS query loopback in transport[", contextTransport, "]") } - operation.ctx = contextWithTransportTag(ctx, transport.Tag()) + operation.ctx = adapter.ContextWithDNSTransportTag(ctx, transport.Tag()) if !disableCache && responseChecker != nil && c.rdrc != nil { rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype) if rejected { @@ -644,7 +644,7 @@ func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, key dnsCac } go func() { defer c.backgroundRefresh.Delete(key) - ctx := contextWithTransportTag(c.ctx, transport.Tag()) + ctx := adapter.ContextWithDNSTransportTag(c.ctx, transport.Tag()) response, err := c.exchangeToTransport(ctx, transport, message, options.Timeout) if err != nil { if c.logger != nil { @@ -750,17 +750,6 @@ func MessageToAddresses(response *dns.Msg) []netip.Addr { return adapter.DNSResponseAddresses(response) } -type transportKey struct{} - -func contextWithTransportTag(ctx context.Context, transportTag string) context.Context { - return context.WithValue(ctx, transportKey{}, transportTag) -} - -func transportTagFromContext(ctx context.Context) (string, bool) { - value, loaded := ctx.Value(transportKey{}).(string) - return value, loaded -} - func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg { return &dns.Msg{ MsgHdr: dns.MsgHdr{ diff --git a/experimental/libbox/power_report.go b/experimental/libbox/power_report.go index 39f5f491..d78c72b1 100644 --- a/experimental/libbox/power_report.go +++ b/experimental/libbox/power_report.go @@ -27,6 +27,11 @@ func PowerReportOptions(startedService *daemon.StartedService) powerreport.Optio LogCallback: func() []byte { return formatLogEntries(startedService.SavedLog()) }, + ProfileCallback: func(path string) { + for _, name := range oomReportProfiles { + writeOOMProfile(path, name) + } + }, } } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 22cf6e14..9c3390ad 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -94,7 +94,7 @@ func ReloadSetupOptions(options *SetupOptions) { sOOMMemoryLimit = oomkiller.DefaultAppleNetworkExtensionMemoryLimit } if sOOMMemoryLimit > 0 { - debug.SetMemoryLimit(sOOMMemoryLimit * 3 / 4) + debug.SetMemoryLimit(sOOMMemoryLimit * 4 / 5) } else { debug.SetMemoryLimit(math.MaxInt64) } diff --git a/protocol/shadowtls/outbound.go b/protocol/shadowtls/outbound.go index 41a4a601..81ce0b38 100644 --- a/protocol/shadowtls/outbound.go +++ b/protocol/shadowtls/outbound.go @@ -90,7 +90,6 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() - metadata.Destination = destination switch N.NetworkName(network) { case N.NetworkTCP: return h.client.DialContext(ctx) diff --git a/route/route.go b/route/route.go index 27924181..93a00225 100644 --- a/route/route.go +++ b/route/route.go @@ -163,6 +163,10 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad for _, buffer := range buffers { conn = bufio.NewCachedConn(conn, buffer) } + if selectedRule != nil { + metadata.RouteRule = selectedRule.String() + } + metadata.RouteOutbound = selectedOutbound.Tag() for _, tracker := range r.trackers { conn = tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } @@ -291,6 +295,10 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination) N.PutPacketBuffer(buffer) } + if selectedRule != nil { + metadata.RouteRule = selectedRule.String() + } + metadata.RouteOutbound = selectedOutbound.Tag() for _, tracker := range r.trackers { conn = tracker.RoutedPacketConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } diff --git a/service/powerreport/record.go b/service/powerreport/record.go index c850f1ca..730f97c7 100644 --- a/service/powerreport/record.go +++ b/service/powerreport/record.go @@ -15,11 +15,31 @@ func (d Direction) String() string { } type Attribution struct { - Inbound string `json:"inbound,omitempty"` - Domain string `json:"domain,omitempty"` - Destination string `json:"destination,omitempty"` - Outbound string `json:"outbound,omitempty"` - Endpoint string `json:"endpoint,omitempty"` + Inbound string `json:"inbound,omitempty"` + InboundType string `json:"inboundType,omitempty"` + Network string `json:"network,omitempty"` + Source string `json:"source,omitempty"` + Destination string `json:"destination,omitempty"` + Domain string `json:"domain,omitempty"` + Protocol string `json:"protocol,omitempty"` + User string `json:"user,omitempty"` + Process *ProcessAttribution `json:"process,omitempty"` + Rule string `json:"rule,omitempty"` + Chain []string `json:"chain,omitempty"` + Outbound string `json:"outbound,omitempty"` + OutboundType string `json:"outboundType,omitempty"` + Server string `json:"server,omitempty"` + DNS string `json:"dns,omitempty"` + DNSType string `json:"dnsType,omitempty"` + Endpoint string `json:"endpoint,omitempty"` +} + +type ProcessAttribution struct { + ProcessID uint32 `json:"processId,omitempty"` + UserID int32 `json:"userId,omitempty"` + UserName string `json:"userName,omitempty"` + ProcessPath string `json:"processPath,omitempty"` + PackageNames []string `json:"packageNames,omitempty"` } type timelineRow struct { @@ -37,6 +57,10 @@ type timelineRow struct { DiskBytesWritten uint64 `json:"diskWriteBytes,omitempty"` SleptMS int64 `json:"sleptMS,omitempty"` Goroutines uint64 `json:"goroutines,omitempty"` + GCCycles uint64 `json:"gcCycles,omitempty"` + GoMemoryBytes uint64 `json:"goMemoryBytes,omitempty"` + GoHeapLiveBytes uint64 `json:"goHeapLiveBytes,omitempty"` + MemoryBytes uint64 `json:"memoryBytes,omitempty"` DNSQueries uint64 `json:"dnsQueries,omitempty"` ConnectionsOpened uint64 `json:"connectionsOpened,omitempty"` InterfacePackets map[string]uint64 `json:"interfacePackets,omitempty"` diff --git a/service/powerreport/recorder.go b/service/powerreport/recorder.go index 0e389bd9..26fe4a32 100644 --- a/service/powerreport/recorder.go +++ b/service/powerreport/recorder.go @@ -12,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/memory" ) const ( @@ -41,6 +42,7 @@ type Options struct { Metadata any OwnerCallback func(path string) LogCallback func() []byte + ProfileCallback func(path string) GateInterval time.Duration SampleInterval time.Duration FlushInterval time.Duration @@ -53,6 +55,7 @@ type Recorder struct { metadata any ownerCallback func(path string) logCallback func() []byte + profileCallback func(path string) gateNano int64 sampleNano int64 flushInterval time.Duration @@ -96,6 +99,7 @@ type previousSample struct { at time.Time usage systemUsage gcSeconds float64 + gcCycles uint64 absoluteTime int64 continuousTime int64 interfaces map[string]interfaceCounters @@ -130,6 +134,7 @@ func NewRecorder(options Options) *Recorder { metadata: options.Metadata, ownerCallback: options.OwnerCallback, logCallback: options.LogCallback, + profileCallback: options.ProfileCallback, gateNano: int64(gateInterval), sampleNano: int64(sampleInterval), flushInterval: flushInterval, @@ -139,6 +144,9 @@ func NewRecorder(options Options) *Recorder { metricsSamples: []metrics.Sample{ {Name: "/cpu/classes/gc/total:cpu-seconds"}, {Name: "/sched/goroutines:goroutines"}, + {Name: "/gc/cycles/total:gc-cycles"}, + {Name: "/memory/classes/total:bytes"}, + {Name: "/gc/heap/live:bytes"}, }, done: make(chan struct{}), workerDone: make(chan struct{}), @@ -190,7 +198,7 @@ func (r *Recorder) Close() error { r.sampleLocked(now) r.flushLocked(now) r.access.Unlock() - r.writeGoroutineProfile() + r.writeProfiles() r.writeLog() finalizeDraft(r.draftPath) return nil @@ -340,6 +348,7 @@ func (r *Recorder) resetPreviousLocked(now time.Time) { 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(), @@ -356,10 +365,16 @@ func (r *Recorder) sampleLocked(now time.Time) { 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, } + if memory.TotalAvailable() { + row.MemoryBytes = memory.Total() + } if current.usage.valid && previous.usage.valid { row.CPUUserMS = (current.usage.userTime - previous.usage.userTime) / int64(time.Millisecond) row.CPUSystemMS = (current.usage.systemTime - previous.usage.systemTime) / int64(time.Millisecond) @@ -460,7 +475,11 @@ func appendRecords[T any](r *Recorder, path string, records []T) error { return nil } -func (r *Recorder) writeGoroutineProfile() { +func (r *Recorder) writeProfiles() { + if r.profileCallback != nil { + r.profileCallback(r.draftPath) + return + } profilePath := filepath.Join(r.draftPath, goroutineProfileFileName) file, err := os.OpenFile(profilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666) if err != nil {