Add sing-box API service
This commit is contained in:
+76
-65
@@ -11,17 +11,15 @@ import (
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/networkquality"
|
||||
"github.com/sagernet/sing-box/common/stun"
|
||||
"github.com/sagernet/sing-box/common/trafficcontrol"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/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-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/batch"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
@@ -34,6 +32,8 @@ import (
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const APIVersion = 1
|
||||
|
||||
var _ StartedServiceServer = (*StartedService)(nil)
|
||||
|
||||
type StartedService struct {
|
||||
@@ -65,9 +65,6 @@ type StartedService struct {
|
||||
urlTestHistoryStorage *urltest.HistoryStorage
|
||||
clashModeSubscriber *observable.Subscriber[struct{}]
|
||||
clashModeObserver *observable.Observer[struct{}]
|
||||
|
||||
connectionEventSubscriber *observable.Subscriber[trafficontrol.ConnectionEvent]
|
||||
connectionEventObserver *observable.Observer[trafficontrol.ConnectionEvent]
|
||||
}
|
||||
|
||||
type ServiceOptions struct {
|
||||
@@ -101,22 +98,27 @@ func NewStartedService(options ServiceOptions) *StartedService {
|
||||
// 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),
|
||||
connectionEventSubscriber: observable.NewSubscriber[trafficontrol.ConnectionEvent](256),
|
||||
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)
|
||||
s.connectionEventObserver = observable.NewObserver(s.connectionEventSubscriber, 64)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StartedService) GetVersion(ctx context.Context, empty *emptypb.Empty) (*Version, error) {
|
||||
return &Version{
|
||||
Version: C.Version,
|
||||
ApiVersion: APIVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) resetLogs() {
|
||||
s.logAccess.Lock()
|
||||
s.logLines = list.List[*log.Entry]{}
|
||||
@@ -163,12 +165,12 @@ func (s *StartedService) waitForStarted(ctx context.Context) error {
|
||||
return ctx.Err()
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case status := <-subscription:
|
||||
switch status.Status {
|
||||
case statusUpdate := <-subscription:
|
||||
switch statusUpdate.Status {
|
||||
case ServiceStatus_STARTED:
|
||||
return nil
|
||||
case ServiceStatus_FATAL:
|
||||
return E.New(status.ErrorMessage)
|
||||
return status.Error(codes.FailedPrecondition, statusUpdate.ErrorMessage)
|
||||
case ServiceStatus_IDLE, ServiceStatus_STOPPING:
|
||||
return os.ErrInvalid
|
||||
}
|
||||
@@ -203,7 +205,6 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
|
||||
instance.urlTestHistoryStorage.SetHook(s.urlTestSubscriber)
|
||||
if instance.clashServer != nil {
|
||||
instance.clashServer.SetModeUpdateHook(s.clashModeSubscriber)
|
||||
instance.clashServer.(*clashapi.Server).TrafficManager().SetEventHook(s.connectionEventSubscriber)
|
||||
}
|
||||
s.serviceAccess.Unlock()
|
||||
err = instance.Start()
|
||||
@@ -227,7 +228,6 @@ func (s *StartedService) Close() {
|
||||
s.logSubscriber.Close()
|
||||
s.urlTestSubscriber.Close()
|
||||
s.clashModeSubscriber.Close()
|
||||
s.connectionEventSubscriber.Close()
|
||||
}
|
||||
|
||||
func (s *StartedService) CloseService() error {
|
||||
@@ -363,7 +363,7 @@ func (s *StartedService) GetDefaultLogLevel(ctx context.Context, empty *emptypb.
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
logLevel := s.instance.instance.LogFactory().Level()
|
||||
logLevel := s.instance.logFactory.Level()
|
||||
s.serviceAccess.RUnlock()
|
||||
return &DefaultLogLevel{Level: LogLevel(logLevel)}, nil
|
||||
}
|
||||
@@ -415,13 +415,10 @@ func (s *StartedService) readStatus() *Status {
|
||||
if nowService != nil && nowService.connectionManager != nil {
|
||||
status.ConnectionsOut = int32(nowService.connectionManager.Count())
|
||||
}
|
||||
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())
|
||||
}
|
||||
if nowService != nil && nowService.trafficManager != nil {
|
||||
status.TrafficAvailable = true
|
||||
status.UplinkTotal, status.DownlinkTotal = nowService.trafficManager.Total()
|
||||
status.ConnectionsIn = int32(nowService.trafficManager.ConnectionsLen())
|
||||
}
|
||||
return &status
|
||||
}
|
||||
@@ -463,7 +460,7 @@ func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.Serve
|
||||
func (s *StartedService) readGroups() *Groups {
|
||||
historyStorage := s.instance.urlTestHistoryStorage
|
||||
boxService := s.instance
|
||||
outbounds := boxService.instance.Outbound().Outbounds()
|
||||
outbounds := boxService.outboundManager.Outbounds()
|
||||
var iGroups []adapter.OutboundGroup
|
||||
for _, it := range outbounds {
|
||||
if group, isGroup := it.(adapter.OutboundGroup); isGroup {
|
||||
@@ -484,7 +481,7 @@ func (s *StartedService) readGroups() *Groups {
|
||||
}
|
||||
|
||||
for _, itemTag := range iGroup.All() {
|
||||
itemOutbound, isLoaded := boxService.instance.Outbound().Outbound(itemTag)
|
||||
itemOutbound, isLoaded := boxService.outboundManager.Outbound(itemTag)
|
||||
if !isLoaded {
|
||||
continue
|
||||
}
|
||||
@@ -515,7 +512,7 @@ func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.
|
||||
clashServer := s.instance.clashServer
|
||||
s.serviceAccess.RUnlock()
|
||||
if clashServer == nil {
|
||||
return nil, os.ErrInvalid
|
||||
return nil, status.Error(codes.Unimplemented, "clash mode not available")
|
||||
}
|
||||
return &ClashModeStatus{
|
||||
ModeList: clashServer.ModeList(),
|
||||
@@ -539,7 +536,12 @@ func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.Se
|
||||
s.serviceAccess.RUnlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
message := &ClashMode{Mode: s.instance.clashServer.Mode()}
|
||||
clashServer := s.instance.clashServer
|
||||
if clashServer == nil {
|
||||
s.serviceAccess.RUnlock()
|
||||
return status.Error(codes.Unimplemented, "clash mode not available")
|
||||
}
|
||||
message := &ClashMode{Mode: clashServer.Mode()}
|
||||
s.serviceAccess.RUnlock()
|
||||
err = server.Send(message)
|
||||
if err != nil {
|
||||
@@ -565,7 +567,10 @@ func (s *StartedService) SetClashMode(ctx context.Context, request *ClashMode) (
|
||||
}
|
||||
clashServer := s.instance.clashServer
|
||||
s.serviceAccess.RUnlock()
|
||||
clashServer.(*clashapi.Server).SetMode(request.Mode)
|
||||
if clashServer == nil {
|
||||
return nil, status.Error(codes.Unimplemented, "clash mode not available")
|
||||
}
|
||||
clashServer.SetMode(request.Mode)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
@@ -578,13 +583,13 @@ func (s *StartedService) URLTest(ctx context.Context, request *URLTestRequest) (
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
groupTag := request.OutboundTag
|
||||
abstractOutboundGroup, isLoaded := boxService.instance.Outbound().Outbound(groupTag)
|
||||
abstractOutboundGroup, isLoaded := boxService.outboundManager.Outbound(groupTag)
|
||||
if !isLoaded {
|
||||
return nil, E.New("outbound group not found: ", groupTag)
|
||||
return nil, status.Error(codes.NotFound, "outbound group not found: "+groupTag)
|
||||
}
|
||||
outboundGroup, isOutboundGroup := abstractOutboundGroup.(adapter.OutboundGroup)
|
||||
if !isOutboundGroup {
|
||||
return nil, E.New("outbound is not a group: ", groupTag)
|
||||
return nil, status.Error(codes.InvalidArgument, "outbound is not a group: "+groupTag)
|
||||
}
|
||||
urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest)
|
||||
if isURLTest {
|
||||
@@ -593,7 +598,7 @@ func (s *StartedService) URLTest(ctx context.Context, request *URLTestRequest) (
|
||||
historyStorage := boxService.urlTestHistoryStorage
|
||||
|
||||
outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
|
||||
itOutbound, _ := boxService.instance.Outbound().Outbound(it)
|
||||
itOutbound, _ := boxService.outboundManager.Outbound(it)
|
||||
return itOutbound
|
||||
}), func(it adapter.Outbound) bool {
|
||||
if it == nil {
|
||||
@@ -631,18 +636,18 @@ func (s *StartedService) SelectOutbound(ctx context.Context, request *SelectOutb
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
boxService := s.instance.instance
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
outboundGroup, isLoaded := boxService.Outbound().Outbound(request.GroupTag)
|
||||
outboundGroup, isLoaded := boxService.outboundManager.Outbound(request.GroupTag)
|
||||
if !isLoaded {
|
||||
return nil, E.New("selector not found: ", request.GroupTag)
|
||||
return nil, status.Error(codes.NotFound, "selector not found: "+request.GroupTag)
|
||||
}
|
||||
selector, isSelector := outboundGroup.(*group.Selector)
|
||||
if !isSelector {
|
||||
return nil, E.New("outbound is not a selector: ", request.GroupTag)
|
||||
return nil, status.Error(codes.InvalidArgument, "outbound is not a selector: "+request.GroupTag)
|
||||
}
|
||||
if !selector.SelectOutbound(request.OutboundTag) {
|
||||
return nil, E.New("outbound not found in selector: ", request.OutboundTag)
|
||||
return nil, status.Error(codes.NotFound, "outbound not found in selector: "+request.OutboundTag)
|
||||
}
|
||||
s.urlTestObserver.Emit(struct{}{})
|
||||
return &emptypb.Empty{}, nil
|
||||
@@ -688,17 +693,16 @@ func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsReque
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
if boxService.clashServer == nil {
|
||||
return E.New("clash server not available")
|
||||
trafficManager := boxService.trafficManager
|
||||
if trafficManager == nil {
|
||||
return status.Error(codes.Unimplemented, "connection tracking not available")
|
||||
}
|
||||
|
||||
trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
|
||||
|
||||
subscription, done, err := s.connectionEventObserver.Subscribe()
|
||||
subscription, done, err := trafficManager.SubscribeEvents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.connectionEventObserver.UnSubscribe(subscription)
|
||||
defer trafficManager.UnSubscribeEvents(subscription)
|
||||
|
||||
connectionSnapshots := make(map[uuid.UUID]connectionSnapshot)
|
||||
initialEvents := s.buildInitialConnectionState(trafficManager, connectionSnapshots)
|
||||
@@ -768,7 +772,7 @@ type connectionSnapshot struct {
|
||||
hadTraffic bool
|
||||
}
|
||||
|
||||
func (s *StartedService) buildInitialConnectionState(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent {
|
||||
func (s *StartedService) buildInitialConnectionState(manager *trafficcontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent {
|
||||
var events []*ConnectionEvent
|
||||
|
||||
for _, metadata := range manager.Connections() {
|
||||
@@ -796,9 +800,9 @@ func (s *StartedService) buildInitialConnectionState(manager *trafficontrol.Mana
|
||||
return events
|
||||
}
|
||||
|
||||
func (s *StartedService) applyConnectionEvent(event trafficontrol.ConnectionEvent, snapshots map[uuid.UUID]connectionSnapshot) *ConnectionEvent {
|
||||
func (s *StartedService) applyConnectionEvent(event trafficcontrol.ConnectionEvent, snapshots map[uuid.UUID]connectionSnapshot) *ConnectionEvent {
|
||||
switch event.Type {
|
||||
case trafficontrol.ConnectionEventNew:
|
||||
case trafficcontrol.ConnectionEventNew:
|
||||
if _, exists := snapshots[event.ID]; exists {
|
||||
return nil
|
||||
}
|
||||
@@ -811,7 +815,7 @@ func (s *StartedService) applyConnectionEvent(event trafficontrol.ConnectionEven
|
||||
Id: event.ID.String(),
|
||||
Connection: buildConnectionProto(event.Metadata),
|
||||
}
|
||||
case trafficontrol.ConnectionEventClosed:
|
||||
case trafficcontrol.ConnectionEventClosed:
|
||||
delete(snapshots, event.ID)
|
||||
protoEvent := &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_CLOSED,
|
||||
@@ -836,9 +840,9 @@ func (s *StartedService) applyConnectionEvent(event trafficontrol.ConnectionEven
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) buildTrafficUpdates(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent {
|
||||
func (s *StartedService) buildTrafficUpdates(manager *trafficcontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent {
|
||||
activeConnections := manager.Connections()
|
||||
activeIndex := make(map[uuid.UUID]*trafficontrol.TrackerMetadata, len(activeConnections))
|
||||
activeIndex := make(map[uuid.UUID]*trafficcontrol.TrackerMetadata, len(activeConnections))
|
||||
var events []*ConnectionEvent
|
||||
|
||||
for _, metadata := range activeConnections {
|
||||
@@ -902,13 +906,13 @@ func (s *StartedService) buildTrafficUpdates(manager *trafficontrol.Manager, sna
|
||||
}
|
||||
}
|
||||
|
||||
var closedIndex map[uuid.UUID]*trafficontrol.TrackerMetadata
|
||||
var closedIndex map[uuid.UUID]*trafficcontrol.TrackerMetadata
|
||||
for id := range snapshots {
|
||||
if _, exists := activeIndex[id]; exists {
|
||||
continue
|
||||
}
|
||||
if closedIndex == nil {
|
||||
closedIndex = make(map[uuid.UUID]*trafficontrol.TrackerMetadata)
|
||||
closedIndex = make(map[uuid.UUID]*trafficcontrol.TrackerMetadata)
|
||||
for _, metadata := range manager.ClosedConnections() {
|
||||
closedIndex[metadata.ID] = metadata
|
||||
}
|
||||
@@ -934,7 +938,7 @@ func (s *StartedService) buildTrafficUpdates(manager *trafficontrol.Manager, sna
|
||||
return events
|
||||
}
|
||||
|
||||
func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
|
||||
func buildConnectionProto(metadata *trafficcontrol.TrackerMetadata) *Connection {
|
||||
var rule string
|
||||
if metadata.Rule != nil {
|
||||
rule = metadata.Rule.String()
|
||||
@@ -984,7 +988,10 @@ func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConn
|
||||
}
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
targetConn := boxService.clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(request.Id))
|
||||
if boxService.trafficManager == nil {
|
||||
return nil, status.Error(codes.Unimplemented, "connection tracking not available")
|
||||
}
|
||||
targetConn := boxService.trafficManager.Connection(uuid.FromStringOrNil(request.Id))
|
||||
if targetConn != nil {
|
||||
targetConn.Close()
|
||||
}
|
||||
@@ -1009,7 +1016,11 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty
|
||||
}
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
notes := service.FromContext[deprecated.Manager](boxService.ctx).(*deprecatedManager).Get()
|
||||
manager, isCollecting := service.FromContext[deprecated.Manager](boxService.ctx).(*deprecatedManager)
|
||||
if !isCollecting {
|
||||
return &DeprecatedWarnings{}, nil
|
||||
}
|
||||
notes := manager.Get()
|
||||
return &DeprecatedWarnings{
|
||||
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
|
||||
return &DeprecatedWarning{
|
||||
@@ -1050,7 +1061,7 @@ func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.Server
|
||||
s.serviceAccess.RUnlock()
|
||||
historyStorage := boxService.urlTestHistoryStorage
|
||||
var list OutboundList
|
||||
for _, ob := range boxService.instance.Outbound().Outbounds() {
|
||||
for _, ob := range boxService.outboundManager.Outbounds() {
|
||||
item := &GroupItem{
|
||||
Tag: ob.Tag(),
|
||||
Type: ob.Type(),
|
||||
@@ -1061,7 +1072,7 @@ func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.Server
|
||||
}
|
||||
list.Outbounds = append(list.Outbounds, item)
|
||||
}
|
||||
for _, ep := range boxService.instance.Endpoint().Endpoints() {
|
||||
for _, ep := range boxService.endpointManager.Endpoints() {
|
||||
item := &GroupItem{
|
||||
Tag: ep.Tag(),
|
||||
Type: ep.Type(),
|
||||
@@ -1090,11 +1101,11 @@ func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.Server
|
||||
|
||||
func resolveOutbound(instance *Instance, tag string) (adapter.Outbound, error) {
|
||||
if tag == "" {
|
||||
return instance.instance.Outbound().Default(), nil
|
||||
return instance.outboundManager.Default(), nil
|
||||
}
|
||||
outbound, loaded := instance.instance.Outbound().Outbound(tag)
|
||||
outbound, loaded := instance.outboundManager.Outbound(tag)
|
||||
if !loaded {
|
||||
return nil, E.New("outbound not found: ", tag)
|
||||
return nil, status.Error(codes.NotFound, "outbound not found: "+tag)
|
||||
}
|
||||
return outbound, nil
|
||||
}
|
||||
@@ -1103,10 +1114,10 @@ func resolveTailscaleEndpoint(instance *Instance, tag string) (adapter.Endpoint,
|
||||
endpointManager := service.FromContext[adapter.EndpointManager](instance.ctx)
|
||||
endpoint, loaded := endpointManager.Get(tag)
|
||||
if !loaded {
|
||||
return nil, E.New("endpoint not found: ", tag)
|
||||
return nil, status.Error(codes.NotFound, "endpoint not found: "+tag)
|
||||
}
|
||||
if endpoint.Type() != C.TypeTailscale {
|
||||
return nil, E.New("endpoint is not Tailscale: ", tag)
|
||||
return nil, status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+tag)
|
||||
}
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user