Compare commits

...

6 Commits

Author SHA1 Message Date
世界
f4de7d622a tools: Network Quality 2026-04-08 14:44:14 +08:00
世界
fcd2b90852 tun: Fixes 2026-04-07 23:39:49 +08:00
世界
0d772e6893 oom-killer: Free memory on pressure notification and use gradual interval backoff 2026-04-07 23:38:44 +08:00
世界
4806d5e588 Fix deprecated warning double-formatting on localized clients 2026-04-07 23:37:48 +08:00
世界
2df43e4d1b platform: Fix set local 2026-04-07 23:37:48 +08:00
世界
5cbd7974df Reformat code 2026-04-07 23:37:41 +08:00
20 changed files with 2677 additions and 128 deletions

View File

@@ -0,0 +1,122 @@
package main
import (
"fmt"
"os"
"strings"
"time"
"github.com/sagernet/sing-box/common/networkquality"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var (
commandNetworkQualityFlagConfigURL string
commandNetworkQualityFlagSerial bool
commandNetworkQualityFlagMaxRuntime int
)
var commandNetworkQuality = &cobra.Command{
Use: "networkquality",
Short: "Run a network quality test",
Run: func(cmd *cobra.Command, args []string) {
err := runNetworkQuality()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandNetworkQuality.Flags().StringVar(
&commandNetworkQualityFlagConfigURL,
"config-url", "",
"Network quality test config URL (default: Apple mensura)",
)
commandNetworkQuality.Flags().BoolVar(
&commandNetworkQualityFlagSerial,
"serial", false,
"Run download and upload tests sequentially instead of in parallel",
)
commandNetworkQuality.Flags().IntVar(
&commandNetworkQualityFlagMaxRuntime,
"max-runtime", int(networkquality.DefaultMaxRuntime/time.Second),
"Network quality maximum runtime in seconds",
)
commandTools.AddCommand(commandNetworkQuality)
}
func runNetworkQuality() error {
instance, err := createPreStartedClient()
if err != nil {
return err
}
defer instance.Close()
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
httpClient := networkquality.NewHTTPClient(dialer)
defer httpClient.CloseIdleConnections()
fmt.Fprintln(os.Stderr, "==== NETWORK QUALITY TEST ====")
result, err := networkquality.Run(networkquality.Options{
ConfigURL: commandNetworkQualityFlagConfigURL,
HTTPClient: httpClient,
Serial: commandNetworkQualityFlagSerial,
MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second,
Context: globalCtx,
OnProgress: func(p networkquality.Progress) {
if !commandNetworkQualityFlagSerial && p.Phase != networkquality.PhaseIdle {
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d Upload: %s RPM: %d",
formatBitrate(p.DownloadCapacity), p.DownloadRPM,
formatBitrate(p.UploadCapacity), p.UploadRPM)
return
}
switch networkquality.Phase(p.Phase) {
case networkquality.PhaseIdle:
if p.IdleLatencyMs > 0 {
fmt.Fprintf(os.Stderr, "\rIdle Latency: %d ms", p.IdleLatencyMs)
} else {
fmt.Fprint(os.Stderr, "\rMeasuring idle latency...")
}
case networkquality.PhaseDownload:
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d",
formatBitrate(p.DownloadCapacity), p.DownloadRPM)
case networkquality.PhaseUpload:
fmt.Fprintf(os.Stderr, "\rUpload: %s RPM: %d",
formatBitrate(p.UploadCapacity), p.UploadRPM)
}
},
})
if err != nil {
return err
}
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, strings.Repeat("-", 40))
fmt.Fprintf(os.Stderr, "Idle Latency: %d ms\n", result.IdleLatencyMs)
fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", formatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy)
fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", formatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy)
fmt.Fprintf(os.Stderr, "Download Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.DownloadRPM), result.DownloadRPMAccuracy)
fmt.Fprintf(os.Stderr, "Upload Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.UploadRPM), result.UploadRPMAccuracy)
return nil
}
func formatBitrate(bps int64) string {
switch {
case bps >= 1_000_000_000:
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
case bps >= 1_000_000:
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
case bps >= 1_000:
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
default:
return fmt.Sprintf("%d bps", bps)
}
}

View File

@@ -0,0 +1,109 @@
package networkquality
import (
"context"
"net"
"net/http"
"strings"
C "github.com/sagernet/sing-box/constant"
sBufio "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
// NewHTTPClient creates an http.Client that dials through the given dialer.
// The dialer should already handle DNS resolution if needed.
func NewHTTPClient(dialer N.Dialer) *http.Client {
transport := &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
}
if dialer != nil {
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
}
}
return &http.Client{Transport: transport}
}
func baseTransportFromClient(client *http.Client) (*http.Transport, error) {
if client == nil {
return nil, E.New("http client is nil")
}
if client.Transport == nil {
return http.DefaultTransport.(*http.Transport).Clone(), nil
}
transport, ok := client.Transport.(*http.Transport)
if !ok {
return nil, E.New("http client transport must be *http.Transport")
}
return transport.Clone(), nil
}
func newMeasurementClient(
baseClient *http.Client,
connectEndpoint string,
singleConnection bool,
disableKeepAlives bool,
readCounters []N.CountFunc,
writeCounters []N.CountFunc,
) (*http.Client, error) {
transport, err := baseTransportFromClient(baseClient)
if err != nil {
return nil, err
}
transport.DisableCompression = true
transport.DisableKeepAlives = disableKeepAlives
if singleConnection {
transport.MaxConnsPerHost = 1
transport.MaxIdleConnsPerHost = 1
transport.MaxIdleConns = 1
}
baseDialContext := transport.DialContext
if baseDialContext == nil {
dialer := &net.Dialer{}
baseDialContext = dialer.DialContext
}
connectEndpoint = strings.TrimSpace(connectEndpoint)
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
dialAddr := addr
if connectEndpoint != "" {
dialAddr = rewriteDialAddress(addr, connectEndpoint)
}
conn, dialErr := baseDialContext(ctx, network, dialAddr)
if dialErr != nil {
return nil, dialErr
}
if len(readCounters) > 0 || len(writeCounters) > 0 {
return sBufio.NewCounterConn(conn, readCounters, writeCounters), nil
}
return conn, nil
}
return &http.Client{
Transport: transport,
CheckRedirect: baseClient.CheckRedirect,
Jar: baseClient.Jar,
Timeout: baseClient.Timeout,
}, nil
}
func rewriteDialAddress(addr string, connectEndpoint string) string {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
endpointHost, endpointPort, err := net.SplitHostPort(connectEndpoint)
if err == nil {
host = endpointHost
if endpointPort != "" {
port = endpointPort
}
} else if connectEndpoint != "" {
host = connectEndpoint
}
return net.JoinHostPort(host, port)
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,8 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/networkquality"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
@@ -1063,9 +1065,12 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty
return &DeprecatedWarnings{
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
return &DeprecatedWarning{
Message: it.Message(),
Impending: it.Impending(),
MigrationLink: it.MigrationLink,
Message: it.Message(),
Impending: it.Impending(),
MigrationLink: it.MigrationLink,
Description: it.Description,
DeprecatedVersion: it.DeprecatedVersion,
ScheduledVersion: it.ScheduledVersion,
}
}),
}, nil
@@ -1077,6 +1082,149 @@ func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty)
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
}
func (s *StartedService) ListOutbounds(ctx context.Context, _ *emptypb.Empty) (*OutboundList, error) {
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return nil, os.ErrInvalid
}
boxService := s.instance
s.serviceAccess.RUnlock()
historyStorage := boxService.urlTestHistoryStorage
outbounds := boxService.instance.Outbound().Outbounds()
var list OutboundList
for _, ob := range outbounds {
item := &GroupItem{
Tag: ob.Tag(),
Type: ob.Type(),
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
}
list.Outbounds = append(list.Outbounds, item)
}
return &list, nil
}
func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
subscription, done, err := s.urlTestObserver.Subscribe()
if err != nil {
return err
}
defer s.urlTestObserver.UnSubscribe(subscription)
for {
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return os.ErrInvalid
}
boxService := s.instance
s.serviceAccess.RUnlock()
historyStorage := boxService.urlTestHistoryStorage
outbounds := boxService.instance.Outbound().Outbounds()
var list OutboundList
for _, ob := range outbounds {
item := &GroupItem{
Tag: ob.Tag(),
Type: ob.Type(),
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
}
list.Outbounds = append(list.Outbounds, item)
}
err = server.Send(&list)
if err != nil {
return err
}
select {
case <-subscription:
case <-s.ctx.Done():
return s.ctx.Err()
case <-server.Context().Done():
return server.Context().Err()
case <-done:
return nil
}
}
}
func (s *StartedService) StartNetworkQualityTest(
request *NetworkQualityTestRequest,
server grpc.ServerStreamingServer[NetworkQualityTestProgress],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
var outbound adapter.Outbound
if request.OutboundTag == "" {
outbound = boxService.instance.Outbound().Default()
} else {
var loaded bool
outbound, loaded = boxService.instance.Outbound().Outbound(request.OutboundTag)
if !loaded {
return E.New("outbound not found: ", request.OutboundTag)
}
}
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
httpClient := networkquality.NewHTTPClient(resolvedDialer)
defer httpClient.CloseIdleConnections()
result, nqErr := networkquality.Run(networkquality.Options{
ConfigURL: request.ConfigURL,
HTTPClient: httpClient,
Serial: request.Serial,
MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second,
Context: server.Context(),
OnProgress: func(p networkquality.Progress) {
_ = server.Send(&NetworkQualityTestProgress{
Phase: int32(p.Phase),
DownloadCapacity: p.DownloadCapacity,
UploadCapacity: p.UploadCapacity,
DownloadRPM: p.DownloadRPM,
UploadRPM: p.UploadRPM,
IdleLatencyMs: p.IdleLatencyMs,
ElapsedMs: p.ElapsedMs,
DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(p.UploadRPMAccuracy),
})
},
})
if nqErr != nil {
return server.Send(&NetworkQualityTestProgress{
IsFinal: true,
Error: nqErr.Error(),
})
}
return server.Send(&NetworkQualityTestProgress{
Phase: int32(networkquality.PhaseDone),
DownloadCapacity: result.DownloadCapacity,
UploadCapacity: result.UploadCapacity,
DownloadRPM: result.DownloadRPM,
UploadRPM: result.UploadRPM,
IdleLatencyMs: result.IdleLatencyMs,
IsFinal: true,
DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(result.UploadRPMAccuracy),
})
}
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
}

View File

@@ -1709,12 +1709,15 @@ func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning {
}
type DeprecatedWarning struct {
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
Impending bool `protobuf:"varint,2,opt,name=impending,proto3" json:"impending,omitempty"`
MigrationLink string `protobuf:"bytes,3,opt,name=migrationLink,proto3" json:"migrationLink,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
Impending bool `protobuf:"varint,2,opt,name=impending,proto3" json:"impending,omitempty"`
MigrationLink string `protobuf:"bytes,3,opt,name=migrationLink,proto3" json:"migrationLink,omitempty"`
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"`
DeprecatedVersion string `protobuf:"bytes,5,opt,name=deprecatedVersion,proto3" json:"deprecatedVersion,omitempty"`
ScheduledVersion string `protobuf:"bytes,6,opt,name=scheduledVersion,proto3" json:"scheduledVersion,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeprecatedWarning) Reset() {
@@ -1768,6 +1771,27 @@ func (x *DeprecatedWarning) GetMigrationLink() string {
return ""
}
func (x *DeprecatedWarning) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *DeprecatedWarning) GetDeprecatedVersion() string {
if x != nil {
return x.DeprecatedVersion
}
return ""
}
func (x *DeprecatedWarning) GetScheduledVersion() string {
if x != nil {
return x.ScheduledVersion
}
return ""
}
type StartedAt struct {
state protoimpl.MessageState `protogen:"open.v1"`
StartedAt int64 `protobuf:"varint,1,opt,name=startedAt,proto3" json:"startedAt,omitempty"`
@@ -1812,6 +1836,258 @@ func (x *StartedAt) GetStartedAt() int64 {
return 0
}
type OutboundList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Outbounds []*GroupItem `protobuf:"bytes,1,rep,name=outbounds,proto3" json:"outbounds,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *OutboundList) Reset() {
*x = OutboundList{}
mi := &file_daemon_started_service_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *OutboundList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*OutboundList) ProtoMessage() {}
func (x *OutboundList) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use OutboundList.ProtoReflect.Descriptor instead.
func (*OutboundList) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{26}
}
func (x *OutboundList) GetOutbounds() []*GroupItem {
if x != nil {
return x.Outbounds
}
return nil
}
type NetworkQualityTestRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConfigURL string `protobuf:"bytes,1,opt,name=configURL,proto3" json:"configURL,omitempty"`
OutboundTag string `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"`
Serial bool `protobuf:"varint,3,opt,name=serial,proto3" json:"serial,omitempty"`
MaxRuntimeSeconds int32 `protobuf:"varint,4,opt,name=maxRuntimeSeconds,proto3" json:"maxRuntimeSeconds,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NetworkQualityTestRequest) Reset() {
*x = NetworkQualityTestRequest{}
mi := &file_daemon_started_service_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NetworkQualityTestRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NetworkQualityTestRequest) ProtoMessage() {}
func (x *NetworkQualityTestRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NetworkQualityTestRequest.ProtoReflect.Descriptor instead.
func (*NetworkQualityTestRequest) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{27}
}
func (x *NetworkQualityTestRequest) GetConfigURL() string {
if x != nil {
return x.ConfigURL
}
return ""
}
func (x *NetworkQualityTestRequest) GetOutboundTag() string {
if x != nil {
return x.OutboundTag
}
return ""
}
func (x *NetworkQualityTestRequest) GetSerial() bool {
if x != nil {
return x.Serial
}
return false
}
func (x *NetworkQualityTestRequest) GetMaxRuntimeSeconds() int32 {
if x != nil {
return x.MaxRuntimeSeconds
}
return 0
}
type NetworkQualityTestProgress struct {
state protoimpl.MessageState `protogen:"open.v1"`
Phase int32 `protobuf:"varint,1,opt,name=phase,proto3" json:"phase,omitempty"`
DownloadCapacity int64 `protobuf:"varint,2,opt,name=downloadCapacity,proto3" json:"downloadCapacity,omitempty"`
UploadCapacity int64 `protobuf:"varint,3,opt,name=uploadCapacity,proto3" json:"uploadCapacity,omitempty"`
DownloadRPM int32 `protobuf:"varint,4,opt,name=downloadRPM,proto3" json:"downloadRPM,omitempty"`
UploadRPM int32 `protobuf:"varint,5,opt,name=uploadRPM,proto3" json:"uploadRPM,omitempty"`
IdleLatencyMs int32 `protobuf:"varint,6,opt,name=idleLatencyMs,proto3" json:"idleLatencyMs,omitempty"`
ElapsedMs int64 `protobuf:"varint,7,opt,name=elapsedMs,proto3" json:"elapsedMs,omitempty"`
IsFinal bool `protobuf:"varint,8,opt,name=isFinal,proto3" json:"isFinal,omitempty"`
Error string `protobuf:"bytes,9,opt,name=error,proto3" json:"error,omitempty"`
DownloadCapacityAccuracy int32 `protobuf:"varint,10,opt,name=downloadCapacityAccuracy,proto3" json:"downloadCapacityAccuracy,omitempty"`
UploadCapacityAccuracy int32 `protobuf:"varint,11,opt,name=uploadCapacityAccuracy,proto3" json:"uploadCapacityAccuracy,omitempty"`
DownloadRPMAccuracy int32 `protobuf:"varint,12,opt,name=downloadRPMAccuracy,proto3" json:"downloadRPMAccuracy,omitempty"`
UploadRPMAccuracy int32 `protobuf:"varint,13,opt,name=uploadRPMAccuracy,proto3" json:"uploadRPMAccuracy,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NetworkQualityTestProgress) Reset() {
*x = NetworkQualityTestProgress{}
mi := &file_daemon_started_service_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NetworkQualityTestProgress) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NetworkQualityTestProgress) ProtoMessage() {}
func (x *NetworkQualityTestProgress) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NetworkQualityTestProgress.ProtoReflect.Descriptor instead.
func (*NetworkQualityTestProgress) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{28}
}
func (x *NetworkQualityTestProgress) GetPhase() int32 {
if x != nil {
return x.Phase
}
return 0
}
func (x *NetworkQualityTestProgress) GetDownloadCapacity() int64 {
if x != nil {
return x.DownloadCapacity
}
return 0
}
func (x *NetworkQualityTestProgress) GetUploadCapacity() int64 {
if x != nil {
return x.UploadCapacity
}
return 0
}
func (x *NetworkQualityTestProgress) GetDownloadRPM() int32 {
if x != nil {
return x.DownloadRPM
}
return 0
}
func (x *NetworkQualityTestProgress) GetUploadRPM() int32 {
if x != nil {
return x.UploadRPM
}
return 0
}
func (x *NetworkQualityTestProgress) GetIdleLatencyMs() int32 {
if x != nil {
return x.IdleLatencyMs
}
return 0
}
func (x *NetworkQualityTestProgress) GetElapsedMs() int64 {
if x != nil {
return x.ElapsedMs
}
return 0
}
func (x *NetworkQualityTestProgress) GetIsFinal() bool {
if x != nil {
return x.IsFinal
}
return false
}
func (x *NetworkQualityTestProgress) GetError() string {
if x != nil {
return x.Error
}
return ""
}
func (x *NetworkQualityTestProgress) GetDownloadCapacityAccuracy() int32 {
if x != nil {
return x.DownloadCapacityAccuracy
}
return 0
}
func (x *NetworkQualityTestProgress) GetUploadCapacityAccuracy() int32 {
if x != nil {
return x.UploadCapacityAccuracy
}
return 0
}
func (x *NetworkQualityTestProgress) GetDownloadRPMAccuracy() int32 {
if x != nil {
return x.DownloadRPMAccuracy
}
return 0
}
func (x *NetworkQualityTestProgress) GetUploadRPMAccuracy() int32 {
if x != nil {
return x.UploadRPMAccuracy
}
return 0
}
type Log_Message struct {
state protoimpl.MessageState `protogen:"open.v1"`
Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"`
@@ -1822,7 +2098,7 @@ type Log_Message struct {
func (x *Log_Message) Reset() {
*x = Log_Message{}
mi := &file_daemon_started_service_proto_msgTypes[26]
mi := &file_daemon_started_service_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1834,7 +2110,7 @@ func (x *Log_Message) String() string {
func (*Log_Message) ProtoMessage() {}
func (x *Log_Message) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[26]
mi := &file_daemon_started_service_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1990,13 +2266,38 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x16CloseConnectionRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
"\x12DeprecatedWarnings\x125\n" +
"\bwarnings\x18\x01 \x03(\v2\x19.daemon.DeprecatedWarningR\bwarnings\"q\n" +
"\bwarnings\x18\x01 \x03(\v2\x19.daemon.DeprecatedWarningR\bwarnings\"\xed\x01\n" +
"\x11DeprecatedWarning\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" +
"\timpending\x18\x02 \x01(\bR\timpending\x12$\n" +
"\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink\")\n" +
"\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink\x12 \n" +
"\vdescription\x18\x04 \x01(\tR\vdescription\x12,\n" +
"\x11deprecatedVersion\x18\x05 \x01(\tR\x11deprecatedVersion\x12*\n" +
"\x10scheduledVersion\x18\x06 \x01(\tR\x10scheduledVersion\")\n" +
"\tStartedAt\x12\x1c\n" +
"\tstartedAt\x18\x01 \x01(\x03R\tstartedAt*U\n" +
"\tstartedAt\x18\x01 \x01(\x03R\tstartedAt\"?\n" +
"\fOutboundList\x12/\n" +
"\toutbounds\x18\x01 \x03(\v2\x11.daemon.GroupItemR\toutbounds\"\xa1\x01\n" +
"\x19NetworkQualityTestRequest\x12\x1c\n" +
"\tconfigURL\x18\x01 \x01(\tR\tconfigURL\x12 \n" +
"\voutboundTag\x18\x02 \x01(\tR\voutboundTag\x12\x16\n" +
"\x06serial\x18\x03 \x01(\bR\x06serial\x12,\n" +
"\x11maxRuntimeSeconds\x18\x04 \x01(\x05R\x11maxRuntimeSeconds\"\x8e\x04\n" +
"\x1aNetworkQualityTestProgress\x12\x14\n" +
"\x05phase\x18\x01 \x01(\x05R\x05phase\x12*\n" +
"\x10downloadCapacity\x18\x02 \x01(\x03R\x10downloadCapacity\x12&\n" +
"\x0euploadCapacity\x18\x03 \x01(\x03R\x0euploadCapacity\x12 \n" +
"\vdownloadRPM\x18\x04 \x01(\x05R\vdownloadRPM\x12\x1c\n" +
"\tuploadRPM\x18\x05 \x01(\x05R\tuploadRPM\x12$\n" +
"\ridleLatencyMs\x18\x06 \x01(\x05R\ridleLatencyMs\x12\x1c\n" +
"\telapsedMs\x18\a \x01(\x03R\telapsedMs\x12\x18\n" +
"\aisFinal\x18\b \x01(\bR\aisFinal\x12\x14\n" +
"\x05error\x18\t \x01(\tR\x05error\x12:\n" +
"\x18downloadCapacityAccuracy\x18\n" +
" \x01(\x05R\x18downloadCapacityAccuracy\x126\n" +
"\x16uploadCapacityAccuracy\x18\v \x01(\x05R\x16uploadCapacityAccuracy\x120\n" +
"\x13downloadRPMAccuracy\x18\f \x01(\x05R\x13downloadRPMAccuracy\x12,\n" +
"\x11uploadRPMAccuracy\x18\r \x01(\x05R\x11uploadRPMAccuracy*U\n" +
"\bLogLevel\x12\t\n" +
"\x05PANIC\x10\x00\x12\t\n" +
"\x05FATAL\x10\x01\x12\t\n" +
@@ -2008,7 +2309,7 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x13ConnectionEventType\x12\x18\n" +
"\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" +
"\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" +
"\x17CONNECTION_EVENT_CLOSED\x10\x022\xf5\f\n" +
"\x17CONNECTION_EVENT_CLOSED\x10\x022\xe4\x0e\n" +
"\x0eStartedService\x12=\n" +
"\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" +
"\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" +
@@ -2032,7 +2333,10 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" +
"\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" +
"\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\n" +
"\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
"\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00\x12?\n" +
"\rListOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x00\x12F\n" +
"\x12SubscribeOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x000\x01\x12d\n" +
"\x17StartNetworkQualityTest\x12!.daemon.NetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
var (
file_daemon_started_service_proto_rawDescOnce sync.Once
@@ -2048,7 +2352,7 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte {
var (
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
file_daemon_started_service_proto_goTypes = []any{
(LogLevel)(0), // 0: daemon.LogLevel
(ConnectionEventType)(0), // 1: daemon.ConnectionEventType
@@ -2080,14 +2384,17 @@ var (
(*DeprecatedWarnings)(nil), // 27: daemon.DeprecatedWarnings
(*DeprecatedWarning)(nil), // 28: daemon.DeprecatedWarning
(*StartedAt)(nil), // 29: daemon.StartedAt
(*Log_Message)(nil), // 30: daemon.Log.Message
(*emptypb.Empty)(nil), // 31: google.protobuf.Empty
(*OutboundList)(nil), // 30: daemon.OutboundList
(*NetworkQualityTestRequest)(nil), // 31: daemon.NetworkQualityTestRequest
(*NetworkQualityTestProgress)(nil), // 32: daemon.NetworkQualityTestProgress
(*Log_Message)(nil), // 33: daemon.Log.Message
(*emptypb.Empty)(nil), // 34: google.protobuf.Empty
}
)
var file_daemon_started_service_proto_depIdxs = []int32{
2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
30, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
33, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel
11, // 3: daemon.Groups.group:type_name -> daemon.Group
12, // 4: daemon.Group.items:type_name -> daemon.GroupItem
@@ -2097,58 +2404,65 @@ var file_daemon_started_service_proto_depIdxs = []int32{
22, // 8: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent
25, // 9: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo
28, // 10: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning
0, // 11: daemon.Log.Message.level:type_name -> daemon.LogLevel
31, // 12: daemon.StartedService.StopService:input_type -> google.protobuf.Empty
31, // 13: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty
31, // 14: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
31, // 15: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
31, // 16: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
31, // 17: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
6, // 18: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
31, // 19: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
31, // 20: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
31, // 21: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
16, // 22: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
13, // 23: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
14, // 24: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
15, // 25: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
31, // 26: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
19, // 27: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
20, // 28: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest
31, // 29: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty
21, // 30: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
26, // 31: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
31, // 32: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
31, // 33: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
31, // 34: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
31, // 35: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
31, // 36: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
4, // 37: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
7, // 38: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
8, // 39: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
31, // 40: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
9, // 41: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
10, // 42: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
17, // 43: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
16, // 44: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
31, // 45: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
31, // 46: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
31, // 47: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
31, // 48: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
18, // 49: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
31, // 50: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
31, // 51: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty
31, // 52: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty
23, // 53: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents
31, // 54: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
31, // 55: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
27, // 56: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
29, // 57: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt
35, // [35:58] is the sub-list for method output_type
12, // [12:35] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
12, // 11: daemon.OutboundList.outbounds:type_name -> daemon.GroupItem
0, // 12: daemon.Log.Message.level:type_name -> daemon.LogLevel
34, // 13: daemon.StartedService.StopService:input_type -> google.protobuf.Empty
34, // 14: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty
34, // 15: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
34, // 16: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
34, // 17: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
34, // 18: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
6, // 19: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
34, // 20: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
34, // 21: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
34, // 22: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
16, // 23: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
13, // 24: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
14, // 25: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
15, // 26: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
34, // 27: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
19, // 28: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
20, // 29: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest
34, // 30: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty
21, // 31: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
26, // 32: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
34, // 33: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
34, // 34: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
34, // 35: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
34, // 36: daemon.StartedService.ListOutbounds:input_type -> google.protobuf.Empty
34, // 37: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty
31, // 38: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest
34, // 39: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
34, // 40: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
4, // 41: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
7, // 42: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
8, // 43: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
34, // 44: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
9, // 45: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
10, // 46: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
17, // 47: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
16, // 48: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
34, // 49: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
34, // 50: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
34, // 51: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
34, // 52: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
18, // 53: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
34, // 54: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
34, // 55: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty
34, // 56: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty
23, // 57: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents
34, // 58: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
34, // 59: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
27, // 60: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
29, // 61: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt
30, // 62: daemon.StartedService.ListOutbounds:output_type -> daemon.OutboundList
30, // 63: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList
32, // 64: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
39, // [39:65] is the sub-list for method output_type
13, // [13:39] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
}
func init() { file_daemon_started_service_proto_init() }
@@ -2162,7 +2476,7 @@ func file_daemon_started_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)),
NumEnums: 4,
NumMessages: 27,
NumMessages: 30,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -34,6 +34,10 @@ service StartedService {
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
rpc ListOutbounds(google.protobuf.Empty) returns (OutboundList) {}
rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {}
rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {}
}
message ServiceStatus {
@@ -221,8 +225,38 @@ message DeprecatedWarning {
string message = 1;
bool impending = 2;
string migrationLink = 3;
string description = 4;
string deprecatedVersion = 5;
string scheduledVersion = 6;
}
message StartedAt {
int64 startedAt = 1;
}
message OutboundList {
repeated GroupItem outbounds = 1;
}
message NetworkQualityTestRequest {
string configURL = 1;
string outboundTag = 2;
bool serial = 3;
int32 maxRuntimeSeconds = 4;
}
message NetworkQualityTestProgress {
int32 phase = 1;
int64 downloadCapacity = 2;
int64 uploadCapacity = 3;
int32 downloadRPM = 4;
int32 uploadRPM = 5;
int32 idleLatencyMs = 6;
int64 elapsedMs = 7;
bool isFinal = 8;
string error = 9;
int32 downloadCapacityAccuracy = 10;
int32 uploadCapacityAccuracy = 11;
int32 downloadRPMAccuracy = 12;
int32 uploadRPMAccuracy = 13;
}

View File

@@ -15,29 +15,32 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
StartedService_ListOutbounds_FullMethodName = "/daemon.StartedService/ListOutbounds"
StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds"
StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest"
)
// StartedServiceClient is the client API for StartedService service.
@@ -67,6 +70,9 @@ type StartedServiceClient interface {
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
ListOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OutboundList, error)
SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error)
StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error)
}
type startedServiceClient struct {
@@ -361,6 +367,54 @@ func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Emp
return out, nil
}
func (c *startedServiceClient) ListOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OutboundList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(OutboundList)
err := c.cc.Invoke(ctx, StartedService_ListOutbounds_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeOutbounds_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, OutboundList]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeOutboundsClient = grpc.ServerStreamingClient[OutboundList]
func (c *startedServiceClient) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[7], StartedService_StartNetworkQualityTest_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartNetworkQualityTestClient = grpc.ServerStreamingClient[NetworkQualityTestProgress]
// StartedServiceServer is the server API for StartedService service.
// All implementations must embed UnimplementedStartedServiceServer
// for forward compatibility.
@@ -388,6 +442,9 @@ type StartedServiceServer interface {
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
ListOutbounds(context.Context, *emptypb.Empty) (*OutboundList, error)
SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error
StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error
mustEmbedUnimplementedStartedServiceServer()
}
@@ -489,6 +546,18 @@ func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context,
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
}
func (UnimplementedStartedServiceServer) ListOutbounds(context.Context, *emptypb.Empty) (*OutboundList, error) {
return nil, status.Error(codes.Unimplemented, "method ListOutbounds not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error {
return status.Error(codes.Unimplemented, "method SubscribeOutbounds not implemented")
}
func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error {
return status.Error(codes.Unimplemented, "method StartNetworkQualityTest not implemented")
}
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
@@ -882,6 +951,46 @@ func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _StartedService_ListOutbounds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).ListOutbounds(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_ListOutbounds_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).ListOutbounds(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeOutbounds_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeOutbounds(m, &grpc.GenericServerStream[emptypb.Empty, OutboundList]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeOutboundsServer = grpc.ServerStreamingServer[OutboundList]
func _StartedService_StartNetworkQualityTest_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(NetworkQualityTestRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).StartNetworkQualityTest(m, &grpc.GenericServerStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartNetworkQualityTestServer = grpc.ServerStreamingServer[NetworkQualityTestProgress]
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -957,6 +1066,10 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetStartedAt",
Handler: _StartedService_GetStartedAt_Handler,
},
{
MethodName: "ListOutbounds",
Handler: _StartedService_ListOutbounds_Handler,
},
},
Streams: []grpc.StreamDesc{
{
@@ -989,6 +1102,16 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
Handler: _StartedService_SubscribeConnections_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeOutbounds",
Handler: _StartedService_SubscribeOutbounds_Handler,
ServerStreams: true,
},
{
StreamName: "StartNetworkQualityTest",
Handler: _StartedService_StartNetworkQualityTest_Handler,
ServerStreams: true,
},
},
Metadata: "daemon/started_service.proto",
}

View File

@@ -29,24 +29,26 @@ import (
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSRouter = (*Router)(nil)
var _ adapter.DNSRuleSetUpdateValidator = (*Router)(nil)
var (
_ adapter.DNSRouter = (*Router)(nil)
_ adapter.DNSRuleSetUpdateValidator = (*Router)(nil)
)
type Router struct {
ctx context.Context
logger logger.ContextLogger
transport adapter.DNSTransportManager
outbound adapter.OutboundManager
client adapter.DNSClient
rawRules []option.DNSRule
rules []adapter.DNSRule
defaultDomainStrategy C.DomainStrategy
dnsReverseMapping freelru.Cache[netip.Addr, string]
platformInterface adapter.PlatformInterface
legacyDNSMode bool
rulesAccess sync.RWMutex
started bool
closing bool
ctx context.Context
logger logger.ContextLogger
transport adapter.DNSTransportManager
outbound adapter.OutboundManager
client adapter.DNSClient
rawRules []option.DNSRule
rules []adapter.DNSRule
defaultDomainStrategy C.DomainStrategy
dnsReverseMapping freelru.Cache[netip.Addr, string]
platformInterface adapter.PlatformInterface
legacyDNSMode bool
rulesAccess sync.RWMutex
started bool
closing bool
}
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {

View File

@@ -6,4 +6,5 @@ const (
CommandGroup
CommandClashMode
CommandConnections
CommandOutbounds
)

View File

@@ -47,6 +47,7 @@ type CommandClientHandler interface {
WriteLogs(messageList LogIterator)
WriteStatus(message *StatusMessage)
WriteGroups(message OutboundGroupIterator)
WriteOutbounds(message OutboundGroupItemIterator)
InitializeClashMode(modeList StringIterator, currentMode string)
UpdateClashMode(newMode string)
WriteConnectionEvents(events *ConnectionEvents)
@@ -243,6 +244,8 @@ func (c *CommandClient) dispatchCommands() error {
go c.handleClashModeStream()
case CommandConnections:
go c.handleConnectionsStream()
case CommandOutbounds:
go c.handleOutboundsStream()
default:
return E.New("unknown command: ", command)
}
@@ -456,6 +459,25 @@ func (c *CommandClient) handleConnectionsStream() {
}
}
func (c *CommandClient) handleOutboundsStream() {
client, ctx := c.getStreamContext()
stream, err := client.SubscribeOutbounds(ctx, &emptypb.Empty{})
if err != nil {
c.handler.Disconnected(err.Error())
return
}
for {
list, err := stream.Recv()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteOutbounds(outboundGroupItemListFromGRPC(list))
}
}
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{
@@ -574,8 +596,10 @@ func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) {
var notes []*DeprecatedNote
for _, warning := range warnings.Warnings {
notes = append(notes, &DeprecatedNote{
Description: warning.Message,
MigrationLink: warning.MigrationLink,
Description: warning.Description,
DeprecatedVersion: warning.DeprecatedVersion,
ScheduledVersion: warning.ScheduledVersion,
MigrationLink: warning.MigrationLink,
})
}
return newIterator(notes), nil
@@ -601,3 +625,78 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
})
return err
}
func (c *CommandClient) ListOutbounds() (OutboundGroupItemIterator, error) {
return callWithResult(c, func(client daemon.StartedServiceClient) (OutboundGroupItemIterator, error) {
list, err := client.ListOutbounds(context.Background(), &emptypb.Empty{})
if err != nil {
return nil, err
}
return outboundGroupItemListFromGRPC(list), nil
})
}
func (c *CommandClient) StartNetworkQualityTest(configURL string, outboundTag string, handler NetworkQualityTestHandler) error {
return c.StartNetworkQualityTestWithSerialAndRuntime(
configURL,
outboundTag,
false,
NetworkQualityDefaultMaxRuntimeSeconds,
handler,
)
}
func (c *CommandClient) StartNetworkQualityTestWithSerial(configURL string, outboundTag string, serial bool, handler NetworkQualityTestHandler) error {
return c.StartNetworkQualityTestWithSerialAndRuntime(
configURL,
outboundTag,
serial,
NetworkQualityDefaultMaxRuntimeSeconds,
handler,
)
}
func (c *CommandClient) StartNetworkQualityTestWithSerialAndRuntime(configURL string, outboundTag string, serial bool, maxRuntimeSeconds int32, handler NetworkQualityTestHandler) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
if c.standalone {
defer c.closeConnection()
}
stream, err := client.StartNetworkQualityTest(context.Background(), &daemon.NetworkQualityTestRequest{
ConfigURL: configURL,
OutboundTag: outboundTag,
Serial: serial,
MaxRuntimeSeconds: maxRuntimeSeconds,
})
if err != nil {
return err
}
for {
event, recvErr := stream.Recv()
if recvErr != nil {
handler.OnError(recvErr.Error())
return recvErr
}
if event.IsFinal {
if event.Error != "" {
handler.OnError(event.Error)
} else {
handler.OnResult(&NetworkQualityResult{
DownloadCapacity: event.DownloadCapacity,
UploadCapacity: event.UploadCapacity,
DownloadRPM: event.DownloadRPM,
UploadRPM: event.UploadRPM,
IdleLatencyMs: event.IdleLatencyMs,
DownloadCapacityAccuracy: event.DownloadCapacityAccuracy,
UploadCapacityAccuracy: event.UploadCapacityAccuracy,
DownloadRPMAccuracy: event.DownloadRPMAccuracy,
UploadRPMAccuracy: event.UploadRPMAccuracy,
})
}
return nil
}
handler.OnProgress(networkQualityProgressFromGRPC(event))
}
}

View File

@@ -0,0 +1,71 @@
package libbox
import "github.com/sagernet/sing-box/daemon"
type NetworkQualityProgress struct {
Phase int32
DownloadCapacity int64
UploadCapacity int64
DownloadRPM int32
UploadRPM int32
IdleLatencyMs int32
ElapsedMs int64
IsFinal bool
Error string
DownloadCapacityAccuracy int32
UploadCapacityAccuracy int32
DownloadRPMAccuracy int32
UploadRPMAccuracy int32
}
type NetworkQualityResult struct {
DownloadCapacity int64
UploadCapacity int64
DownloadRPM int32
UploadRPM int32
IdleLatencyMs int32
DownloadCapacityAccuracy int32
UploadCapacityAccuracy int32
DownloadRPMAccuracy int32
UploadRPMAccuracy int32
}
type NetworkQualityTestHandler interface {
OnProgress(progress *NetworkQualityProgress)
OnResult(result *NetworkQualityResult)
OnError(message string)
}
func outboundGroupItemListFromGRPC(list *daemon.OutboundList) OutboundGroupItemIterator {
if list == nil || len(list.Outbounds) == 0 {
return newIterator([]*OutboundGroupItem{})
}
var items []*OutboundGroupItem
for _, ob := range list.Outbounds {
items = append(items, &OutboundGroupItem{
Tag: ob.Tag,
Type: ob.Type,
URLTestTime: ob.UrlTestTime,
URLTestDelay: ob.UrlTestDelay,
})
}
return newIterator(items)
}
func networkQualityProgressFromGRPC(event *daemon.NetworkQualityTestProgress) *NetworkQualityProgress {
return &NetworkQualityProgress{
Phase: event.Phase,
DownloadCapacity: event.DownloadCapacity,
UploadCapacity: event.UploadCapacity,
DownloadRPM: event.DownloadRPM,
UploadRPM: event.UploadRPM,
IdleLatencyMs: event.IdleLatencyMs,
ElapsedMs: event.ElapsedMs,
IsFinal: event.IsFinal,
Error: event.Error,
DownloadCapacityAccuracy: event.DownloadCapacityAccuracy,
UploadCapacityAccuracy: event.UploadCapacityAccuracy,
DownloadRPMAccuracy: event.DownloadRPMAccuracy,
UploadRPMAccuracy: event.UploadRPMAccuracy,
}
}

View File

@@ -6,7 +6,6 @@ import (
"runtime"
_ "runtime/pprof"
"unsafe"
_ "unsafe"
)

View File

@@ -6,7 +6,6 @@ import (
"encoding/binary"
"os"
"unsafe"
_ "unsafe"
)

View File

@@ -0,0 +1,75 @@
package libbox
import (
"context"
"time"
"github.com/sagernet/sing-box/common/networkquality"
)
type NetworkQualityTest struct {
ctx context.Context
cancel context.CancelFunc
}
func NewNetworkQualityTest() *NetworkQualityTest {
ctx, cancel := context.WithCancel(context.Background())
return &NetworkQualityTest{ctx: ctx, cancel: cancel}
}
func (t *NetworkQualityTest) Start(configURL string, handler NetworkQualityTestHandler) {
t.StartWithSerialAndRuntime(configURL, false, NetworkQualityDefaultMaxRuntimeSeconds, handler)
}
func (t *NetworkQualityTest) StartWithSerial(configURL string, serial bool, handler NetworkQualityTestHandler) {
t.StartWithSerialAndRuntime(configURL, serial, NetworkQualityDefaultMaxRuntimeSeconds, handler)
}
func (t *NetworkQualityTest) StartWithSerialAndRuntime(configURL string, serial bool, maxRuntimeSeconds int32, handler NetworkQualityTestHandler) {
go func() {
httpClient := networkquality.NewHTTPClient(nil)
defer httpClient.CloseIdleConnections()
result, err := networkquality.Run(networkquality.Options{
ConfigURL: configURL,
HTTPClient: httpClient,
Serial: serial,
MaxRuntime: time.Duration(maxRuntimeSeconds) * time.Second,
Context: t.ctx,
OnProgress: func(p networkquality.Progress) {
handler.OnProgress(&NetworkQualityProgress{
Phase: int32(p.Phase),
DownloadCapacity: p.DownloadCapacity,
UploadCapacity: p.UploadCapacity,
DownloadRPM: p.DownloadRPM,
UploadRPM: p.UploadRPM,
IdleLatencyMs: p.IdleLatencyMs,
ElapsedMs: p.ElapsedMs,
DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(p.UploadRPMAccuracy),
})
},
})
if err != nil {
handler.OnError(err.Error())
return
}
handler.OnResult(&NetworkQualityResult{
DownloadCapacity: result.DownloadCapacity,
UploadCapacity: result.UploadCapacity,
DownloadRPM: result.DownloadRPM,
UploadRPM: result.UploadRPM,
IdleLatencyMs: result.IdleLatencyMs,
DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(result.UploadRPMAccuracy),
})
}()
}
func (t *NetworkQualityTest) Cancel() {
t.cancel()
}

View File

@@ -1,18 +1,22 @@
package libbox
import (
"fmt"
"math"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"time"
"github.com/sagernet/sing-box/common/networkquality"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/locale"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions"
)
var (
@@ -97,8 +101,14 @@ func Setup(options *SetupOptions) error {
return redirectStderr(filepath.Join(sWorkingPath, "CrashReport-"+sCrashReportSource+".log"))
}
func SetLocale(localeId string) {
locale.Set(localeId)
func SetLocale(localeId string) error {
if strings.Contains(localeId, "@") {
localeId = strings.Split(localeId, "@")[0]
}
if !locale.Set(localeId) {
return E.New("unsupported locale: ", localeId)
}
return nil
}
func Version() string {
@@ -121,6 +131,29 @@ func FormatDuration(duration int64) string {
return log.FormatDuration(time.Duration(duration) * time.Millisecond)
}
func FormatBitrate(bps int64) string {
switch {
case bps >= 1_000_000_000:
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
case bps >= 1_000_000:
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
case bps >= 1_000:
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
default:
return fmt.Sprintf("%d bps", bps)
}
}
const NetworkQualityDefaultConfigURL = networkquality.DefaultConfigURL
const NetworkQualityDefaultMaxRuntimeSeconds = int32(networkquality.DefaultMaxRuntime / time.Second)
const (
NetworkQualityAccuracyLow = int32(networkquality.AccuracyLow)
NetworkQualityAccuracyMedium = int32(networkquality.AccuracyMedium)
NetworkQualityAccuracyHigh = int32(networkquality.AccuracyHigh)
)
func ProxyDisplayType(proxyType string) string {
return C.ProxyDisplayName(proxyType)
}

2
go.mod
View File

@@ -43,7 +43,7 @@ require (
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.8.7-0.20260407061419-c15a3c764d88
github.com/sagernet/sing-tun v0.8.7-0.20260407152316-3ded9b354c8a
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7

4
go.sum
View File

@@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.7-0.20260407061419-c15a3c764d88 h1:jRxRE0nlEWkm4z+1rQxJzp/ji9yZ1FMDqMyV/evMlWA=
github.com/sagernet/sing-tun v0.8.7-0.20260407061419-c15a3c764d88/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-tun v0.8.7-0.20260407152316-3ded9b354c8a h1:L3757AYMq32oOb9iW2j7D/tat7eE7nvnthi7V1rJvwM=
github.com/sagernet/sing-tun v0.8.7-0.20260407152316-3ded9b354c8a/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
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/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=

View File

@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing/common/json/badoption"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
"github.com/stretchr/testify/require"
)

View File

@@ -33,6 +33,7 @@ static void stopMemoryPressureMonitor() {
import "C"
import (
runtimeDebug "runtime/debug"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -88,6 +89,7 @@ func (s *Service) Close() error {
//export goMemoryPressureCallback
func goMemoryPressureCallback(status C.ulong) {
runtimeDebug.FreeOSMemory()
globalAccess.Lock()
services := make([]*Service, len(globalServices))
copy(services, globalServices)

View File

@@ -107,6 +107,7 @@ type adaptiveTimer struct {
access sync.Mutex
timer *time.Timer
state pressureState
currentInterval time.Duration
forceMinInterval bool
pendingPressureBaseline bool
pressureBaseline memorySample
@@ -178,7 +179,9 @@ func (t *adaptiveTimer) poll() {
t.state = t.nextState(sample)
if t.state == pressureStateNormal {
t.forceMinInterval = false
t.pressureBaselineTime = time.Time{}
if !t.pressureBaselineTime.IsZero() && time.Since(t.pressureBaselineTime) > t.maxInterval {
t.pressureBaselineTime = time.Time{}
}
}
t.timer.Reset(t.intervalForState())
triggered = previousState != pressureStateTriggered && t.state == pressureStateTriggered
@@ -272,13 +275,19 @@ func (t *adaptiveTimer) availableThresholds(sample memorySample) pressureThresho
}
func (t *adaptiveTimer) intervalForState() time.Duration {
if t.state == pressureStateNormal {
return t.maxInterval
switch {
case t.forceMinInterval || t.state == pressureStateTriggered:
t.currentInterval = t.minInterval
case t.state == pressureStateArmed:
t.currentInterval = t.armedInterval
default:
if t.currentInterval == 0 {
t.currentInterval = t.maxInterval
} else {
t.currentInterval = min(t.currentInterval*2, t.maxInterval)
}
}
if t.forceMinInterval || t.state == pressureStateTriggered {
return t.minInterval
}
return t.armedInterval
return t.currentInterval
}
func (t *adaptiveTimer) logDetails(sample memorySample) string {