浏览代码

A bit of refactor

世界 1 周之前
父节点
当前提交
f1e48a1cad
共有 62 个文件被更改,包括 6006 次插入1901 次删除
  1. 1 2
      adapter/inbound.go
  2. 68 0
      adapter/platform.go
  3. 5 2
      box.go
  4. 1 2
      common/certificate/store.go
  5. 1 2
      common/dialer/default.go
  6. 4 11
      common/process/searcher.go
  7. 9 8
      common/process/searcher_android.go
  8. 3 2
      common/process/searcher_darwin.go
  9. 3 2
      common/process/searcher_linux.go
  10. 4 3
      common/process/searcher_windows.go
  11. 29 0
      daemon/deprecated.go
  12. 702 0
      daemon/helper.pb.go
  13. 61 0
      daemon/helper.proto
  14. 147 0
      daemon/instance.go
  15. 22 0
      daemon/platform.go
  16. 775 0
      daemon/started_service.go
  17. 1906 0
      daemon/started_service.pb.go
  18. 204 0
      daemon/started_service.proto
  19. 919 0
      daemon/started_service_grpc.pb.go
  20. 1 2
      dns/router.go
  21. 48 48
      docs/configuration/dns/server/index.zh.md
  22. 4 4
      experimental/clashapi/trafficontrol/tracker.go
  23. 0 11
      experimental/libbox/command.go
  24. 0 124
      experimental/libbox/command_clash_mode.go
  25. 380 69
      experimental/libbox/command_client.go
  26. 0 54
      experimental/libbox/command_close_connection.go
  27. 0 269
      experimental/libbox/command_connections.go
  28. 0 28
      experimental/libbox/command_conntrack.go
  29. 0 46
      experimental/libbox/command_deprecated_report.go
  30. 0 198
      experimental/libbox/command_group.go
  31. 0 160
      experimental/libbox/command_log.go
  32. 0 59
      experimental/libbox/command_power.go
  33. 0 58
      experimental/libbox/command_select.go
  34. 177 134
      experimental/libbox/command_server.go
  35. 0 39
      experimental/libbox/command_shared.go
  36. 0 85
      experimental/libbox/command_status.go
  37. 0 80
      experimental/libbox/command_system_proxy.go
  38. 276 0
      experimental/libbox/command_types.go
  39. 0 86
      experimental/libbox/command_urltest.go
  40. 36 11
      experimental/libbox/config.go
  41. 14 11
      experimental/libbox/http.go
  42. 6 0
      experimental/libbox/iterator.go
  43. 1 1
      experimental/libbox/monitor.go
  44. 0 6
      experimental/libbox/platform.go
  45. 0 35
      experimental/libbox/platform/interface.go
  46. 67 118
      experimental/libbox/service.go
  47. 0 36
      experimental/libbox/service_pause.go
  48. 27 31
      experimental/libbox/setup.go
  49. 1 1
      experimental/libbox/tun.go
  50. 23 21
      log/observable.go
  51. 0 1
      log/platform.go
  52. 3 4
      protocol/tailscale/endpoint.go
  53. 2 2
      protocol/tailscale/protect_android.go
  54. 2 2
      protocol/tailscale/protect_nonandroid.go
  55. 5 6
      protocol/tun/inbound.go
  56. 4 5
      route/network.go
  57. 45 0
      route/platform_searcher.go
  58. 6 6
      route/route.go
  59. 4 5
      route/router.go
  60. 2 2
      route/rule/rule_item_package_name.go
  61. 2 2
      route/rule/rule_item_user.go
  62. 6 7
      service/resolved/resolve1.go

+ 1 - 2
adapter/inbound.go

@@ -5,7 +5,6 @@ import (
 	"net/netip"
 	"time"
 
-	"github.com/sagernet/sing-box/common/process"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -85,7 +84,7 @@ type InboundContext struct {
 	DestinationAddresses []netip.Addr
 	SourceGeoIPCode      string
 	GeoIPCode            string
-	ProcessInfo          *process.Info
+	ProcessInfo          *ConnectionOwner
 	QueryType            uint16
 	FakeIP               bool
 

+ 68 - 0
adapter/platform.go

@@ -0,0 +1,68 @@
+package adapter
+
+import (
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-tun"
+	"github.com/sagernet/sing/common/logger"
+)
+
+type PlatformInterface interface {
+	Initialize(networkManager NetworkManager) error
+
+	UsePlatformAutoDetectInterfaceControl() bool
+	AutoDetectInterfaceControl(fd int) error
+
+	UsePlatformInterface() bool
+	OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
+
+	UsePlatformDefaultInterfaceMonitor() bool
+	CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
+
+	UsePlatformNetworkInterfaces() bool
+	NetworkInterfaces() ([]NetworkInterface, error)
+
+	UnderNetworkExtension() bool
+	NetworkExtensionIncludeAllNetworks() bool
+
+	ClearDNSCache()
+	RequestPermissionForWIFIState() error
+	ReadWIFIState() WIFIState
+	SystemCertificates() []string
+
+	UsePlatformConnectionOwnerFinder() bool
+	FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
+
+	UsePlatformNotification() bool
+	SendNotification(notification *Notification) error
+}
+
+type FindConnectionOwnerRequest struct {
+	IpProtocol         int32
+	SourceAddress      string
+	SourcePort         int32
+	DestinationAddress string
+	DestinationPort    int32
+}
+
+type ConnectionOwner struct {
+	ProcessID          uint32
+	UserId             int32
+	UserName           string
+	ProcessPath        string
+	AndroidPackageName string
+}
+
+type Notification struct {
+	Identifier string
+	TypeName   string
+	TypeID     int32
+	Title      string
+	Subtitle   string
+	Body       string
+	OpenURL    string
+}
+
+type SystemProxyStatus struct {
+	Available bool
+	Enabled   bool
+}

+ 5 - 2
box.go

@@ -22,7 +22,6 @@ import (
 	"github.com/sagernet/sing-box/dns/transport/local"
 	"github.com/sagernet/sing-box/experimental"
 	"github.com/sagernet/sing-box/experimental/cachefile"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/protocol/direct"
@@ -139,7 +138,7 @@ func New(options Options) (*Box, error) {
 	if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
 		needV2RayAPI = true
 	}
-	platformInterface := service.FromContext[platform.Interface](ctx)
+	platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
 	var defaultLogWriter io.Writer
 	if platformInterface != nil {
 		defaultLogWriter = io.Discard
@@ -527,3 +526,7 @@ func (s *Box) Inbound() adapter.InboundManager {
 func (s *Box) Outbound() adapter.OutboundManager {
 	return s.outbound
 }
+
+func (s *Box) LogFactory() log.Factory {
+	return s.logFactory
+}

+ 1 - 2
common/certificate/store.go

@@ -12,7 +12,6 @@ import (
 	"github.com/sagernet/fswatch"
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
@@ -36,7 +35,7 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
 	switch options.Store {
 	case C.CertificateStoreSystem, "":
 		systemPool = x509.NewCertPool()
-		platformInterface := service.FromContext[platform.Interface](ctx)
+		platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
 		var systemValid bool
 		if platformInterface != nil {
 			for _, cert := range platformInterface.SystemCertificates() {

+ 1 - 2
common/dialer/default.go

@@ -12,7 +12,6 @@ import (
 	"github.com/sagernet/sing-box/common/conntrack"
 	"github.com/sagernet/sing-box/common/listener"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/control"
@@ -49,7 +48,7 @@ type DefaultDialer struct {
 
 func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
 	networkManager := service.FromContext[adapter.NetworkManager](ctx)
-	platformInterface := service.FromContext[platform.Interface](ctx)
+	platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
 
 	var (
 		dialer                 net.Dialer

+ 4 - 11
common/process/searcher.go

@@ -5,6 +5,7 @@ import (
 	"net/netip"
 	"os/user"
 
+	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-tun"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -12,7 +13,7 @@ import (
 )
 
 type Searcher interface {
-	FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error)
+	FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
 }
 
 var ErrNotFound = E.New("process not found")
@@ -22,15 +23,7 @@ type Config struct {
 	PackageManager tun.PackageManager
 }
 
-type Info struct {
-	ProcessID   uint32
-	ProcessPath string
-	PackageName string
-	User        string
-	UserId      int32
-}
-
-func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
+func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
 	info, err := searcher.FindProcessInfo(ctx, network, source, destination)
 	if err != nil {
 		return nil, err
@@ -38,7 +31,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
 	if info.UserId != -1 {
 		osUser, _ := user.LookupId(F.ToString(info.UserId))
 		if osUser != nil {
-			info.User = osUser.Username
+			info.UserName = osUser.Username
 		}
 	}
 	return info, nil

+ 9 - 8
common/process/searcher_android.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"net/netip"
 
+	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-tun"
 )
 
@@ -17,22 +18,22 @@ func NewSearcher(config Config) (Searcher, error) {
 	return &androidSearcher{config.PackageManager}, nil
 }
 
-func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
+func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
 	_, uid, err := resolveSocketByNetlink(network, source, destination)
 	if err != nil {
 		return nil, err
 	}
 	if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
-		return &Info{
-			UserId:      int32(uid),
-			PackageName: sharedPackage,
+		return &adapter.ConnectionOwner{
+			UserId:             int32(uid),
+			AndroidPackageName: sharedPackage,
 		}, nil
 	}
 	if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
-		return &Info{
-			UserId:      int32(uid),
-			PackageName: packageName,
+		return &adapter.ConnectionOwner{
+			UserId:             int32(uid),
+			AndroidPackageName: packageName,
 		}, nil
 	}
-	return &Info{UserId: int32(uid)}, nil
+	return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
 }

+ 3 - 2
common/process/searcher_darwin.go

@@ -10,6 +10,7 @@ import (
 	"syscall"
 	"unsafe"
 
+	"github.com/sagernet/sing-box/adapter"
 	N "github.com/sagernet/sing/common/network"
 
 	"golang.org/x/sys/unix"
@@ -23,12 +24,12 @@ func NewSearcher(_ Config) (Searcher, error) {
 	return &darwinSearcher{}, nil
 }
 
-func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
+func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
 	processName, err := findProcessName(network, source.Addr(), int(source.Port()))
 	if err != nil {
 		return nil, err
 	}
-	return &Info{ProcessPath: processName, UserId: -1}, nil
+	return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
 }
 
 var structSize = func() int {

+ 3 - 2
common/process/searcher_linux.go

@@ -6,6 +6,7 @@ import (
 	"context"
 	"net/netip"
 
+	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/log"
 )
 
@@ -19,7 +20,7 @@ func NewSearcher(config Config) (Searcher, error) {
 	return &linuxSearcher{config.Logger}, nil
 }
 
-func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
+func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
 	inode, uid, err := resolveSocketByNetlink(network, source, destination)
 	if err != nil {
 		return nil, err
@@ -28,7 +29,7 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
 	if err != nil {
 		s.logger.DebugContext(ctx, "find process path: ", err)
 	}
-	return &Info{
+	return &adapter.ConnectionOwner{
 		UserId:      int32(uid),
 		ProcessPath: processPath,
 	}, nil

+ 4 - 3
common/process/searcher_windows.go

@@ -5,6 +5,7 @@ import (
 	"net/netip"
 	"syscall"
 
+	"github.com/sagernet/sing-box/adapter"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/winiphlpapi"
 
@@ -27,16 +28,16 @@ func initWin32API() error {
 	return winiphlpapi.LoadExtendedTable()
 }
 
-func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
+func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
 	pid, err := winiphlpapi.FindPid(network, source)
 	if err != nil {
 		return nil, err
 	}
 	path, err := getProcessPath(pid)
 	if err != nil {
-		return &Info{ProcessID: pid, UserId: -1}, err
+		return &adapter.ConnectionOwner{ProcessID: pid, UserId: -1}, err
 	}
-	return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
+	return &adapter.ConnectionOwner{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
 }
 
 func getProcessPath(pid uint32) (string, error) {

+ 29 - 0
daemon/deprecated.go

@@ -0,0 +1,29 @@
+package daemon
+
+import (
+	"sync"
+
+	"github.com/sagernet/sing-box/experimental/deprecated"
+	"github.com/sagernet/sing/common"
+)
+
+var _ deprecated.Manager = (*deprecatedManager)(nil)
+
+type deprecatedManager struct {
+	access sync.Mutex
+	notes  []deprecated.Note
+}
+
+func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) {
+	m.access.Lock()
+	defer m.access.Unlock()
+	m.notes = common.Uniq(append(m.notes, feature))
+}
+
+func (m *deprecatedManager) Get() []deprecated.Note {
+	m.access.Lock()
+	defer m.access.Unlock()
+	notes := m.notes
+	m.notes = nil
+	return notes
+}

+ 702 - 0
daemon/helper.pb.go

@@ -0,0 +1,702 @@
+package daemon
+
+import (
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	emptypb "google.golang.org/protobuf/types/known/emptypb"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type SubscribeHelperRequestRequest struct {
+	state                             protoimpl.MessageState `protogen:"open.v1"`
+	AcceptGetWIFIStateRequests        bool                   `protobuf:"varint,1,opt,name=acceptGetWIFIStateRequests,proto3" json:"acceptGetWIFIStateRequests,omitempty"`
+	AcceptFindConnectionOwnerRequests bool                   `protobuf:"varint,2,opt,name=acceptFindConnectionOwnerRequests,proto3" json:"acceptFindConnectionOwnerRequests,omitempty"`
+	AcceptSendNotificationRequests    bool                   `protobuf:"varint,3,opt,name=acceptSendNotificationRequests,proto3" json:"acceptSendNotificationRequests,omitempty"`
+	unknownFields                     protoimpl.UnknownFields
+	sizeCache                         protoimpl.SizeCache
+}
+
+func (x *SubscribeHelperRequestRequest) Reset() {
+	*x = SubscribeHelperRequestRequest{}
+	mi := &file_daemon_helper_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SubscribeHelperRequestRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SubscribeHelperRequestRequest) ProtoMessage() {}
+
+func (x *SubscribeHelperRequestRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_helper_proto_msgTypes[0]
+	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 SubscribeHelperRequestRequest.ProtoReflect.Descriptor instead.
+func (*SubscribeHelperRequestRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_helper_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SubscribeHelperRequestRequest) GetAcceptGetWIFIStateRequests() bool {
+	if x != nil {
+		return x.AcceptGetWIFIStateRequests
+	}
+	return false
+}
+
+func (x *SubscribeHelperRequestRequest) GetAcceptFindConnectionOwnerRequests() bool {
+	if x != nil {
+		return x.AcceptFindConnectionOwnerRequests
+	}
+	return false
+}
+
+func (x *SubscribeHelperRequestRequest) GetAcceptSendNotificationRequests() bool {
+	if x != nil {
+		return x.AcceptSendNotificationRequests
+	}
+	return false
+}
+
+type HelperRequest struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	Id    int64                  `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+	// Types that are valid to be assigned to Request:
+	//
+	//	*HelperRequest_GetWIFIState
+	//	*HelperRequest_FindConnectionOwner
+	//	*HelperRequest_SendNotification
+	Request       isHelperRequest_Request `protobuf_oneof:"request"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *HelperRequest) Reset() {
+	*x = HelperRequest{}
+	mi := &file_daemon_helper_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *HelperRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HelperRequest) ProtoMessage() {}
+
+func (x *HelperRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_helper_proto_msgTypes[1]
+	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 HelperRequest.ProtoReflect.Descriptor instead.
+func (*HelperRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_helper_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *HelperRequest) GetId() int64 {
+	if x != nil {
+		return x.Id
+	}
+	return 0
+}
+
+func (x *HelperRequest) GetRequest() isHelperRequest_Request {
+	if x != nil {
+		return x.Request
+	}
+	return nil
+}
+
+func (x *HelperRequest) GetGetWIFIState() *emptypb.Empty {
+	if x != nil {
+		if x, ok := x.Request.(*HelperRequest_GetWIFIState); ok {
+			return x.GetWIFIState
+		}
+	}
+	return nil
+}
+
+func (x *HelperRequest) GetFindConnectionOwner() *FindConnectionOwnerRequest {
+	if x != nil {
+		if x, ok := x.Request.(*HelperRequest_FindConnectionOwner); ok {
+			return x.FindConnectionOwner
+		}
+	}
+	return nil
+}
+
+func (x *HelperRequest) GetSendNotification() *Notification {
+	if x != nil {
+		if x, ok := x.Request.(*HelperRequest_SendNotification); ok {
+			return x.SendNotification
+		}
+	}
+	return nil
+}
+
+type isHelperRequest_Request interface {
+	isHelperRequest_Request()
+}
+
+type HelperRequest_GetWIFIState struct {
+	GetWIFIState *emptypb.Empty `protobuf:"bytes,2,opt,name=getWIFIState,proto3,oneof"`
+}
+
+type HelperRequest_FindConnectionOwner struct {
+	FindConnectionOwner *FindConnectionOwnerRequest `protobuf:"bytes,3,opt,name=findConnectionOwner,proto3,oneof"`
+}
+
+type HelperRequest_SendNotification struct {
+	SendNotification *Notification `protobuf:"bytes,4,opt,name=sendNotification,proto3,oneof"`
+}
+
+func (*HelperRequest_GetWIFIState) isHelperRequest_Request() {}
+
+func (*HelperRequest_FindConnectionOwner) isHelperRequest_Request() {}
+
+func (*HelperRequest_SendNotification) isHelperRequest_Request() {}
+
+type FindConnectionOwnerRequest struct {
+	state              protoimpl.MessageState `protogen:"open.v1"`
+	IpProtocol         int32                  `protobuf:"varint,1,opt,name=ipProtocol,proto3" json:"ipProtocol,omitempty"`
+	SourceAddress      string                 `protobuf:"bytes,2,opt,name=sourceAddress,proto3" json:"sourceAddress,omitempty"`
+	SourcePort         int32                  `protobuf:"varint,3,opt,name=sourcePort,proto3" json:"sourcePort,omitempty"`
+	DestinationAddress string                 `protobuf:"bytes,4,opt,name=destinationAddress,proto3" json:"destinationAddress,omitempty"`
+	DestinationPort    int32                  `protobuf:"varint,5,opt,name=destinationPort,proto3" json:"destinationPort,omitempty"`
+	unknownFields      protoimpl.UnknownFields
+	sizeCache          protoimpl.SizeCache
+}
+
+func (x *FindConnectionOwnerRequest) Reset() {
+	*x = FindConnectionOwnerRequest{}
+	mi := &file_daemon_helper_proto_msgTypes[2]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *FindConnectionOwnerRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FindConnectionOwnerRequest) ProtoMessage() {}
+
+func (x *FindConnectionOwnerRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_helper_proto_msgTypes[2]
+	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 FindConnectionOwnerRequest.ProtoReflect.Descriptor instead.
+func (*FindConnectionOwnerRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_helper_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *FindConnectionOwnerRequest) GetIpProtocol() int32 {
+	if x != nil {
+		return x.IpProtocol
+	}
+	return 0
+}
+
+func (x *FindConnectionOwnerRequest) GetSourceAddress() string {
+	if x != nil {
+		return x.SourceAddress
+	}
+	return ""
+}
+
+func (x *FindConnectionOwnerRequest) GetSourcePort() int32 {
+	if x != nil {
+		return x.SourcePort
+	}
+	return 0
+}
+
+func (x *FindConnectionOwnerRequest) GetDestinationAddress() string {
+	if x != nil {
+		return x.DestinationAddress
+	}
+	return ""
+}
+
+func (x *FindConnectionOwnerRequest) GetDestinationPort() int32 {
+	if x != nil {
+		return x.DestinationPort
+	}
+	return 0
+}
+
+type Notification struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Identifier    string                 `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
+	TypeName      string                 `protobuf:"bytes,2,opt,name=typeName,proto3" json:"typeName,omitempty"`
+	TypeId        int32                  `protobuf:"varint,3,opt,name=typeId,proto3" json:"typeId,omitempty"`
+	Title         string                 `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"`
+	Subtitle      string                 `protobuf:"bytes,5,opt,name=subtitle,proto3" json:"subtitle,omitempty"`
+	Body          string                 `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
+	OpenURL       string                 `protobuf:"bytes,7,opt,name=openURL,proto3" json:"openURL,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Notification) Reset() {
+	*x = Notification{}
+	mi := &file_daemon_helper_proto_msgTypes[3]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Notification) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Notification) ProtoMessage() {}
+
+func (x *Notification) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_helper_proto_msgTypes[3]
+	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 Notification.ProtoReflect.Descriptor instead.
+func (*Notification) Descriptor() ([]byte, []int) {
+	return file_daemon_helper_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *Notification) GetIdentifier() string {
+	if x != nil {
+		return x.Identifier
+	}
+	return ""
+}
+
+func (x *Notification) GetTypeName() string {
+	if x != nil {
+		return x.TypeName
+	}
+	return ""
+}
+
+func (x *Notification) GetTypeId() int32 {
+	if x != nil {
+		return x.TypeId
+	}
+	return 0
+}
+
+func (x *Notification) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *Notification) GetSubtitle() string {
+	if x != nil {
+		return x.Subtitle
+	}
+	return ""
+}
+
+func (x *Notification) GetBody() string {
+	if x != nil {
+		return x.Body
+	}
+	return ""
+}
+
+func (x *Notification) GetOpenURL() string {
+	if x != nil {
+		return x.OpenURL
+	}
+	return ""
+}
+
+type HelperResponse struct {
+	state protoimpl.MessageState `protogen:"open.v1"`
+	Id    int64                  `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+	// Types that are valid to be assigned to Response:
+	//
+	//	*HelperResponse_WifiState
+	//	*HelperResponse_Error
+	//	*HelperResponse_ConnectionOwner
+	Response      isHelperResponse_Response `protobuf_oneof:"response"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *HelperResponse) Reset() {
+	*x = HelperResponse{}
+	mi := &file_daemon_helper_proto_msgTypes[4]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *HelperResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HelperResponse) ProtoMessage() {}
+
+func (x *HelperResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_helper_proto_msgTypes[4]
+	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 HelperResponse.ProtoReflect.Descriptor instead.
+func (*HelperResponse) Descriptor() ([]byte, []int) {
+	return file_daemon_helper_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *HelperResponse) GetId() int64 {
+	if x != nil {
+		return x.Id
+	}
+	return 0
+}
+
+func (x *HelperResponse) GetResponse() isHelperResponse_Response {
+	if x != nil {
+		return x.Response
+	}
+	return nil
+}
+
+func (x *HelperResponse) GetWifiState() *WIFIState {
+	if x != nil {
+		if x, ok := x.Response.(*HelperResponse_WifiState); ok {
+			return x.WifiState
+		}
+	}
+	return nil
+}
+
+func (x *HelperResponse) GetError() string {
+	if x != nil {
+		if x, ok := x.Response.(*HelperResponse_Error); ok {
+			return x.Error
+		}
+	}
+	return ""
+}
+
+func (x *HelperResponse) GetConnectionOwner() *ConnectionOwner {
+	if x != nil {
+		if x, ok := x.Response.(*HelperResponse_ConnectionOwner); ok {
+			return x.ConnectionOwner
+		}
+	}
+	return nil
+}
+
+type isHelperResponse_Response interface {
+	isHelperResponse_Response()
+}
+
+type HelperResponse_WifiState struct {
+	WifiState *WIFIState `protobuf:"bytes,2,opt,name=wifiState,proto3,oneof"`
+}
+
+type HelperResponse_Error struct {
+	Error string `protobuf:"bytes,3,opt,name=error,proto3,oneof"`
+}
+
+type HelperResponse_ConnectionOwner struct {
+	ConnectionOwner *ConnectionOwner `protobuf:"bytes,4,opt,name=connectionOwner,proto3,oneof"`
+}
+
+func (*HelperResponse_WifiState) isHelperResponse_Response() {}
+
+func (*HelperResponse_Error) isHelperResponse_Response() {}
+
+func (*HelperResponse_ConnectionOwner) isHelperResponse_Response() {}
+
+type ConnectionOwner struct {
+	state              protoimpl.MessageState `protogen:"open.v1"`
+	UserId             int32                  `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"`
+	UserName           string                 `protobuf:"bytes,2,opt,name=userName,proto3" json:"userName,omitempty"`
+	ProcessPath        string                 `protobuf:"bytes,3,opt,name=processPath,proto3" json:"processPath,omitempty"`
+	AndroidPackageName string                 `protobuf:"bytes,4,opt,name=androidPackageName,proto3" json:"androidPackageName,omitempty"`
+	unknownFields      protoimpl.UnknownFields
+	sizeCache          protoimpl.SizeCache
+}
+
+func (x *ConnectionOwner) Reset() {
+	*x = ConnectionOwner{}
+	mi := &file_daemon_helper_proto_msgTypes[5]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ConnectionOwner) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ConnectionOwner) ProtoMessage() {}
+
+func (x *ConnectionOwner) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_helper_proto_msgTypes[5]
+	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 ConnectionOwner.ProtoReflect.Descriptor instead.
+func (*ConnectionOwner) Descriptor() ([]byte, []int) {
+	return file_daemon_helper_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *ConnectionOwner) GetUserId() int32 {
+	if x != nil {
+		return x.UserId
+	}
+	return 0
+}
+
+func (x *ConnectionOwner) GetUserName() string {
+	if x != nil {
+		return x.UserName
+	}
+	return ""
+}
+
+func (x *ConnectionOwner) GetProcessPath() string {
+	if x != nil {
+		return x.ProcessPath
+	}
+	return ""
+}
+
+func (x *ConnectionOwner) GetAndroidPackageName() string {
+	if x != nil {
+		return x.AndroidPackageName
+	}
+	return ""
+}
+
+type WIFIState struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Ssid          string                 `protobuf:"bytes,1,opt,name=ssid,proto3" json:"ssid,omitempty"`
+	Bssid         string                 `protobuf:"bytes,2,opt,name=bssid,proto3" json:"bssid,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *WIFIState) Reset() {
+	*x = WIFIState{}
+	mi := &file_daemon_helper_proto_msgTypes[6]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *WIFIState) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WIFIState) ProtoMessage() {}
+
+func (x *WIFIState) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_helper_proto_msgTypes[6]
+	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 WIFIState.ProtoReflect.Descriptor instead.
+func (*WIFIState) Descriptor() ([]byte, []int) {
+	return file_daemon_helper_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *WIFIState) GetSsid() string {
+	if x != nil {
+		return x.Ssid
+	}
+	return ""
+}
+
+func (x *WIFIState) GetBssid() string {
+	if x != nil {
+		return x.Bssid
+	}
+	return ""
+}
+
+var File_daemon_helper_proto protoreflect.FileDescriptor
+
+const file_daemon_helper_proto_rawDesc = "" +
+	"\n" +
+	"\x13daemon/helper.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xf5\x01\n" +
+	"\x1dSubscribeHelperRequestRequest\x12>\n" +
+	"\x1aacceptGetWIFIStateRequests\x18\x01 \x01(\bR\x1aacceptGetWIFIStateRequests\x12L\n" +
+	"!acceptFindConnectionOwnerRequests\x18\x02 \x01(\bR!acceptFindConnectionOwnerRequests\x12F\n" +
+	"\x1eacceptSendNotificationRequests\x18\x03 \x01(\bR\x1eacceptSendNotificationRequests\"\x84\x02\n" +
+	"\rHelperRequest\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\x03R\x02id\x12<\n" +
+	"\fgetWIFIState\x18\x02 \x01(\v2\x16.google.protobuf.EmptyH\x00R\fgetWIFIState\x12V\n" +
+	"\x13findConnectionOwner\x18\x03 \x01(\v2\".daemon.FindConnectionOwnerRequestH\x00R\x13findConnectionOwner\x12B\n" +
+	"\x10sendNotification\x18\x04 \x01(\v2\x14.daemon.NotificationH\x00R\x10sendNotificationB\t\n" +
+	"\arequest\"\xdc\x01\n" +
+	"\x1aFindConnectionOwnerRequest\x12\x1e\n" +
+	"\n" +
+	"ipProtocol\x18\x01 \x01(\x05R\n" +
+	"ipProtocol\x12$\n" +
+	"\rsourceAddress\x18\x02 \x01(\tR\rsourceAddress\x12\x1e\n" +
+	"\n" +
+	"sourcePort\x18\x03 \x01(\x05R\n" +
+	"sourcePort\x12.\n" +
+	"\x12destinationAddress\x18\x04 \x01(\tR\x12destinationAddress\x12(\n" +
+	"\x0fdestinationPort\x18\x05 \x01(\x05R\x0fdestinationPort\"\xc2\x01\n" +
+	"\fNotification\x12\x1e\n" +
+	"\n" +
+	"identifier\x18\x01 \x01(\tR\n" +
+	"identifier\x12\x1a\n" +
+	"\btypeName\x18\x02 \x01(\tR\btypeName\x12\x16\n" +
+	"\x06typeId\x18\x03 \x01(\x05R\x06typeId\x12\x14\n" +
+	"\x05title\x18\x04 \x01(\tR\x05title\x12\x1a\n" +
+	"\bsubtitle\x18\x05 \x01(\tR\bsubtitle\x12\x12\n" +
+	"\x04body\x18\x06 \x01(\tR\x04body\x12\x18\n" +
+	"\aopenURL\x18\a \x01(\tR\aopenURL\"\xbc\x01\n" +
+	"\x0eHelperResponse\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\x03R\x02id\x121\n" +
+	"\twifiState\x18\x02 \x01(\v2\x11.daemon.WIFIStateH\x00R\twifiState\x12\x16\n" +
+	"\x05error\x18\x03 \x01(\tH\x00R\x05error\x12C\n" +
+	"\x0fconnectionOwner\x18\x04 \x01(\v2\x17.daemon.ConnectionOwnerH\x00R\x0fconnectionOwnerB\n" +
+	"\n" +
+	"\bresponse\"\x97\x01\n" +
+	"\x0fConnectionOwner\x12\x16\n" +
+	"\x06userId\x18\x01 \x01(\x05R\x06userId\x12\x1a\n" +
+	"\buserName\x18\x02 \x01(\tR\buserName\x12 \n" +
+	"\vprocessPath\x18\x03 \x01(\tR\vprocessPath\x12.\n" +
+	"\x12androidPackageName\x18\x04 \x01(\tR\x12androidPackageName\"5\n" +
+	"\tWIFIState\x12\x12\n" +
+	"\x04ssid\x18\x01 \x01(\tR\x04ssid\x12\x14\n" +
+	"\x05bssid\x18\x02 \x01(\tR\x05bssidB%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
+
+var (
+	file_daemon_helper_proto_rawDescOnce sync.Once
+	file_daemon_helper_proto_rawDescData []byte
+)
+
+func file_daemon_helper_proto_rawDescGZIP() []byte {
+	file_daemon_helper_proto_rawDescOnce.Do(func() {
+		file_daemon_helper_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_daemon_helper_proto_rawDesc), len(file_daemon_helper_proto_rawDesc)))
+	})
+	return file_daemon_helper_proto_rawDescData
+}
+
+var (
+	file_daemon_helper_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
+	file_daemon_helper_proto_goTypes  = []any{
+		(*SubscribeHelperRequestRequest)(nil), // 0: daemon.SubscribeHelperRequestRequest
+		(*HelperRequest)(nil),                 // 1: daemon.HelperRequest
+		(*FindConnectionOwnerRequest)(nil),    // 2: daemon.FindConnectionOwnerRequest
+		(*Notification)(nil),                  // 3: daemon.Notification
+		(*HelperResponse)(nil),                // 4: daemon.HelperResponse
+		(*ConnectionOwner)(nil),               // 5: daemon.ConnectionOwner
+		(*WIFIState)(nil),                     // 6: daemon.WIFIState
+		(*emptypb.Empty)(nil),                 // 7: google.protobuf.Empty
+	}
+)
+
+var file_daemon_helper_proto_depIdxs = []int32{
+	7, // 0: daemon.HelperRequest.getWIFIState:type_name -> google.protobuf.Empty
+	2, // 1: daemon.HelperRequest.findConnectionOwner:type_name -> daemon.FindConnectionOwnerRequest
+	3, // 2: daemon.HelperRequest.sendNotification:type_name -> daemon.Notification
+	6, // 3: daemon.HelperResponse.wifiState:type_name -> daemon.WIFIState
+	5, // 4: daemon.HelperResponse.connectionOwner:type_name -> daemon.ConnectionOwner
+	5, // [5:5] is the sub-list for method output_type
+	5, // [5:5] is the sub-list for method input_type
+	5, // [5:5] is the sub-list for extension type_name
+	5, // [5:5] is the sub-list for extension extendee
+	0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_daemon_helper_proto_init() }
+func file_daemon_helper_proto_init() {
+	if File_daemon_helper_proto != nil {
+		return
+	}
+	file_daemon_helper_proto_msgTypes[1].OneofWrappers = []any{
+		(*HelperRequest_GetWIFIState)(nil),
+		(*HelperRequest_FindConnectionOwner)(nil),
+		(*HelperRequest_SendNotification)(nil),
+	}
+	file_daemon_helper_proto_msgTypes[4].OneofWrappers = []any{
+		(*HelperResponse_WifiState)(nil),
+		(*HelperResponse_Error)(nil),
+		(*HelperResponse_ConnectionOwner)(nil),
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_helper_proto_rawDesc), len(file_daemon_helper_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   7,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_daemon_helper_proto_goTypes,
+		DependencyIndexes: file_daemon_helper_proto_depIdxs,
+		MessageInfos:      file_daemon_helper_proto_msgTypes,
+	}.Build()
+	File_daemon_helper_proto = out.File
+	file_daemon_helper_proto_goTypes = nil
+	file_daemon_helper_proto_depIdxs = nil
+}

+ 61 - 0
daemon/helper.proto

@@ -0,0 +1,61 @@
+syntax = "proto3";
+
+package daemon;
+option go_package = "github.com/sagernet/sing-box/daemon";
+
+import "google/protobuf/empty.proto";
+
+message SubscribeHelperRequestRequest {
+  bool acceptGetWIFIStateRequests = 1;
+  bool acceptFindConnectionOwnerRequests = 2;
+  bool acceptSendNotificationRequests = 3;
+}
+
+message HelperRequest {
+  int64 id = 1;
+  oneof request {
+    google.protobuf.Empty getWIFIState = 2;
+    FindConnectionOwnerRequest findConnectionOwner = 3;
+    Notification sendNotification = 4;
+  }
+}
+
+message FindConnectionOwnerRequest {
+  int32 ipProtocol = 1;
+  string sourceAddress = 2;
+  int32 sourcePort = 3;
+  string destinationAddress = 4;
+  int32 destinationPort = 5;
+}
+
+message Notification {
+  string identifier = 1;
+  string typeName = 2;
+  int32 typeId = 3;
+  string title = 4;
+  string subtitle = 5;
+  string body = 6;
+  string openURL = 7;
+}
+
+message HelperResponse {
+  int64 id = 1;
+  oneof response {
+    WIFIState wifiState = 2;
+    string error = 3;
+    ConnectionOwner connectionOwner = 4;
+  }
+}
+
+message ConnectionOwner {
+  int32 userId = 1;
+  string userName = 2;
+  string processPath = 3;
+  string androidPackageName = 4;
+}
+
+message WIFIState {
+  string ssid = 1;
+  string bssid = 2;
+}
+

+ 147 - 0
daemon/instance.go

@@ -0,0 +1,147 @@
+package daemon
+
+import (
+	"bytes"
+	"context"
+
+	"github.com/sagernet/sing-box"
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/urltest"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/dns"
+	"github.com/sagernet/sing-box/experimental/deprecated"
+	"github.com/sagernet/sing-box/include"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/service"
+	"github.com/sagernet/sing/service/filemanager"
+	"github.com/sagernet/sing/service/pause"
+)
+
+type Instance struct {
+	ctx                   context.Context
+	cancel                context.CancelFunc
+	instance              *box.Box
+	clashServer           adapter.ClashServer
+	cacheFile             adapter.CacheFile
+	pauseManager          pause.Manager
+	urlTestHistoryStorage *urltest.HistoryStorage
+}
+
+func (s *StartedService) baseContext() context.Context {
+	dnsRegistry := include.DNSTransportRegistry()
+	if s.platform != nil && s.platform.UsePlatformLocalDNSTransport() {
+		dns.RegisterTransport[option.LocalDNSServerOptions](dnsRegistry, C.DNSTypeLocal, s.platform.LocalDNSTransport())
+	}
+	ctx := box.Context(s.ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry())
+	ctx = filemanager.WithDefault(ctx, s.workingDirectory, s.tempDirectory, s.userID, s.groupID)
+	return ctx
+}
+
+func (s *StartedService) CheckConfig(configContent string) error {
+	ctx := s.baseContext()
+	options, err := parseConfig(ctx, configContent)
+	if err != nil {
+		return err
+	}
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+	instance, err := box.New(box.Options{
+		Context: ctx,
+		Options: options,
+	})
+	if err == nil {
+		instance.Close()
+	}
+	return err
+}
+
+func (s *StartedService) FormatConfig(configContent string) (string, error) {
+	options, err := parseConfig(s.baseContext(), configContent)
+	if err != nil {
+		return "", err
+	}
+	var buffer bytes.Buffer
+	encoder := json.NewEncoder(&buffer)
+	encoder.SetIndent("", "  ")
+	err = encoder.Encode(options)
+	if err != nil {
+		return "", err
+	}
+	return buffer.String(), nil
+}
+
+type OverrideOptions struct {
+	AutoRedirect   bool
+	IncludePackage []string
+	ExcludePackage []string
+}
+
+func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
+	ctx := s.baseContext()
+	service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
+	ctx, cancel := context.WithCancel(include.Context(ctx))
+	options, err := parseConfig(ctx, profileContent)
+	if err != nil {
+		cancel()
+		return nil, err
+	}
+	if overrideOptions != nil {
+		for _, inbound := range options.Inbounds {
+			if tunInboundOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN {
+				tunInboundOptions.AutoRedirect = overrideOptions.AutoRedirect
+				tunInboundOptions.IncludePackage = append(tunInboundOptions.IncludePackage, overrideOptions.IncludePackage...)
+				tunInboundOptions.ExcludePackage = append(tunInboundOptions.ExcludePackage, overrideOptions.ExcludePackage...)
+				break
+			}
+		}
+	}
+	urlTestHistoryStorage := urltest.NewHistoryStorage()
+	ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
+	i := &Instance{
+		ctx:                   ctx,
+		cancel:                cancel,
+		urlTestHistoryStorage: urlTestHistoryStorage,
+	}
+	boxInstance, err := box.New(box.Options{
+		Context:           ctx,
+		Options:           options,
+		PlatformLogWriter: s,
+	})
+	if err != nil {
+		cancel()
+		return nil, err
+	}
+	i.instance = boxInstance
+	i.clashServer = service.FromContext[adapter.ClashServer](ctx)
+	i.pauseManager = service.FromContext[pause.Manager](ctx)
+	i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
+	return i, nil
+}
+
+func (i *Instance) Start() error {
+	return i.instance.Start()
+}
+
+func (i *Instance) Close() error {
+	i.cancel()
+	i.urlTestHistoryStorage.Close()
+	return i.instance.Close()
+}
+
+func (i *Instance) Box() *box.Box {
+	return i.instance
+}
+
+func (i *Instance) PauseManager() pause.Manager {
+	return i.pauseManager
+}
+
+func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
+	options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent))
+	if err != nil {
+		return option.Options{}, E.Cause(err, "decode config")
+	}
+	return options, nil
+}

+ 22 - 0
daemon/platform.go

@@ -0,0 +1,22 @@
+package daemon
+
+import (
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/dns"
+	"github.com/sagernet/sing-box/option"
+)
+
+type PlatformHandler interface {
+	ServiceStop() error
+	ServiceReload() error
+	SystemProxyStatus() (*SystemProxyStatus, error)
+	SetSystemProxyEnabled(enabled bool) error
+	WriteDebugMessage(message string)
+}
+
+type PlatformInterface interface {
+	adapter.PlatformInterface
+
+	UsePlatformLocalDNSTransport() bool
+	LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions]
+}

+ 775 - 0
daemon/started_service.go

@@ -0,0 +1,775 @@
+package daemon
+
+import (
+	"context"
+	"os"
+	"runtime"
+	"sync"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/conntrack"
+	"github.com/sagernet/sing-box/common/urltest"
+	"github.com/sagernet/sing-box/experimental/clashapi"
+	"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
+	"github.com/sagernet/sing-box/experimental/deprecated"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/protocol/group"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/batch"
+	E "github.com/sagernet/sing/common/exceptions"
+	F "github.com/sagernet/sing/common/format"
+	"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"
+	"google.golang.org/grpc"
+	"google.golang.org/protobuf/types/known/emptypb"
+)
+
+var _ StartedServiceServer = (*StartedService)(nil)
+
+type StartedService struct {
+	ctx                     context.Context
+	platform                PlatformInterface
+	platformHandler         PlatformHandler
+	debug                   bool
+	logMaxLines             int
+	workingDirectory        string
+	tempDirectory           string
+	userID                  int
+	groupID                 int
+	systemProxyEnabled      bool
+	serviceAccess           sync.RWMutex
+	serviceStatus           *ServiceStatus
+	serviceStatusSubscriber *observable.Subscriber[*ServiceStatus]
+	serviceStatusObserver   *observable.Observer[*ServiceStatus]
+	logAccess               sync.RWMutex
+	logLines                list.List[*log.Entry]
+	logSubscriber           *observable.Subscriber[*log.Entry]
+	logObserver             *observable.Observer[*log.Entry]
+	instance                *Instance
+	urlTestSubscriber       *observable.Subscriber[struct{}]
+	urlTestObserver         *observable.Observer[struct{}]
+	urlTestHistoryStorage   *urltest.HistoryStorage
+	clashModeSubscriber     *observable.Subscriber[struct{}]
+	clashModeObserver       *observable.Observer[struct{}]
+}
+
+type ServiceOptions struct {
+	Context            context.Context
+	Platform           PlatformInterface
+	PlatformHandler    PlatformHandler
+	Debug              bool
+	LogMaxLines        int
+	WorkingDirectory   string
+	TempDirectory      string
+	UserID             int
+	GroupID            int
+	SystemProxyEnabled bool
+}
+
+func NewStartedService(options ServiceOptions) *StartedService {
+	s := &StartedService{
+		ctx:                     options.Context,
+		platform:                options.Platform,
+		platformHandler:         options.PlatformHandler,
+		debug:                   options.Debug,
+		logMaxLines:             options.LogMaxLines,
+		workingDirectory:        options.WorkingDirectory,
+		tempDirectory:           options.TempDirectory,
+		userID:                  options.UserID,
+		groupID:                 options.GroupID,
+		systemProxyEnabled:      options.SystemProxyEnabled,
+		serviceStatus:           &ServiceStatus{Status: ServiceStatus_IDLE},
+		serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4),
+		logSubscriber:           observable.NewSubscriber[*log.Entry](128),
+		urlTestSubscriber:       observable.NewSubscriber[struct{}](1),
+		urlTestHistoryStorage:   urltest.NewHistoryStorage(),
+		clashModeSubscriber:     observable.NewSubscriber[struct{}](1),
+	}
+	s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2)
+	s.logObserver = observable.NewObserver(s.logSubscriber, 64)
+	s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1)
+	s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1)
+	return s
+}
+
+func (s *StartedService) resetLogs() {
+	s.logAccess.Lock()
+	s.logLines = list.List[*log.Entry]{}
+	s.logAccess.Unlock()
+	s.logSubscriber.Emit(nil)
+}
+
+func (s *StartedService) updateStatus(newStatus ServiceStatus_Type) {
+	statusObject := &ServiceStatus{Status: newStatus}
+	s.serviceStatusSubscriber.Emit(statusObject)
+	s.serviceStatus = statusObject
+}
+
+func (s *StartedService) updateStatusError(err error) error {
+	statusObject := &ServiceStatus{Status: ServiceStatus_FATAL, ErrorMessage: err.Error()}
+	s.serviceStatusSubscriber.Emit(statusObject)
+	s.serviceStatus = statusObject
+	s.serviceAccess.Unlock()
+	return err
+}
+
+func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
+	s.serviceAccess.Lock()
+	switch s.serviceStatus.Status {
+	case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
+	default:
+		s.serviceAccess.Unlock()
+		return os.ErrInvalid
+	}
+	s.updateStatus(ServiceStatus_STARTING)
+	s.resetLogs()
+	instance, err := s.newInstance(profileContent, options)
+	if err != nil {
+		return s.updateStatusError(err)
+	}
+	s.instance = instance
+	s.serviceAccess.Unlock()
+	err = instance.Start()
+	s.serviceAccess.Lock()
+	if s.serviceStatus.Status != ServiceStatus_STARTING {
+		s.serviceAccess.Unlock()
+		return nil
+	}
+	if err != nil {
+		return s.updateStatusError(err)
+	}
+	s.updateStatus(ServiceStatus_STARTED)
+	s.serviceAccess.Unlock()
+	runtime.GC()
+	return nil
+}
+
+func (s *StartedService) CloseService() error {
+	s.serviceAccess.Lock()
+	switch s.serviceStatus.Status {
+	case ServiceStatus_STARTING, ServiceStatus_STARTED:
+	default:
+		s.serviceAccess.Unlock()
+		return os.ErrInvalid
+	}
+	s.updateStatus(ServiceStatus_STOPPING)
+	if s.instance != nil {
+		err := s.instance.Close()
+		if err != nil {
+			return s.updateStatusError(err)
+		}
+	}
+	s.instance = nil
+	s.updateStatus(ServiceStatus_IDLE)
+	s.serviceAccess.Unlock()
+	runtime.GC()
+	return nil
+}
+
+func (s *StartedService) SetError(err error) {
+	s.serviceAccess.Lock()
+	s.updateStatusError(err)
+	s.serviceAccess.Unlock()
+	s.WriteMessage(log.LevelError, err.Error())
+}
+
+func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
+	err := s.platformHandler.ServiceStop()
+	if err != nil {
+		return nil, err
+	}
+	return &emptypb.Empty{}, nil
+}
+
+func (s *StartedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
+	err := s.platformHandler.ServiceReload()
+	if err != nil {
+		return nil, err
+	}
+	return &emptypb.Empty{}, nil
+}
+
+func (s *StartedService) SubscribeServiceStatus(empty *emptypb.Empty, server grpc.ServerStreamingServer[ServiceStatus]) error {
+	subscription, done, err := s.serviceStatusObserver.Subscribe()
+	if err != nil {
+		return err
+	}
+	defer s.serviceStatusObserver.UnSubscribe(subscription)
+	err = server.Send(s.serviceStatus)
+	if err != nil {
+		return err
+	}
+	for {
+		select {
+		case <-s.ctx.Done():
+			return s.ctx.Err()
+		case <-server.Context().Done():
+			return server.Context().Err()
+		case newStatus := <-subscription:
+			err = server.Send(newStatus)
+			if err != nil {
+				return err
+			}
+		case <-done:
+			return nil
+		}
+	}
+}
+
+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)
+	}
+	s.logAccess.Unlock()
+	subscription, done, err := s.logObserver.Subscribe()
+	if err != nil {
+		return err
+	}
+	defer s.logObserver.UnSubscribe(subscription)
+	err = server.Send(&Log{
+		Messages: common.Map(savedLines, func(it *log.Entry) *Log_Message {
+			return &Log_Message{
+				Level:   LogLevel(it.Level),
+				Message: it.Message,
+			}
+		}),
+		Reset_: true,
+	})
+	if err != nil {
+		return err
+	}
+	for {
+		select {
+		case <-s.ctx.Done():
+			return s.ctx.Err()
+		case <-server.Context().Done():
+			return server.Context().Err()
+		case message := <-subscription:
+			if message == nil {
+				err = server.Send(&Log{Reset_: true})
+				if err != nil {
+					return err
+				}
+				continue
+			}
+			messages := []*Log_Message{{
+				Level:   LogLevel(message.Level),
+				Message: message.Message,
+			}}
+		fetch:
+			for {
+				select {
+				case message = <-subscription:
+					messages = append(messages, &Log_Message{
+						Level:   LogLevel(message.Level),
+						Message: message.Message,
+					})
+				default:
+					break fetch
+				}
+			}
+			err = server.Send(&Log{Messages: messages})
+			if err != nil {
+				return err
+			}
+		case <-done:
+			return nil
+		}
+	}
+}
+
+func (s *StartedService) GetDefaultLogLevel(ctx context.Context, empty *emptypb.Empty) (*DefaultLogLevel, error) {
+	s.serviceAccess.RLock()
+	switch s.serviceStatus.Status {
+	case ServiceStatus_STARTING, ServiceStatus_STARTED:
+	default:
+		s.serviceAccess.RUnlock()
+		return nil, os.ErrInvalid
+	}
+	logLevel := s.instance.instance.LogFactory().Level()
+	s.serviceAccess.RUnlock()
+	return &DefaultLogLevel{Level: LogLevel(logLevel)}, nil
+}
+
+func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server grpc.ServerStreamingServer[Status]) error {
+	interval := time.Duration(request.Interval)
+	if interval <= 0 {
+		interval = time.Second // Default to 1 second
+	}
+	ticker := time.NewTicker(interval)
+	defer ticker.Stop()
+	status := s.readStatus()
+	uploadTotal := status.UplinkTotal
+	downloadTotal := status.DownlinkTotal
+	for {
+		err := server.Send(status)
+		if err != nil {
+			return err
+		}
+		select {
+		case <-s.ctx.Done():
+			return s.ctx.Err()
+		case <-server.Context().Done():
+			return server.Context().Err()
+		case <-ticker.C:
+		}
+		status = s.readStatus()
+		upload := status.UplinkTotal - uploadTotal
+		download := status.DownlinkTotal - downloadTotal
+		uploadTotal = status.UplinkTotal
+		downloadTotal = status.DownlinkTotal
+		status.Uplink = upload
+		status.Downlink = download
+	}
+}
+
+func (s *StartedService) readStatus() *Status {
+	var status Status
+	status.Memory = memory.Inuse()
+	status.Goroutines = int32(runtime.NumGoroutine())
+	status.ConnectionsOut = int32(conntrack.Count())
+	nowService := s.instance
+	if nowService != nil {
+		if clashServer := nowService.clashServer; clashServer != nil {
+			status.TrafficAvailable = true
+			trafficManager := clashServer.(*clashapi.Server).TrafficManager()
+			status.UplinkTotal, status.DownlinkTotal = trafficManager.Total()
+			status.ConnectionsIn = int32(trafficManager.ConnectionsLen())
+		}
+	}
+	return &status
+}
+
+func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.ServerStreamingServer[Groups]) error {
+	subscription, done, err := s.urlTestObserver.Subscribe()
+	if err != nil {
+		return err
+	}
+	defer s.urlTestObserver.UnSubscribe(subscription)
+	for {
+		s.serviceAccess.RLock()
+		switch s.serviceStatus.Status {
+		case ServiceStatus_STARTING, ServiceStatus_STARTED:
+			groups := s.readGroups()
+			s.serviceAccess.RUnlock()
+			err = server.Send(groups)
+			if err != nil {
+				return err
+			}
+		default:
+			s.serviceAccess.RUnlock()
+			return os.ErrInvalid
+		}
+		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) readGroups() *Groups {
+	historyStorage := s.instance.urlTestHistoryStorage
+	boxService := s.instance
+	outbounds := boxService.instance.Outbound().Outbounds()
+	var iGroups []adapter.OutboundGroup
+	for _, it := range outbounds {
+		if group, isGroup := it.(adapter.OutboundGroup); isGroup {
+			iGroups = append(iGroups, group)
+		}
+	}
+	var gs Groups
+	for _, iGroup := range iGroups {
+		var g Group
+		g.Tag = iGroup.Tag()
+		g.Type = iGroup.Type()
+		_, g.Selectable = iGroup.(*group.Selector)
+		g.Selected = iGroup.Now()
+		if boxService.cacheFile != nil {
+			if isExpand, loaded := boxService.cacheFile.LoadGroupExpand(g.Tag); loaded {
+				g.IsExpand = isExpand
+			}
+		}
+
+		for _, itemTag := range iGroup.All() {
+			itemOutbound, isLoaded := boxService.instance.Outbound().Outbound(itemTag)
+			if !isLoaded {
+				continue
+			}
+
+			var item GroupItem
+			item.Tag = itemTag
+			item.Type = itemOutbound.Type()
+			if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(itemOutbound)); history != nil {
+				item.UrlTestTime = history.Time.Unix()
+				item.UrlTestDelay = int32(history.Delay)
+			}
+			g.Items = append(g.Items, &item)
+		}
+		if len(g.Items) < 2 {
+			continue
+		}
+		gs.Group = append(gs.Group, &g)
+	}
+	return &gs
+}
+
+func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.Empty) (*ClashModeStatus, error) {
+	s.serviceAccess.RLock()
+	if s.serviceStatus.Status != ServiceStatus_STARTED {
+		s.serviceAccess.RUnlock()
+		return nil, os.ErrInvalid
+	}
+	clashServer := s.instance.clashServer
+	s.serviceAccess.RUnlock()
+	if clashServer == nil {
+		return nil, os.ErrInvalid
+	}
+	return &ClashModeStatus{
+		ModeList:    clashServer.ModeList(),
+		CurrentMode: clashServer.Mode(),
+	}, nil
+}
+
+func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.ServerStreamingServer[ClashMode]) error {
+	subscription, done, err := s.clashModeObserver.Subscribe()
+	if err != nil {
+		return err
+	}
+	defer s.clashModeObserver.UnSubscribe(subscription)
+	for {
+		select {
+		case <-subscription:
+		case <-s.ctx.Done():
+			return s.ctx.Err()
+		case <-server.Context().Done():
+			return server.Context().Err()
+		case <-done:
+			return nil
+		}
+		s.serviceAccess.RLock()
+		if s.serviceStatus.Status != ServiceStatus_STARTED {
+			return nil
+		}
+		message := &ClashMode{Mode: s.instance.clashServer.Mode()}
+		s.serviceAccess.RUnlock()
+		err = server.Send(message)
+		if err != nil {
+			return err
+		}
+	}
+}
+
+func (s *StartedService) SetClashMode(ctx context.Context, request *ClashMode) (*emptypb.Empty, error) {
+	s.serviceAccess.RLock()
+	if s.serviceStatus.Status != ServiceStatus_STARTED {
+		s.serviceAccess.RUnlock()
+		return nil, os.ErrInvalid
+	}
+	clashServer := s.instance.clashServer
+	s.serviceAccess.RUnlock()
+	clashServer.(*clashapi.Server).SetMode(request.Mode)
+	return &emptypb.Empty{}, nil
+}
+
+func (s *StartedService) URLTest(ctx context.Context, request *URLTestRequest) (*emptypb.Empty, error) {
+	s.serviceAccess.RLock()
+	if s.serviceStatus.Status != ServiceStatus_STARTED {
+		s.serviceAccess.RUnlock()
+		return nil, os.ErrInvalid
+	}
+	boxService := s.instance
+	s.serviceAccess.RUnlock()
+	groupTag := request.OutboundTag
+	abstractOutboundGroup, isLoaded := boxService.instance.Outbound().Outbound(groupTag)
+	if !isLoaded {
+		return nil, E.New("outbound group not found: ", groupTag)
+	}
+	outboundGroup, isOutboundGroup := abstractOutboundGroup.(adapter.OutboundGroup)
+	if !isOutboundGroup {
+		return nil, E.New("outbound is not a group: ", groupTag)
+	}
+	urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest)
+	if isURLTest {
+		go urlTest.CheckOutbounds()
+	} else {
+		var historyStorage adapter.URLTestHistoryStorage
+		if s.instance.clashServer != nil {
+			historyStorage = s.instance.clashServer.HistoryStorage()
+		} else {
+			return nil, E.New("Clash API is required for URLTest on non-URLTest group")
+		}
+
+		outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
+			itOutbound, _ := boxService.instance.Outbound().Outbound(it)
+			return itOutbound
+		}), func(it adapter.Outbound) bool {
+			if it == nil {
+				return false
+			}
+			_, isGroup := it.(adapter.OutboundGroup)
+			if isGroup {
+				return false
+			}
+			return true
+		})
+		b, _ := batch.New(boxService.ctx, batch.WithConcurrencyNum[any](10))
+		for _, detour := range outbounds {
+			outboundToTest := detour
+			outboundTag := outboundToTest.Tag()
+			b.Go(outboundTag, func() (any, error) {
+				t, err := urltest.URLTest(boxService.ctx, "", outboundToTest)
+				if err != nil {
+					historyStorage.DeleteURLTestHistory(outboundTag)
+				} else {
+					historyStorage.StoreURLTestHistory(outboundTag, &adapter.URLTestHistory{
+						Time:  time.Now(),
+						Delay: t,
+					})
+				}
+				return nil, nil
+			})
+		}
+	}
+	return &emptypb.Empty{}, nil
+}
+
+func (s *StartedService) SelectOutbound(ctx context.Context, request *SelectOutboundRequest) (*emptypb.Empty, error) {
+	s.serviceAccess.RLock()
+	switch s.serviceStatus.Status {
+	case ServiceStatus_STARTING, ServiceStatus_STARTED:
+	default:
+		s.serviceAccess.RUnlock()
+		return nil, os.ErrInvalid
+	}
+	boxService := s.instance.instance
+	s.serviceAccess.RUnlock()
+	outboundGroup, isLoaded := boxService.Outbound().Outbound(request.GroupTag)
+	if !isLoaded {
+		return nil, E.New("selector not found: ", request.GroupTag)
+	}
+	selector, isSelector := outboundGroup.(*group.Selector)
+	if !isSelector {
+		return nil, E.New("outbound is not a selector: ", request.GroupTag)
+	}
+	if !selector.SelectOutbound(request.OutboundTag) {
+		return nil, E.New("outbound not found in selector: ", request.OutboundTag)
+	}
+	return &emptypb.Empty{}, nil
+}
+
+func (s *StartedService) SetGroupExpand(ctx context.Context, request *SetGroupExpandRequest) (*emptypb.Empty, error) {
+	s.serviceAccess.RLock()
+	switch s.serviceStatus.Status {
+	case ServiceStatus_STARTING, ServiceStatus_STARTED:
+	default:
+		s.serviceAccess.RUnlock()
+		return nil, os.ErrInvalid
+	}
+	boxService := s.instance
+	s.serviceAccess.RUnlock()
+	if boxService.cacheFile != nil {
+		err := boxService.cacheFile.StoreGroupExpand(request.GroupTag, request.IsExpand)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return &emptypb.Empty{}, nil
+}
+
+func (s *StartedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) {
+	return s.platformHandler.SystemProxyStatus()
+}
+
+func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
+	err := s.platformHandler.SetSystemProxyEnabled(request.Enabled)
+	if err != nil {
+		return nil, err
+	}
+	return nil, err
+}
+
+func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[Connections]) error {
+	s.serviceAccess.RLock()
+	switch s.serviceStatus.Status {
+	case ServiceStatus_STARTING, ServiceStatus_STARTED:
+	default:
+		s.serviceAccess.RUnlock()
+		return os.ErrInvalid
+	}
+	boxService := s.instance
+	s.serviceAccess.RUnlock()
+	ticker := time.NewTicker(time.Duration(request.Interval))
+	defer ticker.Stop()
+	trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
+	var (
+		connections    = make(map[uuid.UUID]*Connection)
+		outConnections []*Connection
+	)
+	for {
+		outConnections = outConnections[:0]
+		for _, connection := range trafficManager.Connections() {
+			outConnections = append(outConnections, newConnection(connections, connection, false))
+		}
+		for _, connection := range trafficManager.ClosedConnections() {
+			outConnections = append(outConnections, newConnection(connections, connection, true))
+		}
+		err := server.Send(&Connections{Connections: outConnections})
+		if err != nil {
+			return err
+		}
+		select {
+		case <-s.ctx.Done():
+			return s.ctx.Err()
+		case <-server.Context().Done():
+			return server.Context().Err()
+		case <-ticker.C:
+		}
+	}
+}
+
+func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) *Connection {
+	if oldConnection, loaded := connections[metadata.ID]; loaded {
+		if isClosed {
+			if oldConnection.ClosedAt == 0 {
+				oldConnection.Uplink = 0
+				oldConnection.Downlink = 0
+				oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
+			}
+			return oldConnection
+		}
+		lastUplink := oldConnection.UplinkTotal
+		lastDownlink := oldConnection.DownlinkTotal
+		uplinkTotal := metadata.Upload.Load()
+		downlinkTotal := metadata.Download.Load()
+		oldConnection.Uplink = uplinkTotal - lastUplink
+		oldConnection.Downlink = downlinkTotal - lastDownlink
+		oldConnection.UplinkTotal = uplinkTotal
+		oldConnection.DownlinkTotal = downlinkTotal
+		return oldConnection
+	}
+	var rule string
+	if metadata.Rule != nil {
+		rule = metadata.Rule.String()
+	}
+	uplinkTotal := metadata.Upload.Load()
+	downlinkTotal := metadata.Download.Load()
+	uplink := uplinkTotal
+	downlink := downlinkTotal
+	var closedAt int64
+	if !metadata.ClosedAt.IsZero() {
+		closedAt = metadata.ClosedAt.UnixMilli()
+		uplink = 0
+		downlink = 0
+	}
+	connection := &Connection{
+		Id:            metadata.ID.String(),
+		Inbound:       metadata.Metadata.Inbound,
+		InboundType:   metadata.Metadata.InboundType,
+		IpVersion:     int32(metadata.Metadata.IPVersion),
+		Network:       metadata.Metadata.Network,
+		Source:        metadata.Metadata.Source.String(),
+		Destination:   metadata.Metadata.Destination.String(),
+		Domain:        metadata.Metadata.Domain,
+		Protocol:      metadata.Metadata.Protocol,
+		User:          metadata.Metadata.User,
+		FromOutbound:  metadata.Metadata.Outbound,
+		CreatedAt:     metadata.CreatedAt.UnixMilli(),
+		ClosedAt:      closedAt,
+		Uplink:        uplink,
+		Downlink:      downlink,
+		UplinkTotal:   uplinkTotal,
+		DownlinkTotal: downlinkTotal,
+		Rule:          rule,
+		Outbound:      metadata.Outbound,
+		OutboundType:  metadata.OutboundType,
+		ChainList:     metadata.Chain,
+	}
+	connections[metadata.ID] = connection
+	return connection
+}
+
+func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConnectionRequest) (*emptypb.Empty, error) {
+	s.serviceAccess.RLock()
+	switch s.serviceStatus.Status {
+	case ServiceStatus_STARTING, ServiceStatus_STARTED:
+	default:
+		s.serviceAccess.RUnlock()
+		return nil, os.ErrInvalid
+	}
+	boxService := s.instance
+	s.serviceAccess.RUnlock()
+	targetConn := boxService.clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(request.Id))
+	if targetConn != nil {
+		targetConn.Close()
+	}
+	return &emptypb.Empty{}, nil
+}
+
+func (s *StartedService) CloseAllConnections(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
+	conntrack.Close()
+	return &emptypb.Empty{}, nil
+}
+
+func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *emptypb.Empty) (*DeprecatedWarnings, error) {
+	s.serviceAccess.RLock()
+	if s.serviceStatus.Status != ServiceStatus_STARTED {
+		s.serviceAccess.RUnlock()
+		return nil, os.ErrInvalid
+	}
+	boxService := s.instance
+	s.serviceAccess.RUnlock()
+	notes := service.FromContext[deprecated.Manager](boxService.ctx).(*deprecatedManager).Get()
+	return &DeprecatedWarnings{
+		Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
+			return &DeprecatedWarning{
+				Message:       it.Message(),
+				Impending:     it.Impending(),
+				MigrationLink: it.MigrationLink,
+			}
+		}),
+	}, nil
+}
+
+func (s *StartedService) SubscribeHelperEvents(empty *emptypb.Empty, server grpc.ServerStreamingServer[HelperRequest]) error {
+	return os.ErrInvalid
+}
+
+func (s *StartedService) SendHelperResponse(ctx context.Context, response *HelperResponse) (*emptypb.Empty, error) {
+	return nil, os.ErrInvalid
+}
+
+func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
+}
+
+func (s *StartedService) WriteMessage(level log.Level, message string) {
+	item := &log.Entry{Level: level, Message: message}
+	s.logSubscriber.Emit(item)
+	s.logAccess.Lock()
+	s.logLines.PushBack(item)
+	if s.logLines.Len() > s.logMaxLines {
+		s.logLines.Remove(s.logLines.Front())
+	}
+	s.logAccess.Unlock()
+	if s.debug {
+		s.platformHandler.WriteDebugMessage(message)
+	}
+}
+
+func (s *StartedService) Instance() *Instance {
+	s.serviceAccess.RLock()
+	defer s.serviceAccess.RUnlock()
+	return s.instance
+}

+ 1906 - 0
daemon/started_service.pb.go

@@ -0,0 +1,1906 @@
+package daemon
+
+import (
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	emptypb "google.golang.org/protobuf/types/known/emptypb"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type LogLevel int32
+
+const (
+	LogLevel_PANIC LogLevel = 0
+	LogLevel_FATAL LogLevel = 1
+	LogLevel_ERROR LogLevel = 2
+	LogLevel_WARN  LogLevel = 3
+	LogLevel_INFO  LogLevel = 4
+	LogLevel_DEBUG LogLevel = 5
+	LogLevel_TRACE LogLevel = 6
+)
+
+// Enum value maps for LogLevel.
+var (
+	LogLevel_name = map[int32]string{
+		0: "PANIC",
+		1: "FATAL",
+		2: "ERROR",
+		3: "WARN",
+		4: "INFO",
+		5: "DEBUG",
+		6: "TRACE",
+	}
+	LogLevel_value = map[string]int32{
+		"PANIC": 0,
+		"FATAL": 1,
+		"ERROR": 2,
+		"WARN":  3,
+		"INFO":  4,
+		"DEBUG": 5,
+		"TRACE": 6,
+	}
+)
+
+func (x LogLevel) Enum() *LogLevel {
+	p := new(LogLevel)
+	*p = x
+	return p
+}
+
+func (x LogLevel) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (LogLevel) Descriptor() protoreflect.EnumDescriptor {
+	return file_daemon_started_service_proto_enumTypes[0].Descriptor()
+}
+
+func (LogLevel) Type() protoreflect.EnumType {
+	return &file_daemon_started_service_proto_enumTypes[0]
+}
+
+func (x LogLevel) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use LogLevel.Descriptor instead.
+func (LogLevel) EnumDescriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{0}
+}
+
+type ConnectionFilter int32
+
+const (
+	ConnectionFilter_ALL    ConnectionFilter = 0
+	ConnectionFilter_ACTIVE ConnectionFilter = 1
+	ConnectionFilter_CLOSED ConnectionFilter = 2
+)
+
+// Enum value maps for ConnectionFilter.
+var (
+	ConnectionFilter_name = map[int32]string{
+		0: "ALL",
+		1: "ACTIVE",
+		2: "CLOSED",
+	}
+	ConnectionFilter_value = map[string]int32{
+		"ALL":    0,
+		"ACTIVE": 1,
+		"CLOSED": 2,
+	}
+)
+
+func (x ConnectionFilter) Enum() *ConnectionFilter {
+	p := new(ConnectionFilter)
+	*p = x
+	return p
+}
+
+func (x ConnectionFilter) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (ConnectionFilter) Descriptor() protoreflect.EnumDescriptor {
+	return file_daemon_started_service_proto_enumTypes[1].Descriptor()
+}
+
+func (ConnectionFilter) Type() protoreflect.EnumType {
+	return &file_daemon_started_service_proto_enumTypes[1]
+}
+
+func (x ConnectionFilter) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use ConnectionFilter.Descriptor instead.
+func (ConnectionFilter) EnumDescriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{1}
+}
+
+type ConnectionSortBy int32
+
+const (
+	ConnectionSortBy_DATE          ConnectionSortBy = 0
+	ConnectionSortBy_TRAFFIC       ConnectionSortBy = 1
+	ConnectionSortBy_TOTAL_TRAFFIC ConnectionSortBy = 2
+)
+
+// Enum value maps for ConnectionSortBy.
+var (
+	ConnectionSortBy_name = map[int32]string{
+		0: "DATE",
+		1: "TRAFFIC",
+		2: "TOTAL_TRAFFIC",
+	}
+	ConnectionSortBy_value = map[string]int32{
+		"DATE":          0,
+		"TRAFFIC":       1,
+		"TOTAL_TRAFFIC": 2,
+	}
+)
+
+func (x ConnectionSortBy) Enum() *ConnectionSortBy {
+	p := new(ConnectionSortBy)
+	*p = x
+	return p
+}
+
+func (x ConnectionSortBy) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (ConnectionSortBy) Descriptor() protoreflect.EnumDescriptor {
+	return file_daemon_started_service_proto_enumTypes[2].Descriptor()
+}
+
+func (ConnectionSortBy) Type() protoreflect.EnumType {
+	return &file_daemon_started_service_proto_enumTypes[2]
+}
+
+func (x ConnectionSortBy) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use ConnectionSortBy.Descriptor instead.
+func (ConnectionSortBy) EnumDescriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{2}
+}
+
+type ServiceStatus_Type int32
+
+const (
+	ServiceStatus_IDLE     ServiceStatus_Type = 0
+	ServiceStatus_STARTING ServiceStatus_Type = 1
+	ServiceStatus_STARTED  ServiceStatus_Type = 2
+	ServiceStatus_STOPPING ServiceStatus_Type = 3
+	ServiceStatus_FATAL    ServiceStatus_Type = 4
+)
+
+// Enum value maps for ServiceStatus_Type.
+var (
+	ServiceStatus_Type_name = map[int32]string{
+		0: "IDLE",
+		1: "STARTING",
+		2: "STARTED",
+		3: "STOPPING",
+		4: "FATAL",
+	}
+	ServiceStatus_Type_value = map[string]int32{
+		"IDLE":     0,
+		"STARTING": 1,
+		"STARTED":  2,
+		"STOPPING": 3,
+		"FATAL":    4,
+	}
+)
+
+func (x ServiceStatus_Type) Enum() *ServiceStatus_Type {
+	p := new(ServiceStatus_Type)
+	*p = x
+	return p
+}
+
+func (x ServiceStatus_Type) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (ServiceStatus_Type) Descriptor() protoreflect.EnumDescriptor {
+	return file_daemon_started_service_proto_enumTypes[3].Descriptor()
+}
+
+func (ServiceStatus_Type) Type() protoreflect.EnumType {
+	return &file_daemon_started_service_proto_enumTypes[3]
+}
+
+func (x ServiceStatus_Type) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use ServiceStatus_Type.Descriptor instead.
+func (ServiceStatus_Type) EnumDescriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{0, 0}
+}
+
+type ServiceStatus struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Status        ServiceStatus_Type     `protobuf:"varint,1,opt,name=status,proto3,enum=daemon.ServiceStatus_Type" json:"status,omitempty"`
+	ErrorMessage  string                 `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ServiceStatus) Reset() {
+	*x = ServiceStatus{}
+	mi := &file_daemon_started_service_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ServiceStatus) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ServiceStatus) ProtoMessage() {}
+
+func (x *ServiceStatus) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[0]
+	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 ServiceStatus.ProtoReflect.Descriptor instead.
+func (*ServiceStatus) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *ServiceStatus) GetStatus() ServiceStatus_Type {
+	if x != nil {
+		return x.Status
+	}
+	return ServiceStatus_IDLE
+}
+
+func (x *ServiceStatus) GetErrorMessage() string {
+	if x != nil {
+		return x.ErrorMessage
+	}
+	return ""
+}
+
+type ReloadServiceRequest struct {
+	state             protoimpl.MessageState `protogen:"open.v1"`
+	NewProfileContent string                 `protobuf:"bytes,1,opt,name=newProfileContent,proto3" json:"newProfileContent,omitempty"`
+	unknownFields     protoimpl.UnknownFields
+	sizeCache         protoimpl.SizeCache
+}
+
+func (x *ReloadServiceRequest) Reset() {
+	*x = ReloadServiceRequest{}
+	mi := &file_daemon_started_service_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ReloadServiceRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ReloadServiceRequest) ProtoMessage() {}
+
+func (x *ReloadServiceRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[1]
+	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 ReloadServiceRequest.ProtoReflect.Descriptor instead.
+func (*ReloadServiceRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *ReloadServiceRequest) GetNewProfileContent() string {
+	if x != nil {
+		return x.NewProfileContent
+	}
+	return ""
+}
+
+type SubscribeStatusRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Interval      int64                  `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SubscribeStatusRequest) Reset() {
+	*x = SubscribeStatusRequest{}
+	mi := &file_daemon_started_service_proto_msgTypes[2]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SubscribeStatusRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SubscribeStatusRequest) ProtoMessage() {}
+
+func (x *SubscribeStatusRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[2]
+	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 SubscribeStatusRequest.ProtoReflect.Descriptor instead.
+func (*SubscribeStatusRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *SubscribeStatusRequest) GetInterval() int64 {
+	if x != nil {
+		return x.Interval
+	}
+	return 0
+}
+
+type Log struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Messages      []*Log_Message         `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
+	Reset_        bool                   `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Log) Reset() {
+	*x = Log{}
+	mi := &file_daemon_started_service_proto_msgTypes[3]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Log) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Log) ProtoMessage() {}
+
+func (x *Log) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[3]
+	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 Log.ProtoReflect.Descriptor instead.
+func (*Log) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *Log) GetMessages() []*Log_Message {
+	if x != nil {
+		return x.Messages
+	}
+	return nil
+}
+
+func (x *Log) GetReset_() bool {
+	if x != nil {
+		return x.Reset_
+	}
+	return false
+}
+
+type DefaultLogLevel struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Level         LogLevel               `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DefaultLogLevel) Reset() {
+	*x = DefaultLogLevel{}
+	mi := &file_daemon_started_service_proto_msgTypes[4]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DefaultLogLevel) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DefaultLogLevel) ProtoMessage() {}
+
+func (x *DefaultLogLevel) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[4]
+	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 DefaultLogLevel.ProtoReflect.Descriptor instead.
+func (*DefaultLogLevel) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *DefaultLogLevel) GetLevel() LogLevel {
+	if x != nil {
+		return x.Level
+	}
+	return LogLevel_PANIC
+}
+
+type Status struct {
+	state            protoimpl.MessageState `protogen:"open.v1"`
+	Memory           uint64                 `protobuf:"varint,1,opt,name=memory,proto3" json:"memory,omitempty"`
+	Goroutines       int32                  `protobuf:"varint,2,opt,name=goroutines,proto3" json:"goroutines,omitempty"`
+	ConnectionsIn    int32                  `protobuf:"varint,3,opt,name=connectionsIn,proto3" json:"connectionsIn,omitempty"`
+	ConnectionsOut   int32                  `protobuf:"varint,4,opt,name=connectionsOut,proto3" json:"connectionsOut,omitempty"`
+	TrafficAvailable bool                   `protobuf:"varint,5,opt,name=trafficAvailable,proto3" json:"trafficAvailable,omitempty"`
+	Uplink           int64                  `protobuf:"varint,6,opt,name=uplink,proto3" json:"uplink,omitempty"`
+	Downlink         int64                  `protobuf:"varint,7,opt,name=downlink,proto3" json:"downlink,omitempty"`
+	UplinkTotal      int64                  `protobuf:"varint,8,opt,name=uplinkTotal,proto3" json:"uplinkTotal,omitempty"`
+	DownlinkTotal    int64                  `protobuf:"varint,9,opt,name=downlinkTotal,proto3" json:"downlinkTotal,omitempty"`
+	unknownFields    protoimpl.UnknownFields
+	sizeCache        protoimpl.SizeCache
+}
+
+func (x *Status) Reset() {
+	*x = Status{}
+	mi := &file_daemon_started_service_proto_msgTypes[5]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Status) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Status) ProtoMessage() {}
+
+func (x *Status) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[5]
+	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 Status.ProtoReflect.Descriptor instead.
+func (*Status) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *Status) GetMemory() uint64 {
+	if x != nil {
+		return x.Memory
+	}
+	return 0
+}
+
+func (x *Status) GetGoroutines() int32 {
+	if x != nil {
+		return x.Goroutines
+	}
+	return 0
+}
+
+func (x *Status) GetConnectionsIn() int32 {
+	if x != nil {
+		return x.ConnectionsIn
+	}
+	return 0
+}
+
+func (x *Status) GetConnectionsOut() int32 {
+	if x != nil {
+		return x.ConnectionsOut
+	}
+	return 0
+}
+
+func (x *Status) GetTrafficAvailable() bool {
+	if x != nil {
+		return x.TrafficAvailable
+	}
+	return false
+}
+
+func (x *Status) GetUplink() int64 {
+	if x != nil {
+		return x.Uplink
+	}
+	return 0
+}
+
+func (x *Status) GetDownlink() int64 {
+	if x != nil {
+		return x.Downlink
+	}
+	return 0
+}
+
+func (x *Status) GetUplinkTotal() int64 {
+	if x != nil {
+		return x.UplinkTotal
+	}
+	return 0
+}
+
+func (x *Status) GetDownlinkTotal() int64 {
+	if x != nil {
+		return x.DownlinkTotal
+	}
+	return 0
+}
+
+type Groups struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Group         []*Group               `protobuf:"bytes,1,rep,name=group,proto3" json:"group,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Groups) Reset() {
+	*x = Groups{}
+	mi := &file_daemon_started_service_proto_msgTypes[6]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Groups) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Groups) ProtoMessage() {}
+
+func (x *Groups) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[6]
+	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 Groups.ProtoReflect.Descriptor instead.
+func (*Groups) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *Groups) GetGroup() []*Group {
+	if x != nil {
+		return x.Group
+	}
+	return nil
+}
+
+type Group struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Tag           string                 `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
+	Type          string                 `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
+	Selectable    bool                   `protobuf:"varint,3,opt,name=selectable,proto3" json:"selectable,omitempty"`
+	Selected      string                 `protobuf:"bytes,4,opt,name=selected,proto3" json:"selected,omitempty"`
+	IsExpand      bool                   `protobuf:"varint,5,opt,name=isExpand,proto3" json:"isExpand,omitempty"`
+	Items         []*GroupItem           `protobuf:"bytes,6,rep,name=items,proto3" json:"items,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Group) Reset() {
+	*x = Group{}
+	mi := &file_daemon_started_service_proto_msgTypes[7]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Group) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Group) ProtoMessage() {}
+
+func (x *Group) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[7]
+	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 Group.ProtoReflect.Descriptor instead.
+func (*Group) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *Group) GetTag() string {
+	if x != nil {
+		return x.Tag
+	}
+	return ""
+}
+
+func (x *Group) GetType() string {
+	if x != nil {
+		return x.Type
+	}
+	return ""
+}
+
+func (x *Group) GetSelectable() bool {
+	if x != nil {
+		return x.Selectable
+	}
+	return false
+}
+
+func (x *Group) GetSelected() string {
+	if x != nil {
+		return x.Selected
+	}
+	return ""
+}
+
+func (x *Group) GetIsExpand() bool {
+	if x != nil {
+		return x.IsExpand
+	}
+	return false
+}
+
+func (x *Group) GetItems() []*GroupItem {
+	if x != nil {
+		return x.Items
+	}
+	return nil
+}
+
+type GroupItem struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Tag           string                 `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
+	Type          string                 `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
+	UrlTestTime   int64                  `protobuf:"varint,3,opt,name=urlTestTime,proto3" json:"urlTestTime,omitempty"`
+	UrlTestDelay  int32                  `protobuf:"varint,4,opt,name=urlTestDelay,proto3" json:"urlTestDelay,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *GroupItem) Reset() {
+	*x = GroupItem{}
+	mi := &file_daemon_started_service_proto_msgTypes[8]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *GroupItem) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GroupItem) ProtoMessage() {}
+
+func (x *GroupItem) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[8]
+	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 GroupItem.ProtoReflect.Descriptor instead.
+func (*GroupItem) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *GroupItem) GetTag() string {
+	if x != nil {
+		return x.Tag
+	}
+	return ""
+}
+
+func (x *GroupItem) GetType() string {
+	if x != nil {
+		return x.Type
+	}
+	return ""
+}
+
+func (x *GroupItem) GetUrlTestTime() int64 {
+	if x != nil {
+		return x.UrlTestTime
+	}
+	return 0
+}
+
+func (x *GroupItem) GetUrlTestDelay() int32 {
+	if x != nil {
+		return x.UrlTestDelay
+	}
+	return 0
+}
+
+type URLTestRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	OutboundTag   string                 `protobuf:"bytes,1,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *URLTestRequest) Reset() {
+	*x = URLTestRequest{}
+	mi := &file_daemon_started_service_proto_msgTypes[9]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *URLTestRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*URLTestRequest) ProtoMessage() {}
+
+func (x *URLTestRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[9]
+	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 URLTestRequest.ProtoReflect.Descriptor instead.
+func (*URLTestRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *URLTestRequest) GetOutboundTag() string {
+	if x != nil {
+		return x.OutboundTag
+	}
+	return ""
+}
+
+type SelectOutboundRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	GroupTag      string                 `protobuf:"bytes,1,opt,name=groupTag,proto3" json:"groupTag,omitempty"`
+	OutboundTag   string                 `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SelectOutboundRequest) Reset() {
+	*x = SelectOutboundRequest{}
+	mi := &file_daemon_started_service_proto_msgTypes[10]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SelectOutboundRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SelectOutboundRequest) ProtoMessage() {}
+
+func (x *SelectOutboundRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[10]
+	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 SelectOutboundRequest.ProtoReflect.Descriptor instead.
+func (*SelectOutboundRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *SelectOutboundRequest) GetGroupTag() string {
+	if x != nil {
+		return x.GroupTag
+	}
+	return ""
+}
+
+func (x *SelectOutboundRequest) GetOutboundTag() string {
+	if x != nil {
+		return x.OutboundTag
+	}
+	return ""
+}
+
+type SetGroupExpandRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	GroupTag      string                 `protobuf:"bytes,1,opt,name=groupTag,proto3" json:"groupTag,omitempty"`
+	IsExpand      bool                   `protobuf:"varint,2,opt,name=isExpand,proto3" json:"isExpand,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SetGroupExpandRequest) Reset() {
+	*x = SetGroupExpandRequest{}
+	mi := &file_daemon_started_service_proto_msgTypes[11]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SetGroupExpandRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SetGroupExpandRequest) ProtoMessage() {}
+
+func (x *SetGroupExpandRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[11]
+	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 SetGroupExpandRequest.ProtoReflect.Descriptor instead.
+func (*SetGroupExpandRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *SetGroupExpandRequest) GetGroupTag() string {
+	if x != nil {
+		return x.GroupTag
+	}
+	return ""
+}
+
+func (x *SetGroupExpandRequest) GetIsExpand() bool {
+	if x != nil {
+		return x.IsExpand
+	}
+	return false
+}
+
+type ClashMode struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Mode          string                 `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ClashMode) Reset() {
+	*x = ClashMode{}
+	mi := &file_daemon_started_service_proto_msgTypes[12]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ClashMode) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ClashMode) ProtoMessage() {}
+
+func (x *ClashMode) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[12]
+	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 ClashMode.ProtoReflect.Descriptor instead.
+func (*ClashMode) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *ClashMode) GetMode() string {
+	if x != nil {
+		return x.Mode
+	}
+	return ""
+}
+
+type ClashModeStatus struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	ModeList      []string               `protobuf:"bytes,1,rep,name=modeList,proto3" json:"modeList,omitempty"`
+	CurrentMode   string                 `protobuf:"bytes,2,opt,name=currentMode,proto3" json:"currentMode,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ClashModeStatus) Reset() {
+	*x = ClashModeStatus{}
+	mi := &file_daemon_started_service_proto_msgTypes[13]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ClashModeStatus) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ClashModeStatus) ProtoMessage() {}
+
+func (x *ClashModeStatus) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[13]
+	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 ClashModeStatus.ProtoReflect.Descriptor instead.
+func (*ClashModeStatus) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *ClashModeStatus) GetModeList() []string {
+	if x != nil {
+		return x.ModeList
+	}
+	return nil
+}
+
+func (x *ClashModeStatus) GetCurrentMode() string {
+	if x != nil {
+		return x.CurrentMode
+	}
+	return ""
+}
+
+type SystemProxyStatus struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Available     bool                   `protobuf:"varint,1,opt,name=available,proto3" json:"available,omitempty"`
+	Enabled       bool                   `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SystemProxyStatus) Reset() {
+	*x = SystemProxyStatus{}
+	mi := &file_daemon_started_service_proto_msgTypes[14]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SystemProxyStatus) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SystemProxyStatus) ProtoMessage() {}
+
+func (x *SystemProxyStatus) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[14]
+	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 SystemProxyStatus.ProtoReflect.Descriptor instead.
+func (*SystemProxyStatus) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *SystemProxyStatus) GetAvailable() bool {
+	if x != nil {
+		return x.Available
+	}
+	return false
+}
+
+func (x *SystemProxyStatus) GetEnabled() bool {
+	if x != nil {
+		return x.Enabled
+	}
+	return false
+}
+
+type SetSystemProxyEnabledRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Enabled       bool                   `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SetSystemProxyEnabledRequest) Reset() {
+	*x = SetSystemProxyEnabledRequest{}
+	mi := &file_daemon_started_service_proto_msgTypes[15]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SetSystemProxyEnabledRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SetSystemProxyEnabledRequest) ProtoMessage() {}
+
+func (x *SetSystemProxyEnabledRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[15]
+	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 SetSystemProxyEnabledRequest.ProtoReflect.Descriptor instead.
+func (*SetSystemProxyEnabledRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *SetSystemProxyEnabledRequest) GetEnabled() bool {
+	if x != nil {
+		return x.Enabled
+	}
+	return false
+}
+
+type SubscribeConnectionsRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Interval      int64                  `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"`
+	Filter        ConnectionFilter       `protobuf:"varint,2,opt,name=filter,proto3,enum=daemon.ConnectionFilter" json:"filter,omitempty"`
+	SortBy        ConnectionSortBy       `protobuf:"varint,3,opt,name=sortBy,proto3,enum=daemon.ConnectionSortBy" json:"sortBy,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *SubscribeConnectionsRequest) Reset() {
+	*x = SubscribeConnectionsRequest{}
+	mi := &file_daemon_started_service_proto_msgTypes[16]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *SubscribeConnectionsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SubscribeConnectionsRequest) ProtoMessage() {}
+
+func (x *SubscribeConnectionsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[16]
+	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 SubscribeConnectionsRequest.ProtoReflect.Descriptor instead.
+func (*SubscribeConnectionsRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{16}
+}
+
+func (x *SubscribeConnectionsRequest) GetInterval() int64 {
+	if x != nil {
+		return x.Interval
+	}
+	return 0
+}
+
+func (x *SubscribeConnectionsRequest) GetFilter() ConnectionFilter {
+	if x != nil {
+		return x.Filter
+	}
+	return ConnectionFilter_ALL
+}
+
+func (x *SubscribeConnectionsRequest) GetSortBy() ConnectionSortBy {
+	if x != nil {
+		return x.SortBy
+	}
+	return ConnectionSortBy_DATE
+}
+
+type Connections struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Connections   []*Connection          `protobuf:"bytes,1,rep,name=connections,proto3" json:"connections,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Connections) Reset() {
+	*x = Connections{}
+	mi := &file_daemon_started_service_proto_msgTypes[17]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Connections) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Connections) ProtoMessage() {}
+
+func (x *Connections) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[17]
+	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 Connections.ProtoReflect.Descriptor instead.
+func (*Connections) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{17}
+}
+
+func (x *Connections) GetConnections() []*Connection {
+	if x != nil {
+		return x.Connections
+	}
+	return nil
+}
+
+type Connection struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Id            string                 `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+	Inbound       string                 `protobuf:"bytes,2,opt,name=inbound,proto3" json:"inbound,omitempty"`
+	InboundType   string                 `protobuf:"bytes,3,opt,name=inboundType,proto3" json:"inboundType,omitempty"`
+	IpVersion     int32                  `protobuf:"varint,4,opt,name=ipVersion,proto3" json:"ipVersion,omitempty"`
+	Network       string                 `protobuf:"bytes,5,opt,name=network,proto3" json:"network,omitempty"`
+	Source        string                 `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"`
+	Destination   string                 `protobuf:"bytes,7,opt,name=destination,proto3" json:"destination,omitempty"`
+	Domain        string                 `protobuf:"bytes,8,opt,name=domain,proto3" json:"domain,omitempty"`
+	Protocol      string                 `protobuf:"bytes,9,opt,name=protocol,proto3" json:"protocol,omitempty"`
+	User          string                 `protobuf:"bytes,10,opt,name=user,proto3" json:"user,omitempty"`
+	FromOutbound  string                 `protobuf:"bytes,11,opt,name=fromOutbound,proto3" json:"fromOutbound,omitempty"`
+	CreatedAt     int64                  `protobuf:"varint,12,opt,name=createdAt,proto3" json:"createdAt,omitempty"`
+	ClosedAt      int64                  `protobuf:"varint,13,opt,name=closedAt,proto3" json:"closedAt,omitempty"`
+	Uplink        int64                  `protobuf:"varint,14,opt,name=uplink,proto3" json:"uplink,omitempty"`
+	Downlink      int64                  `protobuf:"varint,15,opt,name=downlink,proto3" json:"downlink,omitempty"`
+	UplinkTotal   int64                  `protobuf:"varint,16,opt,name=uplinkTotal,proto3" json:"uplinkTotal,omitempty"`
+	DownlinkTotal int64                  `protobuf:"varint,17,opt,name=downlinkTotal,proto3" json:"downlinkTotal,omitempty"`
+	Rule          string                 `protobuf:"bytes,18,opt,name=rule,proto3" json:"rule,omitempty"`
+	Outbound      string                 `protobuf:"bytes,19,opt,name=outbound,proto3" json:"outbound,omitempty"`
+	OutboundType  string                 `protobuf:"bytes,20,opt,name=outboundType,proto3" json:"outboundType,omitempty"`
+	ChainList     []string               `protobuf:"bytes,21,rep,name=chainList,proto3" json:"chainList,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Connection) Reset() {
+	*x = Connection{}
+	mi := &file_daemon_started_service_proto_msgTypes[18]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Connection) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Connection) ProtoMessage() {}
+
+func (x *Connection) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[18]
+	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 Connection.ProtoReflect.Descriptor instead.
+func (*Connection) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
+}
+
+func (x *Connection) GetId() string {
+	if x != nil {
+		return x.Id
+	}
+	return ""
+}
+
+func (x *Connection) GetInbound() string {
+	if x != nil {
+		return x.Inbound
+	}
+	return ""
+}
+
+func (x *Connection) GetInboundType() string {
+	if x != nil {
+		return x.InboundType
+	}
+	return ""
+}
+
+func (x *Connection) GetIpVersion() int32 {
+	if x != nil {
+		return x.IpVersion
+	}
+	return 0
+}
+
+func (x *Connection) GetNetwork() string {
+	if x != nil {
+		return x.Network
+	}
+	return ""
+}
+
+func (x *Connection) GetSource() string {
+	if x != nil {
+		return x.Source
+	}
+	return ""
+}
+
+func (x *Connection) GetDestination() string {
+	if x != nil {
+		return x.Destination
+	}
+	return ""
+}
+
+func (x *Connection) GetDomain() string {
+	if x != nil {
+		return x.Domain
+	}
+	return ""
+}
+
+func (x *Connection) GetProtocol() string {
+	if x != nil {
+		return x.Protocol
+	}
+	return ""
+}
+
+func (x *Connection) GetUser() string {
+	if x != nil {
+		return x.User
+	}
+	return ""
+}
+
+func (x *Connection) GetFromOutbound() string {
+	if x != nil {
+		return x.FromOutbound
+	}
+	return ""
+}
+
+func (x *Connection) GetCreatedAt() int64 {
+	if x != nil {
+		return x.CreatedAt
+	}
+	return 0
+}
+
+func (x *Connection) GetClosedAt() int64 {
+	if x != nil {
+		return x.ClosedAt
+	}
+	return 0
+}
+
+func (x *Connection) GetUplink() int64 {
+	if x != nil {
+		return x.Uplink
+	}
+	return 0
+}
+
+func (x *Connection) GetDownlink() int64 {
+	if x != nil {
+		return x.Downlink
+	}
+	return 0
+}
+
+func (x *Connection) GetUplinkTotal() int64 {
+	if x != nil {
+		return x.UplinkTotal
+	}
+	return 0
+}
+
+func (x *Connection) GetDownlinkTotal() int64 {
+	if x != nil {
+		return x.DownlinkTotal
+	}
+	return 0
+}
+
+func (x *Connection) GetRule() string {
+	if x != nil {
+		return x.Rule
+	}
+	return ""
+}
+
+func (x *Connection) GetOutbound() string {
+	if x != nil {
+		return x.Outbound
+	}
+	return ""
+}
+
+func (x *Connection) GetOutboundType() string {
+	if x != nil {
+		return x.OutboundType
+	}
+	return ""
+}
+
+func (x *Connection) GetChainList() []string {
+	if x != nil {
+		return x.ChainList
+	}
+	return nil
+}
+
+type CloseConnectionRequest struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Id            string                 `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *CloseConnectionRequest) Reset() {
+	*x = CloseConnectionRequest{}
+	mi := &file_daemon_started_service_proto_msgTypes[19]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *CloseConnectionRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CloseConnectionRequest) ProtoMessage() {}
+
+func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[19]
+	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 CloseConnectionRequest.ProtoReflect.Descriptor instead.
+func (*CloseConnectionRequest) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *CloseConnectionRequest) GetId() string {
+	if x != nil {
+		return x.Id
+	}
+	return ""
+}
+
+type DeprecatedWarnings struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Warnings      []*DeprecatedWarning   `protobuf:"bytes,1,rep,name=warnings,proto3" json:"warnings,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DeprecatedWarnings) Reset() {
+	*x = DeprecatedWarnings{}
+	mi := &file_daemon_started_service_proto_msgTypes[20]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DeprecatedWarnings) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeprecatedWarnings) ProtoMessage() {}
+
+func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[20]
+	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 DeprecatedWarnings.ProtoReflect.Descriptor instead.
+func (*DeprecatedWarnings) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
+}
+
+func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning {
+	if x != nil {
+		return x.Warnings
+	}
+	return nil
+}
+
+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
+}
+
+func (x *DeprecatedWarning) Reset() {
+	*x = DeprecatedWarning{}
+	mi := &file_daemon_started_service_proto_msgTypes[21]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DeprecatedWarning) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeprecatedWarning) ProtoMessage() {}
+
+func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[21]
+	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 DeprecatedWarning.ProtoReflect.Descriptor instead.
+func (*DeprecatedWarning) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{21}
+}
+
+func (x *DeprecatedWarning) GetMessage() string {
+	if x != nil {
+		return x.Message
+	}
+	return ""
+}
+
+func (x *DeprecatedWarning) GetImpending() bool {
+	if x != nil {
+		return x.Impending
+	}
+	return false
+}
+
+func (x *DeprecatedWarning) GetMigrationLink() string {
+	if x != nil {
+		return x.MigrationLink
+	}
+	return ""
+}
+
+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"`
+	Message       string                 `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *Log_Message) Reset() {
+	*x = Log_Message{}
+	mi := &file_daemon_started_service_proto_msgTypes[22]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *Log_Message) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Log_Message) ProtoMessage() {}
+
+func (x *Log_Message) ProtoReflect() protoreflect.Message {
+	mi := &file_daemon_started_service_proto_msgTypes[22]
+	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 Log_Message.ProtoReflect.Descriptor instead.
+func (*Log_Message) Descriptor() ([]byte, []int) {
+	return file_daemon_started_service_proto_rawDescGZIP(), []int{3, 0}
+}
+
+func (x *Log_Message) GetLevel() LogLevel {
+	if x != nil {
+		return x.Level
+	}
+	return LogLevel_PANIC
+}
+
+func (x *Log_Message) GetMessage() string {
+	if x != nil {
+		return x.Message
+	}
+	return ""
+}
+
+var File_daemon_started_service_proto protoreflect.FileDescriptor
+
+const file_daemon_started_service_proto_rawDesc = "" +
+	"\n" +
+	"\x1cdaemon/started_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\x1a\x13daemon/helper.proto\"\xad\x01\n" +
+	"\rServiceStatus\x122\n" +
+	"\x06status\x18\x01 \x01(\x0e2\x1a.daemon.ServiceStatus.TypeR\x06status\x12\"\n" +
+	"\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\"D\n" +
+	"\x04Type\x12\b\n" +
+	"\x04IDLE\x10\x00\x12\f\n" +
+	"\bSTARTING\x10\x01\x12\v\n" +
+	"\aSTARTED\x10\x02\x12\f\n" +
+	"\bSTOPPING\x10\x03\x12\t\n" +
+	"\x05FATAL\x10\x04\"D\n" +
+	"\x14ReloadServiceRequest\x12,\n" +
+	"\x11newProfileContent\x18\x01 \x01(\tR\x11newProfileContent\"4\n" +
+	"\x16SubscribeStatusRequest\x12\x1a\n" +
+	"\binterval\x18\x01 \x01(\x03R\binterval\"\x99\x01\n" +
+	"\x03Log\x12/\n" +
+	"\bmessages\x18\x01 \x03(\v2\x13.daemon.Log.MessageR\bmessages\x12\x14\n" +
+	"\x05reset\x18\x02 \x01(\bR\x05reset\x1aK\n" +
+	"\aMessage\x12&\n" +
+	"\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\x12\x18\n" +
+	"\amessage\x18\x02 \x01(\tR\amessage\"9\n" +
+	"\x0fDefaultLogLevel\x12&\n" +
+	"\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\"\xb6\x02\n" +
+	"\x06Status\x12\x16\n" +
+	"\x06memory\x18\x01 \x01(\x04R\x06memory\x12\x1e\n" +
+	"\n" +
+	"goroutines\x18\x02 \x01(\x05R\n" +
+	"goroutines\x12$\n" +
+	"\rconnectionsIn\x18\x03 \x01(\x05R\rconnectionsIn\x12&\n" +
+	"\x0econnectionsOut\x18\x04 \x01(\x05R\x0econnectionsOut\x12*\n" +
+	"\x10trafficAvailable\x18\x05 \x01(\bR\x10trafficAvailable\x12\x16\n" +
+	"\x06uplink\x18\x06 \x01(\x03R\x06uplink\x12\x1a\n" +
+	"\bdownlink\x18\a \x01(\x03R\bdownlink\x12 \n" +
+	"\vuplinkTotal\x18\b \x01(\x03R\vuplinkTotal\x12$\n" +
+	"\rdownlinkTotal\x18\t \x01(\x03R\rdownlinkTotal\"-\n" +
+	"\x06Groups\x12#\n" +
+	"\x05group\x18\x01 \x03(\v2\r.daemon.GroupR\x05group\"\xae\x01\n" +
+	"\x05Group\x12\x10\n" +
+	"\x03tag\x18\x01 \x01(\tR\x03tag\x12\x12\n" +
+	"\x04type\x18\x02 \x01(\tR\x04type\x12\x1e\n" +
+	"\n" +
+	"selectable\x18\x03 \x01(\bR\n" +
+	"selectable\x12\x1a\n" +
+	"\bselected\x18\x04 \x01(\tR\bselected\x12\x1a\n" +
+	"\bisExpand\x18\x05 \x01(\bR\bisExpand\x12'\n" +
+	"\x05items\x18\x06 \x03(\v2\x11.daemon.GroupItemR\x05items\"w\n" +
+	"\tGroupItem\x12\x10\n" +
+	"\x03tag\x18\x01 \x01(\tR\x03tag\x12\x12\n" +
+	"\x04type\x18\x02 \x01(\tR\x04type\x12 \n" +
+	"\vurlTestTime\x18\x03 \x01(\x03R\vurlTestTime\x12\"\n" +
+	"\furlTestDelay\x18\x04 \x01(\x05R\furlTestDelay\"2\n" +
+	"\x0eURLTestRequest\x12 \n" +
+	"\voutboundTag\x18\x01 \x01(\tR\voutboundTag\"U\n" +
+	"\x15SelectOutboundRequest\x12\x1a\n" +
+	"\bgroupTag\x18\x01 \x01(\tR\bgroupTag\x12 \n" +
+	"\voutboundTag\x18\x02 \x01(\tR\voutboundTag\"O\n" +
+	"\x15SetGroupExpandRequest\x12\x1a\n" +
+	"\bgroupTag\x18\x01 \x01(\tR\bgroupTag\x12\x1a\n" +
+	"\bisExpand\x18\x02 \x01(\bR\bisExpand\"\x1f\n" +
+	"\tClashMode\x12\x12\n" +
+	"\x04mode\x18\x03 \x01(\tR\x04mode\"O\n" +
+	"\x0fClashModeStatus\x12\x1a\n" +
+	"\bmodeList\x18\x01 \x03(\tR\bmodeList\x12 \n" +
+	"\vcurrentMode\x18\x02 \x01(\tR\vcurrentMode\"K\n" +
+	"\x11SystemProxyStatus\x12\x1c\n" +
+	"\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" +
+	"\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" +
+	"\x1cSetSystemProxyEnabledRequest\x12\x18\n" +
+	"\aenabled\x18\x01 \x01(\bR\aenabled\"\x9d\x01\n" +
+	"\x1bSubscribeConnectionsRequest\x12\x1a\n" +
+	"\binterval\x18\x01 \x01(\x03R\binterval\x120\n" +
+	"\x06filter\x18\x02 \x01(\x0e2\x18.daemon.ConnectionFilterR\x06filter\x120\n" +
+	"\x06sortBy\x18\x03 \x01(\x0e2\x18.daemon.ConnectionSortByR\x06sortBy\"C\n" +
+	"\vConnections\x124\n" +
+	"\vconnections\x18\x01 \x03(\v2\x12.daemon.ConnectionR\vconnections\"\xde\x04\n" +
+	"\n" +
+	"Connection\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" +
+	"\ainbound\x18\x02 \x01(\tR\ainbound\x12 \n" +
+	"\vinboundType\x18\x03 \x01(\tR\vinboundType\x12\x1c\n" +
+	"\tipVersion\x18\x04 \x01(\x05R\tipVersion\x12\x18\n" +
+	"\anetwork\x18\x05 \x01(\tR\anetwork\x12\x16\n" +
+	"\x06source\x18\x06 \x01(\tR\x06source\x12 \n" +
+	"\vdestination\x18\a \x01(\tR\vdestination\x12\x16\n" +
+	"\x06domain\x18\b \x01(\tR\x06domain\x12\x1a\n" +
+	"\bprotocol\x18\t \x01(\tR\bprotocol\x12\x12\n" +
+	"\x04user\x18\n" +
+	" \x01(\tR\x04user\x12\"\n" +
+	"\ffromOutbound\x18\v \x01(\tR\ffromOutbound\x12\x1c\n" +
+	"\tcreatedAt\x18\f \x01(\x03R\tcreatedAt\x12\x1a\n" +
+	"\bclosedAt\x18\r \x01(\x03R\bclosedAt\x12\x16\n" +
+	"\x06uplink\x18\x0e \x01(\x03R\x06uplink\x12\x1a\n" +
+	"\bdownlink\x18\x0f \x01(\x03R\bdownlink\x12 \n" +
+	"\vuplinkTotal\x18\x10 \x01(\x03R\vuplinkTotal\x12$\n" +
+	"\rdownlinkTotal\x18\x11 \x01(\x03R\rdownlinkTotal\x12\x12\n" +
+	"\x04rule\x18\x12 \x01(\tR\x04rule\x12\x1a\n" +
+	"\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" +
+	"\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" +
+	"\tchainList\x18\x15 \x03(\tR\tchainList\"(\n" +
+	"\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" +
+	"\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*U\n" +
+	"\bLogLevel\x12\t\n" +
+	"\x05PANIC\x10\x00\x12\t\n" +
+	"\x05FATAL\x10\x01\x12\t\n" +
+	"\x05ERROR\x10\x02\x12\b\n" +
+	"\x04WARN\x10\x03\x12\b\n" +
+	"\x04INFO\x10\x04\x12\t\n" +
+	"\x05DEBUG\x10\x05\x12\t\n" +
+	"\x05TRACE\x10\x06*3\n" +
+	"\x10ConnectionFilter\x12\a\n" +
+	"\x03ALL\x10\x00\x12\n" +
+	"\n" +
+	"\x06ACTIVE\x10\x01\x12\n" +
+	"\n" +
+	"\x06CLOSED\x10\x02*<\n" +
+	"\x10ConnectionSortBy\x12\b\n" +
+	"\x04DATE\x10\x00\x12\v\n" +
+	"\aTRAFFIC\x10\x01\x12\x11\n" +
+	"\rTOTAL_TRAFFIC\x10\x022\xf8\v\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" +
+	"\x16SubscribeServiceStatus\x12\x16.google.protobuf.Empty\x1a\x15.daemon.ServiceStatus\"\x000\x01\x127\n" +
+	"\fSubscribeLog\x12\x16.google.protobuf.Empty\x1a\v.daemon.Log\"\x000\x01\x12G\n" +
+	"\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12E\n" +
+	"\x0fSubscribeStatus\x12\x1e.daemon.SubscribeStatusRequest\x1a\x0e.daemon.Status\"\x000\x01\x12=\n" +
+	"\x0fSubscribeGroups\x12\x16.google.protobuf.Empty\x1a\x0e.daemon.Groups\"\x000\x01\x12G\n" +
+	"\x12GetClashModeStatus\x12\x16.google.protobuf.Empty\x1a\x17.daemon.ClashModeStatus\"\x00\x12C\n" +
+	"\x12SubscribeClashMode\x12\x16.google.protobuf.Empty\x1a\x11.daemon.ClashMode\"\x000\x01\x12;\n" +
+	"\fSetClashMode\x12\x11.daemon.ClashMode\x1a\x16.google.protobuf.Empty\"\x00\x12;\n" +
+	"\aURLTest\x12\x16.daemon.URLTestRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" +
+	"\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" +
+	"\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" +
+	"\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" +
+	"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n" +
+	"\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x13.daemon.Connections\"\x000\x01\x12K\n" +
+	"\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\x12J\n" +
+	"\x15SubscribeHelperEvents\x12\x16.google.protobuf.Empty\x1a\x15.daemon.HelperRequest\"\x000\x01\x12F\n" +
+	"\x12SendHelperResponse\x12\x16.daemon.HelperResponse\x1a\x16.google.protobuf.Empty\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
+
+var (
+	file_daemon_started_service_proto_rawDescOnce sync.Once
+	file_daemon_started_service_proto_rawDescData []byte
+)
+
+func file_daemon_started_service_proto_rawDescGZIP() []byte {
+	file_daemon_started_service_proto_rawDescOnce.Do(func() {
+		file_daemon_started_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)))
+	})
+	return file_daemon_started_service_proto_rawDescData
+}
+
+var (
+	file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
+	file_daemon_started_service_proto_msgTypes  = make([]protoimpl.MessageInfo, 23)
+	file_daemon_started_service_proto_goTypes   = []any{
+		(LogLevel)(0),                        // 0: daemon.LogLevel
+		(ConnectionFilter)(0),                // 1: daemon.ConnectionFilter
+		(ConnectionSortBy)(0),                // 2: daemon.ConnectionSortBy
+		(ServiceStatus_Type)(0),              // 3: daemon.ServiceStatus.Type
+		(*ServiceStatus)(nil),                // 4: daemon.ServiceStatus
+		(*ReloadServiceRequest)(nil),         // 5: daemon.ReloadServiceRequest
+		(*SubscribeStatusRequest)(nil),       // 6: daemon.SubscribeStatusRequest
+		(*Log)(nil),                          // 7: daemon.Log
+		(*DefaultLogLevel)(nil),              // 8: daemon.DefaultLogLevel
+		(*Status)(nil),                       // 9: daemon.Status
+		(*Groups)(nil),                       // 10: daemon.Groups
+		(*Group)(nil),                        // 11: daemon.Group
+		(*GroupItem)(nil),                    // 12: daemon.GroupItem
+		(*URLTestRequest)(nil),               // 13: daemon.URLTestRequest
+		(*SelectOutboundRequest)(nil),        // 14: daemon.SelectOutboundRequest
+		(*SetGroupExpandRequest)(nil),        // 15: daemon.SetGroupExpandRequest
+		(*ClashMode)(nil),                    // 16: daemon.ClashMode
+		(*ClashModeStatus)(nil),              // 17: daemon.ClashModeStatus
+		(*SystemProxyStatus)(nil),            // 18: daemon.SystemProxyStatus
+		(*SetSystemProxyEnabledRequest)(nil), // 19: daemon.SetSystemProxyEnabledRequest
+		(*SubscribeConnectionsRequest)(nil),  // 20: daemon.SubscribeConnectionsRequest
+		(*Connections)(nil),                  // 21: daemon.Connections
+		(*Connection)(nil),                   // 22: daemon.Connection
+		(*CloseConnectionRequest)(nil),       // 23: daemon.CloseConnectionRequest
+		(*DeprecatedWarnings)(nil),           // 24: daemon.DeprecatedWarnings
+		(*DeprecatedWarning)(nil),            // 25: daemon.DeprecatedWarning
+		(*Log_Message)(nil),                  // 26: daemon.Log.Message
+		(*emptypb.Empty)(nil),                // 27: google.protobuf.Empty
+		(*HelperResponse)(nil),               // 28: daemon.HelperResponse
+		(*HelperRequest)(nil),                // 29: daemon.HelperRequest
+	}
+)
+
+var file_daemon_started_service_proto_depIdxs = []int32{
+	3,  // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
+	26, // 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
+	1,  // 5: daemon.SubscribeConnectionsRequest.filter:type_name -> daemon.ConnectionFilter
+	2,  // 6: daemon.SubscribeConnectionsRequest.sortBy:type_name -> daemon.ConnectionSortBy
+	22, // 7: daemon.Connections.connections:type_name -> daemon.Connection
+	25, // 8: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning
+	0,  // 9: daemon.Log.Message.level:type_name -> daemon.LogLevel
+	27, // 10: daemon.StartedService.StopService:input_type -> google.protobuf.Empty
+	27, // 11: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty
+	27, // 12: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
+	27, // 13: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
+	27, // 14: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
+	6,  // 15: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
+	27, // 16: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
+	27, // 17: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
+	27, // 18: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
+	16, // 19: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
+	13, // 20: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
+	14, // 21: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
+	15, // 22: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
+	27, // 23: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
+	19, // 24: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
+	20, // 25: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
+	23, // 26: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
+	27, // 27: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
+	27, // 28: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
+	27, // 29: daemon.StartedService.SubscribeHelperEvents:input_type -> google.protobuf.Empty
+	28, // 30: daemon.StartedService.SendHelperResponse:input_type -> daemon.HelperResponse
+	27, // 31: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
+	27, // 32: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
+	4,  // 33: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
+	7,  // 34: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
+	8,  // 35: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
+	9,  // 36: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
+	10, // 37: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
+	17, // 38: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
+	16, // 39: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
+	27, // 40: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
+	27, // 41: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
+	27, // 42: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
+	27, // 43: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
+	18, // 44: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
+	27, // 45: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
+	21, // 46: daemon.StartedService.SubscribeConnections:output_type -> daemon.Connections
+	27, // 47: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
+	27, // 48: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
+	24, // 49: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
+	29, // 50: daemon.StartedService.SubscribeHelperEvents:output_type -> daemon.HelperRequest
+	27, // 51: daemon.StartedService.SendHelperResponse:output_type -> google.protobuf.Empty
+	31, // [31:52] is the sub-list for method output_type
+	10, // [10:31] is the sub-list for method input_type
+	10, // [10:10] is the sub-list for extension type_name
+	10, // [10:10] is the sub-list for extension extendee
+	0,  // [0:10] is the sub-list for field type_name
+}
+
+func init() { file_daemon_started_service_proto_init() }
+func file_daemon_started_service_proto_init() {
+	if File_daemon_started_service_proto != nil {
+		return
+	}
+	file_daemon_helper_proto_init()
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			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:   23,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_daemon_started_service_proto_goTypes,
+		DependencyIndexes: file_daemon_started_service_proto_depIdxs,
+		EnumInfos:         file_daemon_started_service_proto_enumTypes,
+		MessageInfos:      file_daemon_started_service_proto_msgTypes,
+	}.Build()
+	File_daemon_started_service_proto = out.File
+	file_daemon_started_service_proto_goTypes = nil
+	file_daemon_started_service_proto_depIdxs = nil
+}

+ 204 - 0
daemon/started_service.proto

@@ -0,0 +1,204 @@
+syntax = "proto3";
+
+package daemon;
+option go_package = "github.com/sagernet/sing-box/daemon";
+
+import "google/protobuf/empty.proto";
+import "daemon/helper.proto";
+
+service StartedService {
+  rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty);
+  rpc ReloadService(google.protobuf.Empty) returns (google.protobuf.Empty);
+
+  rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {}
+  rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {}
+  rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {}
+  rpc SubscribeStatus(SubscribeStatusRequest) returns(stream Status) {}
+  rpc SubscribeGroups(google.protobuf.Empty) returns(stream Groups) {}
+
+  rpc GetClashModeStatus(google.protobuf.Empty) returns(ClashModeStatus) {}
+  rpc SubscribeClashMode(google.protobuf.Empty) returns(stream ClashMode) {}
+  rpc SetClashMode(ClashMode) returns(google.protobuf.Empty) {}
+
+  rpc URLTest(URLTestRequest) returns(google.protobuf.Empty) {}
+  rpc SelectOutbound(SelectOutboundRequest) returns (google.protobuf.Empty) {}
+  rpc SetGroupExpand(SetGroupExpandRequest) returns (google.protobuf.Empty) {}
+
+  rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
+  rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
+
+  rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream Connections) {}
+  rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
+  rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
+  rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
+
+  rpc SubscribeHelperEvents(google.protobuf.Empty) returns(stream HelperRequest) {}
+  rpc SendHelperResponse(HelperResponse) returns(google.protobuf.Empty) {}
+}
+
+message ServiceStatus {
+  enum Type {
+    IDLE = 0;
+    STARTING = 1;
+    STARTED = 2;
+    STOPPING = 3;
+    FATAL = 4;
+  }
+  Type status = 1;
+  string errorMessage = 2;
+}
+
+message ReloadServiceRequest {
+  string newProfileContent = 1;
+}
+
+message SubscribeStatusRequest {
+  int64 interval = 1;
+}
+
+enum LogLevel {
+  PANIC = 0;
+  FATAL = 1;
+  ERROR = 2;
+  WARN = 3;
+  INFO = 4;
+  DEBUG = 5;
+  TRACE = 6;
+}
+
+message Log {
+  repeated Message messages = 1;
+  bool reset = 2;
+  message Message {
+    LogLevel level = 1;
+    string message = 2;
+  }
+}
+
+message DefaultLogLevel {
+  LogLevel level = 1;
+}
+
+message Status {
+  uint64 memory = 1;
+  int32 goroutines = 2;
+  int32 connectionsIn = 3;
+  int32 connectionsOut = 4;
+  bool trafficAvailable = 5;
+  int64 uplink = 6;
+  int64 downlink = 7;
+  int64 uplinkTotal = 8;
+  int64 downlinkTotal = 9;
+}
+
+message Groups {
+  repeated Group group = 1;
+}
+
+message Group {
+  string tag = 1;
+  string type = 2;
+  bool selectable = 3;
+  string selected = 4;
+  bool isExpand = 5;
+  repeated GroupItem items = 6;
+}
+
+message GroupItem {
+  string tag = 1;
+  string type = 2;
+  int64 urlTestTime = 3;
+  int32 urlTestDelay = 4;
+}
+
+message URLTestRequest {
+  string outboundTag = 1;
+}
+
+message SelectOutboundRequest {
+  string groupTag = 1;
+  string outboundTag = 2;
+}
+
+message SetGroupExpandRequest {
+  string groupTag = 1;
+  bool isExpand = 2;
+}
+
+message ClashMode {
+  string mode = 3;
+}
+
+message ClashModeStatus {
+  repeated string modeList = 1;
+  string currentMode = 2;
+}
+
+message SystemProxyStatus {
+  bool available = 1;
+  bool enabled = 2;
+}
+
+message SetSystemProxyEnabledRequest {
+  bool enabled = 1;
+}
+
+message SubscribeConnectionsRequest {
+  int64 interval = 1;
+  ConnectionFilter filter = 2;
+  ConnectionSortBy sortBy = 3;
+}
+
+enum ConnectionFilter {
+  ALL = 0;
+  ACTIVE = 1;
+  CLOSED = 2;
+}
+
+enum ConnectionSortBy {
+  DATE = 0;
+  TRAFFIC = 1;
+  TOTAL_TRAFFIC = 2;
+}
+
+message Connections {
+  repeated Connection connections = 1;
+}
+
+message Connection {
+  string id = 1;
+  string inbound = 2;
+  string inboundType = 3;
+  int32 ipVersion = 4;
+  string network = 5;
+  string source = 6;
+  string destination = 7;
+  string domain = 8;
+  string protocol = 9;
+  string user = 10;
+  string fromOutbound = 11;
+  int64 createdAt = 12;
+  int64 closedAt = 13;
+  int64 uplink = 14;
+  int64 downlink = 15;
+  int64 uplinkTotal = 16;
+  int64 downlinkTotal = 17;
+  string rule = 18;
+  string outbound = 19;
+  string outboundType = 20;
+  repeated string chainList = 21;
+}
+
+message CloseConnectionRequest {
+  string id = 1;
+}
+
+message DeprecatedWarnings {
+  repeated DeprecatedWarning warnings = 1;
+}
+
+message DeprecatedWarning {
+  string message = 1;
+  bool impending = 2;
+  string migrationLink = 3;
+}

+ 919 - 0
daemon/started_service_grpc.pb.go

@@ -0,0 +1,919 @@
+package daemon
+
+import (
+	context "context"
+
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+	emptypb "google.golang.org/protobuf/types/known/emptypb"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+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_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_SubscribeConnections_FullMethodName   = "/daemon.StartedService/SubscribeConnections"
+	StartedService_CloseConnection_FullMethodName        = "/daemon.StartedService/CloseConnection"
+	StartedService_CloseAllConnections_FullMethodName    = "/daemon.StartedService/CloseAllConnections"
+	StartedService_GetDeprecatedWarnings_FullMethodName  = "/daemon.StartedService/GetDeprecatedWarnings"
+	StartedService_SubscribeHelperEvents_FullMethodName  = "/daemon.StartedService/SubscribeHelperEvents"
+	StartedService_SendHelperResponse_FullMethodName     = "/daemon.StartedService/SendHelperResponse"
+)
+
+// StartedServiceClient is the client API for StartedService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type StartedServiceClient interface {
+	StopService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
+	ReloadService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
+	SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error)
+	SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error)
+	GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error)
+	SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error)
+	SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error)
+	GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error)
+	SubscribeClashMode(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ClashMode], error)
+	SetClashMode(ctx context.Context, in *ClashMode, opts ...grpc.CallOption) (*emptypb.Empty, error)
+	URLTest(ctx context.Context, in *URLTestRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
+	SelectOutbound(ctx context.Context, in *SelectOutboundRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
+	SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
+	GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
+	SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
+	SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error)
+	CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
+	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)
+	SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error)
+	SendHelperResponse(ctx context.Context, in *HelperResponse, opts ...grpc.CallOption) (*emptypb.Empty, error)
+}
+
+type startedServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewStartedServiceClient(cc grpc.ClientConnInterface) StartedServiceClient {
+	return &startedServiceClient{cc}
+}
+
+func (c *startedServiceClient) StopService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_StopService_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) ReloadService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_ReloadService_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[0], StartedService_SubscribeServiceStatus_FullMethodName, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &grpc.GenericClientStream[emptypb.Empty, ServiceStatus]{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_SubscribeServiceStatusClient = grpc.ServerStreamingClient[ServiceStatus]
+
+func (c *startedServiceClient) SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[1], StartedService_SubscribeLog_FullMethodName, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &grpc.GenericClientStream[emptypb.Empty, Log]{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_SubscribeLogClient = grpc.ServerStreamingClient[Log]
+
+func (c *startedServiceClient) GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(DefaultLogLevel)
+	err := c.cc.Invoke(ctx, StartedService_GetDefaultLogLevel_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[2], StartedService_SubscribeStatus_FullMethodName, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &grpc.GenericClientStream[SubscribeStatusRequest, Status]{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_SubscribeStatusClient = grpc.ServerStreamingClient[Status]
+
+func (c *startedServiceClient) SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[3], StartedService_SubscribeGroups_FullMethodName, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &grpc.GenericClientStream[emptypb.Empty, Groups]{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_SubscribeGroupsClient = grpc.ServerStreamingClient[Groups]
+
+func (c *startedServiceClient) GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(ClashModeStatus)
+	err := c.cc.Invoke(ctx, StartedService_GetClashModeStatus_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) SubscribeClashMode(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ClashMode], error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[4], StartedService_SubscribeClashMode_FullMethodName, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &grpc.GenericClientStream[emptypb.Empty, ClashMode]{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_SubscribeClashModeClient = grpc.ServerStreamingClient[ClashMode]
+
+func (c *startedServiceClient) SetClashMode(ctx context.Context, in *ClashMode, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_SetClashMode_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) URLTest(ctx context.Context, in *URLTestRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_URLTest_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) SelectOutbound(ctx context.Context, in *SelectOutboundRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_SelectOutbound_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_SetGroupExpand_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(SystemProxyStatus)
+	err := c.cc.Invoke(ctx, StartedService_GetSystemProxyStatus_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_SetSystemProxyEnabled_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &grpc.GenericClientStream[SubscribeConnectionsRequest, Connections]{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_SubscribeConnectionsClient = grpc.ServerStreamingClient[Connections]
+
+func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_CloseConnection_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_CloseAllConnections_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(DeprecatedWarnings)
+	err := c.cc.Invoke(ctx, StartedService_GetDeprecatedWarnings_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *startedServiceClient) SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeHelperEvents_FullMethodName, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &grpc.GenericClientStream[emptypb.Empty, HelperRequest]{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_SubscribeHelperEventsClient = grpc.ServerStreamingClient[HelperRequest]
+
+func (c *startedServiceClient) SendHelperResponse(ctx context.Context, in *HelperResponse, opts ...grpc.CallOption) (*emptypb.Empty, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(emptypb.Empty)
+	err := c.cc.Invoke(ctx, StartedService_SendHelperResponse_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// StartedServiceServer is the server API for StartedService service.
+// All implementations must embed UnimplementedStartedServiceServer
+// for forward compatibility.
+type StartedServiceServer interface {
+	StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
+	ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
+	SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error
+	SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error
+	GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error)
+	SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error
+	SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error
+	GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error)
+	SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error
+	SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error)
+	URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error)
+	SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error)
+	SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
+	GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
+	SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
+	SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error
+	CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
+	CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
+	GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
+	SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error
+	SendHelperResponse(context.Context, *HelperResponse) (*emptypb.Empty, error)
+	mustEmbedUnimplementedStartedServiceServer()
+}
+
+// UnimplementedStartedServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedStartedServiceServer struct{}
+
+func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method StopService not implemented")
+}
+
+func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ReloadService not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error {
+	return status.Errorf(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error {
+	return status.Errorf(codes.Unimplemented, "method SubscribeLog not implemented")
+}
+
+func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetDefaultLogLevel not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error {
+	return status.Errorf(codes.Unimplemented, "method SubscribeStatus not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error {
+	return status.Errorf(codes.Unimplemented, "method SubscribeGroups not implemented")
+}
+
+func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetClashModeStatus not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error {
+	return status.Errorf(codes.Unimplemented, "method SubscribeClashMode not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method SetClashMode not implemented")
+}
+
+func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method URLTest not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method SelectOutbound not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method SetGroupExpand not implemented")
+}
+
+func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error {
+	return status.Errorf(codes.Unimplemented, "method SubscribeConnections not implemented")
+}
+
+func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method CloseConnection not implemented")
+}
+
+func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method CloseAllConnections not implemented")
+}
+
+func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetDeprecatedWarnings not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error {
+	return status.Errorf(codes.Unimplemented, "method SubscribeHelperEvents not implemented")
+}
+
+func (UnimplementedStartedServiceServer) SendHelperResponse(context.Context, *HelperResponse) (*emptypb.Empty, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method SendHelperResponse not implemented")
+}
+func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
+func (UnimplementedStartedServiceServer) testEmbeddedByValue()                        {}
+
+// UnsafeStartedServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to StartedServiceServer will
+// result in compilation errors.
+type UnsafeStartedServiceServer interface {
+	mustEmbedUnimplementedStartedServiceServer()
+}
+
+func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) {
+	// If the following call pancis, it indicates UnimplementedStartedServiceServer was
+	// embedded by pointer and is nil.  This will cause panics if an
+	// unimplemented method is ever invoked, so we test this at initialization
+	// time to prevent it from happening at runtime later due to I/O.
+	if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+		t.testEmbeddedByValue()
+	}
+	s.RegisterService(&StartedService_ServiceDesc, srv)
+}
+
+func _StartedService_StopService_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).StopService(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_StopService_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).StopService(ctx, req.(*emptypb.Empty))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_ReloadService_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).ReloadService(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_ReloadService_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).ReloadService(ctx, req.(*emptypb.Empty))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_SubscribeServiceStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(emptypb.Empty)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(StartedServiceServer).SubscribeServiceStatus(m, &grpc.GenericServerStream[emptypb.Empty, ServiceStatus]{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_SubscribeServiceStatusServer = grpc.ServerStreamingServer[ServiceStatus]
+
+func _StartedService_SubscribeLog_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(emptypb.Empty)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(StartedServiceServer).SubscribeLog(m, &grpc.GenericServerStream[emptypb.Empty, Log]{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_SubscribeLogServer = grpc.ServerStreamingServer[Log]
+
+func _StartedService_GetDefaultLogLevel_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).GetDefaultLogLevel(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_GetDefaultLogLevel_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).GetDefaultLogLevel(ctx, req.(*emptypb.Empty))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(SubscribeStatusRequest)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(StartedServiceServer).SubscribeStatus(m, &grpc.GenericServerStream[SubscribeStatusRequest, Status]{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_SubscribeStatusServer = grpc.ServerStreamingServer[Status]
+
+func _StartedService_SubscribeGroups_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(emptypb.Empty)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(StartedServiceServer).SubscribeGroups(m, &grpc.GenericServerStream[emptypb.Empty, Groups]{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_SubscribeGroupsServer = grpc.ServerStreamingServer[Groups]
+
+func _StartedService_GetClashModeStatus_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).GetClashModeStatus(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_GetClashModeStatus_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).GetClashModeStatus(ctx, req.(*emptypb.Empty))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_SubscribeClashMode_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(emptypb.Empty)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(StartedServiceServer).SubscribeClashMode(m, &grpc.GenericServerStream[emptypb.Empty, ClashMode]{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_SubscribeClashModeServer = grpc.ServerStreamingServer[ClashMode]
+
+func _StartedService_SetClashMode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ClashMode)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(StartedServiceServer).SetClashMode(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_SetClashMode_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).SetClashMode(ctx, req.(*ClashMode))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_URLTest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(URLTestRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(StartedServiceServer).URLTest(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_URLTest_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).URLTest(ctx, req.(*URLTestRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_SelectOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(SelectOutboundRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(StartedServiceServer).SelectOutbound(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_SelectOutbound_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).SelectOutbound(ctx, req.(*SelectOutboundRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_SetGroupExpand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(SetGroupExpandRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(StartedServiceServer).SetGroupExpand(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_SetGroupExpand_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).SetGroupExpand(ctx, req.(*SetGroupExpandRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_GetSystemProxyStatus_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).GetSystemProxyStatus(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_GetSystemProxyStatus_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).GetSystemProxyStatus(ctx, req.(*emptypb.Empty))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(SetSystemProxyEnabledRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(StartedServiceServer).SetSystemProxyEnabled(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_SetSystemProxyEnabled_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).SetSystemProxyEnabled(ctx, req.(*SetSystemProxyEnabledRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(SubscribeConnectionsRequest)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, Connections]{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_SubscribeConnectionsServer = grpc.ServerStreamingServer[Connections]
+
+func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(CloseConnectionRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(StartedServiceServer).CloseConnection(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_CloseConnection_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).CloseConnection(ctx, req.(*CloseConnectionRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_CloseAllConnections_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).CloseAllConnections(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_CloseAllConnections_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).CloseAllConnections(ctx, req.(*emptypb.Empty))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_GetDeprecatedWarnings_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).GetDeprecatedWarnings(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_GetDeprecatedWarnings_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).GetDeprecatedWarnings(ctx, req.(*emptypb.Empty))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _StartedService_SubscribeHelperEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(emptypb.Empty)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(StartedServiceServer).SubscribeHelperEvents(m, &grpc.GenericServerStream[emptypb.Empty, HelperRequest]{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_SubscribeHelperEventsServer = grpc.ServerStreamingServer[HelperRequest]
+
+func _StartedService_SendHelperResponse_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(HelperResponse)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(StartedServiceServer).SendHelperResponse(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StartedService_SendHelperResponse_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StartedServiceServer).SendHelperResponse(ctx, req.(*HelperResponse))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// 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)
+var StartedService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "daemon.StartedService",
+	HandlerType: (*StartedServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "StopService",
+			Handler:    _StartedService_StopService_Handler,
+		},
+		{
+			MethodName: "ReloadService",
+			Handler:    _StartedService_ReloadService_Handler,
+		},
+		{
+			MethodName: "GetDefaultLogLevel",
+			Handler:    _StartedService_GetDefaultLogLevel_Handler,
+		},
+		{
+			MethodName: "GetClashModeStatus",
+			Handler:    _StartedService_GetClashModeStatus_Handler,
+		},
+		{
+			MethodName: "SetClashMode",
+			Handler:    _StartedService_SetClashMode_Handler,
+		},
+		{
+			MethodName: "URLTest",
+			Handler:    _StartedService_URLTest_Handler,
+		},
+		{
+			MethodName: "SelectOutbound",
+			Handler:    _StartedService_SelectOutbound_Handler,
+		},
+		{
+			MethodName: "SetGroupExpand",
+			Handler:    _StartedService_SetGroupExpand_Handler,
+		},
+		{
+			MethodName: "GetSystemProxyStatus",
+			Handler:    _StartedService_GetSystemProxyStatus_Handler,
+		},
+		{
+			MethodName: "SetSystemProxyEnabled",
+			Handler:    _StartedService_SetSystemProxyEnabled_Handler,
+		},
+		{
+			MethodName: "CloseConnection",
+			Handler:    _StartedService_CloseConnection_Handler,
+		},
+		{
+			MethodName: "CloseAllConnections",
+			Handler:    _StartedService_CloseAllConnections_Handler,
+		},
+		{
+			MethodName: "GetDeprecatedWarnings",
+			Handler:    _StartedService_GetDeprecatedWarnings_Handler,
+		},
+		{
+			MethodName: "SendHelperResponse",
+			Handler:    _StartedService_SendHelperResponse_Handler,
+		},
+	},
+	Streams: []grpc.StreamDesc{
+		{
+			StreamName:    "SubscribeServiceStatus",
+			Handler:       _StartedService_SubscribeServiceStatus_Handler,
+			ServerStreams: true,
+		},
+		{
+			StreamName:    "SubscribeLog",
+			Handler:       _StartedService_SubscribeLog_Handler,
+			ServerStreams: true,
+		},
+		{
+			StreamName:    "SubscribeStatus",
+			Handler:       _StartedService_SubscribeStatus_Handler,
+			ServerStreams: true,
+		},
+		{
+			StreamName:    "SubscribeGroups",
+			Handler:       _StartedService_SubscribeGroups_Handler,
+			ServerStreams: true,
+		},
+		{
+			StreamName:    "SubscribeClashMode",
+			Handler:       _StartedService_SubscribeClashMode_Handler,
+			ServerStreams: true,
+		},
+		{
+			StreamName:    "SubscribeConnections",
+			Handler:       _StartedService_SubscribeConnections_Handler,
+			ServerStreams: true,
+		},
+		{
+			StreamName:    "SubscribeHelperEvents",
+			Handler:       _StartedService_SubscribeHelperEvents_Handler,
+			ServerStreams: true,
+		},
+	},
+	Metadata: "daemon/started_service.proto",
+}

+ 1 - 2
dns/router.go

@@ -10,7 +10,6 @@ import (
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	R "github.com/sagernet/sing-box/route/rule"
@@ -38,7 +37,7 @@ type Router struct {
 	rules                 []adapter.DNSRule
 	defaultDomainStrategy C.DomainStrategy
 	dnsReverseMapping     freelru.Cache[netip.Addr, string]
-	platformInterface     platform.Interface
+	platformInterface     adapter.PlatformInterface
 }
 
 func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {

+ 48 - 48
docs/configuration/dns/server/index.zh.md

@@ -1,48 +1,48 @@
----
-icon: material/alert-decagram
----
-
-!!! quote "sing-box 1.12.0 中的更改"
-
-    :material-plus: [type](#type)
-
-# DNS Server
-
-### 结构
-
-```json
-{
-  "dns": {
-    "servers": [
-      {
-        "type": "",
-        "tag": ""
-      }
-    ]
-  }
-}
-```
-
-#### type
-
-DNS 服务器的类型。
-
-| 类型              | 格式                        |
-|-----------------|---------------------------|
-| empty (default) | [Legacy](./legacy/)       |
-| `local`         | [Local](./local/)         |
-| `hosts`         | [Hosts](./hosts/)         |
-| `tcp`           | [TCP](./tcp/)             |
-| `udp`           | [UDP](./udp/)             |
-| `tls`           | [TLS](./tls/)             |
-| `quic`          | [QUIC](./quic/)           |
-| `https`         | [HTTPS](./https/)         |
-| `h3`            | [HTTP/3](./http3/)        |
-| `dhcp`          | [DHCP](./dhcp/)           |
-| `fakeip`        | [Fake IP](./fakeip/)      |
-| `tailscale`     | [Tailscale](./tailscale/) |
-| `resolved`      | [Resolved](./resolved/)   |
-
-#### tag
-
-DNS 服务器的标签。
+---
+icon: material/alert-decagram
+---
+
+!!! quote "sing-box 1.12.0 中的更改"
+
+    :material-plus: [type](#type)
+
+# DNS Server
+
+### 结构
+
+```json
+{
+  "dns": {
+    "servers": [
+      {
+        "type": "",
+        "tag": ""
+      }
+    ]
+  }
+}
+```
+
+#### type
+
+DNS 服务器的类型。
+
+| 类型              | 格式                        |
+|-----------------|---------------------------|
+| empty (default) | [Legacy](./legacy/)       |
+| `local`         | [Local](./local/)         |
+| `hosts`         | [Hosts](./hosts/)         |
+| `tcp`           | [TCP](./tcp/)             |
+| `udp`           | [UDP](./udp/)             |
+| `tls`           | [TLS](./tls/)             |
+| `quic`          | [QUIC](./quic/)           |
+| `https`         | [HTTPS](./https/)         |
+| `h3`            | [HTTP/3](./http3/)        |
+| `dhcp`          | [DHCP](./dhcp/)           |
+| `fakeip`        | [Fake IP](./fakeip/)      |
+| `tailscale`     | [Tailscale](./tailscale/) |
+| `resolved`      | [Resolved](./resolved/)   |
+
+#### tag
+
+DNS 服务器的标签。

+ 4 - 4
experimental/clashapi/trafficontrol/tracker.go

@@ -45,15 +45,15 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
 	if t.Metadata.ProcessInfo != nil {
 		if t.Metadata.ProcessInfo.ProcessPath != "" {
 			processPath = t.Metadata.ProcessInfo.ProcessPath
-		} else if t.Metadata.ProcessInfo.PackageName != "" {
-			processPath = t.Metadata.ProcessInfo.PackageName
+		} else if t.Metadata.ProcessInfo.AndroidPackageName != "" {
+			processPath = t.Metadata.ProcessInfo.AndroidPackageName
 		}
 		if processPath == "" {
 			if t.Metadata.ProcessInfo.UserId != -1 {
 				processPath = F.ToString(t.Metadata.ProcessInfo.UserId)
 			}
-		} else if t.Metadata.ProcessInfo.User != "" {
-			processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.User, ")")
+		} else if t.Metadata.ProcessInfo.UserName != "" {
+			processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserName, ")")
 		} else if t.Metadata.ProcessInfo.UserId != -1 {
 			processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")")
 		}

+ 0 - 11
experimental/libbox/command.go

@@ -3,18 +3,7 @@ package libbox
 const (
 	CommandLog int32 = iota
 	CommandStatus
-	CommandServiceReload
-	CommandServiceClose
-	CommandCloseConnections
 	CommandGroup
-	CommandSelectOutbound
-	CommandURLTest
-	CommandGroupExpand
 	CommandClashMode
-	CommandSetClashMode
-	CommandGetSystemProxyStatus
-	CommandSetSystemProxyEnabled
 	CommandConnections
-	CommandCloseConnection
-	CommandGetDeprecatedNotes
 )

+ 0 - 124
experimental/libbox/command_clash_mode.go

@@ -1,124 +0,0 @@
-package libbox
-
-import (
-	"encoding/binary"
-	"io"
-	"net"
-	"time"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/experimental/clashapi"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/varbin"
-)
-
-func (c *CommandClient) SetClashMode(newMode string) error {
-	conn, err := c.directConnect()
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandSetClashMode))
-	if err != nil {
-		return err
-	}
-	err = varbin.Write(conn, binary.BigEndian, newMode)
-	if err != nil {
-		return err
-	}
-	return readError(conn)
-}
-
-func (s *CommandServer) handleSetClashMode(conn net.Conn) error {
-	newMode, err := varbin.ReadValue[string](conn, binary.BigEndian)
-	if err != nil {
-		return err
-	}
-	service := s.service
-	if service == nil {
-		return writeError(conn, E.New("service not ready"))
-	}
-	service.clashServer.(*clashapi.Server).SetMode(newMode)
-	return writeError(conn, nil)
-}
-
-func (c *CommandClient) handleModeConn(conn net.Conn) {
-	defer conn.Close()
-
-	for {
-		newMode, err := varbin.ReadValue[string](conn, binary.BigEndian)
-		if err != nil {
-			c.handler.Disconnected(err.Error())
-			return
-		}
-		c.handler.UpdateClashMode(newMode)
-	}
-}
-
-func (s *CommandServer) handleModeConn(conn net.Conn) error {
-	ctx := connKeepAlive(conn)
-	for s.service == nil {
-		select {
-		case <-time.After(time.Second):
-			continue
-		case <-ctx.Done():
-			return ctx.Err()
-		}
-	}
-	err := writeClashModeList(conn, s.service.clashServer)
-	if err != nil {
-		return err
-	}
-	for {
-		select {
-		case <-s.modeUpdate:
-			err = varbin.Write(conn, binary.BigEndian, s.service.clashServer.Mode())
-			if err != nil {
-				return err
-			}
-		case <-ctx.Done():
-			return ctx.Err()
-		}
-	}
-}
-
-func readClashModeList(reader io.Reader) (modeList []string, currentMode string, err error) {
-	var modeListLength uint16
-	err = binary.Read(reader, binary.BigEndian, &modeListLength)
-	if err != nil {
-		return
-	}
-	if modeListLength == 0 {
-		return
-	}
-	modeList = make([]string, modeListLength)
-	for i := 0; i < int(modeListLength); i++ {
-		modeList[i], err = varbin.ReadValue[string](reader, binary.BigEndian)
-		if err != nil {
-			return
-		}
-	}
-	currentMode, err = varbin.ReadValue[string](reader, binary.BigEndian)
-	return
-}
-
-func writeClashModeList(writer io.Writer, clashServer adapter.ClashServer) error {
-	modeList := clashServer.ModeList()
-	err := binary.Write(writer, binary.BigEndian, uint16(len(modeList)))
-	if err != nil {
-		return err
-	}
-	if len(modeList) > 0 {
-		for _, mode := range modeList {
-			err = varbin.Write(writer, binary.BigEndian, mode)
-			if err != nil {
-				return err
-			}
-		}
-		err = varbin.Write(writer, binary.BigEndian, clashServer.Mode())
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}

+ 380 - 69
experimental/libbox/command_client.go

@@ -1,32 +1,46 @@
 package libbox
 
 import (
-	"encoding/binary"
+	"context"
 	"net"
 	"os"
 	"path/filepath"
+	"strconv"
+	"sync"
 	"time"
 
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/daemon"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
+
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/protobuf/types/known/emptypb"
 )
 
 type CommandClient struct {
-	handler CommandClientHandler
-	conn    net.Conn
-	options CommandClientOptions
+	handler     CommandClientHandler
+	grpcConn    *grpc.ClientConn
+	grpcClient  daemon.StartedServiceClient
+	options     CommandClientOptions
+	ctx         context.Context
+	cancel      context.CancelFunc
+	clientMutex sync.RWMutex
 }
 
 type CommandClientOptions struct {
 	Command        int32
+	Commands       Int32Iterator
 	StatusInterval int64
 }
 
 type CommandClientHandler interface {
 	Connected()
 	Disconnected(message string)
+	SetDefaultLogLevel(level int32)
 	ClearLogs()
-	WriteLogs(messageList StringIterator)
+	WriteLogs(messageList LogIterator)
 	WriteStatus(message *StatusMessage)
 	WriteGroups(message OutboundGroupIterator)
 	InitializeClashMode(modeList StringIterator, currentMode string)
@@ -34,6 +48,17 @@ type CommandClientHandler interface {
 	WriteConnections(message *Connections)
 }
 
+type LogEntry struct {
+	Level   int32
+	Message string
+}
+
+type LogIterator interface {
+	Len() int32
+	HasNext() bool
+	Next() *LogEntry
+}
+
 func NewStandaloneCommandClient() *CommandClient {
 	return new(CommandClient)
 }
@@ -45,24 +70,24 @@ func NewCommandClient(handler CommandClientHandler, options *CommandClientOption
 	}
 }
 
-func (c *CommandClient) directConnect() (net.Conn, error) {
-	if !sTVOS {
-		return net.DialUnix("unix", nil, &net.UnixAddr{
-			Name: filepath.Join(sBasePath, "command.sock"),
-			Net:  "unix",
-		})
+func (c *CommandClient) grpcDial() (*grpc.ClientConn, error) {
+	var target string
+	if C.IsDarwin {
+		port := sCommandServerListenPort
+		if port == 0 {
+			port = 8964
+		}
+		target = net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port)))
 	} else {
-		return net.Dial("tcp", "127.0.0.1:8964")
+		target = "unix://" + filepath.Join(sBasePath, "command.sock")
 	}
-}
 
-func (c *CommandClient) directConnectWithRetry() (net.Conn, error) {
 	var (
-		conn net.Conn
+		conn *grpc.ClientConn
 		err  error
 	)
 	for i := 0; i < 10; i++ {
-		conn, err = c.directConnect()
+		conn, err = grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
 		if err == nil {
 			return conn, nil
 		}
@@ -72,79 +97,365 @@ func (c *CommandClient) directConnectWithRetry() (net.Conn, error) {
 }
 
 func (c *CommandClient) Connect() error {
-	common.Close(c.conn)
-	conn, err := c.directConnectWithRetry()
+	c.clientMutex.Lock()
+	common.Close(common.PtrOrNil(c.grpcConn))
+
+	conn, err := c.grpcDial()
 	if err != nil {
+		c.clientMutex.Unlock()
 		return err
 	}
-	c.conn = conn
-	err = binary.Write(conn, binary.BigEndian, uint8(c.options.Command))
+	c.grpcConn = conn
+	c.grpcClient = daemon.NewStartedServiceClient(conn)
+	c.ctx, c.cancel = context.WithCancel(context.Background())
+	c.clientMutex.Unlock()
+
+	var commands []int32
+	if c.options.Commands != nil {
+		commands = iteratorToArray[int32](c.options.Commands)
+	} else {
+		commands = []int32{c.options.Command}
+	}
+	c.handler.Connected()
+	for _, command := range commands {
+		switch command {
+		case CommandLog:
+			go c.handleLogStream()
+		case CommandStatus:
+			go c.handleStatusStream()
+		case CommandGroup:
+			go c.handleGroupStream()
+		case CommandClashMode:
+			go c.handleClashModeStream()
+		case CommandConnections:
+			go c.handleConnectionsStream()
+		default:
+			return E.New("unknown command: ", command)
+		}
+	}
+	return nil
+}
+
+func (c *CommandClient) Disconnect() error {
+	c.clientMutex.Lock()
+	defer c.clientMutex.Unlock()
+	if c.cancel != nil {
+		c.cancel()
+	}
+	return common.Close(common.PtrOrNil(c.grpcConn))
+}
+
+func (c *CommandClient) getClientForCall() (daemon.StartedServiceClient, error) {
+	c.clientMutex.RLock()
+	if c.grpcClient != nil {
+		defer c.clientMutex.RUnlock()
+		return c.grpcClient, nil
+	}
+	c.clientMutex.RUnlock()
+
+	c.clientMutex.Lock()
+	defer c.clientMutex.Unlock()
+
+	if c.grpcClient != nil {
+		return c.grpcClient, nil
+	}
+
+	conn, err := c.grpcDial()
 	if err != nil {
-		return err
+		return nil, err
 	}
-	switch c.options.Command {
-	case CommandLog:
-		err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
+	c.grpcConn = conn
+	c.grpcClient = daemon.NewStartedServiceClient(conn)
+	if c.ctx == nil {
+		c.ctx, c.cancel = context.WithCancel(context.Background())
+	}
+	return c.grpcClient, nil
+}
+
+func (c *CommandClient) getStreamContext() (daemon.StartedServiceClient, context.Context) {
+	c.clientMutex.RLock()
+	defer c.clientMutex.RUnlock()
+	return c.grpcClient, c.ctx
+}
+
+func (c *CommandClient) handleLogStream() {
+	client, ctx := c.getStreamContext()
+	stream, err := client.SubscribeLog(ctx, &emptypb.Empty{})
+	if err != nil {
+		c.handler.Disconnected(err.Error())
+		return
+	}
+	defaultLogLevel, err := client.GetDefaultLogLevel(ctx, &emptypb.Empty{})
+	if err != nil {
+		c.handler.Disconnected(err.Error())
+		return
+	}
+	c.handler.SetDefaultLogLevel(int32(defaultLogLevel.Level))
+	for {
+		logMessage, err := stream.Recv()
 		if err != nil {
-			return E.Cause(err, "write interval")
+			c.handler.Disconnected(err.Error())
+			return
 		}
-		c.handler.Connected()
-		go c.handleLogConn(conn)
-	case CommandStatus:
-		err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
-		if err != nil {
-			return E.Cause(err, "write interval")
+		if logMessage.Reset_ {
+			c.handler.ClearLogs()
 		}
-		c.handler.Connected()
-		go c.handleStatusConn(conn)
-	case CommandGroup:
-		err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
+		var messages []*LogEntry
+		for _, msg := range logMessage.Messages {
+			messages = append(messages, &LogEntry{
+				Level:   int32(msg.Level),
+				Message: msg.Message,
+			})
+		}
+		c.handler.WriteLogs(newIterator(messages))
+	}
+}
+
+func (c *CommandClient) handleStatusStream() {
+	client, ctx := c.getStreamContext()
+	interval := c.options.StatusInterval
+
+	stream, err := client.SubscribeStatus(ctx, &daemon.SubscribeStatusRequest{
+		Interval: interval,
+	})
+	if err != nil {
+		c.handler.Disconnected(err.Error())
+		return
+	}
+
+	for {
+		status, err := stream.Recv()
 		if err != nil {
-			return E.Cause(err, "write interval")
+			c.handler.Disconnected(err.Error())
+			return
 		}
-		c.handler.Connected()
-		go c.handleGroupConn(conn)
-	case CommandClashMode:
-		var (
-			modeList    []string
-			currentMode string
-		)
-		modeList, currentMode, err = readClashModeList(conn)
+		c.handler.WriteStatus(StatusMessageFromGRPC(status))
+	}
+}
+
+func (c *CommandClient) handleGroupStream() {
+	client, ctx := c.getStreamContext()
+
+	stream, err := client.SubscribeGroups(ctx, &emptypb.Empty{})
+	if err != nil {
+		c.handler.Disconnected(err.Error())
+		return
+	}
+
+	for {
+		groups, err := stream.Recv()
 		if err != nil {
-			return err
+			c.handler.Disconnected(err.Error())
+			return
 		}
-		if sFixAndroidStack {
-			go func() {
-				c.handler.Connected()
-				c.handler.InitializeClashMode(newIterator(modeList), currentMode)
-				if len(modeList) == 0 {
-					conn.Close()
-					c.handler.Disconnected(os.ErrInvalid.Error())
-				}
-			}()
-		} else {
+		c.handler.WriteGroups(OutboundGroupIteratorFromGRPC(groups))
+	}
+}
+
+func (c *CommandClient) handleClashModeStream() {
+	client, ctx := c.getStreamContext()
+
+	modeStatus, err := client.GetClashModeStatus(ctx, &emptypb.Empty{})
+	if err != nil {
+		c.handler.Disconnected(err.Error())
+		return
+	}
+
+	if sFixAndroidStack {
+		go func() {
 			c.handler.Connected()
-			c.handler.InitializeClashMode(newIterator(modeList), currentMode)
-			if len(modeList) == 0 {
-				conn.Close()
+			c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
+			if len(modeStatus.ModeList) == 0 {
 				c.handler.Disconnected(os.ErrInvalid.Error())
 			}
+		}()
+	} else {
+		c.handler.Connected()
+		c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
+		if len(modeStatus.ModeList) == 0 {
+			c.handler.Disconnected(os.ErrInvalid.Error())
+			return
 		}
-		if len(modeList) == 0 {
-			return nil
+	}
+
+	if len(modeStatus.ModeList) == 0 {
+		return
+	}
+
+	stream, err := client.SubscribeClashMode(ctx, &emptypb.Empty{})
+	if err != nil {
+		c.handler.Disconnected(err.Error())
+		return
+	}
+
+	for {
+		mode, err := stream.Recv()
+		if err != nil {
+			c.handler.Disconnected(err.Error())
+			return
 		}
-		go c.handleModeConn(conn)
-	case CommandConnections:
-		err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
+		c.handler.UpdateClashMode(mode.Mode)
+	}
+}
+
+func (c *CommandClient) handleConnectionsStream() {
+	client, ctx := c.getStreamContext()
+	interval := c.options.StatusInterval
+
+	stream, err := client.SubscribeConnections(ctx, &daemon.SubscribeConnectionsRequest{
+		Interval: interval,
+	})
+	if err != nil {
+		c.handler.Disconnected(err.Error())
+		return
+	}
+
+	var connections Connections
+	for {
+		conns, err := stream.Recv()
 		if err != nil {
-			return E.Cause(err, "write interval")
+			c.handler.Disconnected(err.Error())
+			return
 		}
-		c.handler.Connected()
-		go c.handleConnectionsConn(conn)
+		connections.input = ConnectionsFromGRPC(conns)
+		c.handler.WriteConnections(&connections)
 	}
-	return nil
 }
 
-func (c *CommandClient) Disconnect() error {
-	return common.Close(c.conn)
+func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return err
+	}
+
+	_, err = client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{
+		GroupTag:    groupTag,
+		OutboundTag: outboundTag,
+	})
+	return err
+}
+
+func (c *CommandClient) URLTest(groupTag string) error {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return err
+	}
+
+	_, err = client.URLTest(context.Background(), &daemon.URLTestRequest{
+		OutboundTag: groupTag,
+	})
+	return err
+}
+
+func (c *CommandClient) SetClashMode(newMode string) error {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return err
+	}
+
+	_, err = client.SetClashMode(context.Background(), &daemon.ClashMode{
+		Mode: newMode,
+	})
+	return err
+}
+
+func (c *CommandClient) CloseConnection(connId string) error {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return err
+	}
+
+	_, err = client.CloseConnection(context.Background(), &daemon.CloseConnectionRequest{
+		Id: connId,
+	})
+	return err
+}
+
+func (c *CommandClient) CloseConnections() error {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return err
+	}
+
+	_, err = client.CloseAllConnections(context.Background(), &emptypb.Empty{})
+	return err
+}
+
+func (c *CommandClient) ServiceReload() error {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return err
+	}
+
+	_, err = client.ReloadService(context.Background(), &emptypb.Empty{})
+	return err
+}
+
+func (c *CommandClient) ServiceClose() error {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return err
+	}
+
+	_, err = client.StopService(context.Background(), &emptypb.Empty{})
+	return err
+}
+
+func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return nil, err
+	}
+
+	status, err := client.GetSystemProxyStatus(context.Background(), &emptypb.Empty{})
+	if err != nil {
+		return nil, err
+	}
+	return SystemProxyStatusFromGRPC(status), nil
+}
+
+func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return err
+	}
+
+	_, err = client.SetSystemProxyEnabled(context.Background(), &daemon.SetSystemProxyEnabledRequest{
+		Enabled: isEnabled,
+	})
+	return err
+}
+
+func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return nil, err
+	}
+
+	warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{})
+	if err != nil {
+		return nil, err
+	}
+
+	var notes []*DeprecatedNote
+	for _, warning := range warnings.Warnings {
+		notes = append(notes, &DeprecatedNote{
+			Description:   warning.Message,
+			MigrationLink: warning.MigrationLink,
+		})
+	}
+	return newIterator(notes), nil
+}
+
+func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
+	client, err := c.getClientForCall()
+	if err != nil {
+		return err
+	}
+
+	_, err = client.SetGroupExpand(context.Background(), &daemon.SetGroupExpandRequest{
+		GroupTag: groupTag,
+		IsExpand: isExpand,
+	})
+	return err
 }

+ 0 - 54
experimental/libbox/command_close_connection.go

@@ -1,54 +0,0 @@
-package libbox
-
-import (
-	"bufio"
-	"net"
-
-	"github.com/sagernet/sing-box/experimental/clashapi"
-	"github.com/sagernet/sing/common/binary"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/varbin"
-
-	"github.com/gofrs/uuid/v5"
-)
-
-func (c *CommandClient) CloseConnection(connId string) error {
-	conn, err := c.directConnect()
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandCloseConnection))
-	if err != nil {
-		return err
-	}
-	writer := bufio.NewWriter(conn)
-	err = varbin.Write(writer, binary.BigEndian, connId)
-	if err != nil {
-		return err
-	}
-	err = writer.Flush()
-	if err != nil {
-		return err
-	}
-	return readError(conn)
-}
-
-func (s *CommandServer) handleCloseConnection(conn net.Conn) error {
-	reader := bufio.NewReader(conn)
-	var connId string
-	err := varbin.Read(reader, binary.BigEndian, &connId)
-	if err != nil {
-		return E.Cause(err, "read connection id")
-	}
-	service := s.service
-	if service == nil {
-		return writeError(conn, E.New("service not ready"))
-	}
-	targetConn := service.clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId))
-	if targetConn == nil {
-		return writeError(conn, E.New("connection already closed"))
-	}
-	targetConn.Close()
-	return writeError(conn, nil)
-}

+ 0 - 269
experimental/libbox/command_connections.go

@@ -1,269 +0,0 @@
-package libbox
-
-import (
-	"bufio"
-	"net"
-	"slices"
-	"strings"
-	"time"
-
-	"github.com/sagernet/sing-box/experimental/clashapi"
-	"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
-	"github.com/sagernet/sing/common/binary"
-	E "github.com/sagernet/sing/common/exceptions"
-	M "github.com/sagernet/sing/common/metadata"
-	"github.com/sagernet/sing/common/varbin"
-
-	"github.com/gofrs/uuid/v5"
-)
-
-func (c *CommandClient) handleConnectionsConn(conn net.Conn) {
-	defer conn.Close()
-	reader := bufio.NewReader(conn)
-	var (
-		rawConnections []Connection
-		connections    Connections
-	)
-	for {
-		rawConnections = nil
-		err := varbin.Read(reader, binary.BigEndian, &rawConnections)
-		if err != nil {
-			c.handler.Disconnected(err.Error())
-			return
-		}
-		connections.input = rawConnections
-		c.handler.WriteConnections(&connections)
-	}
-}
-
-func (s *CommandServer) handleConnectionsConn(conn net.Conn) error {
-	var interval int64
-	err := binary.Read(conn, binary.BigEndian, &interval)
-	if err != nil {
-		return E.Cause(err, "read interval")
-	}
-	ticker := time.NewTicker(time.Duration(interval))
-	defer ticker.Stop()
-	ctx := connKeepAlive(conn)
-	var trafficManager *trafficontrol.Manager
-	for {
-		service := s.service
-		if service != nil {
-			trafficManager = service.clashServer.(*clashapi.Server).TrafficManager()
-			break
-		}
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-ticker.C:
-		}
-	}
-	var (
-		connections    = make(map[uuid.UUID]*Connection)
-		outConnections []Connection
-	)
-	writer := bufio.NewWriter(conn)
-	for {
-		outConnections = outConnections[:0]
-		for _, connection := range trafficManager.Connections() {
-			outConnections = append(outConnections, newConnection(connections, connection, false))
-		}
-		for _, connection := range trafficManager.ClosedConnections() {
-			outConnections = append(outConnections, newConnection(connections, connection, true))
-		}
-		err = varbin.Write(writer, binary.BigEndian, outConnections)
-		if err != nil {
-			return err
-		}
-		err = writer.Flush()
-		if err != nil {
-			return err
-		}
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-ticker.C:
-		}
-	}
-}
-
-const (
-	ConnectionStateAll = iota
-	ConnectionStateActive
-	ConnectionStateClosed
-)
-
-type Connections struct {
-	input    []Connection
-	filtered []Connection
-}
-
-func (c *Connections) FilterState(state int32) {
-	c.filtered = c.filtered[:0]
-	switch state {
-	case ConnectionStateAll:
-		c.filtered = append(c.filtered, c.input...)
-	case ConnectionStateActive:
-		for _, connection := range c.input {
-			if connection.ClosedAt == 0 {
-				c.filtered = append(c.filtered, connection)
-			}
-		}
-	case ConnectionStateClosed:
-		for _, connection := range c.input {
-			if connection.ClosedAt != 0 {
-				c.filtered = append(c.filtered, connection)
-			}
-		}
-	}
-}
-
-func (c *Connections) SortByDate() {
-	slices.SortStableFunc(c.filtered, func(x, y Connection) int {
-		if x.CreatedAt < y.CreatedAt {
-			return 1
-		} else if x.CreatedAt > y.CreatedAt {
-			return -1
-		} else {
-			return strings.Compare(y.ID, x.ID)
-		}
-	})
-}
-
-func (c *Connections) SortByTraffic() {
-	slices.SortStableFunc(c.filtered, func(x, y Connection) int {
-		xTraffic := x.Uplink + x.Downlink
-		yTraffic := y.Uplink + y.Downlink
-		if xTraffic < yTraffic {
-			return 1
-		} else if xTraffic > yTraffic {
-			return -1
-		} else {
-			return strings.Compare(y.ID, x.ID)
-		}
-	})
-}
-
-func (c *Connections) SortByTrafficTotal() {
-	slices.SortStableFunc(c.filtered, func(x, y Connection) int {
-		xTraffic := x.UplinkTotal + x.DownlinkTotal
-		yTraffic := y.UplinkTotal + y.DownlinkTotal
-		if xTraffic < yTraffic {
-			return 1
-		} else if xTraffic > yTraffic {
-			return -1
-		} else {
-			return strings.Compare(y.ID, x.ID)
-		}
-	})
-}
-
-func (c *Connections) Iterator() ConnectionIterator {
-	return newPtrIterator(c.filtered)
-}
-
-type Connection struct {
-	ID            string
-	Inbound       string
-	InboundType   string
-	IPVersion     int32
-	Network       string
-	Source        string
-	Destination   string
-	Domain        string
-	Protocol      string
-	User          string
-	FromOutbound  string
-	CreatedAt     int64
-	ClosedAt      int64
-	Uplink        int64
-	Downlink      int64
-	UplinkTotal   int64
-	DownlinkTotal int64
-	Rule          string
-	Outbound      string
-	OutboundType  string
-	ChainList     []string
-}
-
-func (c *Connection) Chain() StringIterator {
-	return newIterator(c.ChainList)
-}
-
-func (c *Connection) DisplayDestination() string {
-	destination := M.ParseSocksaddr(c.Destination)
-	if destination.IsIP() && c.Domain != "" {
-		destination = M.Socksaddr{
-			Fqdn: c.Domain,
-			Port: destination.Port,
-		}
-		return destination.String()
-	}
-	return c.Destination
-}
-
-type ConnectionIterator interface {
-	Next() *Connection
-	HasNext() bool
-}
-
-func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) Connection {
-	if oldConnection, loaded := connections[metadata.ID]; loaded {
-		if isClosed {
-			if oldConnection.ClosedAt == 0 {
-				oldConnection.Uplink = 0
-				oldConnection.Downlink = 0
-				oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
-			}
-			return *oldConnection
-		}
-		lastUplink := oldConnection.UplinkTotal
-		lastDownlink := oldConnection.DownlinkTotal
-		uplinkTotal := metadata.Upload.Load()
-		downlinkTotal := metadata.Download.Load()
-		oldConnection.Uplink = uplinkTotal - lastUplink
-		oldConnection.Downlink = downlinkTotal - lastDownlink
-		oldConnection.UplinkTotal = uplinkTotal
-		oldConnection.DownlinkTotal = downlinkTotal
-		return *oldConnection
-	}
-	var rule string
-	if metadata.Rule != nil {
-		rule = metadata.Rule.String()
-	}
-	uplinkTotal := metadata.Upload.Load()
-	downlinkTotal := metadata.Download.Load()
-	uplink := uplinkTotal
-	downlink := downlinkTotal
-	var closedAt int64
-	if !metadata.ClosedAt.IsZero() {
-		closedAt = metadata.ClosedAt.UnixMilli()
-		uplink = 0
-		downlink = 0
-	}
-	connection := Connection{
-		ID:            metadata.ID.String(),
-		Inbound:       metadata.Metadata.Inbound,
-		InboundType:   metadata.Metadata.InboundType,
-		IPVersion:     int32(metadata.Metadata.IPVersion),
-		Network:       metadata.Metadata.Network,
-		Source:        metadata.Metadata.Source.String(),
-		Destination:   metadata.Metadata.Destination.String(),
-		Domain:        metadata.Metadata.Domain,
-		Protocol:      metadata.Metadata.Protocol,
-		User:          metadata.Metadata.User,
-		FromOutbound:  metadata.Metadata.Outbound,
-		CreatedAt:     metadata.CreatedAt.UnixMilli(),
-		ClosedAt:      closedAt,
-		Uplink:        uplink,
-		Downlink:      downlink,
-		UplinkTotal:   uplinkTotal,
-		DownlinkTotal: downlinkTotal,
-		Rule:          rule,
-		Outbound:      metadata.Outbound,
-		OutboundType:  metadata.OutboundType,
-		ChainList:     metadata.Chain,
-	}
-	connections[metadata.ID] = &connection
-	return connection
-}

+ 0 - 28
experimental/libbox/command_conntrack.go

@@ -1,28 +0,0 @@
-package libbox
-
-import (
-	"encoding/binary"
-	"net"
-	runtimeDebug "runtime/debug"
-	"time"
-
-	"github.com/sagernet/sing-box/common/conntrack"
-)
-
-func (c *CommandClient) CloseConnections() error {
-	conn, err := c.directConnect()
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	return binary.Write(conn, binary.BigEndian, uint8(CommandCloseConnections))
-}
-
-func (s *CommandServer) handleCloseConnections(conn net.Conn) error {
-	conntrack.Close()
-	go func() {
-		time.Sleep(time.Second)
-		runtimeDebug.FreeOSMemory()
-	}()
-	return nil
-}

+ 0 - 46
experimental/libbox/command_deprecated_report.go

@@ -1,46 +0,0 @@
-package libbox
-
-import (
-	"encoding/binary"
-	"net"
-
-	"github.com/sagernet/sing-box/experimental/deprecated"
-	"github.com/sagernet/sing/common"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/varbin"
-	"github.com/sagernet/sing/service"
-)
-
-func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) {
-	conn, err := c.directConnect()
-	if err != nil {
-		return nil, err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandGetDeprecatedNotes))
-	if err != nil {
-		return nil, err
-	}
-	err = readError(conn)
-	if err != nil {
-		return nil, err
-	}
-	var features []deprecated.Note
-	err = varbin.Read(conn, binary.BigEndian, &features)
-	if err != nil {
-		return nil, err
-	}
-	return newIterator(common.Map(features, func(it deprecated.Note) *DeprecatedNote { return (*DeprecatedNote)(&it) })), nil
-}
-
-func (s *CommandServer) handleGetDeprecatedNotes(conn net.Conn) error {
-	boxService := s.service
-	if boxService == nil {
-		return writeError(conn, E.New("service not ready"))
-	}
-	err := writeError(conn, nil)
-	if err != nil {
-		return err
-	}
-	return varbin.Write(conn, binary.BigEndian, service.FromContext[deprecated.Manager](boxService.ctx).(*deprecatedManager).Get())
-}

+ 0 - 198
experimental/libbox/command_group.go

@@ -1,198 +0,0 @@
-package libbox
-
-import (
-	"bufio"
-	"encoding/binary"
-	"io"
-	"net"
-	"time"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/urltest"
-	"github.com/sagernet/sing-box/protocol/group"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/varbin"
-	"github.com/sagernet/sing/service"
-)
-
-func (c *CommandClient) handleGroupConn(conn net.Conn) {
-	defer conn.Close()
-
-	for {
-		groups, err := readGroups(conn)
-		if err != nil {
-			c.handler.Disconnected(err.Error())
-			return
-		}
-		c.handler.WriteGroups(groups)
-	}
-}
-
-func (s *CommandServer) handleGroupConn(conn net.Conn) error {
-	var interval int64
-	err := binary.Read(conn, binary.BigEndian, &interval)
-	if err != nil {
-		return E.Cause(err, "read interval")
-	}
-	ticker := time.NewTicker(time.Duration(interval))
-	defer ticker.Stop()
-	ctx := connKeepAlive(conn)
-	writer := bufio.NewWriter(conn)
-	for {
-		service := s.service
-		if service != nil {
-			err = writeGroups(writer, service)
-			if err != nil {
-				return err
-			}
-		} else {
-			err = binary.Write(writer, binary.BigEndian, uint16(0))
-			if err != nil {
-				return err
-			}
-		}
-		err = writer.Flush()
-		if err != nil {
-			return err
-		}
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-ticker.C:
-		}
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-s.urlTestUpdate:
-		}
-	}
-}
-
-type OutboundGroup struct {
-	Tag        string
-	Type       string
-	Selectable bool
-	Selected   string
-	IsExpand   bool
-	ItemList   []*OutboundGroupItem
-}
-
-func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
-	return newIterator(g.ItemList)
-}
-
-type OutboundGroupIterator interface {
-	Next() *OutboundGroup
-	HasNext() bool
-}
-
-type OutboundGroupItem struct {
-	Tag          string
-	Type         string
-	URLTestTime  int64
-	URLTestDelay int32
-}
-
-type OutboundGroupItemIterator interface {
-	Next() *OutboundGroupItem
-	HasNext() bool
-}
-
-func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
-	groups, err := varbin.ReadValue[[]*OutboundGroup](reader, binary.BigEndian)
-	if err != nil {
-		return nil, err
-	}
-	return newIterator(groups), nil
-}
-
-func writeGroups(writer io.Writer, boxService *BoxService) error {
-	historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx)
-	cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx)
-	outbounds := boxService.instance.Outbound().Outbounds()
-	var iGroups []adapter.OutboundGroup
-	for _, it := range outbounds {
-		if group, isGroup := it.(adapter.OutboundGroup); isGroup {
-			iGroups = append(iGroups, group)
-		}
-	}
-	var groups []OutboundGroup
-	for _, iGroup := range iGroups {
-		var outboundGroup OutboundGroup
-		outboundGroup.Tag = iGroup.Tag()
-		outboundGroup.Type = iGroup.Type()
-		_, outboundGroup.Selectable = iGroup.(*group.Selector)
-		outboundGroup.Selected = iGroup.Now()
-		if cacheFile != nil {
-			if isExpand, loaded := cacheFile.LoadGroupExpand(outboundGroup.Tag); loaded {
-				outboundGroup.IsExpand = isExpand
-			}
-		}
-
-		for _, itemTag := range iGroup.All() {
-			itemOutbound, isLoaded := boxService.instance.Outbound().Outbound(itemTag)
-			if !isLoaded {
-				continue
-			}
-
-			var item OutboundGroupItem
-			item.Tag = itemTag
-			item.Type = itemOutbound.Type()
-			if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(itemOutbound)); history != nil {
-				item.URLTestTime = history.Time.Unix()
-				item.URLTestDelay = int32(history.Delay)
-			}
-			outboundGroup.ItemList = append(outboundGroup.ItemList, &item)
-		}
-		if len(outboundGroup.ItemList) < 2 {
-			continue
-		}
-		groups = append(groups, outboundGroup)
-	}
-	return varbin.Write(writer, binary.BigEndian, groups)
-}
-
-func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
-	conn, err := c.directConnect()
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandGroupExpand))
-	if err != nil {
-		return err
-	}
-	err = varbin.Write(conn, binary.BigEndian, groupTag)
-	if err != nil {
-		return err
-	}
-	err = binary.Write(conn, binary.BigEndian, isExpand)
-	if err != nil {
-		return err
-	}
-	return readError(conn)
-}
-
-func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error {
-	groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
-	if err != nil {
-		return err
-	}
-	var isExpand bool
-	err = binary.Read(conn, binary.BigEndian, &isExpand)
-	if err != nil {
-		return err
-	}
-	serviceNow := s.service
-	if serviceNow == nil {
-		return writeError(conn, E.New("service not ready"))
-	}
-	cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx)
-	if cacheFile != nil {
-		err = cacheFile.StoreGroupExpand(groupTag, isExpand)
-		if err != nil {
-			return writeError(conn, err)
-		}
-	}
-	return writeError(conn, nil)
-}

+ 0 - 160
experimental/libbox/command_log.go

@@ -1,160 +0,0 @@
-package libbox
-
-import (
-	"bufio"
-	"context"
-	"io"
-	"net"
-	"time"
-
-	"github.com/sagernet/sing/common/binary"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/varbin"
-)
-
-func (s *CommandServer) ResetLog() {
-	s.access.Lock()
-	defer s.access.Unlock()
-	s.savedLines.Init()
-	select {
-	case s.logReset <- struct{}{}:
-	default:
-	}
-}
-
-func (s *CommandServer) WriteMessage(message string) {
-	s.subscriber.Emit(message)
-	s.access.Lock()
-	s.savedLines.PushBack(message)
-	if s.savedLines.Len() > s.maxLines {
-		s.savedLines.Remove(s.savedLines.Front())
-	}
-	s.access.Unlock()
-}
-
-func (s *CommandServer) handleLogConn(conn net.Conn) error {
-	var (
-		interval int64
-		timer    *time.Timer
-	)
-	err := binary.Read(conn, binary.BigEndian, &interval)
-	if err != nil {
-		return E.Cause(err, "read interval")
-	}
-	timer = time.NewTimer(time.Duration(interval))
-	if !timer.Stop() {
-		<-timer.C
-	}
-	var savedLines []string
-	s.access.Lock()
-	savedLines = make([]string, 0, s.savedLines.Len())
-	for element := s.savedLines.Front(); element != nil; element = element.Next() {
-		savedLines = append(savedLines, element.Value)
-	}
-	s.access.Unlock()
-	subscription, done, err := s.observer.Subscribe()
-	if err != nil {
-		return err
-	}
-	defer s.observer.UnSubscribe(subscription)
-	writer := bufio.NewWriter(conn)
-	select {
-	case <-s.logReset:
-		err = writer.WriteByte(1)
-		if err != nil {
-			return err
-		}
-		err = writer.Flush()
-		if err != nil {
-			return err
-		}
-	default:
-	}
-	if len(savedLines) > 0 {
-		err = writer.WriteByte(0)
-		if err != nil {
-			return err
-		}
-		err = varbin.Write(writer, binary.BigEndian, savedLines)
-		if err != nil {
-			return err
-		}
-	}
-	ctx := connKeepAlive(conn)
-	var logLines []string
-	for {
-		err = writer.Flush()
-		if err != nil {
-			return err
-		}
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-s.logReset:
-			err = writer.WriteByte(1)
-			if err != nil {
-				return err
-			}
-		case <-done:
-			return nil
-		case logLine := <-subscription:
-			logLines = logLines[:0]
-			logLines = append(logLines, logLine)
-			timer.Reset(time.Duration(interval))
-		loopLogs:
-			for {
-				select {
-				case logLine = <-subscription:
-					logLines = append(logLines, logLine)
-				case <-timer.C:
-					break loopLogs
-				}
-			}
-			err = writer.WriteByte(0)
-			if err != nil {
-				return err
-			}
-			err = varbin.Write(writer, binary.BigEndian, logLines)
-			if err != nil {
-				return err
-			}
-		}
-	}
-}
-
-func (c *CommandClient) handleLogConn(conn net.Conn) {
-	reader := bufio.NewReader(conn)
-	for {
-		messageType, err := reader.ReadByte()
-		if err != nil {
-			c.handler.Disconnected(err.Error())
-			return
-		}
-		var messages []string
-		switch messageType {
-		case 0:
-			err = varbin.Read(reader, binary.BigEndian, &messages)
-			if err != nil {
-				c.handler.Disconnected(err.Error())
-				return
-			}
-			c.handler.WriteLogs(newIterator(messages))
-		case 1:
-			c.handler.ClearLogs()
-		}
-	}
-}
-
-func connKeepAlive(reader io.Reader) context.Context {
-	ctx, cancel := context.WithCancelCause(context.Background())
-	go func() {
-		for {
-			_, err := reader.Read(make([]byte, 1))
-			if err != nil {
-				cancel(err)
-				return
-			}
-		}
-	}()
-	return ctx
-}

+ 0 - 59
experimental/libbox/command_power.go

@@ -1,59 +0,0 @@
-package libbox
-
-import (
-	"encoding/binary"
-	"net"
-
-	"github.com/sagernet/sing/common/varbin"
-)
-
-func (c *CommandClient) ServiceReload() error {
-	conn, err := c.directConnect()
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandServiceReload))
-	if err != nil {
-		return err
-	}
-	return readError(conn)
-}
-
-func (s *CommandServer) handleServiceReload(conn net.Conn) error {
-	rErr := s.handler.ServiceReload()
-	err := binary.Write(conn, binary.BigEndian, rErr != nil)
-	if err != nil {
-		return err
-	}
-	if rErr != nil {
-		return varbin.Write(conn, binary.BigEndian, rErr.Error())
-	}
-	return nil
-}
-
-func (c *CommandClient) ServiceClose() error {
-	conn, err := c.directConnect()
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandServiceClose))
-	if err != nil {
-		return err
-	}
-	return readError(conn)
-}
-
-func (s *CommandServer) handleServiceClose(conn net.Conn) error {
-	rErr := s.service.Close()
-	s.handler.PostServiceClose()
-	err := binary.Write(conn, binary.BigEndian, rErr != nil)
-	if err != nil {
-		return err
-	}
-	if rErr != nil {
-		return varbin.Write(conn, binary.BigEndian, rErr.Error())
-	}
-	return nil
-}

+ 0 - 58
experimental/libbox/command_select.go

@@ -1,58 +0,0 @@
-package libbox
-
-import (
-	"encoding/binary"
-	"net"
-
-	"github.com/sagernet/sing-box/protocol/group"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/varbin"
-)
-
-func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
-	conn, err := c.directConnect()
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandSelectOutbound))
-	if err != nil {
-		return err
-	}
-	err = varbin.Write(conn, binary.BigEndian, groupTag)
-	if err != nil {
-		return err
-	}
-	err = varbin.Write(conn, binary.BigEndian, outboundTag)
-	if err != nil {
-		return err
-	}
-	return readError(conn)
-}
-
-func (s *CommandServer) handleSelectOutbound(conn net.Conn) error {
-	groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
-	if err != nil {
-		return err
-	}
-	outboundTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
-	if err != nil {
-		return err
-	}
-	service := s.service
-	if service == nil {
-		return writeError(conn, E.New("service not ready"))
-	}
-	outboundGroup, isLoaded := service.instance.Outbound().Outbound(groupTag)
-	if !isLoaded {
-		return writeError(conn, E.New("selector not found: ", groupTag))
-	}
-	selector, isSelector := outboundGroup.(*group.Selector)
-	if !isSelector {
-		return writeError(conn, E.New("outbound is not a selector: ", groupTag))
-	}
-	if !selector.SelectOutbound(outboundTag) {
-		return writeError(conn, E.New("outbound not found in selector: ", outboundTag))
-	}
-	return writeError(conn, nil)
-}

+ 177 - 134
experimental/libbox/command_server.go

@@ -1,182 +1,225 @@
 package libbox
 
 import (
-	"encoding/binary"
+	"context"
 	"net"
 	"os"
 	"path/filepath"
-	"sync"
+	"strconv"
+	"time"
 
-	"github.com/sagernet/sing-box/common/urltest"
-	"github.com/sagernet/sing-box/experimental/clashapi"
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/daemon"
+	"github.com/sagernet/sing-box/experimental/deprecated"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/debug"
 	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/observable"
-	"github.com/sagernet/sing/common/x/list"
 	"github.com/sagernet/sing/service"
+
+	"google.golang.org/grpc"
 )
 
 type CommandServer struct {
-	listener net.Listener
-	handler  CommandServerHandler
-
-	access     sync.Mutex
-	savedLines list.List[string]
-	maxLines   int
-	subscriber *observable.Subscriber[string]
-	observer   *observable.Observer[string]
-	service    *BoxService
-
-	// These channels only work with a single client. if multi-client support is needed, replace with Subscriber/Observer
-	urlTestUpdate chan struct{}
-	modeUpdate    chan struct{}
-	logReset      chan struct{}
-
-	closedConnections []Connection
+	*daemon.StartedService
+
+	ctx               context.Context
+	cancel            context.CancelFunc
+	handler           CommandServerHandler
+	platformInterface PlatformInterface
+	platformWrapper   *platformInterfaceWrapper
+	grpcServer        *grpc.Server
+	listener          net.Listener
+	endPauseTimer     *time.Timer
 }
 
 type CommandServerHandler interface {
+	ServiceStop() error
 	ServiceReload() error
-	PostServiceClose()
-	GetSystemProxyStatus() *SystemProxyStatus
-	SetSystemProxyEnabled(isEnabled bool) error
+	GetSystemProxyStatus() (*SystemProxyStatus, error)
+	SetSystemProxyEnabled(enabled bool) error
+	WriteDebugMessage(message string)
 }
 
-func NewCommandServer(handler CommandServerHandler, maxLines int32) *CommandServer {
+func NewCommandServer(handler CommandServerHandler, platformInterface PlatformInterface) (*CommandServer, error) {
+	ctx := BaseContext(platformInterface)
+	service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
+	ctx, cancel := context.WithCancel(ctx)
+	platformWrapper := &platformInterfaceWrapper{
+		iif:       platformInterface,
+		useProcFS: platformInterface.UseProcFS(),
+	}
+	service.MustRegister[adapter.PlatformInterface](ctx, platformWrapper)
 	server := &CommandServer{
-		handler:       handler,
-		maxLines:      int(maxLines),
-		subscriber:    observable.NewSubscriber[string](128),
-		urlTestUpdate: make(chan struct{}, 1),
-		modeUpdate:    make(chan struct{}, 1),
-		logReset:      make(chan struct{}, 1),
+		ctx:               ctx,
+		cancel:            cancel,
+		handler:           handler,
+		platformInterface: platformInterface,
+		platformWrapper:   platformWrapper,
 	}
-	server.observer = observable.NewObserver[string](server.subscriber, 64)
-	return server
+	server.StartedService = daemon.NewStartedService(daemon.ServiceOptions{
+		Context:            ctx,
+		Platform:           platformWrapper,
+		PlatformHandler:    (*platformHandler)(server),
+		Debug:              sDebug,
+		LogMaxLines:        sLogMaxLines,
+		WorkingDirectory:   sBasePath,
+		TempDirectory:      os.TempDir(),
+		UserID:             sUserID,
+		GroupID:            sGroupID,
+		SystemProxyEnabled: false,
+	})
+	return server, nil
 }
 
-func (s *CommandServer) SetService(newService *BoxService) {
-	if newService != nil {
-		service.PtrFromContext[urltest.HistoryStorage](newService.ctx).SetHook(s.urlTestUpdate)
-		newService.clashServer.(*clashapi.Server).SetModeUpdateHook(s.modeUpdate)
+func (s *CommandServer) Start() error {
+	var (
+		listener net.Listener
+		err      error
+	)
+	if C.IsAndroid && sCommandServerListenPort == 0 {
+		sockPath := filepath.Join(sBasePath, "command.sock")
+		os.Remove(sockPath)
+		listener, err = net.ListenUnix("unix", &net.UnixAddr{
+			Name: sockPath,
+			Net:  "unix",
+		})
+		if err != nil {
+			return E.Cause(err, "listen command server")
+		}
+		if sUserID != os.Getuid() {
+			err = os.Chown(sockPath, sUserID, sGroupID)
+			if err != nil {
+				listener.Close()
+				os.Remove(sockPath)
+				return E.Cause(err, "chown")
+			}
+		}
+	} else {
+		port := sCommandServerListenPort
+		if port == 0 {
+			port = 8964
+		}
+		listener, err = net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port))))
+		if err != nil {
+			return E.Cause(err, "listen command server")
+		}
 	}
-	s.service = newService
-	s.notifyURLTestUpdate()
+	s.listener = listener
+	s.grpcServer = grpc.NewServer()
+	daemon.RegisterStartedServiceServer(s.grpcServer, s.StartedService)
+	go s.grpcServer.Serve(listener)
+	return nil
 }
 
-func (s *CommandServer) notifyURLTestUpdate() {
-	select {
-	case s.urlTestUpdate <- struct{}{}:
-	default:
+func (s *CommandServer) Close() {
+	s.cancel()
+	if s.grpcServer != nil {
+		s.grpcServer.Stop()
 	}
+	common.Close(s.listener)
 }
 
-func (s *CommandServer) Start() error {
-	if !sTVOS {
-		return s.listenUNIX()
-	} else {
-		return s.listenTCP()
-	}
+type OverrideOptions struct {
+	AutoRedirect   bool
+	IncludePackage StringIterator
+	ExcludePackage StringIterator
 }
 
-func (s *CommandServer) listenUNIX() error {
-	sockPath := filepath.Join(sBasePath, "command.sock")
-	os.Remove(sockPath)
-	listener, err := net.ListenUnix("unix", &net.UnixAddr{
-		Name: sockPath,
-		Net:  "unix",
+func (s *CommandServer) StartOrReloadService(configContent string, options *OverrideOptions) error {
+	return s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{
+		AutoRedirect:   options.AutoRedirect,
+		IncludePackage: iteratorToArray(options.IncludePackage),
+		ExcludePackage: iteratorToArray(options.ExcludePackage),
 	})
-	if err != nil {
-		return E.Cause(err, "listen ", sockPath)
+}
+
+func (s *CommandServer) CloseService() error {
+	return s.StartedService.CloseService()
+}
+
+func (s *CommandServer) WriteMessage(level int32, message string) {
+	s.StartedService.WriteMessage(log.Level(level), message)
+}
+
+func (s *CommandServer) SetError(message string) {
+	s.StartedService.SetError(E.New(message))
+}
+
+func (s *CommandServer) NeedWIFIState() bool {
+	instance := s.StartedService.Instance()
+	if instance == nil || instance.Box() == nil {
+		return false
 	}
-	err = os.Chown(sockPath, sUserID, sGroupID)
-	if err != nil {
-		listener.Close()
-		os.Remove(sockPath)
-		return E.Cause(err, "chown")
+	return instance.Box().Router().NeedWIFIState()
+}
+
+func (s *CommandServer) Pause() {
+	instance := s.StartedService.Instance()
+	if instance == nil || instance.PauseManager() == nil {
+		return
+	}
+	instance.PauseManager().DevicePause()
+	if C.IsIos {
+		if s.endPauseTimer == nil {
+			s.endPauseTimer = time.AfterFunc(time.Minute, instance.PauseManager().DeviceWake)
+		} else {
+			s.endPauseTimer.Reset(time.Minute)
+		}
 	}
-	s.listener = listener
-	go s.loopConnection(listener)
-	return nil
 }
 
-func (s *CommandServer) listenTCP() error {
-	listener, err := net.Listen("tcp", "127.0.0.1:8964")
-	if err != nil {
-		return E.Cause(err, "listen")
+func (s *CommandServer) Wake() {
+	instance := s.StartedService.Instance()
+	if instance == nil || instance.PauseManager() == nil {
+		return
+	}
+	if !C.IsIos {
+		instance.PauseManager().DeviceWake()
 	}
-	s.listener = listener
-	go s.loopConnection(listener)
-	return nil
 }
 
-func (s *CommandServer) Close() error {
-	return common.Close(
-		s.listener,
-		s.observer,
-	)
+func (s *CommandServer) ResetNetwork() {
+	instance := s.StartedService.Instance()
+	if instance == nil || instance.Box() == nil {
+		return
+	}
+	instance.Box().Router().ResetNetwork()
 }
 
-func (s *CommandServer) loopConnection(listener net.Listener) {
-	for {
-		conn, err := listener.Accept()
-		if err != nil {
-			return
-		}
-		go func() {
-			hErr := s.handleConnection(conn)
-			if hErr != nil && !E.IsClosed(err) {
-				if debug.Enabled {
-					log.Warn("log-server: process connection: ", hErr)
-				}
-			}
-		}()
+func (s *CommandServer) UpdateWIFIState() {
+	instance := s.StartedService.Instance()
+	if instance == nil || instance.Box() == nil {
+		return
 	}
+	instance.Box().Network().UpdateWIFIState()
+}
+
+type platformHandler CommandServer
+
+func (h *platformHandler) ServiceStop() error {
+	return (*CommandServer)(h).handler.ServiceStop()
 }
 
-func (s *CommandServer) handleConnection(conn net.Conn) error {
-	defer conn.Close()
-	var command uint8
-	err := binary.Read(conn, binary.BigEndian, &command)
+func (h *platformHandler) ServiceReload() error {
+	return (*CommandServer)(h).handler.ServiceReload()
+}
+
+func (h *platformHandler) SystemProxyStatus() (*daemon.SystemProxyStatus, error) {
+	status, err := (*CommandServer)(h).handler.GetSystemProxyStatus()
 	if err != nil {
-		return E.Cause(err, "read command")
-	}
-	switch int32(command) {
-	case CommandLog:
-		return s.handleLogConn(conn)
-	case CommandStatus:
-		return s.handleStatusConn(conn)
-	case CommandServiceReload:
-		return s.handleServiceReload(conn)
-	case CommandServiceClose:
-		return s.handleServiceClose(conn)
-	case CommandCloseConnections:
-		return s.handleCloseConnections(conn)
-	case CommandGroup:
-		return s.handleGroupConn(conn)
-	case CommandSelectOutbound:
-		return s.handleSelectOutbound(conn)
-	case CommandURLTest:
-		return s.handleURLTest(conn)
-	case CommandGroupExpand:
-		return s.handleSetGroupExpand(conn)
-	case CommandClashMode:
-		return s.handleModeConn(conn)
-	case CommandSetClashMode:
-		return s.handleSetClashMode(conn)
-	case CommandGetSystemProxyStatus:
-		return s.handleGetSystemProxyStatus(conn)
-	case CommandSetSystemProxyEnabled:
-		return s.handleSetSystemProxyEnabled(conn)
-	case CommandConnections:
-		return s.handleConnectionsConn(conn)
-	case CommandCloseConnection:
-		return s.handleCloseConnection(conn)
-	case CommandGetDeprecatedNotes:
-		return s.handleGetDeprecatedNotes(conn)
-	default:
-		return E.New("unknown command: ", command)
+		return nil, err
 	}
+	return &daemon.SystemProxyStatus{
+		Enabled:   status.Enabled,
+		Available: status.Available,
+	}, nil
+}
+
+func (h *platformHandler) SetSystemProxyEnabled(enabled bool) error {
+	return (*CommandServer)(h).handler.SetSystemProxyEnabled(enabled)
+}
+
+func (h *platformHandler) WriteDebugMessage(message string) {
+	(*CommandServer)(h).handler.WriteDebugMessage(message)
 }

+ 0 - 39
experimental/libbox/command_shared.go

@@ -1,39 +0,0 @@
-package libbox
-
-import (
-	"encoding/binary"
-	"io"
-
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/varbin"
-)
-
-func readError(reader io.Reader) error {
-	var hasError bool
-	err := binary.Read(reader, binary.BigEndian, &hasError)
-	if err != nil {
-		return err
-	}
-	if hasError {
-		errorMessage, err := varbin.ReadValue[string](reader, binary.BigEndian)
-		if err != nil {
-			return err
-		}
-		return E.New(errorMessage)
-	}
-	return nil
-}
-
-func writeError(writer io.Writer, wErr error) error {
-	err := binary.Write(writer, binary.BigEndian, wErr != nil)
-	if err != nil {
-		return err
-	}
-	if wErr != nil {
-		err = varbin.Write(writer, binary.BigEndian, wErr.Error())
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}

+ 0 - 85
experimental/libbox/command_status.go

@@ -1,85 +0,0 @@
-package libbox
-
-import (
-	"encoding/binary"
-	"net"
-	"runtime"
-	"time"
-
-	"github.com/sagernet/sing-box/common/conntrack"
-	"github.com/sagernet/sing-box/experimental/clashapi"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/memory"
-)
-
-type StatusMessage struct {
-	Memory           int64
-	Goroutines       int32
-	ConnectionsIn    int32
-	ConnectionsOut   int32
-	TrafficAvailable bool
-	Uplink           int64
-	Downlink         int64
-	UplinkTotal      int64
-	DownlinkTotal    int64
-}
-
-func (s *CommandServer) readStatus() StatusMessage {
-	var message StatusMessage
-	message.Memory = int64(memory.Inuse())
-	message.Goroutines = int32(runtime.NumGoroutine())
-	message.ConnectionsOut = int32(conntrack.Count())
-
-	if s.service != nil {
-		message.TrafficAvailable = true
-		trafficManager := s.service.clashServer.(*clashapi.Server).TrafficManager()
-		message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
-		message.ConnectionsIn = int32(trafficManager.ConnectionsLen())
-	}
-
-	return message
-}
-
-func (s *CommandServer) handleStatusConn(conn net.Conn) error {
-	var interval int64
-	err := binary.Read(conn, binary.BigEndian, &interval)
-	if err != nil {
-		return E.Cause(err, "read interval")
-	}
-	ticker := time.NewTicker(time.Duration(interval))
-	defer ticker.Stop()
-	ctx := connKeepAlive(conn)
-	status := s.readStatus()
-	uploadTotal := status.UplinkTotal
-	downloadTotal := status.DownlinkTotal
-	for {
-		err = binary.Write(conn, binary.BigEndian, status)
-		if err != nil {
-			return err
-		}
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-ticker.C:
-		}
-		status = s.readStatus()
-		upload := status.UplinkTotal - uploadTotal
-		download := status.DownlinkTotal - downloadTotal
-		uploadTotal = status.UplinkTotal
-		downloadTotal = status.DownlinkTotal
-		status.Uplink = upload
-		status.Downlink = download
-	}
-}
-
-func (c *CommandClient) handleStatusConn(conn net.Conn) {
-	for {
-		var message StatusMessage
-		err := binary.Read(conn, binary.BigEndian, &message)
-		if err != nil {
-			c.handler.Disconnected(err.Error())
-			return
-		}
-		c.handler.WriteStatus(&message)
-	}
-}

+ 0 - 80
experimental/libbox/command_system_proxy.go

@@ -1,80 +0,0 @@
-package libbox
-
-import (
-	"encoding/binary"
-	"net"
-)
-
-type SystemProxyStatus struct {
-	Available bool
-	Enabled   bool
-}
-
-func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) {
-	conn, err := c.directConnectWithRetry()
-	if err != nil {
-		return nil, err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandGetSystemProxyStatus))
-	if err != nil {
-		return nil, err
-	}
-	var status SystemProxyStatus
-	err = binary.Read(conn, binary.BigEndian, &status.Available)
-	if err != nil {
-		return nil, err
-	}
-	if status.Available {
-		err = binary.Read(conn, binary.BigEndian, &status.Enabled)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return &status, nil
-}
-
-func (s *CommandServer) handleGetSystemProxyStatus(conn net.Conn) error {
-	status := s.handler.GetSystemProxyStatus()
-	err := binary.Write(conn, binary.BigEndian, status.Available)
-	if err != nil {
-		return err
-	}
-	if status.Available {
-		err = binary.Write(conn, binary.BigEndian, status.Enabled)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error {
-	conn, err := c.directConnect()
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandSetSystemProxyEnabled))
-	if err != nil {
-		return err
-	}
-	err = binary.Write(conn, binary.BigEndian, isEnabled)
-	if err != nil {
-		return err
-	}
-	return readError(conn)
-}
-
-func (s *CommandServer) handleSetSystemProxyEnabled(conn net.Conn) error {
-	var isEnabled bool
-	err := binary.Read(conn, binary.BigEndian, &isEnabled)
-	if err != nil {
-		return err
-	}
-	err = s.handler.SetSystemProxyEnabled(isEnabled)
-	if err != nil {
-		return writeError(conn, err)
-	}
-	return writeError(conn, nil)
-}

+ 276 - 0
experimental/libbox/command_types.go

@@ -0,0 +1,276 @@
+package libbox
+
+import (
+	"slices"
+	"strings"
+
+	"github.com/sagernet/sing-box/daemon"
+	M "github.com/sagernet/sing/common/metadata"
+)
+
+type StatusMessage struct {
+	Memory           int64
+	Goroutines       int32
+	ConnectionsIn    int32
+	ConnectionsOut   int32
+	TrafficAvailable bool
+	Uplink           int64
+	Downlink         int64
+	UplinkTotal      int64
+	DownlinkTotal    int64
+}
+
+type SystemProxyStatus struct {
+	Available bool
+	Enabled   bool
+}
+
+type OutboundGroup struct {
+	Tag        string
+	Type       string
+	Selectable bool
+	Selected   string
+	IsExpand   bool
+	ItemList   []*OutboundGroupItem
+}
+
+func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
+	return newIterator(g.ItemList)
+}
+
+type OutboundGroupIterator interface {
+	Next() *OutboundGroup
+	HasNext() bool
+}
+
+type OutboundGroupItem struct {
+	Tag          string
+	Type         string
+	URLTestTime  int64
+	URLTestDelay int32
+}
+
+type OutboundGroupItemIterator interface {
+	Next() *OutboundGroupItem
+	HasNext() bool
+}
+
+const (
+	ConnectionStateAll = iota
+	ConnectionStateActive
+	ConnectionStateClosed
+)
+
+type Connections struct {
+	input    []Connection
+	filtered []Connection
+}
+
+func (c *Connections) FilterState(state int32) {
+	c.filtered = c.filtered[:0]
+	switch state {
+	case ConnectionStateAll:
+		c.filtered = append(c.filtered, c.input...)
+	case ConnectionStateActive:
+		for _, connection := range c.input {
+			if connection.ClosedAt == 0 {
+				c.filtered = append(c.filtered, connection)
+			}
+		}
+	case ConnectionStateClosed:
+		for _, connection := range c.input {
+			if connection.ClosedAt != 0 {
+				c.filtered = append(c.filtered, connection)
+			}
+		}
+	}
+}
+
+func (c *Connections) SortByDate() {
+	slices.SortStableFunc(c.filtered, func(x, y Connection) int {
+		if x.CreatedAt < y.CreatedAt {
+			return 1
+		} else if x.CreatedAt > y.CreatedAt {
+			return -1
+		} else {
+			return strings.Compare(y.ID, x.ID)
+		}
+	})
+}
+
+func (c *Connections) SortByTraffic() {
+	slices.SortStableFunc(c.filtered, func(x, y Connection) int {
+		xTraffic := x.Uplink + x.Downlink
+		yTraffic := y.Uplink + y.Downlink
+		if xTraffic < yTraffic {
+			return 1
+		} else if xTraffic > yTraffic {
+			return -1
+		} else {
+			return strings.Compare(y.ID, x.ID)
+		}
+	})
+}
+
+func (c *Connections) SortByTrafficTotal() {
+	slices.SortStableFunc(c.filtered, func(x, y Connection) int {
+		xTraffic := x.UplinkTotal + x.DownlinkTotal
+		yTraffic := y.UplinkTotal + y.DownlinkTotal
+		if xTraffic < yTraffic {
+			return 1
+		} else if xTraffic > yTraffic {
+			return -1
+		} else {
+			return strings.Compare(y.ID, x.ID)
+		}
+	})
+}
+
+func (c *Connections) Iterator() ConnectionIterator {
+	return newPtrIterator(c.filtered)
+}
+
+type Connection struct {
+	ID            string
+	Inbound       string
+	InboundType   string
+	IPVersion     int32
+	Network       string
+	Source        string
+	Destination   string
+	Domain        string
+	Protocol      string
+	User          string
+	FromOutbound  string
+	CreatedAt     int64
+	ClosedAt      int64
+	Uplink        int64
+	Downlink      int64
+	UplinkTotal   int64
+	DownlinkTotal int64
+	Rule          string
+	Outbound      string
+	OutboundType  string
+	ChainList     []string
+}
+
+func (c *Connection) Chain() StringIterator {
+	return newIterator(c.ChainList)
+}
+
+func (c *Connection) DisplayDestination() string {
+	destination := M.ParseSocksaddr(c.Destination)
+	if destination.IsIP() && c.Domain != "" {
+		destination = M.Socksaddr{
+			Fqdn: c.Domain,
+			Port: destination.Port,
+		}
+		return destination.String()
+	}
+	return c.Destination
+}
+
+type ConnectionIterator interface {
+	Next() *Connection
+	HasNext() bool
+}
+
+func StatusMessageFromGRPC(status *daemon.Status) *StatusMessage {
+	if status == nil {
+		return nil
+	}
+	return &StatusMessage{
+		Memory:           int64(status.Memory),
+		Goroutines:       status.Goroutines,
+		ConnectionsIn:    status.ConnectionsIn,
+		ConnectionsOut:   status.ConnectionsOut,
+		TrafficAvailable: status.TrafficAvailable,
+		Uplink:           status.Uplink,
+		Downlink:         status.Downlink,
+		UplinkTotal:      status.UplinkTotal,
+		DownlinkTotal:    status.DownlinkTotal,
+	}
+}
+
+func OutboundGroupIteratorFromGRPC(groups *daemon.Groups) OutboundGroupIterator {
+	if groups == nil || len(groups.Group) == 0 {
+		return newIterator([]*OutboundGroup{})
+	}
+	var libboxGroups []*OutboundGroup
+	for _, g := range groups.Group {
+		libboxGroup := &OutboundGroup{
+			Tag:        g.Tag,
+			Type:       g.Type,
+			Selectable: g.Selectable,
+			Selected:   g.Selected,
+			IsExpand:   g.IsExpand,
+		}
+		for _, item := range g.Items {
+			libboxGroup.ItemList = append(libboxGroup.ItemList, &OutboundGroupItem{
+				Tag:          item.Tag,
+				Type:         item.Type,
+				URLTestTime:  item.UrlTestTime,
+				URLTestDelay: item.UrlTestDelay,
+			})
+		}
+		libboxGroups = append(libboxGroups, libboxGroup)
+	}
+	return newIterator(libboxGroups)
+}
+
+func ConnectionFromGRPC(conn *daemon.Connection) Connection {
+	return Connection{
+		ID:            conn.Id,
+		Inbound:       conn.Inbound,
+		InboundType:   conn.InboundType,
+		IPVersion:     conn.IpVersion,
+		Network:       conn.Network,
+		Source:        conn.Source,
+		Destination:   conn.Destination,
+		Domain:        conn.Domain,
+		Protocol:      conn.Protocol,
+		User:          conn.User,
+		FromOutbound:  conn.FromOutbound,
+		CreatedAt:     conn.CreatedAt,
+		ClosedAt:      conn.ClosedAt,
+		Uplink:        conn.Uplink,
+		Downlink:      conn.Downlink,
+		UplinkTotal:   conn.UplinkTotal,
+		DownlinkTotal: conn.DownlinkTotal,
+		Rule:          conn.Rule,
+		Outbound:      conn.Outbound,
+		OutboundType:  conn.OutboundType,
+		ChainList:     conn.ChainList,
+	}
+}
+
+func ConnectionsFromGRPC(connections *daemon.Connections) []Connection {
+	if connections == nil || len(connections.Connections) == 0 {
+		return nil
+	}
+	var libboxConnections []Connection
+	for _, conn := range connections.Connections {
+		libboxConnections = append(libboxConnections, ConnectionFromGRPC(conn))
+	}
+	return libboxConnections
+}
+
+func SystemProxyStatusFromGRPC(status *daemon.SystemProxyStatus) *SystemProxyStatus {
+	if status == nil {
+		return nil
+	}
+	return &SystemProxyStatus{
+		Available: status.Available,
+		Enabled:   status.Enabled,
+	}
+}
+
+func SystemProxyStatusToGRPC(status *SystemProxyStatus) *daemon.SystemProxyStatus {
+	if status == nil {
+		return nil
+	}
+	return &daemon.SystemProxyStatus{
+		Available: status.Available,
+		Enabled:   status.Enabled,
+	}
+}

+ 0 - 86
experimental/libbox/command_urltest.go

@@ -1,86 +0,0 @@
-package libbox
-
-import (
-	"encoding/binary"
-	"net"
-	"time"
-
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/urltest"
-	"github.com/sagernet/sing-box/protocol/group"
-	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/batch"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/varbin"
-	"github.com/sagernet/sing/service"
-)
-
-func (c *CommandClient) URLTest(groupTag string) error {
-	conn, err := c.directConnect()
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	err = binary.Write(conn, binary.BigEndian, uint8(CommandURLTest))
-	if err != nil {
-		return err
-	}
-	err = varbin.Write(conn, binary.BigEndian, groupTag)
-	if err != nil {
-		return err
-	}
-	return readError(conn)
-}
-
-func (s *CommandServer) handleURLTest(conn net.Conn) error {
-	groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
-	if err != nil {
-		return err
-	}
-	serviceNow := s.service
-	if serviceNow == nil {
-		return nil
-	}
-	abstractOutboundGroup, isLoaded := serviceNow.instance.Outbound().Outbound(groupTag)
-	if !isLoaded {
-		return writeError(conn, E.New("outbound group not found: ", groupTag))
-	}
-	outboundGroup, isOutboundGroup := abstractOutboundGroup.(adapter.OutboundGroup)
-	if !isOutboundGroup {
-		return writeError(conn, E.New("outbound is not a group: ", groupTag))
-	}
-	urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest)
-	if isURLTest {
-		go urlTest.CheckOutbounds()
-	} else {
-		historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx)
-		outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
-			itOutbound, _ := serviceNow.instance.Outbound().Outbound(it)
-			return itOutbound
-		}), func(it adapter.Outbound) bool {
-			if it == nil {
-				return false
-			}
-			_, isGroup := it.(adapter.OutboundGroup)
-			return !isGroup
-		})
-		b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10))
-		for _, detour := range outbounds {
-			outboundToTest := detour
-			outboundTag := outboundToTest.Tag()
-			b.Go(outboundTag, func() (any, error) {
-				t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest)
-				if err != nil {
-					historyStorage.DeleteURLTestHistory(outboundTag)
-				} else {
-					historyStorage.StoreURLTestHistory(outboundTag, &adapter.URLTestHistory{
-						Time:  time.Now(),
-						Delay: t,
-					})
-				}
-				return nil, nil
-			})
-		}
-	}
-	return writeError(conn, nil)
-}

+ 36 - 11
experimental/libbox/config.go

@@ -3,19 +3,16 @@ package libbox
 import (
 	"bytes"
 	"context"
-	"net/netip"
 	"os"
 
-	"github.com/sagernet/sing-box"
+	box "github.com/sagernet/sing-box"
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/process"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/dns"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/include"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-tun"
+	tun "github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
@@ -55,7 +52,7 @@ func CheckConfig(configContent string) error {
 	}
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
-	ctx = service.ContextWith[platform.Interface](ctx, (*platformInterfaceStub)(nil))
+	ctx = service.ContextWith[adapter.PlatformInterface](ctx, (*platformInterfaceStub)(nil))
 	instance, err := box.New(box.Options{
 		Context: ctx,
 		Options: options,
@@ -80,7 +77,11 @@ func (s *platformInterfaceStub) AutoDetectInterfaceControl(fd int) error {
 	return nil
 }
 
-func (s *platformInterfaceStub) OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
+func (s *platformInterfaceStub) UsePlatformInterface() bool {
+	return false
+}
+
+func (s *platformInterfaceStub) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
 	return nil, os.ErrInvalid
 }
 
@@ -92,7 +93,11 @@ func (s *platformInterfaceStub) CreateDefaultInterfaceMonitor(logger logger.Logg
 	return (*interfaceMonitorStub)(nil)
 }
 
-func (s *platformInterfaceStub) Interfaces() ([]adapter.NetworkInterface, error) {
+func (s *platformInterfaceStub) UsePlatformNetworkInterfaces() bool {
+	return false
+}
+
+func (s *platformInterfaceStub) NetworkInterfaces() ([]adapter.NetworkInterface, error) {
 	return nil, os.ErrInvalid
 }
 
@@ -100,13 +105,17 @@ func (s *platformInterfaceStub) UnderNetworkExtension() bool {
 	return false
 }
 
-func (s *platformInterfaceStub) IncludeAllNetworks() bool {
+func (s *platformInterfaceStub) NetworkExtensionIncludeAllNetworks() bool {
 	return false
 }
 
 func (s *platformInterfaceStub) ClearDNSCache() {
 }
 
+func (s *platformInterfaceStub) RequestPermissionForWIFIState() error {
+	return nil
+}
+
 func (s *platformInterfaceStub) ReadWIFIState() adapter.WIFIState {
 	return adapter.WIFIState{}
 }
@@ -115,11 +124,27 @@ func (s *platformInterfaceStub) SystemCertificates() []string {
 	return nil
 }
 
-func (s *platformInterfaceStub) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) {
+func (s *platformInterfaceStub) UsePlatformConnectionOwnerFinder() bool {
+	return false
+}
+
+func (s *platformInterfaceStub) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) {
 	return nil, os.ErrInvalid
 }
 
-func (s *platformInterfaceStub) SendNotification(notification *platform.Notification) error {
+func (s *platformInterfaceStub) UsePlatformNotification() bool {
+	return false
+}
+
+func (s *platformInterfaceStub) SendNotification(notification *adapter.Notification) error {
+	return nil
+}
+
+func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool {
+	return false
+}
+
+func (s *platformInterfaceStub) LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions] {
 	return nil
 }
 

+ 14 - 11
experimental/libbox/http.go

@@ -77,22 +77,27 @@ func NewHTTPClient() HTTPClient {
 }
 
 func (c *httpClient) ModernTLS() {
-	c.tls.MinVersion = tls.VersionTLS12
-	c.tls.CipherSuites = common.Map(tls.CipherSuites(), func(it *tls.CipherSuite) uint16 { return it.ID })
+	c.setTLSVersion(tls.VersionTLS12, 0, func(suite *tls.CipherSuite) bool { return true })
 }
 
 func (c *httpClient) RestrictedTLS() {
-	c.tls.MinVersion = tls.VersionTLS13
-	c.tls.CipherSuites = common.Map(common.Filter(tls.CipherSuites(), func(it *tls.CipherSuite) bool {
-		return common.Contains(it.SupportedVersions, uint16(tls.VersionTLS13))
-	}), func(it *tls.CipherSuite) uint16 {
+	c.setTLSVersion(tls.VersionTLS13, 0, func(suite *tls.CipherSuite) bool {
+		return common.Contains(suite.SupportedVersions, uint16(tls.VersionTLS13))
+	})
+}
+
+func (c *httpClient) setTLSVersion(minVersion, maxVersion uint16, filter func(*tls.CipherSuite) bool) {
+	c.tls.MinVersion = minVersion
+	if maxVersion != 0 {
+		c.tls.MaxVersion = maxVersion
+	}
+	c.tls.CipherSuites = common.Map(common.Filter(tls.CipherSuites(), filter), func(it *tls.CipherSuite) uint16 {
 		return it.ID
 	})
 }
 
 func (c *httpClient) PinnedTLS12() {
-	c.tls.MinVersion = tls.VersionTLS12
-	c.tls.MaxVersion = tls.VersionTLS12
+	c.setTLSVersion(tls.VersionTLS12, tls.VersionTLS12, func(suite *tls.CipherSuite) bool { return true })
 }
 
 func (c *httpClient) PinnedSHA256(sumHex string) {
@@ -178,9 +183,7 @@ func (r *httpRequest) SetUserAgent(userAgent string) {
 }
 
 func (r *httpRequest) SetContent(content []byte) {
-	buffer := bytes.Buffer{}
-	buffer.Write(content)
-	r.request.Body = io.NopCloser(bytes.NewReader(buffer.Bytes()))
+	r.request.Body = io.NopCloser(bytes.NewReader(content))
 	r.request.ContentLength = int64(len(content))
 }
 

+ 6 - 0
experimental/libbox/iterator.go

@@ -8,6 +8,12 @@ type StringIterator interface {
 	Next() string
 }
 
+type Int32Iterator interface {
+	Len() int32
+	HasNext() bool
+	Next() int32
+}
+
 var _ StringIterator = (*iterator[string])(nil)
 
 type iterator[T any] struct {

+ 1 - 1
experimental/libbox/monitor.go

@@ -1,7 +1,7 @@
 package libbox
 
 import (
-	"github.com/sagernet/sing-tun"
+	tun "github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"

+ 0 - 6
experimental/libbox/platform.go

@@ -10,7 +10,6 @@ type PlatformInterface interface {
 	UsePlatformAutoDetectInterfaceControl() bool
 	AutoDetectInterfaceControl(fd int32) error
 	OpenTun(options TunOptions) (int32, error)
-	WriteLog(message string)
 	UseProcFS() bool
 	FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (int32, error)
 	PackageNameByUid(uid int32) (string, error)
@@ -26,11 +25,6 @@ type PlatformInterface interface {
 	SendNotification(notification *Notification) error
 }
 
-type TunInterface interface {
-	FileDescriptor() int32
-	Close() error
-}
-
 type InterfaceUpdateListener interface {
 	UpdateDefaultInterface(interfaceName string, interfaceIndex int32, isExpensive bool, isConstrained bool)
 }

+ 0 - 35
experimental/libbox/platform/interface.go

@@ -1,35 +0,0 @@
-package platform
-
-import (
-	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/process"
-	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-tun"
-	"github.com/sagernet/sing/common/logger"
-)
-
-type Interface interface {
-	Initialize(networkManager adapter.NetworkManager) error
-	UsePlatformAutoDetectInterfaceControl() bool
-	AutoDetectInterfaceControl(fd int) error
-	OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
-	CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
-	Interfaces() ([]adapter.NetworkInterface, error)
-	UnderNetworkExtension() bool
-	IncludeAllNetworks() bool
-	ClearDNSCache()
-	ReadWIFIState() adapter.WIFIState
-	SystemCertificates() []string
-	process.Searcher
-	SendNotification(notification *Notification) error
-}
-
-type Notification struct {
-	Identifier string
-	TypeName   string
-	TypeID     int32
-	Title      string
-	Subtitle   string
-	Body       string
-	OpenURL    string
-}

+ 67 - 118
experimental/libbox/service.go

@@ -3,121 +3,25 @@ package libbox
 import (
 	"context"
 	"net/netip"
-	"os"
 	"runtime"
-	runtimeDebug "runtime/debug"
 	"sync"
 	"syscall"
-	"time"
 
-	"github.com/sagernet/sing-box"
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/process"
-	"github.com/sagernet/sing-box/common/urltest"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/experimental/deprecated"
+	"github.com/sagernet/sing-box/daemon"
+	"github.com/sagernet/sing-box/dns"
 	"github.com/sagernet/sing-box/experimental/libbox/internal/procfs"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-tun"
+	tun "github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
-	N "github.com/sagernet/sing/common/network"
-	"github.com/sagernet/sing/service"
-	"github.com/sagernet/sing/service/pause"
 )
 
-type BoxService struct {
-	ctx                   context.Context
-	cancel                context.CancelFunc
-	urlTestHistoryStorage adapter.URLTestHistoryStorage
-	instance              *box.Box
-	clashServer           adapter.ClashServer
-	pauseManager          pause.Manager
-
-	iOSPauseFields
-}
-
-func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
-	ctx := BaseContext(platformInterface)
-	service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
-	options, err := parseConfig(ctx, configContent)
-	if err != nil {
-		return nil, err
-	}
-	runtimeDebug.FreeOSMemory()
-	ctx, cancel := context.WithCancel(ctx)
-	urlTestHistoryStorage := urltest.NewHistoryStorage()
-	ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
-	platformWrapper := &platformInterfaceWrapper{
-		iif:       platformInterface,
-		useProcFS: platformInterface.UseProcFS(),
-	}
-	service.MustRegister[platform.Interface](ctx, platformWrapper)
-	instance, err := box.New(box.Options{
-		Context:           ctx,
-		Options:           options,
-		PlatformLogWriter: platformWrapper,
-	})
-	if err != nil {
-		cancel()
-		return nil, E.Cause(err, "create service")
-	}
-	runtimeDebug.FreeOSMemory()
-	return &BoxService{
-		ctx:                   ctx,
-		cancel:                cancel,
-		instance:              instance,
-		urlTestHistoryStorage: urlTestHistoryStorage,
-		pauseManager:          service.FromContext[pause.Manager](ctx),
-		clashServer:           service.FromContext[adapter.ClashServer](ctx),
-	}, nil
-}
-
-func (s *BoxService) Start() error {
-	if sFixAndroidStack {
-		var err error
-		done := make(chan struct{})
-		go func() {
-			err = s.instance.Start()
-			close(done)
-		}()
-		<-done
-		return err
-	} else {
-		return s.instance.Start()
-	}
-}
-
-func (s *BoxService) Close() error {
-	s.cancel()
-	s.urlTestHistoryStorage.Close()
-	var err error
-	done := make(chan struct{})
-	go func() {
-		err = s.instance.Close()
-		close(done)
-	}()
-	select {
-	case <-done:
-		return err
-	case <-time.After(C.FatalStopTimeout):
-		os.Exit(1)
-		return nil
-	}
-}
-
-func (s *BoxService) NeedWIFIState() bool {
-	return s.instance.Router().NeedWIFIState()
-}
-
-var (
-	_ platform.Interface = (*platformInterfaceWrapper)(nil)
-	_ log.PlatformWriter = (*platformInterfaceWrapper)(nil)
-)
+var _ daemon.PlatformInterface = (*platformInterfaceWrapper)(nil)
 
 type platformInterfaceWrapper struct {
 	iif                    PlatformInterface
@@ -143,7 +47,11 @@ func (w *platformInterfaceWrapper) AutoDetectInterfaceControl(fd int) error {
 	return w.iif.AutoDetectInterfaceControl(int32(fd))
 }
 
-func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
+func (w *platformInterfaceWrapper) UsePlatformInterface() bool {
+	return true
+}
+
+func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
 	if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 {
 		return nil, E.New("platform: unsupported uid options")
 	}
@@ -172,6 +80,10 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions
 	return tun.New(*options)
 }
 
+func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
+	return true
+}
+
 func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor {
 	return &platformDefaultInterfaceMonitor{
 		platformInterfaceWrapper: w,
@@ -179,7 +91,11 @@ func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.L
 	}
 }
 
-func (w *platformInterfaceWrapper) Interfaces() ([]adapter.NetworkInterface, error) {
+func (w *platformInterfaceWrapper) UsePlatformNetworkInterfaces() bool {
+	return true
+}
+
+func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterface, error) {
 	interfaceIterator, err := w.iif.GetInterfaces()
 	if err != nil {
 		return nil, err
@@ -216,7 +132,7 @@ func (w *platformInterfaceWrapper) UnderNetworkExtension() bool {
 	return w.iif.UnderNetworkExtension()
 }
 
-func (w *platformInterfaceWrapper) IncludeAllNetworks() bool {
+func (w *platformInterfaceWrapper) NetworkExtensionIncludeAllNetworks() bool {
 	return w.iif.IncludeAllNetworks()
 }
 
@@ -224,6 +140,10 @@ func (w *platformInterfaceWrapper) ClearDNSCache() {
 	w.iif.ClearDNSCache()
 }
 
+func (w *platformInterfaceWrapper) RequestPermissionForWIFIState() error {
+	return nil
+}
+
 func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
 	wifiState := w.iif.ReadWIFIState()
 	if wifiState == nil {
@@ -236,41 +156,70 @@ func (w *platformInterfaceWrapper) SystemCertificates() []string {
 	return iteratorToArray[string](w.iif.SystemCertificates())
 }
 
-func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) {
+func (w *platformInterfaceWrapper) UsePlatformConnectionOwnerFinder() bool {
+	return true
+}
+
+func (w *platformInterfaceWrapper) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) {
 	var uid int32
 	if w.useProcFS {
+		var source netip.AddrPort
+		var destination netip.AddrPort
+		sourceAddr, _ := netip.ParseAddr(request.SourceAddress)
+		source = netip.AddrPortFrom(sourceAddr, uint16(request.SourcePort))
+		destAddr, _ := netip.ParseAddr(request.DestinationAddress)
+		destination = netip.AddrPortFrom(destAddr, uint16(request.DestinationPort))
+
+		var network string
+		switch request.IpProtocol {
+		case int32(syscall.IPPROTO_TCP):
+			network = "tcp"
+		case int32(syscall.IPPROTO_UDP):
+			network = "udp"
+		default:
+			return nil, E.New("unknown protocol: ", request.IpProtocol)
+		}
+
 		uid = procfs.ResolveSocketByProcSearch(network, source, destination)
 		if uid == -1 {
 			return nil, E.New("procfs: not found")
 		}
 	} else {
-		var ipProtocol int32
-		switch N.NetworkName(network) {
-		case N.NetworkTCP:
-			ipProtocol = syscall.IPPROTO_TCP
-		case N.NetworkUDP:
-			ipProtocol = syscall.IPPROTO_UDP
-		default:
-			return nil, E.New("unknown network: ", network)
-		}
 		var err error
-		uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port()))
+		uid, err = w.iif.FindConnectionOwner(request.IpProtocol, request.SourceAddress, request.SourcePort, request.DestinationAddress, request.DestinationPort)
 		if err != nil {
 			return nil, err
 		}
 	}
 	packageName, _ := w.iif.PackageNameByUid(uid)
-	return &process.Info{UserId: uid, PackageName: packageName}, nil
+	return &adapter.ConnectionOwner{
+		UserId:             uid,
+		AndroidPackageName: packageName,
+	}, nil
 }
 
 func (w *platformInterfaceWrapper) DisableColors() bool {
 	return runtime.GOOS != "android"
 }
 
-func (w *platformInterfaceWrapper) WriteMessage(level log.Level, message string) {
-	w.iif.WriteLog(message)
+func (w *platformInterfaceWrapper) UsePlatformNotification() bool {
+	return true
 }
 
-func (w *platformInterfaceWrapper) SendNotification(notification *platform.Notification) error {
+func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notification) error {
 	return w.iif.SendNotification((*Notification)(notification))
 }
+
+func (w *platformInterfaceWrapper) UsePlatformLocalDNSTransport() bool {
+	return C.IsAndroid
+}
+
+func (w *platformInterfaceWrapper) LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions] {
+	localTransport := w.iif.LocalDNSTransport()
+	if localTransport == nil {
+		return nil
+	}
+	return func(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
+		return newPlatformTransport(localTransport, tag, options), nil
+	}
+}

+ 0 - 36
experimental/libbox/service_pause.go

@@ -1,36 +0,0 @@
-package libbox
-
-import (
-	"time"
-
-	C "github.com/sagernet/sing-box/constant"
-)
-
-type iOSPauseFields struct {
-	endPauseTimer *time.Timer
-}
-
-func (s *BoxService) Pause() {
-	s.pauseManager.DevicePause()
-	if C.IsIos {
-		if s.endPauseTimer == nil {
-			s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake)
-		} else {
-			s.endPauseTimer.Reset(time.Minute)
-		}
-	}
-}
-
-func (s *BoxService) Wake() {
-	if !C.IsIos {
-		s.pauseManager.DeviceWake()
-	}
-}
-
-func (s *BoxService) ResetNetwork() {
-	s.instance.Router().ResetNetwork()
-}
-
-func (s *BoxService) UpdateWIFIState() {
-	s.instance.Network().UpdateWIFIState()
-}

+ 27 - 31
experimental/libbox/setup.go

@@ -2,9 +2,7 @@ package libbox
 
 import (
 	"os"
-	"os/user"
 	"runtime/debug"
-	"strconv"
 	"time"
 
 	C "github.com/sagernet/sing-box/constant"
@@ -14,55 +12,53 @@ import (
 )
 
 var (
-	sBasePath        string
-	sWorkingPath     string
-	sTempPath        string
-	sUserID          int
-	sGroupID         int
-	sTVOS            bool
-	sFixAndroidStack bool
+	sBasePath                string
+	sWorkingPath             string
+	sTempPath                string
+	sUserID                  int
+	sGroupID                 int
+	sFixAndroidStack         bool
+	sCommandServerListenPort uint16
+	sCommandServerSecret     string
+	sLogMaxLines             int
+	sDebug                   bool
 )
 
 func init() {
 	debug.SetPanicOnFault(true)
+	debug.SetTraceback("all")
 }
 
 type SetupOptions struct {
-	BasePath        string
-	WorkingPath     string
-	TempPath        string
-	Username        string
-	IsTVOS          bool
-	FixAndroidStack bool
+	BasePath                string
+	WorkingPath             string
+	TempPath                string
+	FixAndroidStack         bool
+	CommandServerListenPort int32
+	CommandServerSecret     string
+	LogMaxLines             int
+	Debug                   bool
 }
 
 func Setup(options *SetupOptions) error {
 	sBasePath = options.BasePath
 	sWorkingPath = options.WorkingPath
 	sTempPath = options.TempPath
-	if options.Username != "" {
-		sUser, err := user.Lookup(options.Username)
-		if err != nil {
-			return err
-		}
-		sUserID, _ = strconv.Atoi(sUser.Uid)
-		sGroupID, _ = strconv.Atoi(sUser.Gid)
-	} else {
-		sUserID = os.Getuid()
-		sGroupID = os.Getgid()
-	}
-	sTVOS = options.IsTVOS
+
+	sUserID = os.Getuid()
+	sGroupID = os.Getgid()
 
 	// TODO: remove after fixed
 	// https://github.com/golang/go/issues/68760
 	sFixAndroidStack = options.FixAndroidStack
 
+	sCommandServerListenPort = uint16(options.CommandServerListenPort)
+	sCommandServerSecret = options.CommandServerSecret
+	sLogMaxLines = options.LogMaxLines
+	sDebug = options.Debug
+
 	os.MkdirAll(sWorkingPath, 0o777)
 	os.MkdirAll(sTempPath, 0o777)
-	if options.Username != "" {
-		os.Chown(sWorkingPath, sUserID, sGroupID)
-		os.Chown(sTempPath, sUserID, sGroupID)
-	}
 	return nil
 }
 

+ 1 - 1
experimental/libbox/tun.go

@@ -5,7 +5,7 @@ import (
 	"net/netip"
 
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-tun"
+	tun "github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 )

+ 23 - 21
log/observable.go

@@ -50,9 +50,9 @@ func NewDefaultFactory(
 		level:          LevelTrace,
 		subscriber:     observable.NewSubscriber[Entry](128),
 	}
-	if platformWriter != nil {
+	/*if platformWriter != nil {
 		factory.platformFormatter.DisableColors = platformWriter.DisableColors()
-	}
+	}*/
 	if needObservable {
 		factory.observer = observable.NewObserver[Entry](factory.subscriber, 64)
 	}
@@ -111,28 +111,30 @@ type observableLogger struct {
 
 func (l *observableLogger) Log(ctx context.Context, level Level, args []any) {
 	level = OverrideLevelFromContext(level, ctx)
-	if level > l.level {
+	if level > l.level && l.platformWriter == nil {
 		return
 	}
 	nowTime := time.Now()
-	if l.needObservable {
-		message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), nowTime)
-		if level == LevelPanic {
-			panic(message)
-		}
-		l.writer.Write([]byte(message))
-		if level == LevelFatal {
-			os.Exit(1)
-		}
-		l.subscriber.Emit(Entry{level, messageSimple})
-	} else {
-		message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime)
-		if level == LevelPanic {
-			panic(message)
-		}
-		l.writer.Write([]byte(message))
-		if level == LevelFatal {
-			os.Exit(1)
+	if level <= l.level {
+		if l.needObservable {
+			message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), nowTime)
+			if level == LevelPanic {
+				panic(message)
+			}
+			l.writer.Write([]byte(message))
+			if level == LevelFatal {
+				os.Exit(1)
+			}
+			l.subscriber.Emit(Entry{level, messageSimple})
+		} else {
+			message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime)
+			if level == LevelPanic {
+				panic(message)
+			}
+			l.writer.Write([]byte(message))
+			if level == LevelFatal {
+				os.Exit(1)
+			}
 		}
 	}
 	if l.platformWriter != nil {

+ 0 - 1
log/platform.go

@@ -1,6 +1,5 @@
 package log
 
 type PlatformWriter interface {
-	DisableColors() bool
 	WriteMessage(level Level, message string)
 }

+ 3 - 4
protocol/tailscale/endpoint.go

@@ -28,7 +28,6 @@ import (
 	"github.com/sagernet/sing-box/adapter/endpoint"
 	"github.com/sagernet/sing-box/common/dialer"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/route/rule"
@@ -79,7 +78,7 @@ type Endpoint struct {
 	logger            logger.ContextLogger
 	dnsRouter         adapter.DNSRouter
 	network           adapter.NetworkManager
-	platformInterface platform.Interface
+	platformInterface adapter.PlatformInterface
 	server            *tsnet.Server
 	stack             *stack.Stack
 	icmpForwarder     *tun.ICMPForwarder
@@ -188,7 +187,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
 		logger:                 logger,
 		dnsRouter:              dnsRouter,
 		network:                service.FromContext[adapter.NetworkManager](ctx),
-		platformInterface:      service.FromContext[platform.Interface](ctx),
+		platformInterface:      service.FromContext[adapter.PlatformInterface](ctx),
 		server:                 server,
 		acceptRoutes:           options.AcceptRoutes,
 		exitNode:               options.ExitNode,
@@ -288,7 +287,7 @@ func (t *Endpoint) watchState() {
 		if authURL != "" {
 			t.logger.Info("Waiting for authentication: ", authURL)
 			if t.platformInterface != nil {
-				err := t.platformInterface.SendNotification(&platform.Notification{
+				err := t.platformInterface.SendNotification(&adapter.Notification{
 					Identifier: "tailscale-authentication",
 					TypeName:   "Tailscale Authentication Notifications",
 					TypeID:     10,

+ 2 - 2
protocol/tailscale/protect_android.go

@@ -1,11 +1,11 @@
 package tailscale
 
 import (
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
+	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/tailscale/net/netns"
 )
 
-func setAndroidProtectFunc(platformInterface platform.Interface) {
+func setAndroidProtectFunc(platformInterface adapter.PlatformInterface) {
 	if platformInterface != nil {
 		netns.SetAndroidProtectFunc(func(fd int) error {
 			return platformInterface.AutoDetectInterfaceControl(fd)

+ 2 - 2
protocol/tailscale/protect_nonandroid.go

@@ -2,7 +2,7 @@
 
 package tailscale
 
-import "github.com/sagernet/sing-box/experimental/libbox/platform"
+import "github.com/sagernet/sing-box/adapter"
 
-func setAndroidProtectFunc(platformInterface platform.Interface) {
+func setAndroidProtectFunc(platformInterface adapter.PlatformInterface) {
 }

+ 5 - 6
protocol/tun/inbound.go

@@ -15,7 +15,6 @@ import (
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/experimental/deprecated"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/route/rule"
@@ -49,7 +48,7 @@ type Inbound struct {
 	stack                       string
 	tunIf                       tun.Tun
 	tunStack                    tun.Stack
-	platformInterface           platform.Interface
+	platformInterface           adapter.PlatformInterface
 	platformOptions             option.TunPlatformOptions
 	autoRedirect                tun.AutoRedirect
 	routeRuleSet                []adapter.RuleSet
@@ -131,7 +130,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 		deprecated.Report(ctx, deprecated.OptionTUNGSO)
 	}
 
-	platformInterface := service.FromContext[platform.Interface](ctx)
+	platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
 	tunMTU := options.MTU
 	enableGSO := C.IsLinux && options.Stack == "gvisor" && platformInterface == nil && tunMTU > 0 && tunMTU < 49152
 	if tunMTU == 0 {
@@ -372,8 +371,8 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
 			}
 		}
 		monitor.Start("open interface")
-		if t.platformInterface != nil {
-			tunInterface, err = t.platformInterface.OpenTun(&tunOptions, t.platformOptions)
+		if t.platformInterface != nil && t.platformInterface.UsePlatformInterface() {
+			tunInterface, err = t.platformInterface.OpenInterface(&tunOptions, t.platformOptions)
 		} else {
 			if HookBeforeCreatePlatformInterface != nil {
 				HookBeforeCreatePlatformInterface()
@@ -393,7 +392,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
 		)
 		if t.platformInterface != nil {
 			forwarderBindInterface = true
-			includeAllNetworks = t.platformInterface.IncludeAllNetworks()
+			includeAllNetworks = t.platformInterface.NetworkExtensionIncludeAllNetworks()
 		}
 		tunStack, err := tun.NewStack(t.stack, tun.StackOptions{
 			Context:                t.ctx,

+ 4 - 5
route/network.go

@@ -15,7 +15,6 @@ import (
 	"github.com/sagernet/sing-box/common/conntrack"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
@@ -46,7 +45,7 @@ type NetworkManager struct {
 	packageManager         tun.PackageManager
 	powerListener          winpowrprof.EventListener
 	pauseManager           pause.Manager
-	platformInterface      platform.Interface
+	platformInterface      adapter.PlatformInterface
 	endpoint               adapter.EndpointManager
 	inbound                adapter.InboundManager
 	outbound               adapter.OutboundManager
@@ -85,7 +84,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp
 			FallbackDelay:       time.Duration(routeOptions.DefaultFallbackDelay),
 		},
 		pauseManager:      service.FromContext[pause.Manager](ctx),
-		platformInterface: service.FromContext[platform.Interface](ctx),
+		platformInterface: service.FromContext[adapter.PlatformInterface](ctx),
 		endpoint:          service.FromContext[adapter.EndpointManager](ctx),
 		inbound:           service.FromContext[adapter.InboundManager](ctx),
 		outbound:          service.FromContext[adapter.OutboundManager](ctx),
@@ -227,10 +226,10 @@ func (r *NetworkManager) InterfaceFinder() control.InterfaceFinder {
 }
 
 func (r *NetworkManager) UpdateInterfaces() error {
-	if r.platformInterface == nil {
+	if r.platformInterface == nil || !r.platformInterface.UsePlatformNetworkInterfaces() {
 		return r.interfaceFinder.Update()
 	} else {
-		interfaces, err := r.platformInterface.Interfaces()
+		interfaces, err := r.platformInterface.NetworkInterfaces()
 		if err != nil {
 			return err
 		}

+ 45 - 0
route/platform_searcher.go

@@ -0,0 +1,45 @@
+package route
+
+import (
+	"context"
+	"net/netip"
+	"syscall"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/process"
+	N "github.com/sagernet/sing/common/network"
+)
+
+type platformSearcher struct {
+	platform adapter.PlatformInterface
+}
+
+func newPlatformSearcher(platform adapter.PlatformInterface) process.Searcher {
+	return &platformSearcher{platform: platform}
+}
+
+func (s *platformSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
+	if !s.platform.UsePlatformConnectionOwnerFinder() {
+		return nil, process.ErrNotFound
+	}
+
+	var ipProtocol int32
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
+		ipProtocol = syscall.IPPROTO_TCP
+	case N.NetworkUDP:
+		ipProtocol = syscall.IPPROTO_UDP
+	default:
+		return nil, process.ErrNotFound
+	}
+
+	request := &adapter.FindConnectionOwnerRequest{
+		IpProtocol:         ipProtocol,
+		SourceAddress:      source.Addr().String(),
+		SourcePort:         int32(source.Port()),
+		DestinationAddress: destination.Addr().String(),
+		DestinationPort:    int32(destination.Port()),
+	}
+
+	return s.platform.FindConnectionOwner(request)
+}

+ 6 - 6
route/route.go

@@ -382,18 +382,18 @@ func (r *Router) matchRule(
 			r.logger.InfoContext(ctx, "failed to search process: ", fErr)
 		} else {
 			if processInfo.ProcessPath != "" {
-				if processInfo.User != "" {
-					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.User)
+				if processInfo.UserName != "" {
+					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.UserName)
 				} else if processInfo.UserId != -1 {
 					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId)
 				} else {
 					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
 				}
-			} else if processInfo.PackageName != "" {
-				r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
+			} else if processInfo.AndroidPackageName != "" {
+				r.logger.InfoContext(ctx, "found package name: ", processInfo.AndroidPackageName)
 			} else if processInfo.UserId != -1 {
-				if processInfo.User != "" {
-					r.logger.InfoContext(ctx, "found user: ", processInfo.User)
+				if processInfo.UserName != "" {
+					r.logger.InfoContext(ctx, "found user: ", processInfo.UserName)
 				} else {
 					r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId)
 				}

+ 4 - 5
route/router.go

@@ -9,7 +9,6 @@ import (
 	"github.com/sagernet/sing-box/common/process"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	R "github.com/sagernet/sing-box/route/rule"
@@ -37,7 +36,7 @@ type Router struct {
 	processSearcher   process.Searcher
 	pauseManager      pause.Manager
 	trackers          []adapter.ConnectionTracker
-	platformInterface platform.Interface
+	platformInterface adapter.PlatformInterface
 	needWIFIState     bool
 	started           bool
 }
@@ -56,7 +55,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route
 		ruleSetMap:        make(map[string]adapter.RuleSet),
 		needFindProcess:   hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess,
 		pauseManager:      service.FromContext[pause.Manager](ctx),
-		platformInterface: service.FromContext[platform.Interface](ctx),
+		platformInterface: service.FromContext[adapter.PlatformInterface](ctx),
 		needWIFIState:     hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule),
 	}
 }
@@ -124,8 +123,8 @@ func (r *Router) Start(stage adapter.StartStage) error {
 			}
 		}
 		if needFindProcess {
-			if r.platformInterface != nil {
-				r.processSearcher = r.platformInterface
+			if r.platformInterface != nil && r.platformInterface.UsePlatformConnectionOwnerFinder() {
+				r.processSearcher = newPlatformSearcher(r.platformInterface)
 			} else {
 				monitor.Start("initialize process searcher")
 				searcher, err := process.NewSearcher(process.Config{

+ 2 - 2
route/rule/rule_item_package_name.go

@@ -25,10 +25,10 @@ func NewPackageNameItem(packageNameList []string) *PackageNameItem {
 }
 
 func (r *PackageNameItem) Match(metadata *adapter.InboundContext) bool {
-	if metadata.ProcessInfo == nil || metadata.ProcessInfo.PackageName == "" {
+	if metadata.ProcessInfo == nil || metadata.ProcessInfo.AndroidPackageName == "" {
 		return false
 	}
-	return r.packageMap[metadata.ProcessInfo.PackageName]
+	return r.packageMap[metadata.ProcessInfo.AndroidPackageName]
 }
 
 func (r *PackageNameItem) String() string {

+ 2 - 2
route/rule/rule_item_user.go

@@ -26,10 +26,10 @@ func NewUserItem(users []string) *UserItem {
 }
 
 func (r *UserItem) Match(metadata *adapter.InboundContext) bool {
-	if metadata.ProcessInfo == nil || metadata.ProcessInfo.User == "" {
+	if metadata.ProcessInfo == nil || metadata.ProcessInfo.UserName == "" {
 		return false
 	}
-	return r.userMap[metadata.ProcessInfo.User]
+	return r.userMap[metadata.ProcessInfo.UserName]
 }
 
 func (r *UserItem) String() string {

+ 6 - 7
service/resolved/resolve1.go

@@ -15,7 +15,6 @@ import (
 	"syscall"
 
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/process"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/dns"
 	"github.com/sagernet/sing-box/log"
@@ -111,7 +110,7 @@ func (t *resolve1Manager) createMetadata(sender dbus.Sender) adapter.InboundCont
 	if err != nil {
 		return metadata
 	}
-	var processInfo process.Info
+	var processInfo adapter.ConnectionOwner
 	metadata.ProcessInfo = &processInfo
 	processInfo.ProcessID = uint32(senderPid)
 
@@ -140,7 +139,7 @@ func (t *resolve1Manager) createMetadata(sender dbus.Sender) adapter.InboundCont
 					processInfo.UserId = int32(uid)
 					uidFound = true
 					if osUser, _ := user.LookupId(F.ToString(uid)); osUser != nil {
-						processInfo.User = osUser.Username
+						processInfo.UserName = osUser.Username
 					}
 					break
 				}
@@ -159,8 +158,8 @@ func (t *resolve1Manager) log(sender dbus.Sender, message ...any) {
 		var prefix string
 		if metadata.ProcessInfo.ProcessPath != "" {
 			prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
-		} else if metadata.ProcessInfo.User != "" {
-			prefix = F.ToString("user:", metadata.ProcessInfo.User)
+		} else if metadata.ProcessInfo.UserName != "" {
+			prefix = F.ToString("user:", metadata.ProcessInfo.UserName)
 		} else if metadata.ProcessInfo.UserId != 0 {
 			prefix = F.ToString("uid:", metadata.ProcessInfo.UserId)
 		}
@@ -177,8 +176,8 @@ func (t *resolve1Manager) logRequest(sender dbus.Sender, message ...any) context
 		var prefix string
 		if metadata.ProcessInfo.ProcessPath != "" {
 			prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
-		} else if metadata.ProcessInfo.User != "" {
-			prefix = F.ToString("user:", metadata.ProcessInfo.User)
+		} else if metadata.ProcessInfo.UserName != "" {
+			prefix = F.ToString("user:", metadata.ProcessInfo.UserName)
 		} else if metadata.ProcessInfo.UserId != 0 {
 			prefix = F.ToString("uid:", metadata.ProcessInfo.UserId)
 		}