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
+28
View File
@@ -0,0 +1,28 @@
package daemon
import (
"context"
"time"
"github.com/sagernet/sing-box/log"
)
const defaultAttachedLogMaxLines = 3000
// StartOrReloadService and CloseService must not be called on an attached service.
func NewAttachedService(ctx context.Context) *StartedService {
instance := attachInstance(ctx)
s := NewStartedService(ServiceOptions{
Context: ctx,
LogMaxLines: defaultAttachedLogMaxLines,
})
s.instance = instance
s.serviceStatus = &ServiceStatus{Status: ServiceStatus_STARTED}
s.startedAt = time.Now()
instance.urlTestHistoryStorage.SetHook(s.urlTestSubscriber)
if instance.clashServer != nil {
instance.clashServer.SetModeUpdateHook(s.clashModeSubscriber)
}
instance.logFactory.(log.ObservableFactory).AttachPlatformWriter(s)
return s
}
+85
View File
@@ -0,0 +1,85 @@
package daemon
import (
"context"
"crypto/tls"
"net"
"net/url"
E "github.com/sagernet/sing/common/exceptions"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
type RemoteClientOptions struct {
ServerURL string
Secret string
}
func (o RemoteClientOptions) ServerTarget() (string, credentials.TransportCredentials, error) {
if o.ServerURL == "" {
return "", nil, E.New("missing server URL")
}
serverURL, err := url.Parse(o.ServerURL)
if err != nil {
return "", nil, E.Cause(err, "invalid server URL: ", o.ServerURL)
}
var enableTLS bool
switch serverURL.Scheme {
case "http":
case "https":
enableTLS = true
default:
return "", nil, E.New("invalid server URL scheme: ", serverURL.Scheme, ", expected http or https")
}
host := serverURL.Hostname()
if host == "" {
return "", nil, E.New("missing host in server URL: ", o.ServerURL)
}
port := serverURL.Port()
if port == "" {
if enableTLS {
port = "443"
} else {
port = "80"
}
}
transportCredentials := insecure.NewCredentials()
if enableTLS {
transportCredentials = credentials.NewTLS(&tls.Config{ServerName: host})
}
return net.JoinHostPort(host, port), transportCredentials, nil
}
func NewRemoteClient(options RemoteClientOptions) (*grpc.ClientConn, error) {
target, transportCredentials, err := options.ServerTarget()
if err != nil {
return nil, err
}
return grpc.NewClient(target,
grpc.WithTransportCredentials(transportCredentials),
grpc.WithUnaryInterceptor(NewClientAuthUnaryInterceptor(options.Secret)),
grpc.WithStreamInterceptor(NewClientAuthStreamInterceptor(options.Secret)),
)
}
func NewClientAuthUnaryInterceptor(secret string) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, request, reply any, clientConn *grpc.ClientConn, invoker grpc.UnaryInvoker, options ...grpc.CallOption) error {
if secret != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+secret)
}
return invoker(ctx, method, request, reply, clientConn, options...)
}
}
func NewClientAuthStreamInterceptor(secret string) grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, clientConn *grpc.ClientConn, method string, streamer grpc.Streamer, options ...grpc.CallOption) (grpc.ClientStream, error) {
if secret != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+secret)
}
return streamer(ctx, desc, clientConn, method, options...)
}
}
+42
View File
@@ -0,0 +1,42 @@
package daemon
import (
"context"
"errors"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func UnaryErrorInterceptor(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
response, err := handler(ctx, request)
if err != nil {
return nil, mapStatusError(err)
}
return response, nil
}
func StreamErrorInterceptor(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
err := handler(server, stream)
if err != nil {
return mapStatusError(err)
}
return nil
}
func mapStatusError(err error) error {
if _, loaded := status.FromError(err); loaded {
return err
}
switch {
case errors.Is(err, os.ErrInvalid):
return status.Error(codes.FailedPrecondition, "service not started")
case errors.Is(err, os.ErrClosed):
return status.Error(codes.Unavailable, "service is closing")
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
return status.FromContextError(err).Err()
}
return err
}
+26 -3
View File
@@ -6,10 +6,10 @@ import (
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"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/deprecated"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
@@ -25,9 +25,13 @@ type Instance struct {
instance *box.Box
connectionManager adapter.ConnectionManager
clashServer adapter.ClashServer
trafficManager *trafficcontrol.Manager
cacheFile adapter.CacheFile
pauseManager pause.Manager
urlTestHistoryStorage *urltest.HistoryStorage
urlTestHistoryStorage adapter.URLTestHistoryStorage
outboundManager adapter.OutboundManager
endpointManager adapter.EndpointManager
logFactory log.Factory
}
func (s *StartedService) CheckConfig(configContent string) error {
@@ -71,7 +75,7 @@ type OverrideOptions struct {
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
ctx := service.ExtendContext(s.ctx)
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
ctx, cancel := context.WithCancel(include.Context(ctx))
ctx, cancel := context.WithCancel(ctx)
options, err := parseConfig(ctx, profileContent)
if err != nil {
cancel()
@@ -120,12 +124,31 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
i.instance = boxInstance
i.connectionManager = service.FromContext[adapter.ConnectionManager](ctx)
i.clashServer = service.FromContext[adapter.ClashServer](ctx)
i.trafficManager = service.PtrFromContext[trafficcontrol.Manager](ctx)
i.pauseManager = service.FromContext[pause.Manager](ctx)
i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
i.outboundManager = service.FromContext[adapter.OutboundManager](ctx)
i.endpointManager = service.FromContext[adapter.EndpointManager](ctx)
i.logFactory = boxInstance.LogFactory()
log.SetStdLogger(boxInstance.LogFactory().Logger())
return i, nil
}
func attachInstance(ctx context.Context) *Instance {
return &Instance{
ctx: ctx,
connectionManager: service.FromContext[adapter.ConnectionManager](ctx),
clashServer: service.FromContext[adapter.ClashServer](ctx),
trafficManager: service.PtrFromContext[trafficcontrol.Manager](ctx),
pauseManager: service.FromContext[pause.Manager](ctx),
cacheFile: service.FromContext[adapter.CacheFile](ctx),
urlTestHistoryStorage: service.PtrFromContext[urltest.HistoryStorage](ctx),
outboundManager: service.FromContext[adapter.OutboundManager](ctx),
endpointManager: service.FromContext[adapter.EndpointManager](ctx),
logFactory: service.FromContext[log.Factory](ctx),
}
}
func (i *Instance) Start() error {
return i.instance.Start()
}
+66
View File
@@ -0,0 +1,66 @@
package daemon
import (
"context"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
)
func NewServer(startedService *StartedService, secret string) *grpc.Server {
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(newUnaryAuthInterceptor(secret), UnaryErrorInterceptor),
grpc.ChainStreamInterceptor(newStreamAuthInterceptor(secret), StreamErrorInterceptor),
)
healthServer := health.NewServer()
RegisterStartedServiceServer(server, startedService)
healthServer.SetServingStatus(StartedService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(server, healthServer)
reflection.Register(server)
return server
}
func newUnaryAuthInterceptor(secret string) grpc.UnaryServerInterceptor {
return func(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
err := authenticate(ctx, secret)
if err != nil {
return nil, err
}
return handler(ctx, request)
}
}
func newStreamAuthInterceptor(secret string) grpc.StreamServerInterceptor {
return func(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
err := authenticate(stream.Context(), secret)
if err != nil {
return err
}
return handler(server, stream)
}
}
func authenticate(ctx context.Context, secret string) error {
if secret == "" {
return nil
}
md, loaded := metadata.FromIncomingContext(ctx)
if !loaded {
return status.Error(codes.Unauthenticated, "missing metadata")
}
values := md.Get("authorization")
if len(values) == 0 {
return status.Error(codes.Unauthenticated, "missing authorization")
}
token, isBearer := strings.CutPrefix(values[0], "Bearer ")
if !isBearer || token != secret {
return status.Error(codes.Unauthenticated, "invalid authorization")
}
return nil
}
+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
}
File diff suppressed because it is too large Load Diff
+6
View File
@@ -6,6 +6,7 @@ option go_package = "github.com/sagernet/sing-box/daemon";
import "google/protobuf/empty.proto";
service StartedService {
rpc GetVersion(google.protobuf.Empty) returns(Version) {}
rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {}
rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {}
rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {}
@@ -39,6 +40,11 @@ service StartedService {
rpc StartTailscaleSSHSession(stream TailscaleSSHClientMessage) returns (stream TailscaleSSHServerMessage) {}
}
message Version {
string version = 1;
int32 apiVersion = 2;
}
message ServiceStatus {
enum Type {
IDLE = 0;
+39
View File
@@ -15,6 +15,7 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
StartedService_GetVersion_FullMethodName = "/daemon.StartedService/GetVersion"
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
@@ -47,6 +48,7 @@ const (
//
// 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 {
GetVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Version, 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)
@@ -83,6 +85,16 @@ func NewStartedServiceClient(cc grpc.ClientConnInterface) StartedServiceClient {
return &startedServiceClient{cc}
}
func (c *startedServiceClient) GetVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Version, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Version)
err := c.cc.Invoke(ctx, StartedService_GetVersion_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...)
@@ -449,6 +461,7 @@ type StartedService_StartTailscaleSSHSessionClient = grpc.BidiStreamingClient[Ta
// All implementations must embed UnimplementedStartedServiceServer
// for forward compatibility.
type StartedServiceServer interface {
GetVersion(context.Context, *emptypb.Empty) (*Version, error)
SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error
SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error
GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error)
@@ -485,6 +498,10 @@ type StartedServiceServer interface {
// pointer dereference when methods are called.
type UnimplementedStartedServiceServer struct{}
func (UnimplementedStartedServiceServer) GetVersion(context.Context, *emptypb.Empty) (*Version, error) {
return nil, status.Error(codes.Unimplemented, "method GetVersion not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error {
return status.Error(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
}
@@ -609,6 +626,24 @@ func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceSer
s.RegisterService(&StartedService_ServiceDesc, srv)
}
func _StartedService_GetVersion_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).GetVersion(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_GetVersion_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).GetVersion(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 {
@@ -996,6 +1031,10 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "daemon.StartedService",
HandlerType: (*StartedServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetVersion",
Handler: _StartedService_GetVersion_Handler,
},
{
MethodName: "GetDefaultLogLevel",
Handler: _StartedService_GetDefaultLogLevel_Handler,
+2 -2
View File
@@ -121,7 +121,7 @@ func (s *StartedService) StartTailscaleSSHSession(
}
sshClient := ssh.NewClient(sshConn, chans, reqs)
if start.ForwardAgent {
if start.ForwardAgent && s.handler != nil {
agentChannels := sshClient.HandleChannelOpen("auth-agent@openssh.com")
if agentChannels != nil {
go func() {
@@ -176,7 +176,7 @@ func (s *StartedService) StartTailscaleSSHSession(
}))
}
if start.ForwardAgent {
if start.ForwardAgent && s.handler != nil {
err = agent.RequestAgentForwarding(sshSession)
if err != nil {
common.Close(sshSession, sshClient)