Add sing-box API service
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
+330
-268
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user