Merge tag 'v1.14.0'

This commit is contained in:
Shtorm
2026-09-04 01:08:30 +03:00
987 changed files with 141481 additions and 11302 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.AddUpdateHook(s.urlTestSubscriber)
if instance.clashServer != nil {
instance.clashServer.AddModeUpdateHook(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.WithChainUnaryInterceptor(UnaryClientLocaleInterceptor, NewClientAuthUnaryInterceptor(options.Secret)),
grpc.WithChainStreamInterceptor(StreamClientLocaleInterceptor, 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...)
}
}
+50 -11
View File
@@ -6,10 +6,11 @@ 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/experimental/locale"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
@@ -25,17 +26,23 @@ type Instance struct {
instance *box.Box
connectionManager adapter.ConnectionManager
clashServer adapter.ClashServer
trafficManager *trafficcontrol.Manager
cacheFile adapter.CacheFile
pauseManager pause.Manager
urlTestHistoryStorage *urltest.HistoryStorage
outboundManager adapter.OutboundManager
endpointManager adapter.EndpointManager
logFactory log.Factory
}
func (s *StartedService) CheckConfig(configContent string) error {
options, err := parseConfig(s.ctx, configContent)
func (s *StartedService) CheckConfig(ctx context.Context, configContent string) error {
selectedLocale := locale.FromContext(ctx)
ctx, _ = locale.ContextWithLocale(s.ctx, selectedLocale.Locale)
options, err := parseConfig(ctx, configContent)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(s.ctx)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
instance, err := box.New(box.Options{
Context: ctx,
@@ -47,8 +54,10 @@ func (s *StartedService) CheckConfig(configContent string) error {
return err
}
func (s *StartedService) FormatConfig(configContent string) (string, error) {
options, err := parseConfig(s.ctx, configContent)
func (s *StartedService) FormatConfig(ctx context.Context, configContent string) (string, error) {
selectedLocale := locale.FromContext(ctx)
ctx, _ = locale.ContextWithLocale(s.ctx, selectedLocale.Locale)
options, err := parseConfig(ctx, configContent)
if err != nil {
return "", err
}
@@ -68,10 +77,12 @@ type OverrideOptions struct {
ExcludePackage []string
}
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
ctx := service.ExtendContext(s.ctx)
func (s *StartedService) newInstance(ctx context.Context, profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
selectedLocale := locale.FromContext(ctx)
ctx, _ = locale.ContextWithLocale(s.ctx, selectedLocale.Locale)
ctx = service.ExtendContext(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()
@@ -87,12 +98,17 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
}
}
}
if s.oomKiller && C.IsIos {
if s.oomKillerEnabled {
if !common.Any(options.Services, func(it option.Service) bool {
return it.Type == C.TypeOOMKiller
}) {
oomOptions := &option.OOMKillerServiceOptions{
KillerDisabled: s.oomKillerDisabled,
MemoryLimitOverride: s.oomMemoryLimit,
}
options.Services = append(options.Services, option.Service{
Type: C.TypeOOMKiller,
Type: C.TypeOOMKiller,
Options: oomOptions,
})
}
}
@@ -119,12 +135,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()
}
@@ -143,6 +178,10 @@ func (i *Instance) PauseManager() pause.Manager {
return i.pauseManager
}
func (i *Instance) TrafficManager() *trafficcontrol.Manager {
return i.trafficManager
}
func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent))
if err != nil {
+54
View File
@@ -0,0 +1,54 @@
package daemon
import (
"context"
"github.com/sagernet/sing-box/experimental/locale"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func contextWithLocale(ctx context.Context) context.Context {
requestMetadata, loaded := metadata.FromIncomingContext(ctx)
if !loaded {
return ctx
}
for _, localeID := range requestMetadata.Get("accept-language") {
localizedContext, matched := locale.ContextWithLocale(ctx, localeID)
if matched {
return localizedContext
}
}
return ctx
}
func UnaryLocaleInterceptor(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
return handler(contextWithLocale(ctx), request)
}
func StreamLocaleInterceptor(server any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
return handler(server, &localeServerStream{
ServerStream: stream,
ctx: contextWithLocale(stream.Context()),
})
}
func UnaryClientLocaleInterceptor(ctx context.Context, method string, request, reply any, connection *grpc.ClientConn, invoker grpc.UnaryInvoker, options ...grpc.CallOption) error {
ctx = metadata.AppendToOutgoingContext(ctx, "accept-language", locale.FromContext(ctx).Locale)
return invoker(ctx, method, request, reply, connection, options...)
}
func StreamClientLocaleInterceptor(ctx context.Context, description *grpc.StreamDesc, connection *grpc.ClientConn, method string, streamer grpc.Streamer, options ...grpc.CallOption) (grpc.ClientStream, error) {
ctx = metadata.AppendToOutgoingContext(ctx, "accept-language", locale.FromContext(ctx).Locale)
return streamer(ctx, description, connection, method, options...)
}
type localeServerStream struct {
grpc.ServerStream
ctx context.Context
}
func (s *localeServerStream) Context() context.Context {
return s.ctx
}
+97
View File
@@ -0,0 +1,97 @@
package daemon
import (
"context"
"time"
"unsafe"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing/common/memory"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
var _ ManagedServiceServer = (*ManagedService)(nil)
type ManagedService struct {
handler ManagedHandler
debug bool
oomReporter oomkiller.OOMReporter
}
type ManagedServiceOptions struct {
Handler ManagedHandler
Debug bool
OOMReporter oomkiller.OOMReporter
}
func NewManagedService(options ManagedServiceOptions) *ManagedService {
return &ManagedService{
handler: options.Handler,
debug: options.Debug,
oomReporter: options.OOMReporter,
}
}
func (s *ManagedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
err := s.handler.ServiceStop()
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
func (s *ManagedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
err := s.handler.ServiceReload(ctx)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
func (s *ManagedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) {
return s.handler.SystemProxyStatus()
}
func (s *ManagedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
err := s.handler.SetSystemProxyEnabled(request.Enabled)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
func (s *ManagedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) {
if !s.debug {
return nil, status.Error(codes.PermissionDenied, "debug crash trigger unavailable")
}
if request == nil {
return nil, status.Error(codes.InvalidArgument, "missing debug crash request")
}
switch request.Type {
case DebugCrashRequest_GO:
time.AfterFunc(200*time.Millisecond, func() {
*(*int)(unsafe.Pointer(uintptr(0))) = 0
})
case DebugCrashRequest_NATIVE:
err := s.handler.TriggerNativeCrash()
if err != nil {
return nil, err
}
default:
return nil, status.Error(codes.InvalidArgument, "unknown debug crash type")
}
return &emptypb.Empty{}, nil
}
func (s *ManagedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
if s.oomReporter == nil {
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
}
return &emptypb.Empty{}, s.oomReporter.WriteReport(memory.Total())
}
func (s *ManagedService) mustEmbedUnimplementedManagedServiceServer() {
}
+298
View File
@@ -0,0 +1,298 @@
package daemon
import (
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type DebugCrashRequest_Type int32
const (
DebugCrashRequest_GO DebugCrashRequest_Type = 0
DebugCrashRequest_NATIVE DebugCrashRequest_Type = 1
)
// Enum value maps for DebugCrashRequest_Type.
var (
DebugCrashRequest_Type_name = map[int32]string{
0: "GO",
1: "NATIVE",
}
DebugCrashRequest_Type_value = map[string]int32{
"GO": 0,
"NATIVE": 1,
}
)
func (x DebugCrashRequest_Type) Enum() *DebugCrashRequest_Type {
p := new(DebugCrashRequest_Type)
*p = x
return p
}
func (x DebugCrashRequest_Type) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (DebugCrashRequest_Type) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_managed_service_proto_enumTypes[0].Descriptor()
}
func (DebugCrashRequest_Type) Type() protoreflect.EnumType {
return &file_daemon_managed_service_proto_enumTypes[0]
}
func (x DebugCrashRequest_Type) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use DebugCrashRequest_Type.Descriptor instead.
func (DebugCrashRequest_Type) EnumDescriptor() ([]byte, []int) {
return file_daemon_managed_service_proto_rawDescGZIP(), []int{2, 0}
}
type SystemProxyStatus struct {
state protoimpl.MessageState `protogen:"open.v1"`
Available bool `protobuf:"varint,1,opt,name=available,proto3" json:"available,omitempty"`
Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SystemProxyStatus) Reset() {
*x = SystemProxyStatus{}
mi := &file_daemon_managed_service_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SystemProxyStatus) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SystemProxyStatus) ProtoMessage() {}
func (x *SystemProxyStatus) ProtoReflect() protoreflect.Message {
mi := &file_daemon_managed_service_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SystemProxyStatus.ProtoReflect.Descriptor instead.
func (*SystemProxyStatus) Descriptor() ([]byte, []int) {
return file_daemon_managed_service_proto_rawDescGZIP(), []int{0}
}
func (x *SystemProxyStatus) GetAvailable() bool {
if x != nil {
return x.Available
}
return false
}
func (x *SystemProxyStatus) GetEnabled() bool {
if x != nil {
return x.Enabled
}
return false
}
type SetSystemProxyEnabledRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetSystemProxyEnabledRequest) Reset() {
*x = SetSystemProxyEnabledRequest{}
mi := &file_daemon_managed_service_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetSystemProxyEnabledRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetSystemProxyEnabledRequest) ProtoMessage() {}
func (x *SetSystemProxyEnabledRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_managed_service_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetSystemProxyEnabledRequest.ProtoReflect.Descriptor instead.
func (*SetSystemProxyEnabledRequest) Descriptor() ([]byte, []int) {
return file_daemon_managed_service_proto_rawDescGZIP(), []int{1}
}
func (x *SetSystemProxyEnabledRequest) GetEnabled() bool {
if x != nil {
return x.Enabled
}
return false
}
type DebugCrashRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type DebugCrashRequest_Type `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.DebugCrashRequest_Type" json:"type,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DebugCrashRequest) Reset() {
*x = DebugCrashRequest{}
mi := &file_daemon_managed_service_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DebugCrashRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DebugCrashRequest) ProtoMessage() {}
func (x *DebugCrashRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_managed_service_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DebugCrashRequest.ProtoReflect.Descriptor instead.
func (*DebugCrashRequest) Descriptor() ([]byte, []int) {
return file_daemon_managed_service_proto_rawDescGZIP(), []int{2}
}
func (x *DebugCrashRequest) GetType() DebugCrashRequest_Type {
if x != nil {
return x.Type
}
return DebugCrashRequest_GO
}
var File_daemon_managed_service_proto protoreflect.FileDescriptor
const file_daemon_managed_service_proto_rawDesc = "" +
"\n" +
"\x1cdaemon/managed_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"K\n" +
"\x11SystemProxyStatus\x12\x1c\n" +
"\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" +
"\x1cSetSystemProxyEnabledRequest\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\"c\n" +
"\x11DebugCrashRequest\x122\n" +
"\x04type\x18\x01 \x01(\x0e2\x1e.daemon.DebugCrashRequest.TypeR\x04type\"\x1a\n" +
"\x04Type\x12\x06\n" +
"\x02GO\x10\x00\x12\n" +
"\n" +
"\x06NATIVE\x10\x012\xc6\x03\n" +
"\x0eManagedService\x12=\n" +
"\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" +
"\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" +
"\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" +
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n" +
"\x11TriggerDebugCrash\x12\x19.daemon.DebugCrashRequest\x1a\x16.google.protobuf.Empty\"\x00\x12D\n" +
"\x10TriggerOOMReport\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
var (
file_daemon_managed_service_proto_rawDescOnce sync.Once
file_daemon_managed_service_proto_rawDescData []byte
)
func file_daemon_managed_service_proto_rawDescGZIP() []byte {
file_daemon_managed_service_proto_rawDescOnce.Do(func() {
file_daemon_managed_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_daemon_managed_service_proto_rawDesc), len(file_daemon_managed_service_proto_rawDesc)))
})
return file_daemon_managed_service_proto_rawDescData
}
var (
file_daemon_managed_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
file_daemon_managed_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
file_daemon_managed_service_proto_goTypes = []any{
DebugCrashRequest_Type(0), // 0: daemon.DebugCrashRequest.Type
(*SystemProxyStatus)(nil), // 1: daemon.SystemProxyStatus
(*SetSystemProxyEnabledRequest)(nil), // 2: daemon.SetSystemProxyEnabledRequest
(*DebugCrashRequest)(nil), // 3: daemon.DebugCrashRequest
(*emptypb.Empty)(nil), // 4: google.protobuf.Empty
}
)
var file_daemon_managed_service_proto_depIdxs = []int32{
0, // 0: daemon.DebugCrashRequest.type:type_name -> daemon.DebugCrashRequest.Type
4, // 1: daemon.ManagedService.StopService:input_type -> google.protobuf.Empty
4, // 2: daemon.ManagedService.ReloadService:input_type -> google.protobuf.Empty
4, // 3: daemon.ManagedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
2, // 4: daemon.ManagedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
3, // 5: daemon.ManagedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest
4, // 6: daemon.ManagedService.TriggerOOMReport:input_type -> google.protobuf.Empty
4, // 7: daemon.ManagedService.StopService:output_type -> google.protobuf.Empty
4, // 8: daemon.ManagedService.ReloadService:output_type -> google.protobuf.Empty
1, // 9: daemon.ManagedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
4, // 10: daemon.ManagedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
4, // 11: daemon.ManagedService.TriggerDebugCrash:output_type -> google.protobuf.Empty
4, // 12: daemon.ManagedService.TriggerOOMReport:output_type -> google.protobuf.Empty
7, // [7:13] is the sub-list for method output_type
1, // [1:7] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_daemon_managed_service_proto_init() }
func file_daemon_managed_service_proto_init() {
if File_daemon_managed_service_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_managed_service_proto_rawDesc), len(file_daemon_managed_service_proto_rawDesc)),
NumEnums: 1,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_daemon_managed_service_proto_goTypes,
DependencyIndexes: file_daemon_managed_service_proto_depIdxs,
EnumInfos: file_daemon_managed_service_proto_enumTypes,
MessageInfos: file_daemon_managed_service_proto_msgTypes,
}.Build()
File_daemon_managed_service_proto = out.File
file_daemon_managed_service_proto_goTypes = nil
file_daemon_managed_service_proto_depIdxs = nil
}
+34
View File
@@ -0,0 +1,34 @@
syntax = "proto3";
package daemon;
option go_package = "github.com/sagernet/sing-box/daemon";
import "google/protobuf/empty.proto";
service ManagedService {
rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc ReloadService(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
rpc TriggerDebugCrash(DebugCrashRequest) returns(google.protobuf.Empty) {}
rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {}
}
message SystemProxyStatus {
bool available = 1;
bool enabled = 2;
}
message SetSystemProxyEnabledRequest {
bool enabled = 1;
}
message DebugCrashRequest {
enum Type {
GO = 0;
NATIVE = 1;
}
Type type = 1;
}
+312
View File
@@ -0,0 +1,312 @@
package daemon
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
ManagedService_StopService_FullMethodName = "/daemon.ManagedService/StopService"
ManagedService_ReloadService_FullMethodName = "/daemon.ManagedService/ReloadService"
ManagedService_GetSystemProxyStatus_FullMethodName = "/daemon.ManagedService/GetSystemProxyStatus"
ManagedService_SetSystemProxyEnabled_FullMethodName = "/daemon.ManagedService/SetSystemProxyEnabled"
ManagedService_TriggerDebugCrash_FullMethodName = "/daemon.ManagedService/TriggerDebugCrash"
ManagedService_TriggerOOMReport_FullMethodName = "/daemon.ManagedService/TriggerOOMReport"
)
// ManagedServiceClient is the client API for ManagedService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ManagedServiceClient interface {
StopService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
ReloadService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type managedServiceClient struct {
cc grpc.ClientConnInterface
}
func NewManagedServiceClient(cc grpc.ClientConnInterface) ManagedServiceClient {
return &managedServiceClient{cc}
}
func (c *managedServiceClient) StopService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, ManagedService_StopService_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managedServiceClient) ReloadService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, ManagedService_ReloadService_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managedServiceClient) GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SystemProxyStatus)
err := c.cc.Invoke(ctx, ManagedService_GetSystemProxyStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, ManagedService_SetSystemProxyEnabled_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managedServiceClient) TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, ManagedService_TriggerDebugCrash_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managedServiceClient) TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, ManagedService_TriggerOOMReport_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ManagedServiceServer is the server API for ManagedService service.
// All implementations must embed UnimplementedManagedServiceServer
// for forward compatibility.
type ManagedServiceServer interface {
StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error)
TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
mustEmbedUnimplementedManagedServiceServer()
}
// UnimplementedManagedServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedManagedServiceServer struct{}
func (UnimplementedManagedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method StopService not implemented")
}
func (UnimplementedManagedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ReloadService not implemented")
}
func (UnimplementedManagedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) {
return nil, status.Error(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
}
func (UnimplementedManagedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
}
func (UnimplementedManagedServiceServer) TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method TriggerDebugCrash not implemented")
}
func (UnimplementedManagedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented")
}
func (UnimplementedManagedServiceServer) mustEmbedUnimplementedManagedServiceServer() {}
func (UnimplementedManagedServiceServer) testEmbeddedByValue() {}
// UnsafeManagedServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ManagedServiceServer will
// result in compilation errors.
type UnsafeManagedServiceServer interface {
mustEmbedUnimplementedManagedServiceServer()
}
func RegisterManagedServiceServer(s grpc.ServiceRegistrar, srv ManagedServiceServer) {
// If the following call panics, it indicates UnimplementedManagedServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&ManagedService_ServiceDesc, srv)
}
func _ManagedService_StopService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagedServiceServer).StopService(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ManagedService_StopService_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagedServiceServer).StopService(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _ManagedService_ReloadService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagedServiceServer).ReloadService(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ManagedService_ReloadService_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagedServiceServer).ReloadService(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _ManagedService_GetSystemProxyStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagedServiceServer).GetSystemProxyStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ManagedService_GetSystemProxyStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagedServiceServer).GetSystemProxyStatus(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _ManagedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetSystemProxyEnabledRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagedServiceServer).SetSystemProxyEnabled(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ManagedService_SetSystemProxyEnabled_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagedServiceServer).SetSystemProxyEnabled(ctx, req.(*SetSystemProxyEnabledRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ManagedService_TriggerDebugCrash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DebugCrashRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagedServiceServer).TriggerDebugCrash(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ManagedService_TriggerDebugCrash_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagedServiceServer).TriggerDebugCrash(ctx, req.(*DebugCrashRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ManagedService_TriggerOOMReport_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.(ManagedServiceServer).TriggerOOMReport(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ManagedService_TriggerOOMReport_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
// ManagedService_ServiceDesc is the grpc.ServiceDesc for ManagedService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ManagedService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "daemon.ManagedService",
HandlerType: (*ManagedServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "StopService",
Handler: _ManagedService_StopService_Handler,
},
{
MethodName: "ReloadService",
Handler: _ManagedService_ReloadService_Handler,
},
{
MethodName: "GetSystemProxyStatus",
Handler: _ManagedService_GetSystemProxyStatus_Handler,
},
{
MethodName: "SetSystemProxyEnabled",
Handler: _ManagedService_SetSystemProxyEnabled_Handler,
},
{
MethodName: "TriggerDebugCrash",
Handler: _ManagedService_TriggerDebugCrash_Handler,
},
{
MethodName: "TriggerOOMReport",
Handler: _ManagedService_TriggerOOMReport_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "daemon/managed_service.proto",
}
+9 -2
View File
@@ -1,9 +1,16 @@
package daemon
import "context"
type PlatformHandler interface {
WriteDebugMessage(message string)
ConnectSSHAgent() (int32, error)
}
type ManagedHandler interface {
ServiceStop() error
ServiceReload() error
ServiceReload(ctx context.Context) error
SystemProxyStatus() (*SystemProxyStatus, error)
SetSystemProxyEnabled(enabled bool) error
WriteDebugMessage(message string)
TriggerNativeCrash() error
}
+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), UnaryLocaleInterceptor),
grpc.ChainStreamInterceptor(newStreamAuthInterceptor(secret), StreamLocaleInterceptor),
)
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
}
+1260 -198
View File
File diff suppressed because it is too large Load Diff
+6680 -306
View File
File diff suppressed because it is too large Load Diff
+611 -20
View File
@@ -6,9 +6,7 @@ option go_package = "github.com/sagernet/sing-box/daemon";
import "google/protobuf/empty.proto";
service StartedService {
rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc ReloadService(google.protobuf.Empty) returns (google.protobuf.Empty);
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) {}
@@ -24,14 +22,41 @@ service StartedService {
rpc SelectOutbound(SelectOutboundRequest) returns (google.protobuf.Empty) {}
rpc SetGroupExpand(SetGroupExpandRequest) returns (google.protobuf.Empty) {}
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {}
rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {}
rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {}
rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {}
rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {}
rpc SetTailscaleExitNode(SetTailscaleExitNodeRequest) returns (google.protobuf.Empty) {}
rpc TailscaleLogout(TailscaleLogoutRequest) returns (google.protobuf.Empty) {}
rpc GetTailscaleCertificate(TailscaleCertificateRequest) returns (TailscaleCertificate) {}
rpc StartTailscaleSSHSession(stream TailscaleSSHClientMessage) returns (stream TailscaleSSHServerMessage) {}
rpc SubscribeTaildropInbox(SubscribeTaildropInboxRequest) returns (stream TaildropInbox) {}
rpc MarkTaildropInboxRead(MarkTaildropInboxReadRequest) returns (google.protobuf.Empty) {}
rpc SendTaildropFiles(stream TaildropSendClientMessage) returns (stream TaildropSendServerMessage) {}
rpc DownloadTaildropFile(DownloadTaildropFileRequest) returns (stream DownloadTaildropFileChunk) {}
rpc DeleteTaildropFile(DeleteTaildropFileRequest) returns (google.protobuf.Empty) {}
rpc CancelTaildropReceiving(CancelTaildropReceivingRequest) returns (google.protobuf.Empty) {}
rpc ProvideUSBDevices(stream USBProviderMessage) returns (stream USBServerMessage) {}
rpc SubscribeUSBIPServerStatus(google.protobuf.Empty) returns (stream USBIPServerStatusUpdate) {}
rpc SubscribeOpenConnectStatus(google.protobuf.Empty) returns (stream OpenConnectStatusUpdate) {}
rpc SubmitOpenConnectAuthResponse(OpenConnectAuthResponseSubmission) returns (google.protobuf.Empty) {}
rpc CancelOpenConnectAuthChallenge(OpenConnectAuthChallengeCancel) returns (google.protobuf.Empty) {}
rpc SubscribeOpenVPNStatus(google.protobuf.Empty) returns (stream OpenVPNStatusUpdate) {}
rpc SubmitOpenVPNChallengeResponse(OpenVPNChallengeSubmission) returns (google.protobuf.Empty) {}
rpc CancelOpenVPNChallenge(OpenVPNChallengeCancel) returns (google.protobuf.Empty) {}
rpc SubscribeNotifications(google.protobuf.Empty) returns (stream NotificationEvent) {}
}
message Version {
string version = 1;
int32 apiVersion = 2;
}
message ServiceStatus {
@@ -46,10 +71,6 @@ message ServiceStatus {
string errorMessage = 2;
}
message ReloadServiceRequest {
string newProfileContent = 1;
}
message SubscribeStatusRequest {
int64 interval = 1;
}
@@ -133,15 +154,6 @@ message ClashModeStatus {
string currentMode = 2;
}
message SystemProxyStatus {
bool available = 1;
bool enabled = 2;
}
message SetSystemProxyEnabledRequest {
bool enabled = 1;
}
message SubscribeConnectionsRequest {
int64 interval = 1;
}
@@ -211,8 +223,587 @@ message DeprecatedWarning {
string message = 1;
bool impending = 2;
string migrationLink = 3;
string description = 4;
string deprecatedVersion = 5;
string scheduledVersion = 6;
}
message StartedAt {
int64 startedAt = 1;
}
}
message OutboundList {
repeated GroupItem outbounds = 1;
}
message NetworkQualityTestRequest {
string configURL = 1;
string outboundTag = 2;
bool serial = 3;
int32 maxRuntimeSeconds = 4;
bool http3 = 5;
}
message NetworkQualityTestProgress {
int32 phase = 1;
int64 downloadCapacity = 2;
int64 uploadCapacity = 3;
int32 downloadRPM = 4;
int32 uploadRPM = 5;
int32 idleLatencyMs = 6;
int64 elapsedMs = 7;
bool isFinal = 8;
string error = 9;
int32 downloadCapacityAccuracy = 10;
int32 uploadCapacityAccuracy = 11;
int32 downloadRPMAccuracy = 12;
int32 uploadRPMAccuracy = 13;
}
message STUNTestRequest {
string server = 1;
string outboundTag = 2;
}
message STUNTestProgress {
int32 phase = 1;
string externalAddr = 2;
int32 latencyMs = 3;
int32 natMapping = 4;
int32 natFiltering = 5;
bool isFinal = 6;
string error = 7;
bool natTypeSupported = 8;
}
message TailscaleStatusUpdate {
repeated TailscaleEndpointStatus endpoints = 1;
}
message TailscaleEndpointStatus {
string endpointTag = 1;
string backendState = 2;
string stateText = 3;
string authURL = 4;
string networkName = 5;
string magicDNSSuffix = 6;
TailscalePeer self = 7;
repeated TailscaleUserGroup userGroups = 8;
TailscalePeer exitNode = 9;
bool keyAuth = 10;
bool canShareFiles = 11;
int32 waitingFileCount = 12;
int32 receivingFileCount = 13;
int32 unreadFileCount = 14;
repeated string certDomains = 15;
}
message TailscaleUserGroup {
int64 userID = 1;
string loginName = 2;
string displayName = 3;
string profilePicURL = 4;
repeated TailscalePeer peers = 5;
}
message TailscalePeer {
string hostName = 1;
string dnsName = 2;
string os = 3;
repeated string tailscaleIPs = 4;
bool online = 5;
bool exitNode = 6;
bool exitNodeOption = 7;
bool active = 8;
int64 rxBytes = 9;
int64 txBytes = 10;
int64 keyExpiry = 11;
string stableID = 12;
bool expired = 13;
repeated string sshHostKeys = 14;
bool shareeNode = 15;
int64 lastSeen = 16;
bool canReceiveFiles = 17;
}
message TailscalePingRequest {
string endpointTag = 1;
string peerIP = 2;
}
message TailscalePingResponse {
double latencyMs = 1;
bool isDirect = 2;
string endpoint = 3;
int32 derpRegionID = 4;
string derpRegionCode = 5;
string error = 6;
string peerRelay = 7;
}
message SetTailscaleExitNodeRequest {
string endpointTag = 1;
string stableID = 2;
}
message TailscaleLogoutRequest {
string endpointTag = 1;
}
message TailscaleCertificateRequest {
string endpointTag = 1;
string domain = 2;
int64 minValiditySeconds = 3;
}
message TailscaleCertificate {
bytes certificatePEM = 1;
bytes privateKeyPEM = 2;
}
message TailscaleSSHClientMessage {
oneof message {
TailscaleSSHStart start = 1;
TailscaleSSHInput input = 2;
TailscaleSSHResize resize = 3;
}
}
message TailscaleSSHStart {
string endpointTag = 1;
string peerAddress = 2;
string username = 3;
string terminalType = 4;
int32 columns = 5;
int32 rows = 6;
int32 widthPixels = 7;
int32 heightPixels = 8;
repeated string hostKeys = 9;
bool forward_agent = 10;
}
message TailscaleSSHInput {
bytes data = 1;
}
message TailscaleSSHResize {
int32 columns = 1;
int32 rows = 2;
int32 widthPixels = 3;
int32 heightPixels = 4;
}
message TailscaleSSHServerMessage {
oneof message {
TailscaleSSHAuthBanner authBanner = 1;
TailscaleSSHReady ready = 2;
TailscaleSSHOutput output = 3;
TailscaleSSHExit exit = 4;
TailscaleSSHError error = 5;
}
}
message TailscaleSSHAuthBanner {
string message = 1;
}
message TailscaleSSHReady {
}
message TailscaleSSHOutput {
bytes data = 1;
}
message TailscaleSSHExit {
int32 exitCode = 1;
string signal = 2;
string errorMessage = 3;
}
message TailscaleSSHError {
string message = 1;
}
message SubscribeTaildropInboxRequest {
string endpointTag = 1;
}
message MarkTaildropInboxReadRequest {
string endpointTag = 1;
}
message TaildropInbox {
string endpointTag = 1;
repeated TaildropFile files = 2;
repeated TaildropReceivingFile receiving = 3;
}
message TaildropFile {
string name = 1;
int64 size = 2;
string senderName = 3;
int64 modifiedAt = 4;
}
message TaildropReceivingFile {
string name = 1;
int64 size = 2;
int64 receivedBytes = 3;
string senderID = 4;
string senderName = 5;
}
message TaildropSendClientMessage {
oneof message {
TaildropSendStart start = 1;
TaildropFileChunk chunk = 2;
TaildropFileDone fileDone = 3;
}
}
message TaildropSendStart {
string endpointTag = 1;
string peerStableID = 2;
repeated TaildropOutgoingFile files = 3;
}
message TaildropOutgoingFile {
string name = 1;
int64 size = 2;
}
message TaildropFileChunk {
bytes data = 1;
}
message TaildropFileDone {}
message TaildropSendServerMessage {
oneof message {
TaildropSendProgress progress = 1;
int64 receivedBytes = 2;
}
}
message TaildropSendProgress {
int32 fileIndex = 1;
int64 sentBytes = 2;
bool fileCompleted = 3;
}
message DownloadTaildropFileRequest {
string endpointTag = 1;
string name = 2;
}
message DownloadTaildropFileChunk {
int64 size = 1;
bytes data = 2;
}
message DeleteTaildropFileRequest {
string endpointTag = 1;
string name = 2;
}
message CancelTaildropReceivingRequest {
string endpointTag = 1;
string senderID = 2;
string name = 3;
}
message USBProviderMessage {
oneof message {
USBDeviceAttach attach = 1;
USBDeviceDetach detach = 2;
USBURBResponse urbResponse = 3;
}
}
message USBServerMessage {
oneof message {
USBDeviceReady ready = 1;
USBURBRequest urbRequest = 2;
USBEndpointAbort abort = 3;
USBError error = 4;
}
}
message USBDeviceDescriptor {
string deviceId = 1;
uint32 busNum = 2;
uint32 devNum = 3;
uint32 speed = 4;
uint32 vendorId = 5;
uint32 productId = 6;
uint32 bcdDevice = 7;
uint32 deviceClass = 8;
uint32 deviceSubClass = 9;
uint32 deviceProtocol = 10;
uint32 configurationValue = 11;
uint32 numConfigurations = 12;
repeated USBInterface interfaces = 13;
string serial = 14;
string product = 15;
}
message USBDeviceAttach {
string serverTag = 1;
USBDeviceDescriptor descriptor = 2;
}
message USBInterface {
uint32 interfaceClass = 1;
uint32 interfaceSubClass = 2;
uint32 interfaceProtocol = 3;
}
message USBDeviceDetach {
string deviceId = 1;
}
message USBDeviceReady {
string deviceId = 1;
string busId = 2;
}
message USBURBRequest {
string deviceId = 1;
uint64 seq = 2;
uint32 endpoint = 3;
bool directionIn = 4;
uint32 transferFlags = 5;
bytes setup = 6;
uint32 transferBufferLength = 7;
bytes outData = 8;
int32 numberOfPackets = 9;
int32 startFrame = 10;
int32 interval = 11;
repeated USBIsoPacket isoPackets = 12;
}
message USBURBResponse {
string deviceId = 1;
uint64 seq = 2;
int32 status = 3;
int32 actualLength = 4;
bytes inData = 5;
repeated USBIsoPacket isoPackets = 6;
}
message USBIsoPacket {
int32 offset = 1;
int32 length = 2;
int32 actualLength = 3;
int32 status = 4;
}
message USBEndpointAbort {
string deviceId = 1;
uint32 endpoint = 2;
}
message USBError {
string deviceId = 1;
string message = 2;
}
message USBIPServerStatusUpdate {
repeated USBIPServerStatus servers = 1;
}
message USBIPServerStatus {
string serverTag = 1;
repeated USBSharedDevice devices = 2;
}
message USBSharedDevice {
USBDeviceDescriptor descriptor = 1;
string busId = 2;
string stableId = 3;
USBBackend backend = 4;
USBDeviceState state = 5;
}
enum USBDeviceState {
USB_DEVICE_STATE_IDLE = 0;
USB_DEVICE_STATE_ATTACHED = 1;
USB_DEVICE_STATE_UNAVAILABLE = 2;
}
enum USBBackend {
USB_BACKEND_UNSPECIFIED = 0;
USB_BACKEND_LINUX_SYSFS = 1;
USB_BACKEND_DYNAMIC = 2;
USB_BACKEND_DARWIN_IOKIT = 3;
USB_BACKEND_WINDOWS_VBOXUSB = 4;
}
message OpenConnectStatusUpdate {
repeated OpenConnectEndpointStatus endpoints = 1;
}
message OpenConnectEndpointStatus {
string endpointTag = 1;
string state = 2;
string stateText = 3;
OpenConnectAuthChallenge authChallenge = 4;
string error = 5;
OpenConnectTunnelInfo tunnelInfo = 6;
}
message OpenConnectTunnelInfo {
string server = 1;
string flavor = 2;
string transport = 3;
repeated string ipv4 = 4;
repeated string ipv6 = 5;
repeated string dns = 6;
uint32 mtu = 7;
int64 connectedSince = 8;
}
message OpenConnectAuthChallenge {
string id = 1;
string banner = 2;
string message = 3;
string error = 4;
oneof challenge {
OpenConnectAuthForm form = 5;
OpenConnectBrowserRequest browser = 6;
}
}
message OpenConnectAuthForm {
repeated OpenConnectAuthFormField fields = 1;
}
message OpenConnectAuthFormField {
string submissionKey = 1;
string name = 2;
string label = 3;
string kind = 4;
string value = 5;
repeated OpenConnectAuthFormChoice options = 6;
}
message OpenConnectAuthFormChoice {
string value = 1;
string label = 2;
}
message OpenConnectBrowserRequest {
string url = 1;
string finalURL = 2;
repeated string cookieNames = 3;
repeated string headerNames = 4;
repeated string callbackURLPrefixes = 5;
repeated string earlyCookieNames = 6;
string cacheID = 7;
}
message OpenConnectBrowserCookie {
string name = 1;
string value = 2;
}
message OpenConnectBrowserHeader {
string name = 1;
repeated string values = 2;
}
message OpenConnectAuthFormResponse {
map<string, string> values = 1;
}
message OpenConnectBrowserResult {
string finalURL = 1;
repeated OpenConnectBrowserCookie cookies = 2;
repeated OpenConnectBrowserHeader headers = 3;
}
message OpenConnectAuthResponseSubmission {
string endpointTag = 1;
string challengeID = 2;
oneof response {
OpenConnectAuthFormResponse form = 3;
OpenConnectBrowserResult browser = 4;
}
}
message OpenConnectAuthChallengeCancel {
string endpointTag = 1;
string challengeID = 2;
}
message OpenVPNStatusUpdate {
repeated OpenVPNEndpointStatus endpoints = 1;
}
message OpenVPNEndpointStatus {
string endpointTag = 1;
string state = 2;
string stateText = 3;
OpenVPNChallenge challenge = 4;
string error = 5;
OpenVPNTunnelInfo tunnelInfo = 6;
}
message OpenVPNTunnelInfo {
string server = 1;
reserved 2;
string network = 3;
repeated string ipv4 = 4;
repeated string ipv6 = 5;
repeated string dns = 6;
uint32 mtu = 7;
int64 connectedSince = 8;
string cipher = 9;
}
message OpenVPNChallenge {
string id = 1;
string kind = 2;
string username = 3;
string message = 4;
string url = 5;
string secretMessage = 6;
bool echo = 7;
string previousError = 8;
int64 deadline = 9;
}
message OpenVPNChallengeSubmission {
string endpointTag = 1;
string challengeID = 2;
string username = 3;
string password = 4;
string secret = 5;
}
message OpenVPNChallengeCancel {
string endpointTag = 1;
string challengeID = 2;
}
message NotificationEvent {
oneof event {
Notification send = 1;
NotificationCancel cancel = 2;
}
}
message Notification {
string identifier = 1;
string typeName = 2;
int32 typeID = 3;
string title = 4;
string subtitle = 5;
string body = 6;
string openURL = 7;
}
message NotificationCancel {
string identifier = 1;
int32 typeID = 2;
}
File diff suppressed because it is too large Load Diff
+394
View File
@@ -0,0 +1,394 @@
package daemon
import (
"context"
"errors"
"io"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/service"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
const (
// gRPC splits DATA frames at the unexported transport.http2MaxFrameLen (16384),
// counting the 5-byte message prefix. A chunk message that stays inside one frame
// arrives as a single mem.Buffer, so BufferSlice.MaterializeToBuffer returns it
// without taking a pooled buffer, and codecV2.Marshal fits the 16 KiB pool tier.
TaildropChunkSize = 16*1024 - 64
TaildropProgressMinInterval = 200 * time.Millisecond
// Kept well below the sender's flow control window, so a sender that stops at
// the window edge always has an acknowledgement pending.
TaildropReceiveAckInterval = 1 << 20
)
func resolveTailscaleProvider(instance *Instance, tag string) (adapter.TailscaleEndpoint, string, error) {
if tag != "" {
endpoint, err := resolveTailscaleEndpoint(instance, tag)
if err != nil {
return nil, "", err
}
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
if !loaded {
return nil, "", status.Error(codes.FailedPrecondition, "endpoint does not support tailscale")
}
return provider, endpoint.Tag(), nil
}
endpointManager := service.FromContext[adapter.EndpointManager](instance.ctx)
if endpointManager == nil {
return nil, "", status.Error(codes.FailedPrecondition, "endpoint manager not available")
}
for _, endpoint := range endpointManager.Endpoints() {
if endpoint.Type() != C.TypeTailscale {
continue
}
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
if loaded {
return provider, endpoint.Tag(), nil
}
}
return nil, "", status.Error(codes.NotFound, "no Tailscale endpoint found")
}
func taildropError(err error) error {
if errors.Is(err, os.ErrNotExist) {
return status.Error(codes.NotFound, err.Error())
}
return err
}
func (s *StartedService) SubscribeTaildropInbox(
request *SubscribeTaildropInboxRequest,
server grpc.ServerStreamingServer[TaildropInbox],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
return s.followInstance(server.Context(), func(ctx context.Context, instance *Instance) error {
var (
provider adapter.TailscaleEndpoint
endpointTag string
)
if instance != nil {
var resolveErr error
provider, endpointTag, resolveErr = resolveTailscaleProvider(instance, request.EndpointTag)
if resolveErr != nil && status.Code(resolveErr) != codes.NotFound {
return resolveErr
}
}
if provider == nil {
sendErr := server.Send(&TaildropInbox{})
if sendErr != nil {
return sendErr
}
<-ctx.Done()
return nil
}
subscribeCtx, cancel := context.WithCancel(ctx)
defer cancel()
var sendErr error
subscribeErr := provider.SubscribeTaildropInbox(subscribeCtx, func(inbox *adapter.TaildropInbox) {
if sendErr != nil {
return
}
sendErr = server.Send(taildropInboxToProto(endpointTag, inbox))
if sendErr != nil {
cancel()
}
})
if sendErr != nil {
return sendErr
}
if subscribeErr != nil && !errors.Is(subscribeErr, context.Canceled) {
if !errors.Is(subscribeErr, os.ErrClosed) {
return taildropError(subscribeErr)
}
sendErr = server.Send(&TaildropInbox{EndpointTag: endpointTag})
if sendErr != nil {
return sendErr
}
}
<-ctx.Done()
return nil
})
}
func taildropInboxToProto(endpointTag string, inbox *adapter.TaildropInbox) *TaildropInbox {
result := &TaildropInbox{EndpointTag: endpointTag}
for _, file := range inbox.Files {
result.Files = append(result.Files, &TaildropFile{
Name: file.Name,
Size: file.Size,
SenderName: file.SenderName,
ModifiedAt: file.ModifiedAt,
})
}
for _, file := range inbox.Receiving {
result.Receiving = append(result.Receiving, &TaildropReceivingFile{
Name: file.Name,
Size: file.Size,
ReceivedBytes: file.ReceivedBytes,
SenderID: file.SenderID,
SenderName: file.SenderName,
})
}
return result
}
func (s *StartedService) MarkTaildropInboxRead(ctx context.Context, request *MarkTaildropInboxReadRequest) (*emptypb.Empty, error) {
err := s.waitForStarted(ctx)
if err != nil {
return nil, err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if err != nil {
return nil, err
}
err = provider.MarkTaildropInboxRead()
if err != nil {
return nil, taildropError(err)
}
return &emptypb.Empty{}, nil
}
func (s *StartedService) SendTaildropFiles(
server grpc.BidiStreamingServer[TaildropSendClientMessage, TaildropSendServerMessage],
) error {
streamCtx := server.Context()
err := s.waitForStarted(streamCtx)
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
firstMessage, err := server.Recv()
if err != nil {
return err
}
start := firstMessage.GetStart()
if start == nil {
return status.Error(codes.InvalidArgument, "expected start message")
}
if len(start.Files) == 0 {
return status.Error(codes.InvalidArgument, "no files to send")
}
provider, _, err := resolveTailscaleProvider(boxService, start.EndpointTag)
if err != nil {
return err
}
var sendAccess sync.Mutex
sendMessage := func(message *TaildropSendServerMessage) {
sendAccess.Lock()
defer sendAccess.Unlock()
_ = server.Send(message)
}
chunkSource := &taildropChunkReader{server: server}
chunkSource.reportReceived = func(receivedBytes int64) {
sendMessage(&TaildropSendServerMessage{
Message: &TaildropSendServerMessage_ReceivedBytes{ReceivedBytes: receivedBytes},
})
}
for index, file := range start.Files {
fileIndex := int32(index)
chunkSource.beginFile()
var lastProgress time.Time
err = provider.SendTaildropFile(streamCtx, start.PeerStableID, file.Name, file.Size, chunkSource, func(sentBytes int64) {
now := time.Now()
if now.Sub(lastProgress) < TaildropProgressMinInterval {
return
}
lastProgress = now
sendMessage(&TaildropSendServerMessage{
Message: &TaildropSendServerMessage_Progress{
Progress: &TaildropSendProgress{FileIndex: fileIndex, SentBytes: sentBytes},
},
})
})
if err != nil {
return taildropError(err)
}
err = chunkSource.finishFile(file.Size)
if err != nil {
return err
}
sendMessage(&TaildropSendServerMessage{
Message: &TaildropSendServerMessage_Progress{
Progress: &TaildropSendProgress{FileIndex: fileIndex, SentBytes: chunkSource.fileReceived, FileCompleted: true},
},
})
}
return nil
}
type taildropChunkReader struct {
server grpc.BidiStreamingServer[TaildropSendClientMessage, TaildropSendServerMessage]
buffer []byte
fileDone bool
fileReceived int64
reportReceived func(receivedBytes int64)
received int64
acknowledged int64
}
func (r *taildropChunkReader) beginFile() {
r.fileDone = false
r.fileReceived = 0
}
func (r *taildropChunkReader) Read(destination []byte) (int, error) {
for len(r.buffer) == 0 {
if r.fileDone {
return 0, io.EOF
}
message, err := r.server.Recv()
if err != nil {
return 0, err
}
switch content := message.Message.(type) {
case *TaildropSendClientMessage_Chunk:
r.buffer = content.Chunk.Data
case *TaildropSendClientMessage_FileDone:
r.fileDone = true
default:
return 0, status.Error(codes.InvalidArgument, "expected chunk message")
}
}
n := copy(destination, r.buffer)
r.buffer = r.buffer[n:]
r.fileReceived += int64(n)
r.received += int64(n)
if r.reportReceived != nil && r.received-r.acknowledged >= TaildropReceiveAckInterval {
r.acknowledged = r.received
r.reportReceived(r.received)
}
return n, nil
}
func (r *taildropChunkReader) finishFile(declaredSize int64) error {
for !r.fileDone {
if len(r.buffer) > 0 {
return status.Error(codes.InvalidArgument, "file changed while sending")
}
message, err := r.server.Recv()
if err != nil {
return err
}
switch content := message.Message.(type) {
case *TaildropSendClientMessage_Chunk:
r.buffer = content.Chunk.Data
case *TaildropSendClientMessage_FileDone:
r.fileDone = true
default:
return status.Error(codes.InvalidArgument, "expected chunk message")
}
}
if len(r.buffer) > 0 {
return status.Error(codes.InvalidArgument, "file changed while sending")
}
if declaredSize >= 0 && r.fileReceived != declaredSize {
return status.Error(codes.InvalidArgument, "file changed while sending")
}
return nil
}
func (s *StartedService) DownloadTaildropFile(
request *DownloadTaildropFileRequest,
server grpc.ServerStreamingServer[DownloadTaildropFileChunk],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if err != nil {
return err
}
file, size, err := provider.OpenTaildropFile(request.Name)
if err != nil {
return taildropError(err)
}
defer file.Close()
err = server.Send(&DownloadTaildropFileChunk{Size: size})
if err != nil {
return err
}
buffer := make([]byte, TaildropChunkSize)
for {
n, readErr := file.Read(buffer)
if n > 0 {
err = server.Send(&DownloadTaildropFileChunk{Data: buffer[:n]})
if err != nil {
return err
}
}
if readErr == io.EOF {
return nil
}
if readErr != nil {
return readErr
}
}
}
func (s *StartedService) DeleteTaildropFile(ctx context.Context, request *DeleteTaildropFileRequest) (*emptypb.Empty, error) {
err := s.waitForStarted(ctx)
if err != nil {
return nil, err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if err != nil {
return nil, err
}
err = provider.DeleteTaildropFile(request.Name)
if err != nil {
return nil, taildropError(err)
}
return &emptypb.Empty{}, nil
}
func (s *StartedService) CancelTaildropReceiving(ctx context.Context, request *CancelTaildropReceivingRequest) (*emptypb.Empty, error) {
err := s.waitForStarted(ctx)
if err != nil {
return nil, err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
provider, _, err := resolveTailscaleProvider(boxService, request.EndpointTag)
if err != nil {
return nil, err
}
err = provider.CancelTaildropReceiving(request.SenderID, request.Name)
if err != nil {
return nil, taildropError(err)
}
return &emptypb.Empty{}, nil
}
+331
View File
@@ -0,0 +1,331 @@
package daemon
import (
"bytes"
"context"
"io"
"net"
"os"
"strings"
"sync"
"time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"google.golang.org/grpc"
)
type windowChangeRequest struct {
Columns uint32
Rows uint32
WidthPixels uint32
HeightPixels uint32
}
func (s *StartedService) StartTailscaleSSHSession(
server grpc.BidiStreamingServer[TailscaleSSHClientMessage, TailscaleSSHServerMessage],
) error {
ctx := server.Context()
err := s.waitForStarted(ctx)
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
firstMessage, err := server.Recv()
if err != nil {
return err
}
start := firstMessage.GetStart()
hostKeys := make([]ssh.PublicKey, 0, len(start.HostKeys))
for _, line := range start.HostKeys {
key, _, _, _, parseErr := ssh.ParseAuthorizedKey([]byte(line))
if parseErr != nil {
return E.Cause(parseErr, "parse host key")
}
hostKeys = append(hostKeys, key)
}
endpoint, err := resolveTailscaleEndpoint(boxService, start.EndpointTag)
if err != nil {
return err
}
peerAddr := M.ParseSocksaddrHostPort(start.PeerAddress, 22)
sessionCtx, cancel := context.WithCancel(ctx)
defer cancel()
var sendAccess sync.Mutex
sendMessage := func(msg *TailscaleSSHServerMessage) {
sendAccess.Lock()
defer sendAccess.Unlock()
sendErr := server.Send(msg)
if sendErr != nil {
cancel()
}
}
finishWithError := func(message string) error {
sendMessage(&TailscaleSSHServerMessage{
Message: &TailscaleSSHServerMessage_Error{Error: &TailscaleSSHError{Message: message}},
})
return nil
}
peerConn, err := endpoint.DialContext(ctx, N.NetworkTCP, peerAddr)
if err != nil {
return finishWithError(E.Cause(err, "dial peer").Error())
}
var lastBanner string
config := &ssh.ClientConfig{
User: start.Username,
Auth: nil,
BannerCallback: func(message string) error {
lastBanner = message
sendMessage(&TailscaleSSHServerMessage{
Message: &TailscaleSSHServerMessage_AuthBanner{
AuthBanner: &TailscaleSSHAuthBanner{Message: message},
},
})
return nil
},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
serverKey := key.Marshal()
for _, hostKey := range hostKeys {
if bytes.Equal(serverKey, hostKey.Marshal()) {
return nil
}
}
return E.New("untrusted host key: ", key.Type())
},
}
sshConn, chans, reqs, err := ssh.NewClientConn(peerConn, peerAddr.String(), config)
if err != nil {
common.Close(peerConn)
banner := strings.TrimSpace(lastBanner)
if banner != "" {
return finishWithError(banner)
}
return finishWithError(E.Cause(err, "ssh handshake").Error())
}
sshClient := ssh.NewClient(sshConn, chans, reqs)
if start.ForwardAgent && s.handler != nil {
agentChannels := sshClient.HandleChannelOpen("auth-agent@openssh.com")
if agentChannels != nil {
go func() {
for newChannel := range agentChannels {
channel, reqs, acceptErr := newChannel.Accept()
if acceptErr != nil {
continue
}
go ssh.DiscardRequests(reqs)
go s.forwardSSHAgentChannel(channel)
}
}()
}
}
sshSession, err := sshClient.NewSession()
if err != nil {
common.Close(sshClient)
return finishWithError(E.Cause(err, "open ssh session").Error())
}
cols := int(start.Columns)
rows := int(start.Rows)
err = sshSession.RequestPty(start.TerminalType, rows, cols, ssh.TerminalModes{
ssh.ECHO: 1,
ssh.ECHOE: 1,
ssh.ECHOK: 1,
ssh.ECHOKE: 1,
ssh.ECHOCTL: 1,
ssh.ICANON: 1,
ssh.ISIG: 1,
ssh.IEXTEN: 1,
ssh.ICRNL: 1,
ssh.IXON: 1,
ssh.IXANY: 1,
ssh.IMAXBEL: 1,
ssh.OPOST: 1,
ssh.ONLCR: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
})
if err != nil {
common.Close(sshSession, sshClient)
return finishWithError(E.Cause(err, "request pty").Error())
}
if start.WidthPixels > 0 || start.HeightPixels > 0 {
_, _ = sshSession.SendRequest("window-change", false, ssh.Marshal(&windowChangeRequest{
Columns: uint32(start.Columns),
Rows: uint32(start.Rows),
WidthPixels: uint32(start.WidthPixels),
HeightPixels: uint32(start.HeightPixels),
}))
}
if start.ForwardAgent && s.handler != nil {
err = agent.RequestAgentForwarding(sshSession)
if err != nil {
common.Close(sshSession, sshClient)
return finishWithError(E.Cause(err, "request agent forwarding").Error())
}
}
stdin, err := sshSession.StdinPipe()
if err != nil {
common.Close(sshSession, sshClient)
return finishWithError(E.Cause(err, "stdin pipe").Error())
}
stdout, err := sshSession.StdoutPipe()
if err != nil {
common.Close(sshSession, sshClient)
return finishWithError(E.Cause(err, "stdout pipe").Error())
}
stderr, err := sshSession.StderrPipe()
if err != nil {
common.Close(sshSession, sshClient)
return finishWithError(E.Cause(err, "stderr pipe").Error())
}
err = sshSession.Shell()
if err != nil {
common.Close(sshSession, sshClient)
return finishWithError(E.Cause(err, "start shell").Error())
}
var workersWg sync.WaitGroup
sendMessage(&TailscaleSSHServerMessage{
Message: &TailscaleSSHServerMessage_Ready{Ready: &TailscaleSSHReady{}},
})
workersWg.Go(func() {
for {
msg, recvErr := server.Recv()
if recvErr == io.EOF {
stdin.Close()
return
}
if recvErr != nil {
cancel()
return
}
switch m := msg.GetMessage().(type) {
case *TailscaleSSHClientMessage_Input:
if len(m.Input.Data) == 0 {
continue
}
_, writeErr := stdin.Write(m.Input.Data)
if writeErr != nil {
cancel()
return
}
case *TailscaleSSHClientMessage_Resize:
_, _ = sshSession.SendRequest("window-change", false, ssh.Marshal(&windowChangeRequest{
Columns: uint32(m.Resize.Columns),
Rows: uint32(m.Resize.Rows),
WidthPixels: uint32(m.Resize.WidthPixels),
HeightPixels: uint32(m.Resize.HeightPixels),
}))
}
}
})
pumpReader := func(reader io.Reader) {
buffer := buf.Get(buf.BufferSize)
defer buf.Put(buffer)
for {
n, readErr := reader.Read(buffer)
if n > 0 {
sendMessage(&TailscaleSSHServerMessage{
Message: &TailscaleSSHServerMessage_Output{Output: &TailscaleSSHOutput{Data: bytes.Clone(buffer[:n])}},
})
}
if readErr != nil {
return
}
}
}
workersWg.Go(func() { pumpReader(stdout) })
workersWg.Go(func() { pumpReader(stderr) })
workersWg.Go(func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-sessionCtx.Done():
return
case <-ticker.C:
_, _, keepAliveErr := sshConn.SendRequest("keepalive@openssh.com", true, nil)
if keepAliveErr != nil {
cancel()
return
}
}
}
})
workersWg.Go(func() {
waitErr := sshSession.Wait()
exitMessage := &TailscaleSSHExit{}
switch waitErrTyped := waitErr.(type) {
case nil:
case *ssh.ExitError:
exitMessage.ExitCode = int32(waitErrTyped.ExitStatus())
exitMessage.Signal = waitErrTyped.Signal()
default:
exitMessage.ErrorMessage = waitErrTyped.Error()
}
sendMessage(&TailscaleSSHServerMessage{
Message: &TailscaleSSHServerMessage_Exit{Exit: exitMessage},
})
cancel()
})
go func() {
<-sessionCtx.Done()
common.Close(peerConn, sshSession, sshClient)
}()
workersWg.Wait()
return nil
}
func (s *StartedService) forwardSSHAgentChannel(channel ssh.Channel) {
defer channel.Close()
fd, err := s.handler.ConnectSSHAgent()
if err != nil {
return
}
file := os.NewFile(uintptr(fd), "ssh-agent")
conn, err := net.FileConn(file)
file.Close()
if err != nil {
return
}
defer conn.Close()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
io.Copy(conn, channel)
}()
go func() {
defer wg.Done()
io.Copy(channel, conn)
}()
wg.Wait()
}
+405
View File
@@ -0,0 +1,405 @@
//go:build with_usbip && (linux || (darwin && cgo) || windows)
package daemon
import (
"context"
"io"
"sync"
"sync/atomic"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-usbip"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
func (s *StartedService) ProvideUSBDevices(server grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error {
ctx := server.Context()
err := s.waitForStarted(ctx)
if err != nil {
return err
}
s.serviceAccess.RLock()
instance := s.instance
s.serviceAccess.RUnlock()
if instance == nil {
return nil
}
serviceManager := service.FromContext[adapter.ServiceManager](instance.ctx)
if serviceManager == nil {
return E.New("missing service manager")
}
sessionCtx, cancel := context.WithCancel(ctx)
defer cancel()
var sendAccess sync.Mutex
send := func(message *USBServerMessage) error {
sendAccess.Lock()
defer sendAccess.Unlock()
return server.Send(message)
}
var devicesAccess sync.Mutex
devices := make(map[string]*usbProvidedDevice)
defer func() {
devicesAccess.Lock()
for _, device := range devices {
device.close()
}
devicesAccess.Unlock()
}()
for {
message, recvErr := server.Recv()
if recvErr != nil {
if recvErr == io.EOF {
return nil
}
return recvErr
}
switch body := message.GetMessage().(type) {
case *USBProviderMessage_Attach:
attach := body.Attach
deviceID := attach.GetDescriptor_().GetDeviceId()
device, addErr := addUSBDevice(sessionCtx, serviceManager, send, attach)
if addErr != nil {
_ = send(&USBServerMessage{Message: &USBServerMessage_Error{Error: &USBError{
DeviceId: deviceID,
Message: addErr.Error(),
}}})
continue
}
devicesAccess.Lock()
previous, replaced := devices[deviceID]
devices[deviceID] = device
devicesAccess.Unlock()
if replaced {
previous.close()
}
_ = send(&USBServerMessage{Message: &USBServerMessage_Ready{Ready: &USBDeviceReady{
DeviceId: deviceID,
BusId: device.busID,
}}})
case *USBProviderMessage_Detach:
deviceID := body.Detach.GetDeviceId()
devicesAccess.Lock()
device, found := devices[deviceID]
if found {
delete(devices, deviceID)
}
devicesAccess.Unlock()
if found {
device.close()
}
case *USBProviderMessage_UrbResponse:
response := body.UrbResponse
devicesAccess.Lock()
device, found := devices[response.GetDeviceId()]
devicesAccess.Unlock()
if found {
device.deliver(response)
}
}
}
}
func (s *StartedService) SubscribeUSBIPServerStatus(
_ *emptypb.Empty,
server grpc.ServerStreamingServer[USBIPServerStatusUpdate],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
return s.followInstance(server.Context(), func(ctx context.Context, instance *Instance) error {
type usbipServer struct {
tag string
provider adapter.USBIPDynamicServer
}
var servers []usbipServer
if instance != nil {
serviceManager := service.FromContext[adapter.ServiceManager](instance.ctx)
for _, serverService := range serviceManager.Services() {
provider, isDynamic := serverService.(adapter.USBIPDynamicServer)
if !isDynamic {
continue
}
servers = append(servers, usbipServer{tag: serverService.Tag(), provider: provider})
}
}
// sing-usbip invokes the SubscribeDevices listener with the current snapshot
// before registration returns, and later invocations happen while the ledger's
// broadcast lock is held, so the listener must never block.
sources := common.Map(servers, func(serverService usbipServer) taggedStatusSource[[]usbip.ControlDeviceInfo] {
return taggedStatusSource[[]usbip.ControlDeviceInfo]{
tag: serverService.tag,
subscribe: func(subscribeCtx context.Context, listener func([]usbip.ControlDeviceInfo)) {
serverService.provider.SubscribeDevices(subscribeCtx, listener)
},
}
})
return streamTaggedStatus(ctx, sources, func(deviceStates map[string][]usbip.ControlDeviceInfo) error {
protoServers := make([]*USBIPServerStatus, 0, len(servers))
for _, serverService := range servers {
devices, found := deviceStates[serverService.tag]
if !found {
continue
}
protoServers = append(protoServers, &USBIPServerStatus{
ServerTag: serverService.tag,
Devices: usbSharedDevicesToProto(devices),
})
}
return server.Send(&USBIPServerStatusUpdate{Servers: protoServers})
})
})
}
func usbSharedDevicesToProto(devices []usbip.ControlDeviceInfo) []*USBSharedDevice {
if len(devices) == 0 {
return nil
}
out := make([]*USBSharedDevice, 0, len(devices))
for _, device := range devices {
interfaces := make([]*USBInterface, 0, len(device.Interfaces))
for _, deviceInterface := range device.Interfaces {
interfaces = append(interfaces, &USBInterface{
InterfaceClass: uint32(deviceInterface.Class),
InterfaceSubClass: uint32(deviceInterface.SubClass),
InterfaceProtocol: uint32(deviceInterface.Protocol),
})
}
out = append(out, &USBSharedDevice{
Descriptor_: &USBDeviceDescriptor{
DeviceId: device.BusID,
BusNum: device.BusNum,
DevNum: device.DevNum,
Speed: device.Speed,
VendorId: uint32(device.VendorID),
ProductId: uint32(device.ProductID),
BcdDevice: uint32(device.BCDDevice),
DeviceClass: uint32(device.DeviceClass),
DeviceSubClass: uint32(device.DeviceSubClass),
DeviceProtocol: uint32(device.DeviceProtocol),
ConfigurationValue: uint32(device.ConfigurationValue),
NumConfigurations: uint32(device.NumConfigurations),
Interfaces: interfaces,
Serial: device.Serial,
Product: device.Product,
},
BusId: device.BusID,
StableId: device.StableID,
Backend: USBBackend(device.Backend),
State: USBDeviceState(device.State),
})
}
return out
}
func addUSBDevice(ctx context.Context, serviceManager adapter.ServiceManager, send func(*USBServerMessage) error, attach *USBDeviceAttach) (*usbProvidedDevice, error) {
serverService, found := serviceManager.Get(attach.GetServerTag())
if !found {
return nil, E.New("usbip-server not found: ", attach.GetServerTag())
}
provider, isDynamic := serverService.(adapter.USBIPDynamicServer)
if !isDynamic {
return nil, E.New("service ", attach.GetServerTag(), " is not a dynamic usbip-server")
}
descriptor := attach.GetDescriptor_()
if descriptor == nil {
return nil, E.New("missing device descriptor")
}
device := &usbProvidedDevice{
deviceID: descriptor.GetDeviceId(),
provider: provider,
send: send,
ctx: ctx,
pending: make(map[uint64]chan *USBURBResponse),
}
entry := usbDeviceEntryFromDescriptor(descriptor)
busID, err := provider.AddDevice(usbip.ProvidedDeviceInfo{Entry: entry}, device)
if err != nil {
return nil, err
}
device.busID = busID
return device, nil
}
func usbDeviceEntryFromDescriptor(descriptor *USBDeviceDescriptor) usbip.DeviceEntry {
deviceID := descriptor.GetDeviceId()
interfaces := usbInterfacesFromProto(descriptor.GetInterfaces())
info := usbip.DeviceInfoTruncated{
BusNum: descriptor.GetBusNum(),
DevNum: descriptor.GetDevNum(),
Speed: descriptor.GetSpeed(),
IDVendor: uint16(descriptor.GetVendorId()),
IDProduct: uint16(descriptor.GetProductId()),
BCDDevice: uint16(descriptor.GetBcdDevice()),
BDeviceClass: uint8(descriptor.GetDeviceClass()),
BDeviceSubClass: uint8(descriptor.GetDeviceSubClass()),
BDeviceProtocol: uint8(descriptor.GetDeviceProtocol()),
BConfigurationValue: uint8(descriptor.GetConfigurationValue()),
BNumConfigurations: uint8(descriptor.GetNumConfigurations()),
BNumInterfaces: uint8(len(interfaces)),
}
copy(info.BusID[:], deviceID)
return usbip.DeviceEntry{
Info: info,
Interfaces: interfaces,
Serial: descriptor.GetSerial(),
Product: descriptor.GetProduct(),
}
}
// sing-usbip calls Submit concurrently across endpoints for a single device.
type usbProvidedDevice struct {
deviceID string
busID string
provider adapter.USBIPDynamicServer
send func(*USBServerMessage) error
ctx context.Context
seq atomic.Uint64
access sync.Mutex
pending map[uint64]chan *USBURBResponse
closed bool
}
func (d *usbProvidedDevice) Submit(request usbip.URBRequest) usbip.URBResponse {
seq := d.seq.Add(1)
responseChan := make(chan *USBURBResponse, 1)
d.access.Lock()
if d.closed {
d.access.Unlock()
return usbip.URBResponse{Error: E.New("device detached")}
}
d.pending[seq] = responseChan
d.access.Unlock()
defer func() {
d.access.Lock()
delete(d.pending, seq)
d.access.Unlock()
}()
directionIn := request.Endpoint&0x80 != 0
message := &USBURBRequest{
DeviceId: d.deviceID,
Seq: seq,
Endpoint: uint32(request.Endpoint),
DirectionIn: directionIn,
TransferFlags: uint32(request.Command.TransferFlags),
Setup: append([]byte(nil), request.Command.Setup[:]...),
TransferBufferLength: uint32(request.Command.TransferBufferLength),
NumberOfPackets: request.Command.NumberOfPackets,
StartFrame: request.Command.StartFrame,
Interval: request.Command.Interval,
IsoPackets: isoPacketsToProto(request.IsoPackets),
}
if !directionIn {
message.OutData = request.Buffer
}
sendErr := d.send(&USBServerMessage{Message: &USBServerMessage_UrbRequest{UrbRequest: message}})
if sendErr != nil {
return usbip.URBResponse{Error: sendErr}
}
select {
case <-d.ctx.Done():
return usbip.URBResponse{Error: d.ctx.Err()}
case response := <-responseChan:
result := usbip.URBResponse{
Status: response.GetStatus(),
ActualLength: response.GetActualLength(),
IsoPackets: isoPacketsFromProto(response.GetIsoPackets()),
}
if directionIn {
result.Buffer = response.GetInData()
}
return result
}
}
func (d *usbProvidedDevice) AbortEndpoint(endpoint uint8) error {
return d.send(&USBServerMessage{Message: &USBServerMessage_Abort{Abort: &USBEndpointAbort{
DeviceId: d.deviceID,
Endpoint: uint32(endpoint),
}}})
}
func (d *usbProvidedDevice) deliver(response *USBURBResponse) {
d.access.Lock()
responseChan, found := d.pending[response.GetSeq()]
d.access.Unlock()
if !found {
return
}
select {
case responseChan <- response:
default:
}
}
func (d *usbProvidedDevice) close() {
d.access.Lock()
if d.closed {
d.access.Unlock()
return
}
d.closed = true
d.access.Unlock()
if d.busID != "" {
d.provider.RemoveDevice(d.busID)
}
}
func usbInterfacesFromProto(interfaces []*USBInterface) []usbip.DeviceInterface {
if len(interfaces) == 0 {
return nil
}
deviceInterfaces := make([]usbip.DeviceInterface, 0, len(interfaces))
for _, deviceInterface := range interfaces {
deviceInterfaces = append(deviceInterfaces, usbip.DeviceInterface{
BInterfaceClass: uint8(deviceInterface.GetInterfaceClass()),
BInterfaceSubClass: uint8(deviceInterface.GetInterfaceSubClass()),
BInterfaceProtocol: uint8(deviceInterface.GetInterfaceProtocol()),
})
}
return deviceInterfaces
}
func isoPacketsToProto(packets []usbip.IsoPacketDescriptor) []*USBIsoPacket {
if len(packets) == 0 {
return nil
}
out := make([]*USBIsoPacket, 0, len(packets))
for _, packet := range packets {
out = append(out, &USBIsoPacket{
Offset: packet.Offset,
Length: packet.Length,
ActualLength: packet.ActualLength,
Status: packet.Status,
})
}
return out
}
func isoPacketsFromProto(packets []*USBIsoPacket) []usbip.IsoPacketDescriptor {
if len(packets) == 0 {
return nil
}
out := make([]usbip.IsoPacketDescriptor, 0, len(packets))
for _, packet := range packets {
out = append(out, usbip.IsoPacketDescriptor{
Offset: packet.GetOffset(),
Length: packet.GetLength(),
ActualLength: packet.GetActualLength(),
Status: packet.GetStatus(),
})
}
return out
}
+18
View File
@@ -0,0 +1,18 @@
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
package daemon
import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
func (s *StartedService) ProvideUSBDevices(server grpc.BidiStreamingServer[USBProviderMessage, USBServerMessage]) error {
return status.Error(codes.Unimplemented, "USB/IP is not included in this build, rebuild with -tags with_usbip")
}
func (s *StartedService) SubscribeUSBIPServerStatus(_ *emptypb.Empty, server grpc.ServerStreamingServer[USBIPServerStatusUpdate]) error {
return status.Error(codes.NotFound, "USB/IP is not included in this build, rebuild with -tags with_usbip")
}