Add sing-box API service

This commit is contained in:
世界
2026-06-11 21:11:10 +08:00
parent 1ee3596db9
commit 3a5d654463
36 changed files with 2075 additions and 849 deletions
+76 -65
View File
@@ -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
}