Browse Source

A bit of refactor

世界 1 week ago
parent
commit
f1e48a1cad
62 changed files with 6006 additions and 1901 deletions
  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"
 	"net/netip"
 	"time"
 	"time"
 
 
-	"github.com/sagernet/sing-box/common/process"
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
@@ -85,7 +84,7 @@ type InboundContext struct {
 	DestinationAddresses []netip.Addr
 	DestinationAddresses []netip.Addr
 	SourceGeoIPCode      string
 	SourceGeoIPCode      string
 	GeoIPCode            string
 	GeoIPCode            string
-	ProcessInfo          *process.Info
+	ProcessInfo          *ConnectionOwner
 	QueryType            uint16
 	QueryType            uint16
 	FakeIP               bool
 	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/dns/transport/local"
 	"github.com/sagernet/sing-box/experimental"
 	"github.com/sagernet/sing-box/experimental"
 	"github.com/sagernet/sing-box/experimental/cachefile"
 	"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/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/protocol/direct"
 	"github.com/sagernet/sing-box/protocol/direct"
@@ -139,7 +138,7 @@ func New(options Options) (*Box, error) {
 	if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
 	if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
 		needV2RayAPI = true
 		needV2RayAPI = true
 	}
 	}
-	platformInterface := service.FromContext[platform.Interface](ctx)
+	platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
 	var defaultLogWriter io.Writer
 	var defaultLogWriter io.Writer
 	if platformInterface != nil {
 	if platformInterface != nil {
 		defaultLogWriter = io.Discard
 		defaultLogWriter = io.Discard
@@ -527,3 +526,7 @@ func (s *Box) Inbound() adapter.InboundManager {
 func (s *Box) Outbound() adapter.OutboundManager {
 func (s *Box) Outbound() adapter.OutboundManager {
 	return s.outbound
 	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/fswatch"
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	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-box/option"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
 	"github.com/sagernet/sing/common/logger"
@@ -36,7 +35,7 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
 	switch options.Store {
 	switch options.Store {
 	case C.CertificateStoreSystem, "":
 	case C.CertificateStoreSystem, "":
 		systemPool = x509.NewCertPool()
 		systemPool = x509.NewCertPool()
-		platformInterface := service.FromContext[platform.Interface](ctx)
+		platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
 		var systemValid bool
 		var systemValid bool
 		if platformInterface != nil {
 		if platformInterface != nil {
 			for _, cert := range platformInterface.SystemCertificates() {
 			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/conntrack"
 	"github.com/sagernet/sing-box/common/listener"
 	"github.com/sagernet/sing-box/common/listener"
 	C "github.com/sagernet/sing-box/constant"
 	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-box/option"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/control"
 	"github.com/sagernet/sing/common/control"
@@ -49,7 +48,7 @@ type DefaultDialer struct {
 
 
 func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
 func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
 	networkManager := service.FromContext[adapter.NetworkManager](ctx)
 	networkManager := service.FromContext[adapter.NetworkManager](ctx)
-	platformInterface := service.FromContext[platform.Interface](ctx)
+	platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
 
 
 	var (
 	var (
 		dialer                 net.Dialer
 		dialer                 net.Dialer

+ 4 - 11
common/process/searcher.go

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

+ 9 - 8
common/process/searcher_android.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"context"
 	"net/netip"
 	"net/netip"
 
 
+	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing-tun"
 )
 )
 
 
@@ -17,22 +18,22 @@ func NewSearcher(config Config) (Searcher, error) {
 	return &androidSearcher{config.PackageManager}, nil
 	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)
 	_, uid, err := resolveSocketByNetlink(network, source, destination)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 	if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
 	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
 		}, nil
 	}
 	}
 	if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
 	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
 		}, 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"
 	"syscall"
 	"unsafe"
 	"unsafe"
 
 
+	"github.com/sagernet/sing-box/adapter"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 
 
 	"golang.org/x/sys/unix"
 	"golang.org/x/sys/unix"
@@ -23,12 +24,12 @@ func NewSearcher(_ Config) (Searcher, error) {
 	return &darwinSearcher{}, nil
 	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()))
 	processName, err := findProcessName(network, source.Addr(), int(source.Port()))
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	return &Info{ProcessPath: processName, UserId: -1}, nil
+	return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
 }
 }
 
 
 var structSize = func() int {
 var structSize = func() int {

+ 3 - 2
common/process/searcher_linux.go

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

+ 4 - 3
common/process/searcher_windows.go

@@ -5,6 +5,7 @@ import (
 	"net/netip"
 	"net/netip"
 	"syscall"
 	"syscall"
 
 
+	"github.com/sagernet/sing-box/adapter"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/winiphlpapi"
 	"github.com/sagernet/sing/common/winiphlpapi"
 
 
@@ -27,16 +28,16 @@ func initWin32API() error {
 	return winiphlpapi.LoadExtendedTable()
 	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)
 	pid, err := winiphlpapi.FindPid(network, source)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 	path, err := getProcessPath(pid)
 	path, err := getProcessPath(pid)
 	if err != nil {
 	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) {
 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/adapter"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
 	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/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
 	R "github.com/sagernet/sing-box/route/rule"
 	R "github.com/sagernet/sing-box/route/rule"
@@ -38,7 +37,7 @@ type Router struct {
 	rules                 []adapter.DNSRule
 	rules                 []adapter.DNSRule
 	defaultDomainStrategy C.DomainStrategy
 	defaultDomainStrategy C.DomainStrategy
 	dnsReverseMapping     freelru.Cache[netip.Addr, string]
 	dnsReverseMapping     freelru.Cache[netip.Addr, string]
-	platformInterface     platform.Interface
+	platformInterface     adapter.PlatformInterface
 }
 }
 
 
 func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
 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 != nil {
 		if t.Metadata.ProcessInfo.ProcessPath != "" {
 		if t.Metadata.ProcessInfo.ProcessPath != "" {
 			processPath = 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 processPath == "" {
 			if t.Metadata.ProcessInfo.UserId != -1 {
 			if t.Metadata.ProcessInfo.UserId != -1 {
 				processPath = F.ToString(t.Metadata.ProcessInfo.UserId)
 				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 {
 		} else if t.Metadata.ProcessInfo.UserId != -1 {
 			processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")")
 			processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")")
 		}
 		}

+ 0 - 11
experimental/libbox/command.go

@@ -3,18 +3,7 @@ package libbox
 const (
 const (
 	CommandLog int32 = iota
 	CommandLog int32 = iota
 	CommandStatus
 	CommandStatus
-	CommandServiceReload
-	CommandServiceClose
-	CommandCloseConnections
 	CommandGroup
 	CommandGroup
-	CommandSelectOutbound
-	CommandURLTest
-	CommandGroupExpand
 	CommandClashMode
 	CommandClashMode
-	CommandSetClashMode
-	CommandGetSystemProxyStatus
-	CommandSetSystemProxyEnabled
 	CommandConnections
 	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
 package libbox
 
 
 import (
 import (
-	"encoding/binary"
+	"context"
 	"net"
 	"net"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"strconv"
+	"sync"
 	"time"
 	"time"
 
 
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/daemon"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	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 {
 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 {
 type CommandClientOptions struct {
 	Command        int32
 	Command        int32
+	Commands       Int32Iterator
 	StatusInterval int64
 	StatusInterval int64
 }
 }
 
 
 type CommandClientHandler interface {
 type CommandClientHandler interface {
 	Connected()
 	Connected()
 	Disconnected(message string)
 	Disconnected(message string)
+	SetDefaultLogLevel(level int32)
 	ClearLogs()
 	ClearLogs()
-	WriteLogs(messageList StringIterator)
+	WriteLogs(messageList LogIterator)
 	WriteStatus(message *StatusMessage)
 	WriteStatus(message *StatusMessage)
 	WriteGroups(message OutboundGroupIterator)
 	WriteGroups(message OutboundGroupIterator)
 	InitializeClashMode(modeList StringIterator, currentMode string)
 	InitializeClashMode(modeList StringIterator, currentMode string)
@@ -34,6 +48,17 @@ type CommandClientHandler interface {
 	WriteConnections(message *Connections)
 	WriteConnections(message *Connections)
 }
 }
 
 
+type LogEntry struct {
+	Level   int32
+	Message string
+}
+
+type LogIterator interface {
+	Len() int32
+	HasNext() bool
+	Next() *LogEntry
+}
+
 func NewStandaloneCommandClient() *CommandClient {
 func NewStandaloneCommandClient() *CommandClient {
 	return new(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 {
 	} 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 (
 	var (
-		conn net.Conn
+		conn *grpc.ClientConn
 		err  error
 		err  error
 	)
 	)
 	for i := 0; i < 10; i++ {
 	for i := 0; i < 10; i++ {
-		conn, err = c.directConnect()
+		conn, err = grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
 		if err == nil {
 		if err == nil {
 			return conn, nil
 			return conn, nil
 		}
 		}
@@ -72,79 +97,365 @@ func (c *CommandClient) directConnectWithRetry() (net.Conn, error) {
 }
 }
 
 
 func (c *CommandClient) Connect() 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 {
 	if err != nil {
+		c.clientMutex.Unlock()
 		return err
 		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 {
 	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 {
 		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 {
 		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 {
 		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.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())
 				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 {
 		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
 package libbox
 
 
 import (
 import (
-	"encoding/binary"
+	"context"
 	"net"
 	"net"
 	"os"
 	"os"
 	"path/filepath"
 	"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-box/log"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
-	"github.com/sagernet/sing/common/debug"
 	E "github.com/sagernet/sing/common/exceptions"
 	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"
 	"github.com/sagernet/sing/service"
+
+	"google.golang.org/grpc"
 )
 )
 
 
 type CommandServer struct {
 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 {
 type CommandServerHandler interface {
+	ServiceStop() error
 	ServiceReload() 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{
 	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 {
 	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 (
 import (
 	"bytes"
 	"bytes"
 	"context"
 	"context"
-	"net/netip"
 	"os"
 	"os"
 
 
-	"github.com/sagernet/sing-box"
+	box "github.com/sagernet/sing-box"
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
-	"github.com/sagernet/sing-box/common/process"
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/dns"
 	"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/include"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing-tun"
+	tun "github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common/control"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/json"
 	"github.com/sagernet/sing/common/json"
@@ -55,7 +52,7 @@ func CheckConfig(configContent string) error {
 	}
 	}
 	ctx, cancel := context.WithCancel(ctx)
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 	defer cancel()
-	ctx = service.ContextWith[platform.Interface](ctx, (*platformInterfaceStub)(nil))
+	ctx = service.ContextWith[adapter.PlatformInterface](ctx, (*platformInterfaceStub)(nil))
 	instance, err := box.New(box.Options{
 	instance, err := box.New(box.Options{
 		Context: ctx,
 		Context: ctx,
 		Options: options,
 		Options: options,
@@ -80,7 +77,11 @@ func (s *platformInterfaceStub) AutoDetectInterfaceControl(fd int) error {
 	return nil
 	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
 	return nil, os.ErrInvalid
 }
 }
 
 
@@ -92,7 +93,11 @@ func (s *platformInterfaceStub) CreateDefaultInterfaceMonitor(logger logger.Logg
 	return (*interfaceMonitorStub)(nil)
 	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
 	return nil, os.ErrInvalid
 }
 }
 
 
@@ -100,13 +105,17 @@ func (s *platformInterfaceStub) UnderNetworkExtension() bool {
 	return false
 	return false
 }
 }
 
 
-func (s *platformInterfaceStub) IncludeAllNetworks() bool {
+func (s *platformInterfaceStub) NetworkExtensionIncludeAllNetworks() bool {
 	return false
 	return false
 }
 }
 
 
 func (s *platformInterfaceStub) ClearDNSCache() {
 func (s *platformInterfaceStub) ClearDNSCache() {
 }
 }
 
 
+func (s *platformInterfaceStub) RequestPermissionForWIFIState() error {
+	return nil
+}
+
 func (s *platformInterfaceStub) ReadWIFIState() adapter.WIFIState {
 func (s *platformInterfaceStub) ReadWIFIState() adapter.WIFIState {
 	return adapter.WIFIState{}
 	return adapter.WIFIState{}
 }
 }
@@ -115,11 +124,27 @@ func (s *platformInterfaceStub) SystemCertificates() []string {
 	return nil
 	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
 	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
 	return nil
 }
 }
 
 

+ 14 - 11
experimental/libbox/http.go

@@ -77,22 +77,27 @@ func NewHTTPClient() HTTPClient {
 }
 }
 
 
 func (c *httpClient) ModernTLS() {
 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() {
 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
 		return it.ID
 	})
 	})
 }
 }
 
 
 func (c *httpClient) PinnedTLS12() {
 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) {
 func (c *httpClient) PinnedSHA256(sumHex string) {
@@ -178,9 +183,7 @@ func (r *httpRequest) SetUserAgent(userAgent string) {
 }
 }
 
 
 func (r *httpRequest) SetContent(content []byte) {
 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))
 	r.request.ContentLength = int64(len(content))
 }
 }
 
 

+ 6 - 0
experimental/libbox/iterator.go

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

+ 1 - 1
experimental/libbox/monitor.go

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

+ 0 - 6
experimental/libbox/platform.go

@@ -10,7 +10,6 @@ type PlatformInterface interface {
 	UsePlatformAutoDetectInterfaceControl() bool
 	UsePlatformAutoDetectInterfaceControl() bool
 	AutoDetectInterfaceControl(fd int32) error
 	AutoDetectInterfaceControl(fd int32) error
 	OpenTun(options TunOptions) (int32, error)
 	OpenTun(options TunOptions) (int32, error)
-	WriteLog(message string)
 	UseProcFS() bool
 	UseProcFS() bool
 	FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (int32, error)
 	FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (int32, error)
 	PackageNameByUid(uid int32) (string, error)
 	PackageNameByUid(uid int32) (string, error)
@@ -26,11 +25,6 @@ type PlatformInterface interface {
 	SendNotification(notification *Notification) error
 	SendNotification(notification *Notification) error
 }
 }
 
 
-type TunInterface interface {
-	FileDescriptor() int32
-	Close() error
-}
-
 type InterfaceUpdateListener interface {
 type InterfaceUpdateListener interface {
 	UpdateDefaultInterface(interfaceName string, interfaceIndex int32, isExpensive bool, isConstrained bool)
 	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 (
 import (
 	"context"
 	"context"
 	"net/netip"
 	"net/netip"
-	"os"
 	"runtime"
 	"runtime"
-	runtimeDebug "runtime/debug"
 	"sync"
 	"sync"
 	"syscall"
 	"syscall"
-	"time"
 
 
-	"github.com/sagernet/sing-box"
 	"github.com/sagernet/sing-box/adapter"
 	"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"
 	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/internal/procfs"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
 	"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"
 	"github.com/sagernet/sing/common/control"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
 	"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 {
 type platformInterfaceWrapper struct {
 	iif                    PlatformInterface
 	iif                    PlatformInterface
@@ -143,7 +47,11 @@ func (w *platformInterfaceWrapper) AutoDetectInterfaceControl(fd int) error {
 	return w.iif.AutoDetectInterfaceControl(int32(fd))
 	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 {
 	if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 {
 		return nil, E.New("platform: unsupported uid options")
 		return nil, E.New("platform: unsupported uid options")
 	}
 	}
@@ -172,6 +80,10 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions
 	return tun.New(*options)
 	return tun.New(*options)
 }
 }
 
 
+func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
+	return true
+}
+
 func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor {
 func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor {
 	return &platformDefaultInterfaceMonitor{
 	return &platformDefaultInterfaceMonitor{
 		platformInterfaceWrapper: w,
 		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()
 	interfaceIterator, err := w.iif.GetInterfaces()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -216,7 +132,7 @@ func (w *platformInterfaceWrapper) UnderNetworkExtension() bool {
 	return w.iif.UnderNetworkExtension()
 	return w.iif.UnderNetworkExtension()
 }
 }
 
 
-func (w *platformInterfaceWrapper) IncludeAllNetworks() bool {
+func (w *platformInterfaceWrapper) NetworkExtensionIncludeAllNetworks() bool {
 	return w.iif.IncludeAllNetworks()
 	return w.iif.IncludeAllNetworks()
 }
 }
 
 
@@ -224,6 +140,10 @@ func (w *platformInterfaceWrapper) ClearDNSCache() {
 	w.iif.ClearDNSCache()
 	w.iif.ClearDNSCache()
 }
 }
 
 
+func (w *platformInterfaceWrapper) RequestPermissionForWIFIState() error {
+	return nil
+}
+
 func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
 func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
 	wifiState := w.iif.ReadWIFIState()
 	wifiState := w.iif.ReadWIFIState()
 	if wifiState == nil {
 	if wifiState == nil {
@@ -236,41 +156,70 @@ func (w *platformInterfaceWrapper) SystemCertificates() []string {
 	return iteratorToArray[string](w.iif.SystemCertificates())
 	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
 	var uid int32
 	if w.useProcFS {
 	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)
 		uid = procfs.ResolveSocketByProcSearch(network, source, destination)
 		if uid == -1 {
 		if uid == -1 {
 			return nil, E.New("procfs: not found")
 			return nil, E.New("procfs: not found")
 		}
 		}
 	} else {
 	} 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
 		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 {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 	}
 	}
 	packageName, _ := w.iif.PackageNameByUid(uid)
 	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 {
 func (w *platformInterfaceWrapper) DisableColors() bool {
 	return runtime.GOOS != "android"
 	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))
 	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 (
 import (
 	"os"
 	"os"
-	"os/user"
 	"runtime/debug"
 	"runtime/debug"
-	"strconv"
 	"time"
 	"time"
 
 
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
@@ -14,55 +12,53 @@ import (
 )
 )
 
 
 var (
 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() {
 func init() {
 	debug.SetPanicOnFault(true)
 	debug.SetPanicOnFault(true)
+	debug.SetTraceback("all")
 }
 }
 
 
 type SetupOptions struct {
 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 {
 func Setup(options *SetupOptions) error {
 	sBasePath = options.BasePath
 	sBasePath = options.BasePath
 	sWorkingPath = options.WorkingPath
 	sWorkingPath = options.WorkingPath
 	sTempPath = options.TempPath
 	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
 	// TODO: remove after fixed
 	// https://github.com/golang/go/issues/68760
 	// https://github.com/golang/go/issues/68760
 	sFixAndroidStack = options.FixAndroidStack
 	sFixAndroidStack = options.FixAndroidStack
 
 
+	sCommandServerListenPort = uint16(options.CommandServerListenPort)
+	sCommandServerSecret = options.CommandServerSecret
+	sLogMaxLines = options.LogMaxLines
+	sDebug = options.Debug
+
 	os.MkdirAll(sWorkingPath, 0o777)
 	os.MkdirAll(sWorkingPath, 0o777)
 	os.MkdirAll(sTempPath, 0o777)
 	os.MkdirAll(sTempPath, 0o777)
-	if options.Username != "" {
-		os.Chown(sWorkingPath, sUserID, sGroupID)
-		os.Chown(sTempPath, sUserID, sGroupID)
-	}
 	return nil
 	return nil
 }
 }
 
 

+ 1 - 1
experimental/libbox/tun.go

@@ -5,7 +5,7 @@ import (
 	"net/netip"
 	"net/netip"
 
 
 	"github.com/sagernet/sing-box/option"
 	"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"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 )
 )

+ 23 - 21
log/observable.go

@@ -50,9 +50,9 @@ func NewDefaultFactory(
 		level:          LevelTrace,
 		level:          LevelTrace,
 		subscriber:     observable.NewSubscriber[Entry](128),
 		subscriber:     observable.NewSubscriber[Entry](128),
 	}
 	}
-	if platformWriter != nil {
+	/*if platformWriter != nil {
 		factory.platformFormatter.DisableColors = platformWriter.DisableColors()
 		factory.platformFormatter.DisableColors = platformWriter.DisableColors()
-	}
+	}*/
 	if needObservable {
 	if needObservable {
 		factory.observer = observable.NewObserver[Entry](factory.subscriber, 64)
 		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) {
 func (l *observableLogger) Log(ctx context.Context, level Level, args []any) {
 	level = OverrideLevelFromContext(level, ctx)
 	level = OverrideLevelFromContext(level, ctx)
-	if level > l.level {
+	if level > l.level && l.platformWriter == nil {
 		return
 		return
 	}
 	}
 	nowTime := time.Now()
 	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 {
 	if l.platformWriter != nil {

+ 0 - 1
log/platform.go

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

+ 2 - 2
protocol/tailscale/protect_android.go

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

+ 2 - 2
protocol/tailscale/protect_nonandroid.go

@@ -2,7 +2,7 @@
 
 
 package tailscale
 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"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/experimental/deprecated"
 	"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/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/route/rule"
 	"github.com/sagernet/sing-box/route/rule"
@@ -49,7 +48,7 @@ type Inbound struct {
 	stack                       string
 	stack                       string
 	tunIf                       tun.Tun
 	tunIf                       tun.Tun
 	tunStack                    tun.Stack
 	tunStack                    tun.Stack
-	platformInterface           platform.Interface
+	platformInterface           adapter.PlatformInterface
 	platformOptions             option.TunPlatformOptions
 	platformOptions             option.TunPlatformOptions
 	autoRedirect                tun.AutoRedirect
 	autoRedirect                tun.AutoRedirect
 	routeRuleSet                []adapter.RuleSet
 	routeRuleSet                []adapter.RuleSet
@@ -131,7 +130,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
 		deprecated.Report(ctx, deprecated.OptionTUNGSO)
 		deprecated.Report(ctx, deprecated.OptionTUNGSO)
 	}
 	}
 
 
-	platformInterface := service.FromContext[platform.Interface](ctx)
+	platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
 	tunMTU := options.MTU
 	tunMTU := options.MTU
 	enableGSO := C.IsLinux && options.Stack == "gvisor" && platformInterface == nil && tunMTU > 0 && tunMTU < 49152
 	enableGSO := C.IsLinux && options.Stack == "gvisor" && platformInterface == nil && tunMTU > 0 && tunMTU < 49152
 	if tunMTU == 0 {
 	if tunMTU == 0 {
@@ -372,8 +371,8 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
 			}
 			}
 		}
 		}
 		monitor.Start("open interface")
 		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 {
 		} else {
 			if HookBeforeCreatePlatformInterface != nil {
 			if HookBeforeCreatePlatformInterface != nil {
 				HookBeforeCreatePlatformInterface()
 				HookBeforeCreatePlatformInterface()
@@ -393,7 +392,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
 		)
 		)
 		if t.platformInterface != nil {
 		if t.platformInterface != nil {
 			forwarderBindInterface = true
 			forwarderBindInterface = true
-			includeAllNetworks = t.platformInterface.IncludeAllNetworks()
+			includeAllNetworks = t.platformInterface.NetworkExtensionIncludeAllNetworks()
 		}
 		}
 		tunStack, err := tun.NewStack(t.stack, tun.StackOptions{
 		tunStack, err := tun.NewStack(t.stack, tun.StackOptions{
 			Context:                t.ctx,
 			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/conntrack"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
 	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-box/option"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common"
@@ -46,7 +45,7 @@ type NetworkManager struct {
 	packageManager         tun.PackageManager
 	packageManager         tun.PackageManager
 	powerListener          winpowrprof.EventListener
 	powerListener          winpowrprof.EventListener
 	pauseManager           pause.Manager
 	pauseManager           pause.Manager
-	platformInterface      platform.Interface
+	platformInterface      adapter.PlatformInterface
 	endpoint               adapter.EndpointManager
 	endpoint               adapter.EndpointManager
 	inbound                adapter.InboundManager
 	inbound                adapter.InboundManager
 	outbound               adapter.OutboundManager
 	outbound               adapter.OutboundManager
@@ -85,7 +84,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp
 			FallbackDelay:       time.Duration(routeOptions.DefaultFallbackDelay),
 			FallbackDelay:       time.Duration(routeOptions.DefaultFallbackDelay),
 		},
 		},
 		pauseManager:      service.FromContext[pause.Manager](ctx),
 		pauseManager:      service.FromContext[pause.Manager](ctx),
-		platformInterface: service.FromContext[platform.Interface](ctx),
+		platformInterface: service.FromContext[adapter.PlatformInterface](ctx),
 		endpoint:          service.FromContext[adapter.EndpointManager](ctx),
 		endpoint:          service.FromContext[adapter.EndpointManager](ctx),
 		inbound:           service.FromContext[adapter.InboundManager](ctx),
 		inbound:           service.FromContext[adapter.InboundManager](ctx),
 		outbound:          service.FromContext[adapter.OutboundManager](ctx),
 		outbound:          service.FromContext[adapter.OutboundManager](ctx),
@@ -227,10 +226,10 @@ func (r *NetworkManager) InterfaceFinder() control.InterfaceFinder {
 }
 }
 
 
 func (r *NetworkManager) UpdateInterfaces() error {
 func (r *NetworkManager) UpdateInterfaces() error {
-	if r.platformInterface == nil {
+	if r.platformInterface == nil || !r.platformInterface.UsePlatformNetworkInterfaces() {
 		return r.interfaceFinder.Update()
 		return r.interfaceFinder.Update()
 	} else {
 	} else {
-		interfaces, err := r.platformInterface.Interfaces()
+		interfaces, err := r.platformInterface.NetworkInterfaces()
 		if err != nil {
 		if err != nil {
 			return err
 			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)
 			r.logger.InfoContext(ctx, "failed to search process: ", fErr)
 		} else {
 		} else {
 			if processInfo.ProcessPath != "" {
 			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 {
 				} else if processInfo.UserId != -1 {
 					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId)
 					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId)
 				} else {
 				} else {
 					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
 					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 {
 			} 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 {
 				} else {
 					r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId)
 					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/process"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	"github.com/sagernet/sing-box/common/taskmonitor"
 	C "github.com/sagernet/sing-box/constant"
 	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/log"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
 	R "github.com/sagernet/sing-box/route/rule"
 	R "github.com/sagernet/sing-box/route/rule"
@@ -37,7 +36,7 @@ type Router struct {
 	processSearcher   process.Searcher
 	processSearcher   process.Searcher
 	pauseManager      pause.Manager
 	pauseManager      pause.Manager
 	trackers          []adapter.ConnectionTracker
 	trackers          []adapter.ConnectionTracker
-	platformInterface platform.Interface
+	platformInterface adapter.PlatformInterface
 	needWIFIState     bool
 	needWIFIState     bool
 	started           bool
 	started           bool
 }
 }
@@ -56,7 +55,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route
 		ruleSetMap:        make(map[string]adapter.RuleSet),
 		ruleSetMap:        make(map[string]adapter.RuleSet),
 		needFindProcess:   hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess,
 		needFindProcess:   hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess,
 		pauseManager:      service.FromContext[pause.Manager](ctx),
 		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),
 		needWIFIState:     hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule),
 	}
 	}
 }
 }
@@ -124,8 +123,8 @@ func (r *Router) Start(stage adapter.StartStage) error {
 			}
 			}
 		}
 		}
 		if needFindProcess {
 		if needFindProcess {
-			if r.platformInterface != nil {
-				r.processSearcher = r.platformInterface
+			if r.platformInterface != nil && r.platformInterface.UsePlatformConnectionOwnerFinder() {
+				r.processSearcher = newPlatformSearcher(r.platformInterface)
 			} else {
 			} else {
 				monitor.Start("initialize process searcher")
 				monitor.Start("initialize process searcher")
 				searcher, err := process.NewSearcher(process.Config{
 				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 {
 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 false
 	}
 	}
-	return r.packageMap[metadata.ProcessInfo.PackageName]
+	return r.packageMap[metadata.ProcessInfo.AndroidPackageName]
 }
 }
 
 
 func (r *PackageNameItem) String() string {
 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 {
 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 false
 	}
 	}
-	return r.userMap[metadata.ProcessInfo.User]
+	return r.userMap[metadata.ProcessInfo.UserName]
 }
 }
 
 
 func (r *UserItem) String() string {
 func (r *UserItem) String() string {

+ 6 - 7
service/resolved/resolve1.go

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