From 3a5d6544634629a79dfcf2ed0cdd45ed52b375a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 11 Jun 2026 21:11:10 +0800 Subject: [PATCH] Add sing-box API service --- Makefile | 4 +- adapter/experimental.go | 2 +- box.go | 18 +- common/trafficcontrol/manager.go | 166 +++++ common/trafficcontrol/tracker.go | 163 +++++ constant/proxy.go | 1 + daemon/attached_service.go | 28 + daemon/client.go | 85 +++ daemon/errors.go | 42 ++ daemon/instance.go | 29 +- daemon/server.go | 66 ++ daemon/started_service.go | 141 +++-- daemon/started_service.pb.go | 598 ++++++++++-------- daemon/started_service.proto | 6 + daemon/started_service_grpc.pb.go | 39 ++ daemon/started_service_tailscale_ssh.go | 4 +- docs/configuration/service/api.md | 57 ++ docs/configuration/service/api.zh.md | 56 ++ docs/configuration/service/index.md | 1 + docs/configuration/service/index.zh.md | 1 + experimental/clashapi/api_meta.go | 14 +- experimental/clashapi/connections.go | 111 +++- experimental/clashapi/server.go | 29 +- .../clashapi/trafficontrol/manager.go | 182 ------ .../clashapi/trafficontrol/tracker.go | 254 -------- experimental/libbox/command_client.go | 68 +- experimental/libbox/command_client_remote.go | 101 +++ include/registry.go | 2 + log/factory.go | 1 + log/nop.go | 3 + log/observable.go | 32 +- mkdocs.yml | 1 + option/api.go | 11 + service/api/server.go | 124 ++++ service/api/web_bridge.go | 235 +++++++ service/api/web_bridge_websocket.go | 249 ++++++++ 36 files changed, 2075 insertions(+), 849 deletions(-) create mode 100644 common/trafficcontrol/manager.go create mode 100644 common/trafficcontrol/tracker.go create mode 100644 daemon/attached_service.go create mode 100644 daemon/client.go create mode 100644 daemon/errors.go create mode 100644 daemon/server.go create mode 100644 docs/configuration/service/api.md create mode 100644 docs/configuration/service/api.zh.md delete mode 100644 experimental/clashapi/trafficontrol/manager.go delete mode 100644 experimental/clashapi/trafficontrol/tracker.go create mode 100644 experimental/libbox/command_client_remote.go create mode 100644 option/api.go create mode 100644 service/api/server.go create mode 100644 service/api/web_bridge.go create mode 100644 service/api/web_bridge_websocket.go diff --git a/Makefile b/Makefile index 670f4c0f0..2de14c981 100644 --- a/Makefile +++ b/Makefile @@ -264,8 +264,8 @@ lib_apple_new: $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple lib_install: - go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12 - go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12 + go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.13 + go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.13 docs: venv/bin/mkdocs serve diff --git a/adapter/experimental.go b/adapter/experimental.go index 49fd2bd31..b4b9e6632 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -13,9 +13,9 @@ import ( type ClashServer interface { LifecycleService - ConnectionTracker Mode() string ModeList() []string + SetMode(mode string) SetModeUpdateHook(hook *observable.Subscriber[struct{}]) HistoryStorage() URLTestHistoryStorage } diff --git a/box.go b/box.go index 5a0868238..1b8baf137 100644 --- a/box.go +++ b/box.go @@ -19,6 +19,8 @@ import ( "github.com/sagernet/sing-box/common/httpclient" "github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/tls" + "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/dns" "github.com/sagernet/sing-box/experimental" @@ -154,6 +156,12 @@ func New(options Options) (*Box, error) { if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { needV2RayAPI = true } + needAPIService := common.Any(options.Services, func(it option.Service) bool { + return it.Type == C.TypeAPI + }) + if needAPIService && service.PtrFromContext[urltest.HistoryStorage](ctx) == nil { + ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage()) + } platformInterface := service.FromContext[adapter.PlatformInterface](ctx) var defaultLogWriter io.Writer if platformInterface != nil { @@ -162,7 +170,7 @@ func New(options Options) (*Box, error) { logFactory, err := log.New(log.Options{ Context: ctx, Options: common.PtrValueOrDefault(options.Log), - Observable: needClashAPI, + Observable: needClashAPI || needAPIService, DefaultWriter: defaultLogWriter, BaseTime: createdAt, PlatformWriter: options.PlatformLogWriter, @@ -170,6 +178,7 @@ func New(options Options) (*Box, error) { if err != nil { return nil, E.Cause(err, "create log factory") } + service.MustRegister[log.Factory](ctx, logFactory) var internalServices []adapter.LifecycleService routeOptions := common.PtrValueOrDefault(options.Route) @@ -221,6 +230,12 @@ func New(options Options) (*Box, error) { if err != nil { return nil, E.Cause(err, "initialize router") } + if needClashAPI || needAPIService { + trafficManager := trafficcontrol.NewManager(outboundManager) + service.MustRegisterPtr(ctx, trafficManager) + router.AppendTracker(trafficManager) + internalServices = append(internalServices, trafficManager) + } ntpOptions := common.PtrValueOrDefault(options.NTP) var timeService *tls.TimeServiceWrapper if ntpOptions.Enabled { @@ -398,7 +413,6 @@ func New(options Options) (*Box, error) { if err != nil { return nil, E.Cause(err, "create clash-server") } - router.AppendTracker(clashServer) service.MustRegister[adapter.ClashServer](ctx, clashServer) internalServices = append(internalServices, clashServer) } diff --git a/common/trafficcontrol/manager.go b/common/trafficcontrol/manager.go new file mode 100644 index 000000000..954263a60 --- /dev/null +++ b/common/trafficcontrol/manager.go @@ -0,0 +1,166 @@ +package trafficcontrol + +import ( + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/compatible" + "github.com/sagernet/sing/common/cleanup" + "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/common/x/list" + + "github.com/gofrs/uuid/v5" +) + +type ConnectionEventType int + +const ( + ConnectionEventNew ConnectionEventType = iota + ConnectionEventClosed +) + +type ConnectionEvent struct { + Type ConnectionEventType + ID uuid.UUID + Metadata *TrackerMetadata + ClosedAt time.Time +} + +const closedConnectionsLimit = 1000 + +var ( + _ adapter.ConnectionTracker = (*Manager)(nil) + _ adapter.LifecycleService = (*Manager)(nil) +) + +type Manager struct { + outbound adapter.OutboundManager + uploadTotal atomic.Int64 + downloadTotal atomic.Int64 + + connections compatible.Map[uuid.UUID, Tracker] + closedConnectionsAccess sync.Mutex + closedConnections list.List[TrackerMetadata] + + eventSubscriber *observable.Subscriber[ConnectionEvent] + eventObserver *observable.Observer[ConnectionEvent] + cleaner *cleanup.Cleaner +} + +func NewManager(outbound adapter.OutboundManager) *Manager { + manager := &Manager{ + outbound: outbound, + eventSubscriber: observable.NewSubscriber[ConnectionEvent](256), + } + manager.eventObserver = observable.NewObserver(manager.eventSubscriber, 64) + manager.cleaner = cleanup.Add(manager.Clear) + return manager +} + +func (m *Manager) Name() string { + return "traffic manager" +} + +func (m *Manager) Start(stage adapter.StartStage) error { + return nil +} + +func (m *Manager) Close() error { + m.cleaner.Close() + return m.eventObserver.Close() +} + +func (m *Manager) SubscribeEvents() (observable.Subscription[ConnectionEvent], <-chan struct{}, error) { + return m.eventObserver.Subscribe() +} + +func (m *Manager) UnSubscribeEvents(subscription observable.Subscription[ConnectionEvent]) { + m.eventObserver.UnSubscribe(subscription) +} + +func (m *Manager) join(tracker Tracker) { + metadata := tracker.Metadata() + m.connections.Store(metadata.ID, tracker) + m.eventSubscriber.Emit(ConnectionEvent{ + Type: ConnectionEventNew, + ID: metadata.ID, + Metadata: metadata, + }) +} + +func (m *Manager) leave(tracker Tracker) { + metadata := tracker.Metadata() + _, loaded := m.connections.LoadAndDelete(metadata.ID) + if !loaded { + return + } + closedAt := time.Now() + metadata.ClosedAt = closedAt + metadataCopy := *metadata + m.closedConnectionsAccess.Lock() + if m.closedConnections.Len() >= closedConnectionsLimit { + m.closedConnections.PopFront() + } + m.closedConnections.PushBack(metadataCopy) + m.closedConnectionsAccess.Unlock() + m.eventSubscriber.Emit(ConnectionEvent{ + Type: ConnectionEventClosed, + ID: metadata.ID, + Metadata: &metadataCopy, + ClosedAt: closedAt, + }) +} + +func (m *Manager) Total() (uplinkTotal int64, downlinkTotal int64) { + return m.uploadTotal.Load(), m.downloadTotal.Load() +} + +func (m *Manager) ConnectionsLen() int { + return m.connections.Len() +} + +func (m *Manager) Connections() []*TrackerMetadata { + var connections []*TrackerMetadata + m.connections.Range(func(_ uuid.UUID, tracker Tracker) bool { + connections = append(connections, tracker.Metadata()) + return true + }) + return connections +} + +func (m *Manager) ClosedConnections() []*TrackerMetadata { + m.closedConnectionsAccess.Lock() + values := m.closedConnections.Array() + m.closedConnectionsAccess.Unlock() + if len(values) == 0 { + return nil + } + connections := make([]*TrackerMetadata, len(values)) + for i := range values { + connections[i] = &values[i] + } + return connections +} + +func (m *Manager) Connection(id uuid.UUID) Tracker { + connection, loaded := m.connections.Load(id) + if !loaded { + return nil + } + return connection +} + +func (m *Manager) CloseAllConnections() { + m.connections.Range(func(_ uuid.UUID, tracker Tracker) bool { + tracker.Close() + return true + }) +} + +func (m *Manager) Clear() { + m.closedConnectionsAccess.Lock() + defer m.closedConnectionsAccess.Unlock() + m.closedConnections.Init() +} diff --git a/common/trafficcontrol/tracker.go b/common/trafficcontrol/tracker.go new file mode 100644 index 000000000..259f1a530 --- /dev/null +++ b/common/trafficcontrol/tracker.go @@ -0,0 +1,163 @@ +package trafficcontrol + +import ( + "context" + "net" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + N "github.com/sagernet/sing/common/network" + + "github.com/gofrs/uuid/v5" +) + +type TrackerMetadata struct { + ID uuid.UUID + Metadata adapter.InboundContext + CreatedAt time.Time + ClosedAt time.Time + Upload *atomic.Int64 + Download *atomic.Int64 + Chain []string + Rule adapter.Rule + Outbound string + OutboundType string +} + +type Tracker interface { + Metadata() *TrackerMetadata + Close() error +} + +func (m *Manager) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { + upload := new(atomic.Int64) + download := new(atomic.Int64) + tracker := &connTracker{ + ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) { + upload.Add(n) + m.uploadTotal.Add(n) + }}, []N.CountFunc{func(n int64) { + download.Add(n) + m.downloadTotal.Add(n) + }}), + metadata: m.newTrackerMetadata(metadata, matchedRule, matchOutbound, upload, download), + manager: m, + } + m.join(tracker) + return tracker +} + +func (m *Manager) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { + upload := new(atomic.Int64) + download := new(atomic.Int64) + tracker := &packetConnTracker{ + PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) { + upload.Add(n) + m.uploadTotal.Add(n) + }}, []N.CountFunc{func(n int64) { + download.Add(n) + m.downloadTotal.Add(n) + }}), + metadata: m.newTrackerMetadata(metadata, matchedRule, matchOutbound, upload, download), + manager: m, + } + m.join(tracker) + return tracker +} + +func (m *Manager) newTrackerMetadata(metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound, upload *atomic.Int64, download *atomic.Int64) TrackerMetadata { + id, _ := uuid.NewV4() + var ( + chain []string + next string + outbound string + outboundType string + ) + if matchOutbound != nil { + next = matchOutbound.Tag() + } else { + next = m.outbound.Default().Tag() + } + for { + detour, loaded := m.outbound.Outbound(next) + if !loaded { + break + } + chain = append(chain, next) + outbound = detour.Tag() + outboundType = detour.Type() + outboundGroup, isGroup := detour.(adapter.OutboundGroup) + if !isGroup { + break + } + next = outboundGroup.Now() + } + return TrackerMetadata{ + ID: id, + Metadata: metadata, + CreatedAt: time.Now(), + Upload: upload, + Download: download, + Chain: common.Reverse(chain), + Rule: matchedRule, + Outbound: outbound, + OutboundType: outboundType, + } +} + +type connTracker struct { + N.ExtendedConn + metadata TrackerMetadata + manager *Manager +} + +func (t *connTracker) Metadata() *TrackerMetadata { + return &t.metadata +} + +func (t *connTracker) Close() error { + t.manager.leave(t) + return t.ExtendedConn.Close() +} + +func (t *connTracker) Upstream() any { + return t.ExtendedConn +} + +func (t *connTracker) ReaderReplaceable() bool { + return true +} + +func (t *connTracker) WriterReplaceable() bool { + return true +} + +type packetConnTracker struct { + N.PacketConn + metadata TrackerMetadata + manager *Manager +} + +func (t *packetConnTracker) Metadata() *TrackerMetadata { + return &t.metadata +} + +func (t *packetConnTracker) Close() error { + t.manager.leave(t) + return t.PacketConn.Close() +} + +func (t *packetConnTracker) Upstream() any { + return t.PacketConn +} + +func (t *packetConnTracker) ReaderReplaceable() bool { + return true +} + +func (t *packetConnTracker) WriterReplaceable() bool { + return true +} diff --git a/constant/proxy.go b/constant/proxy.go index 868a3bb85..3aa8f3d78 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -29,6 +29,7 @@ const ( TypeDERP = "derp" TypeResolved = "resolved" TypeSSMAPI = "ssm-api" + TypeAPI = "api" TypeCCM = "ccm" TypeOCM = "ocm" TypeOOMKiller = "oom-killer" diff --git a/daemon/attached_service.go b/daemon/attached_service.go new file mode 100644 index 000000000..bf965d904 --- /dev/null +++ b/daemon/attached_service.go @@ -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 +} diff --git a/daemon/client.go b/daemon/client.go new file mode 100644 index 000000000..c585a818b --- /dev/null +++ b/daemon/client.go @@ -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...) + } +} diff --git a/daemon/errors.go b/daemon/errors.go new file mode 100644 index 000000000..17a4e8006 --- /dev/null +++ b/daemon/errors.go @@ -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 +} diff --git a/daemon/instance.go b/daemon/instance.go index f16e594e2..42c5b66b5 100644 --- a/daemon/instance.go +++ b/daemon/instance.go @@ -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() } diff --git a/daemon/server.go b/daemon/server.go new file mode 100644 index 000000000..b5e401621 --- /dev/null +++ b/daemon/server.go @@ -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 +} diff --git a/daemon/started_service.go b/daemon/started_service.go index 103b2c71b..36298cd83 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -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 } diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index 5304a4106..88ee18225 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -179,7 +179,59 @@ func (x ServiceStatus_Type) Number() protoreflect.EnumNumber { // Deprecated: Use ServiceStatus_Type.Descriptor instead. func (ServiceStatus_Type) EnumDescriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{0, 0} + return file_daemon_started_service_proto_rawDescGZIP(), []int{1, 0} +} + +type Version struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + ApiVersion int32 `protobuf:"varint,2,opt,name=apiVersion,proto3" json:"apiVersion,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Version) Reset() { + *x = Version{} + mi := &file_daemon_started_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Version) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Version) ProtoMessage() {} + +func (x *Version) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_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 Version.ProtoReflect.Descriptor instead. +func (*Version) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{0} +} + +func (x *Version) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Version) GetApiVersion() int32 { + if x != nil { + return x.ApiVersion + } + return 0 } type ServiceStatus struct { @@ -192,7 +244,7 @@ type ServiceStatus struct { func (x *ServiceStatus) Reset() { *x = ServiceStatus{} - mi := &file_daemon_started_service_proto_msgTypes[0] + mi := &file_daemon_started_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -204,7 +256,7 @@ func (x *ServiceStatus) String() string { func (*ServiceStatus) ProtoMessage() {} func (x *ServiceStatus) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[0] + mi := &file_daemon_started_service_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -217,7 +269,7 @@ func (x *ServiceStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceStatus.ProtoReflect.Descriptor instead. func (*ServiceStatus) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{0} + return file_daemon_started_service_proto_rawDescGZIP(), []int{1} } func (x *ServiceStatus) GetStatus() ServiceStatus_Type { @@ -243,7 +295,7 @@ type SubscribeStatusRequest struct { func (x *SubscribeStatusRequest) Reset() { *x = SubscribeStatusRequest{} - mi := &file_daemon_started_service_proto_msgTypes[1] + mi := &file_daemon_started_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -255,7 +307,7 @@ func (x *SubscribeStatusRequest) String() string { func (*SubscribeStatusRequest) ProtoMessage() {} func (x *SubscribeStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[1] + mi := &file_daemon_started_service_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -268,7 +320,7 @@ func (x *SubscribeStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeStatusRequest.ProtoReflect.Descriptor instead. func (*SubscribeStatusRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{1} + return file_daemon_started_service_proto_rawDescGZIP(), []int{2} } func (x *SubscribeStatusRequest) GetInterval() int64 { @@ -288,7 +340,7 @@ type Log struct { func (x *Log) Reset() { *x = Log{} - mi := &file_daemon_started_service_proto_msgTypes[2] + mi := &file_daemon_started_service_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -300,7 +352,7 @@ func (x *Log) String() string { func (*Log) ProtoMessage() {} func (x *Log) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[2] + mi := &file_daemon_started_service_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -313,7 +365,7 @@ func (x *Log) ProtoReflect() protoreflect.Message { // Deprecated: Use Log.ProtoReflect.Descriptor instead. func (*Log) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{2} + return file_daemon_started_service_proto_rawDescGZIP(), []int{3} } func (x *Log) GetMessages() []*Log_Message { @@ -339,7 +391,7 @@ type DefaultLogLevel struct { func (x *DefaultLogLevel) Reset() { *x = DefaultLogLevel{} - mi := &file_daemon_started_service_proto_msgTypes[3] + mi := &file_daemon_started_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -351,7 +403,7 @@ func (x *DefaultLogLevel) String() string { func (*DefaultLogLevel) ProtoMessage() {} func (x *DefaultLogLevel) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[3] + mi := &file_daemon_started_service_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -364,7 +416,7 @@ func (x *DefaultLogLevel) ProtoReflect() protoreflect.Message { // Deprecated: Use DefaultLogLevel.ProtoReflect.Descriptor instead. func (*DefaultLogLevel) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{3} + return file_daemon_started_service_proto_rawDescGZIP(), []int{4} } func (x *DefaultLogLevel) GetLevel() LogLevel { @@ -391,7 +443,7 @@ type Status struct { func (x *Status) Reset() { *x = Status{} - mi := &file_daemon_started_service_proto_msgTypes[4] + mi := &file_daemon_started_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -403,7 +455,7 @@ func (x *Status) String() string { func (*Status) ProtoMessage() {} func (x *Status) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[4] + mi := &file_daemon_started_service_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -416,7 +468,7 @@ func (x *Status) ProtoReflect() protoreflect.Message { // Deprecated: Use Status.ProtoReflect.Descriptor instead. func (*Status) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{4} + return file_daemon_started_service_proto_rawDescGZIP(), []int{5} } func (x *Status) GetMemory() uint64 { @@ -491,7 +543,7 @@ type Groups struct { func (x *Groups) Reset() { *x = Groups{} - mi := &file_daemon_started_service_proto_msgTypes[5] + mi := &file_daemon_started_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -503,7 +555,7 @@ func (x *Groups) String() string { func (*Groups) ProtoMessage() {} func (x *Groups) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[5] + mi := &file_daemon_started_service_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -516,7 +568,7 @@ func (x *Groups) ProtoReflect() protoreflect.Message { // Deprecated: Use Groups.ProtoReflect.Descriptor instead. func (*Groups) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{5} + return file_daemon_started_service_proto_rawDescGZIP(), []int{6} } func (x *Groups) GetGroup() []*Group { @@ -540,7 +592,7 @@ type Group struct { func (x *Group) Reset() { *x = Group{} - mi := &file_daemon_started_service_proto_msgTypes[6] + mi := &file_daemon_started_service_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -552,7 +604,7 @@ func (x *Group) String() string { func (*Group) ProtoMessage() {} func (x *Group) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[6] + mi := &file_daemon_started_service_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -565,7 +617,7 @@ func (x *Group) ProtoReflect() protoreflect.Message { // Deprecated: Use Group.ProtoReflect.Descriptor instead. func (*Group) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{6} + return file_daemon_started_service_proto_rawDescGZIP(), []int{7} } func (x *Group) GetTag() string { @@ -622,7 +674,7 @@ type GroupItem struct { func (x *GroupItem) Reset() { *x = GroupItem{} - mi := &file_daemon_started_service_proto_msgTypes[7] + mi := &file_daemon_started_service_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -634,7 +686,7 @@ func (x *GroupItem) String() string { func (*GroupItem) ProtoMessage() {} func (x *GroupItem) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[7] + mi := &file_daemon_started_service_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -647,7 +699,7 @@ func (x *GroupItem) ProtoReflect() protoreflect.Message { // Deprecated: Use GroupItem.ProtoReflect.Descriptor instead. func (*GroupItem) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{7} + return file_daemon_started_service_proto_rawDescGZIP(), []int{8} } func (x *GroupItem) GetTag() string { @@ -687,7 +739,7 @@ type URLTestRequest struct { func (x *URLTestRequest) Reset() { *x = URLTestRequest{} - mi := &file_daemon_started_service_proto_msgTypes[8] + mi := &file_daemon_started_service_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -699,7 +751,7 @@ func (x *URLTestRequest) String() string { func (*URLTestRequest) ProtoMessage() {} func (x *URLTestRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[8] + mi := &file_daemon_started_service_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -712,7 +764,7 @@ func (x *URLTestRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use URLTestRequest.ProtoReflect.Descriptor instead. func (*URLTestRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{8} + return file_daemon_started_service_proto_rawDescGZIP(), []int{9} } func (x *URLTestRequest) GetOutboundTag() string { @@ -732,7 +784,7 @@ type SelectOutboundRequest struct { func (x *SelectOutboundRequest) Reset() { *x = SelectOutboundRequest{} - mi := &file_daemon_started_service_proto_msgTypes[9] + mi := &file_daemon_started_service_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -744,7 +796,7 @@ func (x *SelectOutboundRequest) String() string { func (*SelectOutboundRequest) ProtoMessage() {} func (x *SelectOutboundRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[9] + mi := &file_daemon_started_service_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -757,7 +809,7 @@ func (x *SelectOutboundRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SelectOutboundRequest.ProtoReflect.Descriptor instead. func (*SelectOutboundRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{9} + return file_daemon_started_service_proto_rawDescGZIP(), []int{10} } func (x *SelectOutboundRequest) GetGroupTag() string { @@ -784,7 +836,7 @@ type SetGroupExpandRequest struct { func (x *SetGroupExpandRequest) Reset() { *x = SetGroupExpandRequest{} - mi := &file_daemon_started_service_proto_msgTypes[10] + mi := &file_daemon_started_service_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -796,7 +848,7 @@ func (x *SetGroupExpandRequest) String() string { func (*SetGroupExpandRequest) ProtoMessage() {} func (x *SetGroupExpandRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[10] + mi := &file_daemon_started_service_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -809,7 +861,7 @@ func (x *SetGroupExpandRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetGroupExpandRequest.ProtoReflect.Descriptor instead. func (*SetGroupExpandRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{10} + return file_daemon_started_service_proto_rawDescGZIP(), []int{11} } func (x *SetGroupExpandRequest) GetGroupTag() string { @@ -835,7 +887,7 @@ type ClashMode struct { func (x *ClashMode) Reset() { *x = ClashMode{} - mi := &file_daemon_started_service_proto_msgTypes[11] + mi := &file_daemon_started_service_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -847,7 +899,7 @@ func (x *ClashMode) String() string { func (*ClashMode) ProtoMessage() {} func (x *ClashMode) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[11] + mi := &file_daemon_started_service_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -860,7 +912,7 @@ func (x *ClashMode) ProtoReflect() protoreflect.Message { // Deprecated: Use ClashMode.ProtoReflect.Descriptor instead. func (*ClashMode) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{11} + return file_daemon_started_service_proto_rawDescGZIP(), []int{12} } func (x *ClashMode) GetMode() string { @@ -880,7 +932,7 @@ type ClashModeStatus struct { func (x *ClashModeStatus) Reset() { *x = ClashModeStatus{} - mi := &file_daemon_started_service_proto_msgTypes[12] + mi := &file_daemon_started_service_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -892,7 +944,7 @@ func (x *ClashModeStatus) String() string { func (*ClashModeStatus) ProtoMessage() {} func (x *ClashModeStatus) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[12] + mi := &file_daemon_started_service_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -905,7 +957,7 @@ func (x *ClashModeStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ClashModeStatus.ProtoReflect.Descriptor instead. func (*ClashModeStatus) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{12} + return file_daemon_started_service_proto_rawDescGZIP(), []int{13} } func (x *ClashModeStatus) GetModeList() []string { @@ -931,7 +983,7 @@ type SubscribeConnectionsRequest struct { func (x *SubscribeConnectionsRequest) Reset() { *x = SubscribeConnectionsRequest{} - mi := &file_daemon_started_service_proto_msgTypes[13] + mi := &file_daemon_started_service_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -943,7 +995,7 @@ func (x *SubscribeConnectionsRequest) String() string { func (*SubscribeConnectionsRequest) ProtoMessage() {} func (x *SubscribeConnectionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[13] + mi := &file_daemon_started_service_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -956,7 +1008,7 @@ func (x *SubscribeConnectionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeConnectionsRequest.ProtoReflect.Descriptor instead. func (*SubscribeConnectionsRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{13} + return file_daemon_started_service_proto_rawDescGZIP(), []int{14} } func (x *SubscribeConnectionsRequest) GetInterval() int64 { @@ -980,7 +1032,7 @@ type ConnectionEvent struct { func (x *ConnectionEvent) Reset() { *x = ConnectionEvent{} - mi := &file_daemon_started_service_proto_msgTypes[14] + mi := &file_daemon_started_service_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -992,7 +1044,7 @@ func (x *ConnectionEvent) String() string { func (*ConnectionEvent) ProtoMessage() {} func (x *ConnectionEvent) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[14] + mi := &file_daemon_started_service_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1005,7 +1057,7 @@ func (x *ConnectionEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ConnectionEvent.ProtoReflect.Descriptor instead. func (*ConnectionEvent) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{14} + return file_daemon_started_service_proto_rawDescGZIP(), []int{15} } func (x *ConnectionEvent) GetType() ConnectionEventType { @@ -1060,7 +1112,7 @@ type ConnectionEvents struct { func (x *ConnectionEvents) Reset() { *x = ConnectionEvents{} - mi := &file_daemon_started_service_proto_msgTypes[15] + mi := &file_daemon_started_service_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1072,7 +1124,7 @@ func (x *ConnectionEvents) String() string { func (*ConnectionEvents) ProtoMessage() {} func (x *ConnectionEvents) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[15] + mi := &file_daemon_started_service_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1085,7 +1137,7 @@ func (x *ConnectionEvents) ProtoReflect() protoreflect.Message { // Deprecated: Use ConnectionEvents.ProtoReflect.Descriptor instead. func (*ConnectionEvents) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{15} + return file_daemon_started_service_proto_rawDescGZIP(), []int{16} } func (x *ConnectionEvents) GetEvents() []*ConnectionEvent { @@ -1132,7 +1184,7 @@ type Connection struct { func (x *Connection) Reset() { *x = Connection{} - mi := &file_daemon_started_service_proto_msgTypes[16] + mi := &file_daemon_started_service_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1144,7 +1196,7 @@ func (x *Connection) String() string { func (*Connection) ProtoMessage() {} func (x *Connection) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[16] + mi := &file_daemon_started_service_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1157,7 +1209,7 @@ func (x *Connection) ProtoReflect() protoreflect.Message { // Deprecated: Use Connection.ProtoReflect.Descriptor instead. func (*Connection) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{16} + return file_daemon_started_service_proto_rawDescGZIP(), []int{17} } func (x *Connection) GetId() string { @@ -1327,7 +1379,7 @@ type ProcessInfo struct { func (x *ProcessInfo) Reset() { *x = ProcessInfo{} - mi := &file_daemon_started_service_proto_msgTypes[17] + mi := &file_daemon_started_service_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1339,7 +1391,7 @@ func (x *ProcessInfo) String() string { func (*ProcessInfo) ProtoMessage() {} func (x *ProcessInfo) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[17] + mi := &file_daemon_started_service_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1352,7 +1404,7 @@ func (x *ProcessInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ProcessInfo.ProtoReflect.Descriptor instead. func (*ProcessInfo) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{17} + return file_daemon_started_service_proto_rawDescGZIP(), []int{18} } func (x *ProcessInfo) GetProcessId() uint32 { @@ -1399,7 +1451,7 @@ type CloseConnectionRequest struct { func (x *CloseConnectionRequest) Reset() { *x = CloseConnectionRequest{} - mi := &file_daemon_started_service_proto_msgTypes[18] + mi := &file_daemon_started_service_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1411,7 +1463,7 @@ func (x *CloseConnectionRequest) String() string { func (*CloseConnectionRequest) ProtoMessage() {} func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[18] + mi := &file_daemon_started_service_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1424,7 +1476,7 @@ func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead. func (*CloseConnectionRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{18} + return file_daemon_started_service_proto_rawDescGZIP(), []int{19} } func (x *CloseConnectionRequest) GetId() string { @@ -1443,7 +1495,7 @@ type DeprecatedWarnings struct { func (x *DeprecatedWarnings) Reset() { *x = DeprecatedWarnings{} - mi := &file_daemon_started_service_proto_msgTypes[19] + mi := &file_daemon_started_service_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1455,7 +1507,7 @@ func (x *DeprecatedWarnings) String() string { func (*DeprecatedWarnings) ProtoMessage() {} func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[19] + mi := &file_daemon_started_service_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1468,7 +1520,7 @@ func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message { // Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead. func (*DeprecatedWarnings) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{19} + return file_daemon_started_service_proto_rawDescGZIP(), []int{20} } func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning { @@ -1492,7 +1544,7 @@ type DeprecatedWarning struct { func (x *DeprecatedWarning) Reset() { *x = DeprecatedWarning{} - mi := &file_daemon_started_service_proto_msgTypes[20] + mi := &file_daemon_started_service_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1504,7 +1556,7 @@ func (x *DeprecatedWarning) String() string { func (*DeprecatedWarning) ProtoMessage() {} func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[20] + mi := &file_daemon_started_service_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1517,7 +1569,7 @@ func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message { // Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead. func (*DeprecatedWarning) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{20} + return file_daemon_started_service_proto_rawDescGZIP(), []int{21} } func (x *DeprecatedWarning) GetMessage() string { @@ -1571,7 +1623,7 @@ type StartedAt struct { func (x *StartedAt) Reset() { *x = StartedAt{} - mi := &file_daemon_started_service_proto_msgTypes[21] + mi := &file_daemon_started_service_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1583,7 +1635,7 @@ func (x *StartedAt) String() string { func (*StartedAt) ProtoMessage() {} func (x *StartedAt) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[21] + mi := &file_daemon_started_service_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1596,7 +1648,7 @@ func (x *StartedAt) ProtoReflect() protoreflect.Message { // Deprecated: Use StartedAt.ProtoReflect.Descriptor instead. func (*StartedAt) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{21} + return file_daemon_started_service_proto_rawDescGZIP(), []int{22} } func (x *StartedAt) GetStartedAt() int64 { @@ -1615,7 +1667,7 @@ type OutboundList struct { func (x *OutboundList) Reset() { *x = OutboundList{} - mi := &file_daemon_started_service_proto_msgTypes[22] + mi := &file_daemon_started_service_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1627,7 +1679,7 @@ func (x *OutboundList) String() string { func (*OutboundList) ProtoMessage() {} func (x *OutboundList) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[22] + mi := &file_daemon_started_service_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1640,7 +1692,7 @@ func (x *OutboundList) ProtoReflect() protoreflect.Message { // Deprecated: Use OutboundList.ProtoReflect.Descriptor instead. func (*OutboundList) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{22} + return file_daemon_started_service_proto_rawDescGZIP(), []int{23} } func (x *OutboundList) GetOutbounds() []*GroupItem { @@ -1663,7 +1715,7 @@ type NetworkQualityTestRequest struct { func (x *NetworkQualityTestRequest) Reset() { *x = NetworkQualityTestRequest{} - mi := &file_daemon_started_service_proto_msgTypes[23] + mi := &file_daemon_started_service_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1675,7 +1727,7 @@ func (x *NetworkQualityTestRequest) String() string { func (*NetworkQualityTestRequest) ProtoMessage() {} func (x *NetworkQualityTestRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[23] + mi := &file_daemon_started_service_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1688,7 +1740,7 @@ func (x *NetworkQualityTestRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkQualityTestRequest.ProtoReflect.Descriptor instead. func (*NetworkQualityTestRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{23} + return file_daemon_started_service_proto_rawDescGZIP(), []int{24} } func (x *NetworkQualityTestRequest) GetConfigURL() string { @@ -1747,7 +1799,7 @@ type NetworkQualityTestProgress struct { func (x *NetworkQualityTestProgress) Reset() { *x = NetworkQualityTestProgress{} - mi := &file_daemon_started_service_proto_msgTypes[24] + mi := &file_daemon_started_service_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1759,7 +1811,7 @@ func (x *NetworkQualityTestProgress) String() string { func (*NetworkQualityTestProgress) ProtoMessage() {} func (x *NetworkQualityTestProgress) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[24] + mi := &file_daemon_started_service_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1772,7 +1824,7 @@ func (x *NetworkQualityTestProgress) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkQualityTestProgress.ProtoReflect.Descriptor instead. func (*NetworkQualityTestProgress) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{24} + return file_daemon_started_service_proto_rawDescGZIP(), []int{25} } func (x *NetworkQualityTestProgress) GetPhase() int32 { @@ -1876,7 +1928,7 @@ type STUNTestRequest struct { func (x *STUNTestRequest) Reset() { *x = STUNTestRequest{} - mi := &file_daemon_started_service_proto_msgTypes[25] + mi := &file_daemon_started_service_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1888,7 +1940,7 @@ func (x *STUNTestRequest) String() string { func (*STUNTestRequest) ProtoMessage() {} func (x *STUNTestRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[25] + mi := &file_daemon_started_service_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1901,7 +1953,7 @@ func (x *STUNTestRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use STUNTestRequest.ProtoReflect.Descriptor instead. func (*STUNTestRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{25} + return file_daemon_started_service_proto_rawDescGZIP(), []int{26} } func (x *STUNTestRequest) GetServer() string { @@ -1934,7 +1986,7 @@ type STUNTestProgress struct { func (x *STUNTestProgress) Reset() { *x = STUNTestProgress{} - mi := &file_daemon_started_service_proto_msgTypes[26] + mi := &file_daemon_started_service_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1946,7 +1998,7 @@ func (x *STUNTestProgress) String() string { func (*STUNTestProgress) ProtoMessage() {} func (x *STUNTestProgress) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[26] + mi := &file_daemon_started_service_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1959,7 +2011,7 @@ func (x *STUNTestProgress) ProtoReflect() protoreflect.Message { // Deprecated: Use STUNTestProgress.ProtoReflect.Descriptor instead. func (*STUNTestProgress) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{26} + return file_daemon_started_service_proto_rawDescGZIP(), []int{27} } func (x *STUNTestProgress) GetPhase() int32 { @@ -2027,7 +2079,7 @@ type TailscaleStatusUpdate struct { func (x *TailscaleStatusUpdate) Reset() { *x = TailscaleStatusUpdate{} - mi := &file_daemon_started_service_proto_msgTypes[27] + mi := &file_daemon_started_service_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2039,7 +2091,7 @@ func (x *TailscaleStatusUpdate) String() string { func (*TailscaleStatusUpdate) ProtoMessage() {} func (x *TailscaleStatusUpdate) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[27] + mi := &file_daemon_started_service_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2052,7 +2104,7 @@ func (x *TailscaleStatusUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleStatusUpdate.ProtoReflect.Descriptor instead. func (*TailscaleStatusUpdate) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{27} + return file_daemon_started_service_proto_rawDescGZIP(), []int{28} } func (x *TailscaleStatusUpdate) GetEndpoints() []*TailscaleEndpointStatus { @@ -2079,7 +2131,7 @@ type TailscaleEndpointStatus struct { func (x *TailscaleEndpointStatus) Reset() { *x = TailscaleEndpointStatus{} - mi := &file_daemon_started_service_proto_msgTypes[28] + mi := &file_daemon_started_service_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2091,7 +2143,7 @@ func (x *TailscaleEndpointStatus) String() string { func (*TailscaleEndpointStatus) ProtoMessage() {} func (x *TailscaleEndpointStatus) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[28] + mi := &file_daemon_started_service_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2104,7 +2156,7 @@ func (x *TailscaleEndpointStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleEndpointStatus.ProtoReflect.Descriptor instead. func (*TailscaleEndpointStatus) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{28} + return file_daemon_started_service_proto_rawDescGZIP(), []int{29} } func (x *TailscaleEndpointStatus) GetEndpointTag() string { @@ -2183,7 +2235,7 @@ type TailscaleUserGroup struct { func (x *TailscaleUserGroup) Reset() { *x = TailscaleUserGroup{} - mi := &file_daemon_started_service_proto_msgTypes[29] + mi := &file_daemon_started_service_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2195,7 +2247,7 @@ func (x *TailscaleUserGroup) String() string { func (*TailscaleUserGroup) ProtoMessage() {} func (x *TailscaleUserGroup) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[29] + mi := &file_daemon_started_service_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2208,7 +2260,7 @@ func (x *TailscaleUserGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleUserGroup.ProtoReflect.Descriptor instead. func (*TailscaleUserGroup) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{29} + return file_daemon_started_service_proto_rawDescGZIP(), []int{30} } func (x *TailscaleUserGroup) GetUserID() int64 { @@ -2270,7 +2322,7 @@ type TailscalePeer struct { func (x *TailscalePeer) Reset() { *x = TailscalePeer{} - mi := &file_daemon_started_service_proto_msgTypes[30] + mi := &file_daemon_started_service_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2282,7 +2334,7 @@ func (x *TailscalePeer) String() string { func (*TailscalePeer) ProtoMessage() {} func (x *TailscalePeer) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[30] + mi := &file_daemon_started_service_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2295,7 +2347,7 @@ func (x *TailscalePeer) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscalePeer.ProtoReflect.Descriptor instead. func (*TailscalePeer) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{30} + return file_daemon_started_service_proto_rawDescGZIP(), []int{31} } func (x *TailscalePeer) GetHostName() string { @@ -2420,7 +2472,7 @@ type TailscalePingRequest struct { func (x *TailscalePingRequest) Reset() { *x = TailscalePingRequest{} - mi := &file_daemon_started_service_proto_msgTypes[31] + mi := &file_daemon_started_service_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2432,7 +2484,7 @@ func (x *TailscalePingRequest) String() string { func (*TailscalePingRequest) ProtoMessage() {} func (x *TailscalePingRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[31] + mi := &file_daemon_started_service_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2445,7 +2497,7 @@ func (x *TailscalePingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscalePingRequest.ProtoReflect.Descriptor instead. func (*TailscalePingRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{31} + return file_daemon_started_service_proto_rawDescGZIP(), []int{32} } func (x *TailscalePingRequest) GetEndpointTag() string { @@ -2476,7 +2528,7 @@ type TailscalePingResponse struct { func (x *TailscalePingResponse) Reset() { *x = TailscalePingResponse{} - mi := &file_daemon_started_service_proto_msgTypes[32] + mi := &file_daemon_started_service_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2488,7 +2540,7 @@ func (x *TailscalePingResponse) String() string { func (*TailscalePingResponse) ProtoMessage() {} func (x *TailscalePingResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[32] + mi := &file_daemon_started_service_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2501,7 +2553,7 @@ func (x *TailscalePingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscalePingResponse.ProtoReflect.Descriptor instead. func (*TailscalePingResponse) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{32} + return file_daemon_started_service_proto_rawDescGZIP(), []int{33} } func (x *TailscalePingResponse) GetLatencyMs() float64 { @@ -2556,7 +2608,7 @@ type SetTailscaleExitNodeRequest struct { func (x *SetTailscaleExitNodeRequest) Reset() { *x = SetTailscaleExitNodeRequest{} - mi := &file_daemon_started_service_proto_msgTypes[33] + mi := &file_daemon_started_service_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2568,7 +2620,7 @@ func (x *SetTailscaleExitNodeRequest) String() string { func (*SetTailscaleExitNodeRequest) ProtoMessage() {} func (x *SetTailscaleExitNodeRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[33] + mi := &file_daemon_started_service_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2581,7 +2633,7 @@ func (x *SetTailscaleExitNodeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetTailscaleExitNodeRequest.ProtoReflect.Descriptor instead. func (*SetTailscaleExitNodeRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{33} + return file_daemon_started_service_proto_rawDescGZIP(), []int{34} } func (x *SetTailscaleExitNodeRequest) GetEndpointTag() string { @@ -2607,7 +2659,7 @@ type TailscaleLogoutRequest struct { func (x *TailscaleLogoutRequest) Reset() { *x = TailscaleLogoutRequest{} - mi := &file_daemon_started_service_proto_msgTypes[34] + mi := &file_daemon_started_service_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2619,7 +2671,7 @@ func (x *TailscaleLogoutRequest) String() string { func (*TailscaleLogoutRequest) ProtoMessage() {} func (x *TailscaleLogoutRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[34] + mi := &file_daemon_started_service_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2632,7 +2684,7 @@ func (x *TailscaleLogoutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleLogoutRequest.ProtoReflect.Descriptor instead. func (*TailscaleLogoutRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{34} + return file_daemon_started_service_proto_rawDescGZIP(), []int{35} } func (x *TailscaleLogoutRequest) GetEndpointTag() string { @@ -2656,7 +2708,7 @@ type TailscaleSSHClientMessage struct { func (x *TailscaleSSHClientMessage) Reset() { *x = TailscaleSSHClientMessage{} - mi := &file_daemon_started_service_proto_msgTypes[35] + mi := &file_daemon_started_service_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2668,7 +2720,7 @@ func (x *TailscaleSSHClientMessage) String() string { func (*TailscaleSSHClientMessage) ProtoMessage() {} func (x *TailscaleSSHClientMessage) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[35] + mi := &file_daemon_started_service_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2681,7 +2733,7 @@ func (x *TailscaleSSHClientMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHClientMessage.ProtoReflect.Descriptor instead. func (*TailscaleSSHClientMessage) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{35} + return file_daemon_started_service_proto_rawDescGZIP(), []int{36} } func (x *TailscaleSSHClientMessage) GetMessage() isTailscaleSSHClientMessage_Message { @@ -2758,7 +2810,7 @@ type TailscaleSSHStart struct { func (x *TailscaleSSHStart) Reset() { *x = TailscaleSSHStart{} - mi := &file_daemon_started_service_proto_msgTypes[36] + mi := &file_daemon_started_service_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2770,7 +2822,7 @@ func (x *TailscaleSSHStart) String() string { func (*TailscaleSSHStart) ProtoMessage() {} func (x *TailscaleSSHStart) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[36] + mi := &file_daemon_started_service_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2783,7 +2835,7 @@ func (x *TailscaleSSHStart) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHStart.ProtoReflect.Descriptor instead. func (*TailscaleSSHStart) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{36} + return file_daemon_started_service_proto_rawDescGZIP(), []int{37} } func (x *TailscaleSSHStart) GetEndpointTag() string { @@ -2865,7 +2917,7 @@ type TailscaleSSHInput struct { func (x *TailscaleSSHInput) Reset() { *x = TailscaleSSHInput{} - mi := &file_daemon_started_service_proto_msgTypes[37] + mi := &file_daemon_started_service_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2877,7 +2929,7 @@ func (x *TailscaleSSHInput) String() string { func (*TailscaleSSHInput) ProtoMessage() {} func (x *TailscaleSSHInput) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[37] + mi := &file_daemon_started_service_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2890,7 +2942,7 @@ func (x *TailscaleSSHInput) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHInput.ProtoReflect.Descriptor instead. func (*TailscaleSSHInput) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{37} + return file_daemon_started_service_proto_rawDescGZIP(), []int{38} } func (x *TailscaleSSHInput) GetData() []byte { @@ -2912,7 +2964,7 @@ type TailscaleSSHResize struct { func (x *TailscaleSSHResize) Reset() { *x = TailscaleSSHResize{} - mi := &file_daemon_started_service_proto_msgTypes[38] + mi := &file_daemon_started_service_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2924,7 +2976,7 @@ func (x *TailscaleSSHResize) String() string { func (*TailscaleSSHResize) ProtoMessage() {} func (x *TailscaleSSHResize) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[38] + mi := &file_daemon_started_service_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2937,7 +2989,7 @@ func (x *TailscaleSSHResize) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHResize.ProtoReflect.Descriptor instead. func (*TailscaleSSHResize) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{38} + return file_daemon_started_service_proto_rawDescGZIP(), []int{39} } func (x *TailscaleSSHResize) GetColumns() int32 { @@ -2984,7 +3036,7 @@ type TailscaleSSHServerMessage struct { func (x *TailscaleSSHServerMessage) Reset() { *x = TailscaleSSHServerMessage{} - mi := &file_daemon_started_service_proto_msgTypes[39] + mi := &file_daemon_started_service_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2996,7 +3048,7 @@ func (x *TailscaleSSHServerMessage) String() string { func (*TailscaleSSHServerMessage) ProtoMessage() {} func (x *TailscaleSSHServerMessage) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[39] + mi := &file_daemon_started_service_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3009,7 +3061,7 @@ func (x *TailscaleSSHServerMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHServerMessage.ProtoReflect.Descriptor instead. func (*TailscaleSSHServerMessage) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{39} + return file_daemon_started_service_proto_rawDescGZIP(), []int{40} } func (x *TailscaleSSHServerMessage) GetMessage() isTailscaleSSHServerMessage_Message { @@ -3107,7 +3159,7 @@ type TailscaleSSHAuthBanner struct { func (x *TailscaleSSHAuthBanner) Reset() { *x = TailscaleSSHAuthBanner{} - mi := &file_daemon_started_service_proto_msgTypes[40] + mi := &file_daemon_started_service_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3119,7 +3171,7 @@ func (x *TailscaleSSHAuthBanner) String() string { func (*TailscaleSSHAuthBanner) ProtoMessage() {} func (x *TailscaleSSHAuthBanner) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[40] + mi := &file_daemon_started_service_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3132,7 +3184,7 @@ func (x *TailscaleSSHAuthBanner) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHAuthBanner.ProtoReflect.Descriptor instead. func (*TailscaleSSHAuthBanner) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{40} + return file_daemon_started_service_proto_rawDescGZIP(), []int{41} } func (x *TailscaleSSHAuthBanner) GetMessage() string { @@ -3150,7 +3202,7 @@ type TailscaleSSHReady struct { func (x *TailscaleSSHReady) Reset() { *x = TailscaleSSHReady{} - mi := &file_daemon_started_service_proto_msgTypes[41] + mi := &file_daemon_started_service_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3162,7 +3214,7 @@ func (x *TailscaleSSHReady) String() string { func (*TailscaleSSHReady) ProtoMessage() {} func (x *TailscaleSSHReady) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[41] + mi := &file_daemon_started_service_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3175,7 +3227,7 @@ func (x *TailscaleSSHReady) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHReady.ProtoReflect.Descriptor instead. func (*TailscaleSSHReady) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{41} + return file_daemon_started_service_proto_rawDescGZIP(), []int{42} } type TailscaleSSHOutput struct { @@ -3187,7 +3239,7 @@ type TailscaleSSHOutput struct { func (x *TailscaleSSHOutput) Reset() { *x = TailscaleSSHOutput{} - mi := &file_daemon_started_service_proto_msgTypes[42] + mi := &file_daemon_started_service_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3199,7 +3251,7 @@ func (x *TailscaleSSHOutput) String() string { func (*TailscaleSSHOutput) ProtoMessage() {} func (x *TailscaleSSHOutput) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[42] + mi := &file_daemon_started_service_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3212,7 +3264,7 @@ func (x *TailscaleSSHOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHOutput.ProtoReflect.Descriptor instead. func (*TailscaleSSHOutput) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{42} + return file_daemon_started_service_proto_rawDescGZIP(), []int{43} } func (x *TailscaleSSHOutput) GetData() []byte { @@ -3233,7 +3285,7 @@ type TailscaleSSHExit struct { func (x *TailscaleSSHExit) Reset() { *x = TailscaleSSHExit{} - mi := &file_daemon_started_service_proto_msgTypes[43] + mi := &file_daemon_started_service_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3245,7 +3297,7 @@ func (x *TailscaleSSHExit) String() string { func (*TailscaleSSHExit) ProtoMessage() {} func (x *TailscaleSSHExit) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[43] + mi := &file_daemon_started_service_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3258,7 +3310,7 @@ func (x *TailscaleSSHExit) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHExit.ProtoReflect.Descriptor instead. func (*TailscaleSSHExit) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{43} + return file_daemon_started_service_proto_rawDescGZIP(), []int{44} } func (x *TailscaleSSHExit) GetExitCode() int32 { @@ -3291,7 +3343,7 @@ type TailscaleSSHError struct { func (x *TailscaleSSHError) Reset() { *x = TailscaleSSHError{} - mi := &file_daemon_started_service_proto_msgTypes[44] + mi := &file_daemon_started_service_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3303,7 +3355,7 @@ func (x *TailscaleSSHError) String() string { func (*TailscaleSSHError) ProtoMessage() {} func (x *TailscaleSSHError) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[44] + mi := &file_daemon_started_service_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3316,7 +3368,7 @@ func (x *TailscaleSSHError) ProtoReflect() protoreflect.Message { // Deprecated: Use TailscaleSSHError.ProtoReflect.Descriptor instead. func (*TailscaleSSHError) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{44} + return file_daemon_started_service_proto_rawDescGZIP(), []int{45} } func (x *TailscaleSSHError) GetMessage() string { @@ -3336,7 +3388,7 @@ type Log_Message struct { func (x *Log_Message) Reset() { *x = Log_Message{} - mi := &file_daemon_started_service_proto_msgTypes[45] + mi := &file_daemon_started_service_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3348,7 +3400,7 @@ func (x *Log_Message) String() string { func (*Log_Message) ProtoMessage() {} func (x *Log_Message) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[45] + mi := &file_daemon_started_service_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3361,7 +3413,7 @@ func (x *Log_Message) ProtoReflect() protoreflect.Message { // Deprecated: Use Log_Message.ProtoReflect.Descriptor instead. func (*Log_Message) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{2, 0} + return file_daemon_started_service_proto_rawDescGZIP(), []int{3, 0} } func (x *Log_Message) GetLevel() LogLevel { @@ -3382,7 +3434,12 @@ var File_daemon_started_service_proto protoreflect.FileDescriptor const file_daemon_started_service_proto_rawDesc = "" + "\n" + - "\x1cdaemon/started_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xad\x01\n" + + "\x1cdaemon/started_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"C\n" + + "\aVersion\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x1e\n" + + "\n" + + "apiVersion\x18\x02 \x01(\x05R\n" + + "apiVersion\"\xad\x01\n" + "\rServiceStatus\x122\n" + "\x06status\x18\x01 \x01(\x0e2\x1a.daemon.ServiceStatus.TypeR\x06status\x12\"\n" + "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\"D\n" + @@ -3648,8 +3705,10 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x13ConnectionEventType\x12\x18\n" + "\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" + "\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" + - "\x17CONNECTION_EVENT_CLOSED\x10\x022\xb5\x0f\n" + - "\x0eStartedService\x12K\n" + + "\x17CONNECTION_EVENT_CLOSED\x10\x022\xee\x0f\n" + + "\x0eStartedService\x127\n" + + "\n" + + "GetVersion\x12\x16.google.protobuf.Empty\x1a\x0f.daemon.Version\"\x00\x12K\n" + "\x16SubscribeServiceStatus\x12\x16.google.protobuf.Empty\x1a\x15.daemon.ServiceStatus\"\x000\x01\x127\n" + "\fSubscribeLog\x12\x16.google.protobuf.Empty\x1a\v.daemon.Log\"\x000\x01\x12G\n" + "\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12=\n" + @@ -3691,141 +3750,144 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte { var ( file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3) - file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 46) + file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 47) file_daemon_started_service_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ConnectionEventType)(0), // 1: daemon.ConnectionEventType (ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type - (*ServiceStatus)(nil), // 3: daemon.ServiceStatus - (*SubscribeStatusRequest)(nil), // 4: daemon.SubscribeStatusRequest - (*Log)(nil), // 5: daemon.Log - (*DefaultLogLevel)(nil), // 6: daemon.DefaultLogLevel - (*Status)(nil), // 7: daemon.Status - (*Groups)(nil), // 8: daemon.Groups - (*Group)(nil), // 9: daemon.Group - (*GroupItem)(nil), // 10: daemon.GroupItem - (*URLTestRequest)(nil), // 11: daemon.URLTestRequest - (*SelectOutboundRequest)(nil), // 12: daemon.SelectOutboundRequest - (*SetGroupExpandRequest)(nil), // 13: daemon.SetGroupExpandRequest - (*ClashMode)(nil), // 14: daemon.ClashMode - (*ClashModeStatus)(nil), // 15: daemon.ClashModeStatus - (*SubscribeConnectionsRequest)(nil), // 16: daemon.SubscribeConnectionsRequest - (*ConnectionEvent)(nil), // 17: daemon.ConnectionEvent - (*ConnectionEvents)(nil), // 18: daemon.ConnectionEvents - (*Connection)(nil), // 19: daemon.Connection - (*ProcessInfo)(nil), // 20: daemon.ProcessInfo - (*CloseConnectionRequest)(nil), // 21: daemon.CloseConnectionRequest - (*DeprecatedWarnings)(nil), // 22: daemon.DeprecatedWarnings - (*DeprecatedWarning)(nil), // 23: daemon.DeprecatedWarning - (*StartedAt)(nil), // 24: daemon.StartedAt - (*OutboundList)(nil), // 25: daemon.OutboundList - (*NetworkQualityTestRequest)(nil), // 26: daemon.NetworkQualityTestRequest - (*NetworkQualityTestProgress)(nil), // 27: daemon.NetworkQualityTestProgress - (*STUNTestRequest)(nil), // 28: daemon.STUNTestRequest - (*STUNTestProgress)(nil), // 29: daemon.STUNTestProgress - (*TailscaleStatusUpdate)(nil), // 30: daemon.TailscaleStatusUpdate - (*TailscaleEndpointStatus)(nil), // 31: daemon.TailscaleEndpointStatus - (*TailscaleUserGroup)(nil), // 32: daemon.TailscaleUserGroup - (*TailscalePeer)(nil), // 33: daemon.TailscalePeer - (*TailscalePingRequest)(nil), // 34: daemon.TailscalePingRequest - (*TailscalePingResponse)(nil), // 35: daemon.TailscalePingResponse - (*SetTailscaleExitNodeRequest)(nil), // 36: daemon.SetTailscaleExitNodeRequest - (*TailscaleLogoutRequest)(nil), // 37: daemon.TailscaleLogoutRequest - (*TailscaleSSHClientMessage)(nil), // 38: daemon.TailscaleSSHClientMessage - (*TailscaleSSHStart)(nil), // 39: daemon.TailscaleSSHStart - (*TailscaleSSHInput)(nil), // 40: daemon.TailscaleSSHInput - (*TailscaleSSHResize)(nil), // 41: daemon.TailscaleSSHResize - (*TailscaleSSHServerMessage)(nil), // 42: daemon.TailscaleSSHServerMessage - (*TailscaleSSHAuthBanner)(nil), // 43: daemon.TailscaleSSHAuthBanner - (*TailscaleSSHReady)(nil), // 44: daemon.TailscaleSSHReady - (*TailscaleSSHOutput)(nil), // 45: daemon.TailscaleSSHOutput - (*TailscaleSSHExit)(nil), // 46: daemon.TailscaleSSHExit - (*TailscaleSSHError)(nil), // 47: daemon.TailscaleSSHError - (*Log_Message)(nil), // 48: daemon.Log.Message - (*emptypb.Empty)(nil), // 49: google.protobuf.Empty + (*Version)(nil), // 3: daemon.Version + (*ServiceStatus)(nil), // 4: daemon.ServiceStatus + (*SubscribeStatusRequest)(nil), // 5: daemon.SubscribeStatusRequest + (*Log)(nil), // 6: daemon.Log + (*DefaultLogLevel)(nil), // 7: daemon.DefaultLogLevel + (*Status)(nil), // 8: daemon.Status + (*Groups)(nil), // 9: daemon.Groups + (*Group)(nil), // 10: daemon.Group + (*GroupItem)(nil), // 11: daemon.GroupItem + (*URLTestRequest)(nil), // 12: daemon.URLTestRequest + (*SelectOutboundRequest)(nil), // 13: daemon.SelectOutboundRequest + (*SetGroupExpandRequest)(nil), // 14: daemon.SetGroupExpandRequest + (*ClashMode)(nil), // 15: daemon.ClashMode + (*ClashModeStatus)(nil), // 16: daemon.ClashModeStatus + (*SubscribeConnectionsRequest)(nil), // 17: daemon.SubscribeConnectionsRequest + (*ConnectionEvent)(nil), // 18: daemon.ConnectionEvent + (*ConnectionEvents)(nil), // 19: daemon.ConnectionEvents + (*Connection)(nil), // 20: daemon.Connection + (*ProcessInfo)(nil), // 21: daemon.ProcessInfo + (*CloseConnectionRequest)(nil), // 22: daemon.CloseConnectionRequest + (*DeprecatedWarnings)(nil), // 23: daemon.DeprecatedWarnings + (*DeprecatedWarning)(nil), // 24: daemon.DeprecatedWarning + (*StartedAt)(nil), // 25: daemon.StartedAt + (*OutboundList)(nil), // 26: daemon.OutboundList + (*NetworkQualityTestRequest)(nil), // 27: daemon.NetworkQualityTestRequest + (*NetworkQualityTestProgress)(nil), // 28: daemon.NetworkQualityTestProgress + (*STUNTestRequest)(nil), // 29: daemon.STUNTestRequest + (*STUNTestProgress)(nil), // 30: daemon.STUNTestProgress + (*TailscaleStatusUpdate)(nil), // 31: daemon.TailscaleStatusUpdate + (*TailscaleEndpointStatus)(nil), // 32: daemon.TailscaleEndpointStatus + (*TailscaleUserGroup)(nil), // 33: daemon.TailscaleUserGroup + (*TailscalePeer)(nil), // 34: daemon.TailscalePeer + (*TailscalePingRequest)(nil), // 35: daemon.TailscalePingRequest + (*TailscalePingResponse)(nil), // 36: daemon.TailscalePingResponse + (*SetTailscaleExitNodeRequest)(nil), // 37: daemon.SetTailscaleExitNodeRequest + (*TailscaleLogoutRequest)(nil), // 38: daemon.TailscaleLogoutRequest + (*TailscaleSSHClientMessage)(nil), // 39: daemon.TailscaleSSHClientMessage + (*TailscaleSSHStart)(nil), // 40: daemon.TailscaleSSHStart + (*TailscaleSSHInput)(nil), // 41: daemon.TailscaleSSHInput + (*TailscaleSSHResize)(nil), // 42: daemon.TailscaleSSHResize + (*TailscaleSSHServerMessage)(nil), // 43: daemon.TailscaleSSHServerMessage + (*TailscaleSSHAuthBanner)(nil), // 44: daemon.TailscaleSSHAuthBanner + (*TailscaleSSHReady)(nil), // 45: daemon.TailscaleSSHReady + (*TailscaleSSHOutput)(nil), // 46: daemon.TailscaleSSHOutput + (*TailscaleSSHExit)(nil), // 47: daemon.TailscaleSSHExit + (*TailscaleSSHError)(nil), // 48: daemon.TailscaleSSHError + (*Log_Message)(nil), // 49: daemon.Log.Message + (*emptypb.Empty)(nil), // 50: google.protobuf.Empty } ) var file_daemon_started_service_proto_depIdxs = []int32{ 2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type - 48, // 1: daemon.Log.messages:type_name -> daemon.Log.Message + 49, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel - 9, // 3: daemon.Groups.group:type_name -> daemon.Group - 10, // 4: daemon.Group.items:type_name -> daemon.GroupItem + 10, // 3: daemon.Groups.group:type_name -> daemon.Group + 11, // 4: daemon.Group.items:type_name -> daemon.GroupItem 1, // 5: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType - 19, // 6: daemon.ConnectionEvent.connection:type_name -> daemon.Connection - 17, // 7: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent - 20, // 8: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo - 23, // 9: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning - 10, // 10: daemon.OutboundList.outbounds:type_name -> daemon.GroupItem - 31, // 11: daemon.TailscaleStatusUpdate.endpoints:type_name -> daemon.TailscaleEndpointStatus - 33, // 12: daemon.TailscaleEndpointStatus.self:type_name -> daemon.TailscalePeer - 32, // 13: daemon.TailscaleEndpointStatus.userGroups:type_name -> daemon.TailscaleUserGroup - 33, // 14: daemon.TailscaleEndpointStatus.exitNode:type_name -> daemon.TailscalePeer - 33, // 15: daemon.TailscaleUserGroup.peers:type_name -> daemon.TailscalePeer - 39, // 16: daemon.TailscaleSSHClientMessage.start:type_name -> daemon.TailscaleSSHStart - 40, // 17: daemon.TailscaleSSHClientMessage.input:type_name -> daemon.TailscaleSSHInput - 41, // 18: daemon.TailscaleSSHClientMessage.resize:type_name -> daemon.TailscaleSSHResize - 43, // 19: daemon.TailscaleSSHServerMessage.authBanner:type_name -> daemon.TailscaleSSHAuthBanner - 44, // 20: daemon.TailscaleSSHServerMessage.ready:type_name -> daemon.TailscaleSSHReady - 45, // 21: daemon.TailscaleSSHServerMessage.output:type_name -> daemon.TailscaleSSHOutput - 46, // 22: daemon.TailscaleSSHServerMessage.exit:type_name -> daemon.TailscaleSSHExit - 47, // 23: daemon.TailscaleSSHServerMessage.error:type_name -> daemon.TailscaleSSHError + 20, // 6: daemon.ConnectionEvent.connection:type_name -> daemon.Connection + 18, // 7: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent + 21, // 8: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo + 24, // 9: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning + 11, // 10: daemon.OutboundList.outbounds:type_name -> daemon.GroupItem + 32, // 11: daemon.TailscaleStatusUpdate.endpoints:type_name -> daemon.TailscaleEndpointStatus + 34, // 12: daemon.TailscaleEndpointStatus.self:type_name -> daemon.TailscalePeer + 33, // 13: daemon.TailscaleEndpointStatus.userGroups:type_name -> daemon.TailscaleUserGroup + 34, // 14: daemon.TailscaleEndpointStatus.exitNode:type_name -> daemon.TailscalePeer + 34, // 15: daemon.TailscaleUserGroup.peers:type_name -> daemon.TailscalePeer + 40, // 16: daemon.TailscaleSSHClientMessage.start:type_name -> daemon.TailscaleSSHStart + 41, // 17: daemon.TailscaleSSHClientMessage.input:type_name -> daemon.TailscaleSSHInput + 42, // 18: daemon.TailscaleSSHClientMessage.resize:type_name -> daemon.TailscaleSSHResize + 44, // 19: daemon.TailscaleSSHServerMessage.authBanner:type_name -> daemon.TailscaleSSHAuthBanner + 45, // 20: daemon.TailscaleSSHServerMessage.ready:type_name -> daemon.TailscaleSSHReady + 46, // 21: daemon.TailscaleSSHServerMessage.output:type_name -> daemon.TailscaleSSHOutput + 47, // 22: daemon.TailscaleSSHServerMessage.exit:type_name -> daemon.TailscaleSSHExit + 48, // 23: daemon.TailscaleSSHServerMessage.error:type_name -> daemon.TailscaleSSHError 0, // 24: daemon.Log.Message.level:type_name -> daemon.LogLevel - 49, // 25: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty - 49, // 26: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty - 49, // 27: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty - 49, // 28: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty - 4, // 29: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest - 49, // 30: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty - 49, // 31: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty - 49, // 32: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty - 14, // 33: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode - 11, // 34: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest - 12, // 35: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest - 13, // 36: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest - 49, // 37: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty - 16, // 38: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest - 21, // 39: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest - 49, // 40: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty - 49, // 41: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty - 49, // 42: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty - 49, // 43: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty - 26, // 44: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest - 28, // 45: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest - 49, // 46: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty - 34, // 47: daemon.StartedService.StartTailscalePing:input_type -> daemon.TailscalePingRequest - 36, // 48: daemon.StartedService.SetTailscaleExitNode:input_type -> daemon.SetTailscaleExitNodeRequest - 37, // 49: daemon.StartedService.TailscaleLogout:input_type -> daemon.TailscaleLogoutRequest - 38, // 50: daemon.StartedService.StartTailscaleSSHSession:input_type -> daemon.TailscaleSSHClientMessage - 3, // 51: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus - 5, // 52: daemon.StartedService.SubscribeLog:output_type -> daemon.Log - 6, // 53: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel - 49, // 54: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty - 7, // 55: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status - 8, // 56: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups - 15, // 57: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus - 14, // 58: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode - 49, // 59: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty - 49, // 60: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty - 49, // 61: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty - 49, // 62: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty - 49, // 63: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty - 18, // 64: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents - 49, // 65: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty - 49, // 66: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty - 22, // 67: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings - 24, // 68: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt - 25, // 69: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList - 27, // 70: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress - 29, // 71: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress - 30, // 72: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate - 35, // 73: daemon.StartedService.StartTailscalePing:output_type -> daemon.TailscalePingResponse - 49, // 74: daemon.StartedService.SetTailscaleExitNode:output_type -> google.protobuf.Empty - 49, // 75: daemon.StartedService.TailscaleLogout:output_type -> google.protobuf.Empty - 42, // 76: daemon.StartedService.StartTailscaleSSHSession:output_type -> daemon.TailscaleSSHServerMessage - 51, // [51:77] is the sub-list for method output_type - 25, // [25:51] is the sub-list for method input_type + 50, // 25: daemon.StartedService.GetVersion:input_type -> google.protobuf.Empty + 50, // 26: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty + 50, // 27: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty + 50, // 28: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty + 50, // 29: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty + 5, // 30: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest + 50, // 31: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty + 50, // 32: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty + 50, // 33: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty + 15, // 34: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode + 12, // 35: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest + 13, // 36: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest + 14, // 37: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest + 50, // 38: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty + 17, // 39: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest + 22, // 40: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest + 50, // 41: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty + 50, // 42: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty + 50, // 43: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty + 50, // 44: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty + 27, // 45: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest + 29, // 46: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest + 50, // 47: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty + 35, // 48: daemon.StartedService.StartTailscalePing:input_type -> daemon.TailscalePingRequest + 37, // 49: daemon.StartedService.SetTailscaleExitNode:input_type -> daemon.SetTailscaleExitNodeRequest + 38, // 50: daemon.StartedService.TailscaleLogout:input_type -> daemon.TailscaleLogoutRequest + 39, // 51: daemon.StartedService.StartTailscaleSSHSession:input_type -> daemon.TailscaleSSHClientMessage + 3, // 52: daemon.StartedService.GetVersion:output_type -> daemon.Version + 4, // 53: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus + 6, // 54: daemon.StartedService.SubscribeLog:output_type -> daemon.Log + 7, // 55: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel + 50, // 56: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty + 8, // 57: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status + 9, // 58: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups + 16, // 59: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus + 15, // 60: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode + 50, // 61: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty + 50, // 62: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty + 50, // 63: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty + 50, // 64: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty + 50, // 65: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty + 19, // 66: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents + 50, // 67: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty + 50, // 68: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty + 23, // 69: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings + 25, // 70: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt + 26, // 71: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList + 28, // 72: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 30, // 73: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress + 31, // 74: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate + 36, // 75: daemon.StartedService.StartTailscalePing:output_type -> daemon.TailscalePingResponse + 50, // 76: daemon.StartedService.SetTailscaleExitNode:output_type -> google.protobuf.Empty + 50, // 77: daemon.StartedService.TailscaleLogout:output_type -> google.protobuf.Empty + 43, // 78: daemon.StartedService.StartTailscaleSSHSession:output_type -> daemon.TailscaleSSHServerMessage + 52, // [52:79] is the sub-list for method output_type + 25, // [25:52] is the sub-list for method input_type 25, // [25:25] is the sub-list for extension type_name 25, // [25:25] is the sub-list for extension extendee 0, // [0:25] is the sub-list for field type_name @@ -3836,12 +3898,12 @@ func file_daemon_started_service_proto_init() { if File_daemon_started_service_proto != nil { return } - file_daemon_started_service_proto_msgTypes[35].OneofWrappers = []any{ + file_daemon_started_service_proto_msgTypes[36].OneofWrappers = []any{ (*TailscaleSSHClientMessage_Start)(nil), (*TailscaleSSHClientMessage_Input)(nil), (*TailscaleSSHClientMessage_Resize)(nil), } - file_daemon_started_service_proto_msgTypes[39].OneofWrappers = []any{ + file_daemon_started_service_proto_msgTypes[40].OneofWrappers = []any{ (*TailscaleSSHServerMessage_AuthBanner)(nil), (*TailscaleSSHServerMessage_Ready)(nil), (*TailscaleSSHServerMessage_Output)(nil), @@ -3854,7 +3916,7 @@ func file_daemon_started_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)), NumEnums: 3, - NumMessages: 46, + NumMessages: 47, NumExtensions: 0, NumServices: 1, }, diff --git a/daemon/started_service.proto b/daemon/started_service.proto index e83f3b84b..f03deef71 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -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; diff --git a/daemon/started_service_grpc.pb.go b/daemon/started_service_grpc.pb.go index 76a6ef536..398edaee3 100644 --- a/daemon/started_service_grpc.pb.go +++ b/daemon/started_service_grpc.pb.go @@ -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, diff --git a/daemon/started_service_tailscale_ssh.go b/daemon/started_service_tailscale_ssh.go index f575e4642..c3030fe5c 100644 --- a/daemon/started_service_tailscale_ssh.go +++ b/daemon/started_service_tailscale_ssh.go @@ -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) diff --git a/docs/configuration/service/api.md b/docs/configuration/service/api.md new file mode 100644 index 000000000..e24478b22 --- /dev/null +++ b/docs/configuration/service/api.md @@ -0,0 +1,57 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +# sing-box API + +The sing-box API service is a gRPC server for observing and controlling the running sing-box instance. + +The server also accepts [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) requests, +including the WebSocket transport of [@improbable-eng/grpc-web](https://github.com/improbable-eng/grpc-web) +for bidirectional streaming methods. + +### Structure + +```json +{ + "type": "api", + + ... // Listen Fields + + "secret": "", + "access_control_allow_origin": [], + "access_control_allow_private_network": false, + "tls": {} +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### secret + +Secret for the API. + +Clients authenticate with the standard `authorization: Bearer ` gRPC metadata header. + +If empty, authentication is disabled. + +#### access_control_allow_origin + +CORS allowed origins, `*` will be used if empty. + +#### access_control_allow_private_network + +Allow access from private network. + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +Connection tracking and Clash mode methods require [Clash API](/configuration/experimental/clash-api/) +to be configured, otherwise they fail with `UNIMPLEMENTED`. diff --git a/docs/configuration/service/api.zh.md b/docs/configuration/service/api.zh.md new file mode 100644 index 000000000..6db5640ba --- /dev/null +++ b/docs/configuration/service/api.zh.md @@ -0,0 +1,56 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +# sing-box API + +sing-box API 服务是用于观察与控制正在运行的 sing-box 实例的 gRPC 服务器。 + +服务器同时接受 [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) 请求, +包括用于双向流方法的 [@improbable-eng/grpc-web](https://github.com/improbable-eng/grpc-web) WebSocket 传输。 + +### 结构 + +```json +{ + "type": "api", + + ... // 监听字段 + + "secret": "", + "access_control_allow_origin": [], + "access_control_allow_private_network": false, + "tls": {} +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### secret + +API 密钥。 + +客户端通过标准的 `authorization: Bearer ` gRPC metadata 头认证。 + +留空则禁用认证。 + +#### access_control_allow_origin + +允许的 CORS 来源,默认使用 `*`。 + +#### access_control_allow_private_network + +允许从私有网络访问。 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 + +连接跟踪与 Clash 模式方法需要配置 [Clash API](/zh/configuration/experimental/clash-api/), +否则将以 `UNIMPLEMENTED` 失败。 diff --git a/docs/configuration/service/index.md b/docs/configuration/service/index.md index dea66d971..8215285da 100644 --- a/docs/configuration/service/index.md +++ b/docs/configuration/service/index.md @@ -23,6 +23,7 @@ icon: material/new-box | Type | Format | |-------------------|---------------------------------------| +| `api` | [sing-box API](./api) | | `ccm` | [CCM](./ccm) | | `derp` | [DERP](./derp) | | `hysteria-realm` | [Hysteria Realm](./hysteria-realm) | diff --git a/docs/configuration/service/index.zh.md b/docs/configuration/service/index.zh.md index 240dce2cd..cc56975c0 100644 --- a/docs/configuration/service/index.zh.md +++ b/docs/configuration/service/index.zh.md @@ -23,6 +23,7 @@ icon: material/new-box | 类型 | 格式 | |-------------------|---------------------------------------| +| `api` | [sing-box API](./api) | | `ccm` | [CCM](./ccm) | | `derp` | [DERP](./derp) | | `hysteria-realm` | [Hysteria Realm](./hysteria-realm) | diff --git a/experimental/clashapi/api_meta.go b/experimental/clashapi/api_meta.go index 8b31d8a42..ac384b3d1 100644 --- a/experimental/clashapi/api_meta.go +++ b/experimental/clashapi/api_meta.go @@ -5,10 +5,10 @@ import ( "context" "net" "net/http" + "runtime" "runtime/debug" "time" - "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing/common/json" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" @@ -28,7 +28,7 @@ func (s *Server) setupMetaAPI(r chi.Router) { }) r.Mount("/", middleware.Profiler()) } - r.Get("/memory", memory(s.ctx, s.trafficManager)) + r.Get("/memory", memory(s.ctx)) r.Mount("/group", groupRouter(s)) r.Mount("/upgrade", upgradeRouter(s)) } @@ -38,7 +38,13 @@ type Memory struct { OSLimit uint64 `json:"oslimit"` // maybe we need it in the future } -func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { +func inuseMemory() uint64 { + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + return memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased +} + +func memory(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var conn net.Conn if r.Header.Get("Upgrade") == "websocket" { @@ -68,7 +74,7 @@ func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w h } buf.Reset() - inuse := trafficManager.Snapshot().Memory + inuse := inuseMemory() // make chat.js begin with zero // this is shit var,but we need output 0 for first time diff --git a/experimental/clashapi/connections.go b/experimental/clashapi/connections.go index e60e6622f..4b6038f27 100644 --- a/experimental/clashapi/connections.go +++ b/experimental/clashapi/connections.go @@ -8,7 +8,10 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" + "github.com/sagernet/sing-box/common/trafficcontrol" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" @@ -18,7 +21,7 @@ import ( "github.com/gofrs/uuid/v5" ) -func connectionRouter(ctx context.Context, network adapter.NetworkManager, trafficManager *trafficontrol.Manager) http.Handler { +func connectionRouter(ctx context.Context, network adapter.NetworkManager, trafficManager *trafficcontrol.Manager) http.Handler { r := chi.NewRouter() r.Get("/", getConnections(ctx, trafficManager)) r.Delete("/", closeAllConnections(network, trafficManager)) @@ -26,11 +29,85 @@ func connectionRouter(ctx context.Context, network adapter.NetworkManager, traff return r } -func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { +func connectionsSnapshot(trafficManager *trafficcontrol.Manager) render.M { + uplinkTotal, downlinkTotal := trafficManager.Total() + connections := common.Filter(trafficManager.Connections(), func(metadata *trafficcontrol.TrackerMetadata) bool { + return metadata.OutboundType != C.TypeDNS + }) + return render.M{ + "downloadTotal": downlinkTotal, + "uploadTotal": uplinkTotal, + "connections": common.Map(connections, func(metadata *trafficcontrol.TrackerMetadata) connectionObject { + return connectionObject(*metadata) + }), + "memory": inuseMemory(), + } +} + +type connectionObject trafficcontrol.TrackerMetadata + +func (c connectionObject) MarshalJSON() ([]byte, error) { + var inbound string + if c.Metadata.Inbound != "" { + inbound = c.Metadata.InboundType + "/" + c.Metadata.Inbound + } else { + inbound = c.Metadata.InboundType + } + var domain string + if c.Metadata.Domain != "" { + domain = c.Metadata.Domain + } else { + domain = c.Metadata.Destination.Fqdn + } + var processPath string + if c.Metadata.ProcessInfo != nil { + if c.Metadata.ProcessInfo.ProcessPath != "" { + processPath = c.Metadata.ProcessInfo.ProcessPath + } else if len(c.Metadata.ProcessInfo.AndroidPackageNames) > 0 { + processPath = c.Metadata.ProcessInfo.AndroidPackageNames[0] + } + if processPath == "" { + if c.Metadata.ProcessInfo.UserId != -1 { + processPath = F.ToString(c.Metadata.ProcessInfo.UserId) + } + } else if c.Metadata.ProcessInfo.UserName != "" { + processPath = F.ToString(processPath, " (", c.Metadata.ProcessInfo.UserName, ")") + } else if c.Metadata.ProcessInfo.UserId != -1 { + processPath = F.ToString(processPath, " (", c.Metadata.ProcessInfo.UserId, ")") + } + } + var rule string + if c.Rule != nil { + rule = F.ToString(c.Rule, " => ", c.Rule.Action()) + } else { + rule = "final" + } + return json.Marshal(map[string]any{ + "id": c.ID, + "metadata": map[string]any{ + "network": c.Metadata.Network, + "type": inbound, + "sourceIP": c.Metadata.Source.Addr, + "destinationIP": c.Metadata.Destination.Addr, + "sourcePort": F.ToString(c.Metadata.Source.Port), + "destinationPort": F.ToString(c.Metadata.Destination.Port), + "host": domain, + "dnsMode": "normal", + "processPath": processPath, + }, + "upload": c.Upload.Load(), + "download": c.Download.Load(), + "start": c.CreatedAt, + "chains": c.Chain, + "rule": rule, + "rulePayload": "", + }) +} + +func getConnections(ctx context.Context, trafficManager *trafficcontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Upgrade") != "websocket" { - snapshot := trafficManager.Snapshot() - render.JSON(w, r, snapshot) + render.JSON(w, r, connectionsSnapshot(trafficManager)) return } @@ -56,9 +133,9 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) buf := &bytes.Buffer{} sendSnapshot := func() error { buf.Reset() - snapshot := trafficManager.Snapshot() - if err := json.NewEncoder(buf).Encode(snapshot); err != nil { - return err + encodeErr := json.NewEncoder(buf).Encode(connectionsSnapshot(trafficManager)) + if encodeErr != nil { + return encodeErr } return wsutil.WriteServerText(conn, buf.Bytes()) } @@ -82,26 +159,20 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) } } -func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { +func closeConnection(trafficManager *trafficcontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { id := uuid.FromStringOrNil(chi.URLParam(r, "id")) - snapshot := trafficManager.Snapshot() - for _, c := range snapshot.Connections { - if id == c.Metadata().ID { - c.Close() - break - } + targetConnection := trafficManager.Connection(id) + if targetConnection != nil { + targetConnection.Close() } render.NoContent(w, r) } } -func closeAllConnections(network adapter.NetworkManager, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { +func closeAllConnections(network adapter.NetworkManager, trafficManager *trafficcontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - snapshot := trafficManager.Snapshot() - for _, c := range snapshot.Connections { - c.Close() - } + trafficManager.CloseAllConnections() network.ResetNetwork() render.NoContent(w, r) } diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index a41adcdf9..149912d50 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -14,17 +14,15 @@ import ( "github.com/sagernet/cors" "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" - "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/cleanup" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" - N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/observable" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" @@ -50,10 +48,9 @@ type Server struct { endpoint adapter.EndpointManager logger log.Logger httpServer *http.Server - trafficManager *trafficontrol.Manager + trafficManager *trafficcontrol.Manager urlTestHistory adapter.URLTestHistoryStorage logDebug bool - cleaner *cleanup.Cleaner mode string modeList []string @@ -66,7 +63,10 @@ type Server struct { } func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { - trafficManager := trafficontrol.NewManager() + trafficManager := service.PtrFromContext[trafficcontrol.Manager](ctx) + if trafficManager == nil { + return nil, E.New("missing traffic manager") + } chiRouter := chi.NewRouter() s := &Server{ ctx: ctx, @@ -86,7 +86,6 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op externalController: options.ExternalController != "", externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, - cleaner: cleanup.Add(trafficManager.Clear), } s.urlTestHistory = service.FromContext[adapter.URLTestHistoryStorage](ctx) if s.urlTestHistory == nil { @@ -196,9 +195,7 @@ func (s *Server) Start(stage adapter.StartStage) error { func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), - s.trafficManager, s.urlTestHistory, - common.PtrOrNil(s.cleaner), ) } @@ -245,18 +242,6 @@ func (s *Server) HistoryStorage() adapter.URLTestHistoryStorage { return s.urlTestHistory } -func (s *Server) TrafficManager() *trafficontrol.Manager { - return s.trafficManager -} - -func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { - return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound) -} - -func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { - return trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound) -} - func authentication(serverSecret string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { @@ -309,7 +294,7 @@ type Traffic struct { Down int64 `json:"down"` } -func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { +func traffic(ctx context.Context, trafficManager *trafficcontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var conn net.Conn if r.Header.Get("Upgrade") == "websocket" { diff --git a/experimental/clashapi/trafficontrol/manager.go b/experimental/clashapi/trafficontrol/manager.go deleted file mode 100644 index 6bd77def1..000000000 --- a/experimental/clashapi/trafficontrol/manager.go +++ /dev/null @@ -1,182 +0,0 @@ -package trafficontrol - -import ( - "runtime" - "sync" - "sync/atomic" - "time" - - "github.com/sagernet/sing-box/common/compatible" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/json" - "github.com/sagernet/sing/common/observable" - "github.com/sagernet/sing/common/x/list" - - "github.com/gofrs/uuid/v5" -) - -type ConnectionEventType int - -const ( - ConnectionEventNew ConnectionEventType = iota - ConnectionEventUpdate - ConnectionEventClosed -) - -type ConnectionEvent struct { - Type ConnectionEventType - ID uuid.UUID - Metadata *TrackerMetadata - UplinkDelta int64 - DownlinkDelta int64 - ClosedAt time.Time -} - -const closedConnectionsLimit = 1000 - -type Manager struct { - uploadTotal atomic.Int64 - downloadTotal atomic.Int64 - - connections compatible.Map[uuid.UUID, Tracker] - closedConnectionsAccess sync.Mutex - closedConnections list.List[TrackerMetadata] - memory uint64 - - eventSubscriber *observable.Subscriber[ConnectionEvent] -} - -func NewManager() *Manager { - return &Manager{} -} - -func (m *Manager) SetEventHook(subscriber *observable.Subscriber[ConnectionEvent]) { - m.eventSubscriber = subscriber -} - -func (m *Manager) Join(c Tracker) { - metadata := c.Metadata() - m.connections.Store(metadata.ID, c) - if m.eventSubscriber != nil { - m.eventSubscriber.Emit(ConnectionEvent{ - Type: ConnectionEventNew, - ID: metadata.ID, - Metadata: metadata, - }) - } -} - -func (m *Manager) Leave(c Tracker) { - metadata := c.Metadata() - _, loaded := m.connections.LoadAndDelete(metadata.ID) - if loaded { - closedAt := time.Now() - metadata.ClosedAt = closedAt - metadataCopy := *metadata - m.closedConnectionsAccess.Lock() - if m.closedConnections.Len() >= closedConnectionsLimit { - m.closedConnections.PopFront() - } - m.closedConnections.PushBack(metadataCopy) - m.closedConnectionsAccess.Unlock() - if m.eventSubscriber != nil { - m.eventSubscriber.Emit(ConnectionEvent{ - Type: ConnectionEventClosed, - ID: metadata.ID, - Metadata: &metadataCopy, - ClosedAt: closedAt, - }) - } - } -} - -func (m *Manager) PushUploaded(size int64) { - m.uploadTotal.Add(size) -} - -func (m *Manager) PushDownloaded(size int64) { - m.downloadTotal.Add(size) -} - -func (m *Manager) Total() (up int64, down int64) { - return m.uploadTotal.Load(), m.downloadTotal.Load() -} - -func (m *Manager) ConnectionsLen() int { - return m.connections.Len() -} - -func (m *Manager) Connections() []*TrackerMetadata { - var connections []*TrackerMetadata - m.connections.Range(func(_ uuid.UUID, value Tracker) bool { - connections = append(connections, value.Metadata()) - return true - }) - return connections -} - -func (m *Manager) ClosedConnections() []*TrackerMetadata { - m.closedConnectionsAccess.Lock() - values := m.closedConnections.Array() - m.closedConnectionsAccess.Unlock() - if len(values) == 0 { - return nil - } - connections := make([]*TrackerMetadata, len(values)) - for i := range values { - connections[i] = &values[i] - } - return connections -} - -func (m *Manager) Connection(id uuid.UUID) Tracker { - connection, loaded := m.connections.Load(id) - if !loaded { - return nil - } - return connection -} - -func (m *Manager) Snapshot() *Snapshot { - var connections []Tracker - m.connections.Range(func(_ uuid.UUID, value Tracker) bool { - if value.Metadata().OutboundType != C.TypeDNS { - connections = append(connections, value) - } - return true - }) - - var memStats runtime.MemStats - runtime.ReadMemStats(&memStats) - m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased - - return &Snapshot{ - Upload: m.uploadTotal.Load(), - Download: m.downloadTotal.Load(), - Connections: connections, - Memory: m.memory, - } -} - -func (m *Manager) Clear() { - m.closedConnectionsAccess.Lock() - defer m.closedConnectionsAccess.Unlock() - m.closedConnections.Init() -} - -type Snapshot struct { - Download int64 - Upload int64 - Connections []Tracker - Memory uint64 -} - -func (s *Snapshot) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]any{ - "downloadTotal": s.Download, - "uploadTotal": s.Upload, - "connections": common.Map(s.Connections, func(t Tracker) *TrackerMetadata { return t.Metadata() }), - "memory": s.Memory, - }) -} diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go deleted file mode 100644 index f001b77b2..000000000 --- a/experimental/clashapi/trafficontrol/tracker.go +++ /dev/null @@ -1,254 +0,0 @@ -package trafficontrol - -import ( - "net" - "sync/atomic" - "time" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/bufio" - F "github.com/sagernet/sing/common/format" - "github.com/sagernet/sing/common/json" - N "github.com/sagernet/sing/common/network" - - "github.com/gofrs/uuid/v5" -) - -type TrackerMetadata struct { - ID uuid.UUID - Metadata adapter.InboundContext - CreatedAt time.Time - ClosedAt time.Time - Upload *atomic.Int64 - Download *atomic.Int64 - Chain []string - Rule adapter.Rule - Outbound string - OutboundType string -} - -func (t TrackerMetadata) MarshalJSON() ([]byte, error) { - var inbound string - if t.Metadata.Inbound != "" { - inbound = t.Metadata.InboundType + "/" + t.Metadata.Inbound - } else { - inbound = t.Metadata.InboundType - } - var domain string - if t.Metadata.Domain != "" { - domain = t.Metadata.Domain - } else { - domain = t.Metadata.Destination.Fqdn - } - var processPath string - if t.Metadata.ProcessInfo != nil { - if t.Metadata.ProcessInfo.ProcessPath != "" { - processPath = t.Metadata.ProcessInfo.ProcessPath - } else if len(t.Metadata.ProcessInfo.AndroidPackageNames) > 0 { - processPath = t.Metadata.ProcessInfo.AndroidPackageNames[0] - } - if processPath == "" { - if t.Metadata.ProcessInfo.UserId != -1 { - processPath = F.ToString(t.Metadata.ProcessInfo.UserId) - } - } else if t.Metadata.ProcessInfo.UserName != "" { - processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserName, ")") - } else if t.Metadata.ProcessInfo.UserId != -1 { - processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")") - } - } - var rule string - if t.Rule != nil { - rule = F.ToString(t.Rule, " => ", t.Rule.Action()) - } else { - rule = "final" - } - return json.Marshal(map[string]any{ - "id": t.ID, - "metadata": map[string]any{ - "network": t.Metadata.Network, - "type": inbound, - "sourceIP": t.Metadata.Source.Addr, - "destinationIP": t.Metadata.Destination.Addr, - "sourcePort": F.ToString(t.Metadata.Source.Port), - "destinationPort": F.ToString(t.Metadata.Destination.Port), - "host": domain, - "dnsMode": "normal", - "processPath": processPath, - }, - "upload": t.Upload.Load(), - "download": t.Download.Load(), - "start": t.CreatedAt, - "chains": t.Chain, - "rule": rule, - "rulePayload": "", - }) -} - -type Tracker interface { - Metadata() *TrackerMetadata - Close() error -} - -type TCPConn struct { - N.ExtendedConn - metadata TrackerMetadata - manager *Manager -} - -func (tt *TCPConn) Metadata() *TrackerMetadata { - return &tt.metadata -} - -func (tt *TCPConn) Close() error { - tt.manager.Leave(tt) - return tt.ExtendedConn.Close() -} - -func (tt *TCPConn) Upstream() any { - return tt.ExtendedConn -} - -func (tt *TCPConn) ReaderReplaceable() bool { - return true -} - -func (tt *TCPConn) WriterReplaceable() bool { - return true -} - -func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *TCPConn { - id, _ := uuid.NewV4() - var ( - chain []string - next string - outbound string - outboundType string - ) - if matchOutbound != nil { - next = matchOutbound.Tag() - } else { - next = outboundManager.Default().Tag() - } - for { - detour, loaded := outboundManager.Outbound(next) - if !loaded { - break - } - chain = append(chain, next) - outbound = detour.Tag() - outboundType = detour.Type() - group, isGroup := detour.(adapter.OutboundGroup) - if !isGroup { - break - } - next = group.Now() - } - upload := new(atomic.Int64) - download := new(atomic.Int64) - tracker := &TCPConn{ - ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) { - upload.Add(n) - manager.PushUploaded(n) - }}, []N.CountFunc{func(n int64) { - download.Add(n) - manager.PushDownloaded(n) - }}), - metadata: TrackerMetadata{ - ID: id, - Metadata: metadata, - CreatedAt: time.Now(), - Upload: upload, - Download: download, - Chain: common.Reverse(chain), - Rule: matchRule, - Outbound: outbound, - OutboundType: outboundType, - }, - manager: manager, - } - manager.Join(tracker) - return tracker -} - -type UDPConn struct { - N.PacketConn `json:"-"` - metadata TrackerMetadata - manager *Manager -} - -func (ut *UDPConn) Metadata() *TrackerMetadata { - return &ut.metadata -} - -func (ut *UDPConn) Close() error { - ut.manager.Leave(ut) - return ut.PacketConn.Close() -} - -func (ut *UDPConn) Upstream() any { - return ut.PacketConn -} - -func (ut *UDPConn) ReaderReplaceable() bool { - return true -} - -func (ut *UDPConn) WriterReplaceable() bool { - return true -} - -func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *UDPConn { - id, _ := uuid.NewV4() - var ( - chain []string - next string - outbound string - outboundType string - ) - if matchOutbound != nil { - next = matchOutbound.Tag() - } else { - next = outboundManager.Default().Tag() - } - for { - detour, loaded := outboundManager.Outbound(next) - if !loaded { - break - } - chain = append(chain, next) - outbound = detour.Tag() - outboundType = detour.Type() - group, isGroup := detour.(adapter.OutboundGroup) - if !isGroup { - break - } - next = group.Now() - } - upload := new(atomic.Int64) - download := new(atomic.Int64) - trackerConn := &UDPConn{ - PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) { - upload.Add(n) - manager.PushUploaded(n) - }}, []N.CountFunc{func(n int64) { - download.Add(n) - manager.PushDownloaded(n) - }}), - metadata: TrackerMetadata{ - ID: id, - Metadata: metadata, - CreatedAt: time.Now(), - Upload: upload, - Download: download, - Chain: common.Reverse(chain), - Rule: matchRule, - Outbound: outbound, - OutboundType: outboundType, - }, - manager: manager, - } - manager.Join(trackerConn) - return trackerConn -} diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index 41e78a6d4..eb245812b 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -28,6 +28,7 @@ type CommandClient struct { grpcClient daemon.StartedServiceClient grpcManagedClient daemon.ManagedServiceClient options CommandClientOptions + remote *remoteConnection ctx context.Context cancel context.CancelFunc clientMutex sync.RWMutex @@ -147,23 +148,41 @@ func networkConnectionFromFileDescriptor(fileDescriptor int32) (net.Conn, error) return networkConnection, nil } -func (c *CommandClient) dialWithRetry(target string, contextDialer func(context.Context, string) (net.Conn, error), retryDial bool) (*grpc.ClientConn, daemon.StartedServiceClient, error) { +func localDialOptions(contextDialer func(context.Context, string) (net.Conn, error)) []grpc.DialOption { + options := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(unaryClientAuthInterceptor), + grpc.WithStreamInterceptor(streamClientAuthInterceptor), + } + if contextDialer != nil { + options = append(options, grpc.WithContextDialer(contextDialer)) + } + return options +} + +// establishConnection dials the command server the client is bound to: the +// local command server (over socket/XPC) or a remote API service. +func (c *CommandClient) establishConnection() (*grpc.ClientConn, daemon.StartedServiceClient, error) { + if c.remote != nil { + return c.dialRemote() + } + target, contextDialer := dialTarget() + return c.dialWithRetry(target, localDialOptions(contextDialer), true) +} + +// dialWithRetry connects to the local command server. The retry loop exists to +// wait out the server starting up: WaitForReady keeps the probe redialing and +// the loop reissues it with a growing delay, so a freshly launched extension is +// picked up without surfacing a transient "unavailable" to the UI. +func (c *CommandClient) dialWithRetry(target string, dialOptions []grpc.DialOption, retryDial bool) (*grpc.ClientConn, daemon.StartedServiceClient, error) { var connection *grpc.ClientConn var client daemon.StartedServiceClient var lastError error for attempt := range commandClientDialAttempts { if connection == nil { - options := []grpc.DialOption{ - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithUnaryInterceptor(unaryClientAuthInterceptor), - grpc.WithStreamInterceptor(streamClientAuthInterceptor), - } - if contextDialer != nil { - options = append(options, grpc.WithContextDialer(contextDialer)) - } var err error - connection, err = grpc.NewClient(target, options...) + connection, err = grpc.NewClient(target, dialOptions...) if err != nil { lastError = err if !retryDial { @@ -174,8 +193,7 @@ func (c *CommandClient) dialWithRetry(target string, contextDialer func(context. } client = daemon.NewStartedServiceClient(connection) } - waitDuration := commandClientDialDelay(attempt) - ctx, cancel := context.WithTimeout(context.Background(), waitDuration) + ctx, cancel := context.WithTimeout(context.Background(), commandClientDialDelay(attempt)) _, err := client.GetStartedAt(ctx, &emptypb.Empty{}, grpc.WaitForReady(true)) cancel() if err == nil { @@ -190,12 +208,27 @@ func (c *CommandClient) dialWithRetry(target string, contextDialer func(context. return nil, nil, E.Cause(lastError, "probe command server") } +func (c *CommandClient) dialRemote() (*grpc.ClientConn, daemon.StartedServiceClient, error) { + connection, err := grpc.NewClient(c.remote.target, c.remote.dialOptions...) + if err != nil { + return nil, nil, E.Cause(err, "create remote command client") + } + client := daemon.NewStartedServiceClient(connection) + ctx, cancel := context.WithTimeout(context.Background(), commandClientRemoteProbeTimeout) + defer cancel() + _, err = client.GetStartedAt(ctx, &emptypb.Empty{}) + if err != nil { + connection.Close() + return nil, nil, E.Cause(err, "connect to remote server") + } + return connection, client, nil +} + func (c *CommandClient) Connect() error { c.clientMutex.Lock() common.Close(common.PtrOrNil(c.grpcConn)) - target, contextDialer := dialTarget() - connection, client, err := c.dialWithRetry(target, contextDialer, true) + connection, client, err := c.establishConnection() if err != nil { c.clientMutex.Unlock() return err @@ -219,9 +252,9 @@ func (c *CommandClient) ConnectWithFD(fd int32) error { c.clientMutex.Unlock() return err } - connection, client, err := c.dialWithRetry("passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) { + connection, client, err := c.dialWithRetry("passthrough:///xpc", localDialOptions(func(ctx context.Context, _ string) (net.Conn, error) { return networkConnection, nil - }, false) + }), false) if err != nil { networkConnection.Close() c.clientMutex.Unlock() @@ -283,8 +316,7 @@ func (c *CommandClient) getClientForCall() (daemon.StartedServiceClient, context return c.grpcClient, c.ctx, nil } - target, contextDialer := dialTarget() - connection, client, err := c.dialWithRetry(target, contextDialer, true) + connection, client, err := c.establishConnection() if err != nil { return nil, nil, E.Cause(err, "get command client") } diff --git a/experimental/libbox/command_client_remote.go b/experimental/libbox/command_client_remote.go new file mode 100644 index 000000000..bd6689fff --- /dev/null +++ b/experimental/libbox/command_client_remote.go @@ -0,0 +1,101 @@ +package libbox + +import ( + "context" + "crypto/tls" + "net" + "net/url" + "strings" + "time" + + 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 RemoteConnectionOptions struct { + URL string + Secret string +} + +const commandClientRemoteProbeTimeout = 10 * time.Second + +type remoteConnection struct { + target string + dialOptions []grpc.DialOption +} + +func newRemoteConnection(options *RemoteConnectionOptions) (*remoteConnection, error) { + if options == nil { + return nil, E.New("missing remote connection options") + } + urlString := options.URL + if !strings.Contains(urlString, "://") { + urlString = "http://" + urlString + } + serverURL, err := url.Parse(urlString) + if err != nil { + return nil, E.Cause(err, "parse server URL") + } + host := serverURL.Hostname() + if host == "" { + return nil, E.New("missing host in server URL: ", options.URL) + } + var ( + transportCredentials credentials.TransportCredentials + defaultPort string + ) + switch serverURL.Scheme { + case "http": + transportCredentials = insecure.NewCredentials() + defaultPort = "80" + case "https": + transportCredentials = credentials.NewTLS(&tls.Config{ServerName: host}) + defaultPort = "443" + default: + return nil, E.New("unsupported server URL scheme: ", serverURL.Scheme, ", expected http or https") + } + port := serverURL.Port() + if port == "" { + port = defaultPort + } + dialOptions := []grpc.DialOption{ + grpc.WithTransportCredentials(transportCredentials), + } + if options.Secret != "" { + authorization := "Bearer " + options.Secret + dialOptions = append(dialOptions, + grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + return invoker(metadata.AppendToOutgoingContext(ctx, "authorization", authorization), method, req, reply, cc, opts...) + }), + grpc.WithStreamInterceptor(func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { + return streamer(metadata.AppendToOutgoingContext(ctx, "authorization", authorization), desc, cc, method, opts...) + }), + ) + } + return &remoteConnection{ + target: net.JoinHostPort(host, port), + dialOptions: dialOptions, + }, nil +} + +func NewRemoteCommandClient(handler CommandClientHandler, options *CommandClientOptions, remoteOptions *RemoteConnectionOptions) (*CommandClient, error) { + remote, err := newRemoteConnection(remoteOptions) + if err != nil { + return nil, err + } + client := NewCommandClient(handler, options) + client.remote = remote + return client, nil +} + +func NewStandaloneRemoteCommandClient(remoteOptions *RemoteConnectionOptions) (*CommandClient, error) { + remote, err := newRemoteConnection(remoteOptions) + if err != nil { + return nil, err + } + return &CommandClient{remote: remote}, nil +} diff --git a/include/registry.go b/include/registry.go index 2d478bfe8..330519c84 100644 --- a/include/registry.go +++ b/include/registry.go @@ -36,6 +36,7 @@ import ( "github.com/sagernet/sing-box/protocol/tun" "github.com/sagernet/sing-box/protocol/vless" "github.com/sagernet/sing-box/protocol/vmess" + "github.com/sagernet/sing-box/service/api" originca "github.com/sagernet/sing-box/service/origin_ca" "github.com/sagernet/sing-box/service/resolved" "github.com/sagernet/sing-box/service/ssmapi" @@ -133,6 +134,7 @@ func DNSTransportRegistry() *dns.TransportRegistry { func ServiceRegistry() *service.Registry { registry := service.NewRegistry() + api.RegisterService(registry) resolved.RegisterService(registry) ssmapi.RegisterService(registry) diff --git a/log/factory.go b/log/factory.go index 54a88228f..b73fb2369 100644 --- a/log/factory.go +++ b/log/factory.go @@ -22,6 +22,7 @@ type Factory interface { type ObservableFactory interface { Factory observable.Observable[Entry] + AttachPlatformWriter(writer PlatformWriter) } type Entry struct { diff --git a/log/nop.go b/log/nop.go index 6369e99b1..4b2afd15c 100644 --- a/log/nop.go +++ b/log/nop.go @@ -80,6 +80,9 @@ func (f *nopFactory) FatalContext(ctx context.Context, args ...any) { func (f *nopFactory) PanicContext(ctx context.Context, args ...any) { } +func (f *nopFactory) AttachPlatformWriter(writer PlatformWriter) { +} + func (f *nopFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { return nil, nil, os.ErrInvalid } diff --git a/log/observable.go b/log/observable.go index efb2450ec..d19001b6b 100644 --- a/log/observable.go +++ b/log/observable.go @@ -4,6 +4,7 @@ import ( "context" "io" "os" + "sync/atomic" "time" "github.com/sagernet/sing/common" @@ -12,7 +13,7 @@ import ( "github.com/sagernet/sing/service/filemanager" ) -var _ Factory = (*defaultFactory)(nil) +var _ ObservableFactory = (*defaultFactory)(nil) type defaultFactory struct { ctx context.Context @@ -21,7 +22,7 @@ type defaultFactory struct { writer io.Writer file *os.File filePath string - platformWriter PlatformWriter + platformWriters atomic.Pointer[[]PlatformWriter] needObservable bool level Level subscriber *observable.Subscriber[Entry] @@ -45,11 +46,13 @@ func NewDefaultFactory( }, writer: writer, filePath: filePath, - platformWriter: platformWriter, needObservable: needObservable, level: LevelTrace, subscriber: observable.NewSubscriber[Entry](128), } + if platformWriter != nil { + factory.platformWriters.Store(&[]PlatformWriter{platformWriter}) + } /*if platformWriter != nil { factory.platformFormatter.DisableColors = platformWriter.DisableColors() }*/ @@ -78,6 +81,19 @@ func (f *defaultFactory) Close() error { ) } +func (f *defaultFactory) AttachPlatformWriter(writer PlatformWriter) { + writers := append(f.loadPlatformWriters(), writer) + f.platformWriters.Store(&writers) +} + +func (f *defaultFactory) loadPlatformWriters() []PlatformWriter { + writers := f.platformWriters.Load() + if writers == nil { + return nil + } + return *writers +} + func (f *defaultFactory) Level() Level { return f.level } @@ -111,7 +127,8 @@ type observableLogger struct { func (l *observableLogger) Log(ctx context.Context, level Level, args []any) { level = OverrideLevelFromContext(level, ctx) - if level > l.level && l.platformWriter == nil && !l.needObservable { + platformWriters := l.loadPlatformWriters() + if level > l.level && len(platformWriters) == 0 && !l.needObservable { return } nowTime := time.Now() @@ -137,8 +154,11 @@ func (l *observableLogger) Log(ctx context.Context, level Level, args []any) { os.Exit(1) } } - if l.platformWriter != nil { - l.platformWriter.WriteMessage(level, l.platformFormatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime)) + if len(platformWriters) > 0 { + message := l.platformFormatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime) + for _, platformWriter := range platformWriters { + platformWriter.WriteMessage(level, message) + } } } diff --git a/mkdocs.yml b/mkdocs.yml index a14ada33e..0e7c84378 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -187,6 +187,7 @@ nav: - URLTest: configuration/outbound/urltest.md - Service: - configuration/service/index.md + - sing-box API: configuration/service/api.md - DERP: configuration/service/derp.md - Resolved: configuration/service/resolved.md - SSM API: configuration/service/ssm-api.md diff --git a/option/api.go b/option/api.go new file mode 100644 index 000000000..56ca50e27 --- /dev/null +++ b/option/api.go @@ -0,0 +1,11 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type APIServiceOptions struct { + ListenOptions + Secret string `json:"secret,omitempty"` + AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"` + AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"` + InboundTLSOptionsContainer +} diff --git a/service/api/server.go b/service/api/server.go new file mode 100644 index 000000000..f18b1a432 --- /dev/null +++ b/service/api/server.go @@ -0,0 +1,124 @@ +package api + +import ( + "context" + "net" + "net/http" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/daemon" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "google.golang.org/grpc" +) + +func RegisterService(registry *boxService.Registry) { + boxService.Register[option.APIServiceOptions](registry, C.TypeAPI, NewService) +} + +type Service struct { + boxService.Adapter + ctx context.Context + cancel context.CancelFunc + logger log.ContextLogger + options option.APIServiceOptions + listener *listener.Listener + tlsConfig tls.ServerConfig + startedService *daemon.StartedService + grpcServer *grpc.Server + httpServer *http.Server +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.APIServiceOptions) (adapter.Service, error) { + ctx, cancel := context.WithCancel(ctx) + s := &Service{ + Adapter: boxService.NewAdapter(C.TypeAPI, tag), + ctx: ctx, + cancel: cancel, + logger: logger, + options: options, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + }), + } + if options.TLS != nil { + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + cancel() + return nil, err + } + s.tlsConfig = tlsConfig + } + return s, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStarted { + return nil + } + s.startedService = daemon.NewAttachedService(s.ctx) + s.grpcServer = daemon.NewServer(s.startedService, s.options.Secret) + s.httpServer = &http.Server{ + Handler: h2c.NewHandler(newHTTPHandler(s.logger, s.grpcServer, s.options), new(http2.Server)), + BaseContext: func(net.Listener) context.Context { + return s.ctx + }, + } + if s.tlsConfig != nil { + err := s.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { + s.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, s.tlsConfig.NextProtos()...)) + } + if !common.Contains(s.tlsConfig.NextProtos(), "http/1.1") { + s.tlsConfig.SetNextProtos(append(s.tlsConfig.NextProtos(), "http/1.1")) + } + } + tcpListener, err := s.listener.ListenTCP() + if err != nil { + return err + } + if s.tlsConfig != nil { + tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig) + } + go func() { + serveErr := s.httpServer.Serve(tcpListener) + if serveErr != nil && s.ctx.Err() == nil { + s.logger.Error("serve error: ", serveErr) + } + }() + return nil +} + +func (s *Service) Close() error { + s.cancel() + if s.httpServer != nil { + s.httpServer.Close() + } + if s.grpcServer != nil { + s.grpcServer.Stop() + } + if s.startedService != nil { + s.startedService.Close() + } + return common.Close( + common.PtrOrNil(s.listener), + s.tlsConfig, + ) +} diff --git a/service/api/web_bridge.go b/service/api/web_bridge.go new file mode 100644 index 000000000..929daffc8 --- /dev/null +++ b/service/api/web_bridge.go @@ -0,0 +1,235 @@ +package api + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "io" + "net/http" + "strings" + + "github.com/sagernet/cors" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "golang.org/x/net/http2" + "google.golang.org/grpc" +) + +const ( + contentTypeGRPC = "application/grpc" + contentTypeGRPCWeb = "application/grpc-web" + contentTypeGRPCWebText = "application/grpc-web-text" +) + +// newHTTPHandler additionally accepts gRPC-Web requests +// (https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) and gRPC-Web +// streams over WebSocket, wire compatible with the improbable-eng/grpc-web +// client transports. +func newHTTPHandler(logger log.ContextLogger, grpcServer *grpc.Server, options option.APIServiceOptions) http.Handler { + allowedOrigins := options.AccessControlAllowOrigin + if len(allowedOrigins) == 0 { + allowedOrigins = []string{"*"} + } + corsHandler := cors.New(cors.Options{ + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{http.MethodPost, http.MethodOptions}, + AllowedHeaders: []string{"Content-Type", "Authorization", "X-Grpc-Web", "X-User-Agent", "Grpc-Timeout"}, + ExposedHeaders: []string{"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"}, + AllowPrivateNetwork: options.AccessControlAllowPrivateNetwork, + MaxAge: 300, + }) + return corsHandler.Handler(&webBridge{ + logger: logger, + grpcServer: grpcServer, + }) +} + +type webBridge struct { + logger log.ContextLogger + grpcServer *grpc.Server +} + +func (b *webBridge) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + contentType := request.Header.Get("Content-Type") + switch { + case isWebSocketGRPCRequest(request): + b.serveWebSocket(writer, request) + case request.Method == http.MethodPost && strings.HasPrefix(contentType, contentTypeGRPCWeb): + b.serveWeb(writer, request) + case request.ProtoMajor == 2 && strings.HasPrefix(contentType, contentTypeGRPC): + b.grpcServer.ServeHTTP(writer, request) + default: + http.NotFound(writer, request) + } +} + +func (b *webBridge) serveWeb(writer http.ResponseWriter, request *http.Request) { + isTextFormat := strings.HasPrefix(request.Header.Get("Content-Type"), contentTypeGRPCWebText) + webContentType := contentTypeGRPCWeb + grpcRequest := request.Clone(request.Context()) + if isTextFormat { + webContentType = contentTypeGRPCWebText + grpcRequest.Body = &bodyReadCloser{ + Reader: base64.NewDecoder(base64.StdEncoding, request.Body), + Closer: request.Body, + } + } + // The gRPC server handler transport only accepts requests it sees as + // native gRPC over HTTP/2. + grpcRequest.ProtoMajor = 2 + grpcRequest.ProtoMinor = 0 + grpcRequest.Header.Set("Content-Type", strings.Replace(request.Header.Get("Content-Type"), webContentType, contentTypeGRPC, 1)) + grpcRequest.Header.Del("Content-Length") + response := newWebResponseWriter(writer, isTextFormat) + b.grpcServer.ServeHTTP(response, grpcRequest) + response.finish() +} + +type bodyReadCloser struct { + io.Reader + io.Closer +} + +// webResponseWriter translates a native gRPC response into a gRPC-Web +// response: headers set after the first write, including the gRPC status the +// handler transport sets via http2.TrailerPrefix keys, become a trailer +// frame at the end of the body instead of HTTP trailers. +type webResponseWriter struct { + writer http.ResponseWriter + rawWriter http.ResponseWriter + header http.Header + contentType string + wroteHeaders bool + wroteBody bool +} + +func newWebResponseWriter(writer http.ResponseWriter, isTextFormat bool) *webResponseWriter { + response := &webResponseWriter{ + writer: writer, + rawWriter: writer, + header: make(http.Header), + contentType: contentTypeGRPCWeb, + } + if isTextFormat { + response.writer = newBase64ResponseWriter(writer) + response.contentType = contentTypeGRPCWebText + } + return response +} + +func (w *webResponseWriter) Header() http.Header { + return w.header +} + +func (w *webResponseWriter) Write(content []byte) (int, error) { + if !w.wroteHeaders { + w.prepareHeaders() + w.wroteHeaders = true + } + w.wroteBody = true + return w.writer.Write(content) +} + +func (w *webResponseWriter) WriteHeader(statusCode int) { + if !w.wroteHeaders { + w.prepareHeaders() + w.wroteHeaders = true + } + w.writer.WriteHeader(statusCode) +} + +func (w *webResponseWriter) Flush() { + // Flushing before anything was written would commit a 200 response + // even for requests that end up as trailers-only responses. + if w.wroteHeaders || w.wroteBody { + flushWriter(w.writer) + } +} + +func (w *webResponseWriter) prepareHeaders() { + rawHeader := w.rawWriter.Header() + for key, values := range w.header { + canonicalKey := http.CanonicalHeaderKey(strings.TrimPrefix(key, http2.TrailerPrefix)) + if canonicalKey == "Trailer" { + continue + } + if canonicalKey == "Content-Type" { + newValues := make([]string, 0, len(values)) + for _, value := range values { + newValues = append(newValues, strings.Replace(value, contentTypeGRPC, w.contentType, 1)) + } + values = newValues + } + rawHeader[canonicalKey] = values + } +} + +func (w *webResponseWriter) finish() { + if w.wroteHeaders || w.wroteBody { + w.writeTrailerFrame() + } else { + w.WriteHeader(http.StatusOK) + flushWriter(w.writer) + } +} + +func (w *webResponseWriter) writeTrailerFrame() { + flushedKeys := make(map[string]bool) + for key := range w.rawWriter.Header() { + flushedKeys[strings.ToLower(key)] = true + } + trailerHeader := make(http.Header) + for key, values := range w.header { + lowerKey := strings.ToLower(strings.TrimPrefix(key, http2.TrailerPrefix)) + if lowerKey == "trailer" || flushedKeys[lowerKey] { + continue + } + trailerHeader[lowerKey] = values + } + var trailerBuffer bytes.Buffer + trailerHeader.Write(&trailerBuffer) + w.writer.Write(webMetadataFrameHeader(trailerBuffer.Len())) + w.writer.Write(trailerBuffer.Bytes()) + flushWriter(w.writer) +} + +func webMetadataFrameHeader(payloadLength int) []byte { + return binary.BigEndian.AppendUint32([]byte{1 << 7}, uint32(payloadLength)) +} + +func flushWriter(writer http.ResponseWriter) { + flusher, isFlusher := writer.(http.Flusher) + if isFlusher { + flusher.Flush() + } +} + +type base64ResponseWriter struct { + wrapped http.ResponseWriter + encoder io.WriteCloser +} + +func newBase64ResponseWriter(wrapped http.ResponseWriter) http.ResponseWriter { + writer := &base64ResponseWriter{wrapped: wrapped} + writer.encoder = base64.NewEncoder(base64.StdEncoding, wrapped) + return writer +} + +func (w *base64ResponseWriter) Header() http.Header { + return w.wrapped.Header() +} + +func (w *base64ResponseWriter) Write(content []byte) (int, error) { + return w.encoder.Write(content) +} + +func (w *base64ResponseWriter) WriteHeader(statusCode int) { + w.wrapped.WriteHeader(statusCode) +} + +func (w *base64ResponseWriter) Flush() { + w.encoder.Close() + w.encoder = base64.NewEncoder(base64.StdEncoding, w.wrapped) + flushWriter(w.wrapped) +} diff --git a/service/api/web_bridge_websocket.go b/service/api/web_bridge_websocket.go new file mode 100644 index 000000000..f7d7216b9 --- /dev/null +++ b/service/api/web_bridge_websocket.go @@ -0,0 +1,249 @@ +package api + +import ( + "bufio" + "bytes" + "context" + "io" + "net/http" + "net/textproto" + "strings" + "time" + + E "github.com/sagernet/sing/common/exceptions" + + "github.com/coder/websocket" + "golang.org/x/net/http/httpguts" + "golang.org/x/net/http2" +) + +const ( + webSocketSubprotocol = "grpc-websockets" + webSocketReadLimit = 1 << 22 + webSocketPingInterval = 30 * time.Second +) + +func isWebSocketGRPCRequest(request *http.Request) bool { + return httpguts.HeaderValuesContainsToken(request.Header.Values("Upgrade"), "websocket") && + httpguts.HeaderValuesContainsToken(request.Header.Values("Sec-Websocket-Protocol"), webSocketSubprotocol) +} + +// serveWebSocket carries a single gRPC stream over a WebSocket connection: +// the first client message contains the request metadata, each subsequent +// binary message is prefixed with 0 for body data or is a single 1 byte for +// the half-close signal, and the server sends gRPC-Web frames back. +func (b *webBridge) serveWebSocket(writer http.ResponseWriter, request *http.Request) { + conn, err := websocket.Accept(writer, request, &websocket.AcceptOptions{ + Subprotocols: []string{webSocketSubprotocol}, + InsecureSkipVerify: true, + }) + if err != nil { + b.logger.Error("upgrade websocket request: ", err) + return + } + conn.SetReadLimit(webSocketReadLimit) + ctx, cancel := context.WithCancel(request.Context()) + defer cancel() + messageType, firstMessage, err := conn.Read(ctx) + if err != nil { + conn.CloseNow() + return + } + if messageType != websocket.MessageBinary { + conn.CloseNow() + return + } + header, err := parseWebSocketHeader(firstMessage) + if err != nil { + b.logger.Error("parse websocket request metadata: ", err) + conn.CloseNow() + return + } + contentType := header.Get("Content-Type") + if contentType == "" { + header.Set("Content-Type", contentTypeGRPC) + } else { + header.Set("Content-Type", strings.Replace(contentType, contentTypeGRPCWeb, contentTypeGRPC, 1)) + } + header.Del("Content-Length") + response := newWebSocketResponseWriter(ctx, conn) + grpcRequest := request.WithContext(ctx) + grpcRequest.Method = http.MethodPost + grpcRequest.ProtoMajor = 2 + grpcRequest.ProtoMinor = 0 + grpcRequest.Header = header + grpcRequest.Body = &webSocketBodyReader{ + ctx: ctx, + cancel: cancel, + conn: conn, + response: response, + } + go keepWebSocketAlive(ctx, conn) + b.grpcServer.ServeHTTP(response, grpcRequest) + response.writeTrailerFrame() + conn.Close(websocket.StatusNormalClosure, "") +} + +func parseWebSocketHeader(content []byte) (http.Header, error) { + reader := textproto.NewReader(bufio.NewReader(io.MultiReader(bytes.NewReader(content), strings.NewReader("\r\n")))) + mimeHeader, err := reader.ReadMIMEHeader() + if err != nil { + return nil, err + } + return http.Header(mimeHeader), nil +} + +func keepWebSocketAlive(ctx context.Context, conn *websocket.Conn) { + ticker := time.NewTicker(webSocketPingInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := conn.Ping(ctx) + if err != nil { + return + } + } + } +} + +type webSocketResponseWriter struct { + ctx context.Context + conn *websocket.Conn + header http.Header + flushedHeader http.Header + wroteHeaders bool + wroteTrailers bool +} + +func newWebSocketResponseWriter(ctx context.Context, conn *websocket.Conn) *webSocketResponseWriter { + return &webSocketResponseWriter{ + ctx: ctx, + conn: conn, + header: make(http.Header), + flushedHeader: make(http.Header), + } +} + +func (w *webSocketResponseWriter) Header() http.Header { + return w.header +} + +func (w *webSocketResponseWriter) Write(content []byte) (int, error) { + if !w.wroteHeaders { + w.WriteHeader(http.StatusOK) + } + err := w.conn.Write(w.ctx, websocket.MessageBinary, content) + if err != nil { + return 0, err + } + return len(content), nil +} + +func (w *webSocketResponseWriter) WriteHeader(statusCode int) { + if w.wroteHeaders { + return + } + w.wroteHeaders = true + headerFrame := make(http.Header) + for key, values := range w.header { + canonicalKey := http.CanonicalHeaderKey(key) + if canonicalKey == "Trailer" { + continue + } + w.flushedHeader[canonicalKey] = values + headerFrame[canonicalKey] = values + } + w.writeHeaderFrame(headerFrame) +} + +func (w *webSocketResponseWriter) Flush() { +} + +func (w *webSocketResponseWriter) writeHeaderFrame(header http.Header) { + var headerBuffer bytes.Buffer + header.Write(&headerBuffer) + frame := make([]byte, 0, 5+headerBuffer.Len()) + frame = append(frame, webMetadataFrameHeader(headerBuffer.Len())...) + frame = append(frame, headerBuffer.Bytes()...) + w.conn.Write(w.ctx, websocket.MessageBinary, frame) +} + +func (w *webSocketResponseWriter) writeTrailerFrame() { + if w.wroteTrailers { + return + } + w.wroteTrailers = true + trailerHeader := make(http.Header) + for key, values := range w.header { + lowerKey := strings.ToLower(strings.TrimPrefix(key, http2.TrailerPrefix)) + if lowerKey == "trailer" { + continue + } + _, flushed := w.flushedHeader[http.CanonicalHeaderKey(lowerKey)] + if flushed { + continue + } + trailerHeader[lowerKey] = values + } + w.writeHeaderFrame(trailerHeader) +} + +type webSocketBodyReader struct { + ctx context.Context + cancel context.CancelFunc + conn *websocket.Conn + response *webSocketResponseWriter + remaining []byte +} + +func (r *webSocketBodyReader) Read(buffer []byte) (int, error) { + if len(r.remaining) > 0 { + n := copy(buffer, r.remaining) + r.remaining = r.remaining[n:] + return n, nil + } + for { + messageType, payload, err := r.conn.Read(r.ctx) + if err != nil { + r.cancel() + return 0, io.EOF + } + if messageType != websocket.MessageBinary { + return 0, E.New("unexpected non-binary websocket message") + } + if len(payload) == 0 { + continue + } + if payload[0] == 1 { + go r.waitForClose() + return 0, io.EOF + } + content := payload[1:] + if len(content) == 0 { + continue + } + n := copy(buffer, content) + r.remaining = content[n:] + return n, nil + } +} + +func (r *webSocketBodyReader) waitForClose() { + for { + _, _, err := r.conn.Read(r.ctx) + if err != nil { + r.cancel() + return + } + } +} + +// Close is called by the gRPC handler transport after the stream status has +// been written; the trailer frame must be sent before the connection closes. +func (r *webSocketBodyReader) Close() error { + r.response.writeTrailerFrame() + return r.conn.Close(websocket.StatusNormalClosure, "") +}