diff --git a/adapter/tailscale.go b/adapter/tailscale.go new file mode 100644 index 000000000..35809a542 --- /dev/null +++ b/adapter/tailscale.go @@ -0,0 +1,39 @@ +package adapter + +import "context" + +type TailscaleStatusProvider interface { + SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error +} + +type TailscaleEndpointStatus struct { + BackendState string + AuthURL string + NetworkName string + MagicDNSSuffix string + Self *TailscalePeer + Users map[int64]*TailscaleUser + Peers []*TailscalePeer +} + +type TailscalePeer struct { + HostName string + DNSName string + OS string + TailscaleIPs []string + Online bool + ExitNode bool + ExitNodeOption bool + Active bool + RxBytes int64 + TxBytes int64 + UserID int64 + KeyExpiry int64 +} + +type TailscaleUser struct { + ID int64 + LoginName string + DisplayName string + ProfilePicURL string +} diff --git a/daemon/started_service.go b/daemon/started_service.go index db1696623..e0043f764 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -6,12 +6,14 @@ import ( "runtime" "sync" "time" + "unsafe" "github.com/sagernet/sing-box/adapter" "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/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" @@ -704,7 +706,7 @@ func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCr switch request.Type { case DebugCrashRequest_GO: time.AfterFunc(200*time.Millisecond, func() { - panic("debug go crash") + *(*int)(unsafe.Pointer(uintptr(0))) = 0 }) case DebugCrashRequest_NATIVE: err := s.handler.TriggerNativeCrash() @@ -1284,6 +1286,142 @@ func (s *StartedService) StartSTUNTest( }) } +func (s *StartedService) SubscribeTailscaleStatus( + _ *emptypb.Empty, + server grpc.ServerStreamingServer[TailscaleStatusUpdate], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + if endpointManager == nil { + return status.Error(codes.FailedPrecondition, "endpoint manager not available") + } + + type tailscaleEndpoint struct { + tag string + provider adapter.TailscaleStatusProvider + } + var endpoints []tailscaleEndpoint + for _, endpoint := range endpointManager.Endpoints() { + if endpoint.Type() != C.TypeTailscale { + continue + } + provider, loaded := endpoint.(adapter.TailscaleStatusProvider) + if !loaded { + continue + } + endpoints = append(endpoints, tailscaleEndpoint{ + tag: endpoint.Tag(), + provider: provider, + }) + } + if len(endpoints) == 0 { + return status.Error(codes.NotFound, "no Tailscale endpoint found") + } + + type taggedStatus struct { + tag string + status *adapter.TailscaleEndpointStatus + } + updates := make(chan taggedStatus, len(endpoints)) + ctx, cancel := context.WithCancel(server.Context()) + defer cancel() + + var waitGroup sync.WaitGroup + for _, endpoint := range endpoints { + waitGroup.Add(1) + go func(tag string, provider adapter.TailscaleStatusProvider) { + defer waitGroup.Done() + _ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) { + select { + case updates <- taggedStatus{tag: tag, status: endpointStatus}: + case <-ctx.Done(): + } + }) + }(endpoint.tag, endpoint.provider) + } + + go func() { + waitGroup.Wait() + close(updates) + }() + + statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints)) + for update := range updates { + statuses[update.tag] = update.status + protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses)) + for tag, endpointStatus := range statuses { + protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, endpointStatus)) + } + sendErr := server.Send(&TailscaleStatusUpdate{ + Endpoints: protoEndpoints, + }) + if sendErr != nil { + return sendErr + } + } + return nil +} + +func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus { + userGroupMap := make(map[int64]*TailscaleUserGroup) + for userID, user := range s.Users { + userGroupMap[userID] = &TailscaleUserGroup{ + UserID: userID, + LoginName: user.LoginName, + DisplayName: user.DisplayName, + ProfilePicURL: user.ProfilePicURL, + } + } + for _, peer := range s.Peers { + protoPeer := tailscalePeerToProto(peer) + group, loaded := userGroupMap[peer.UserID] + if !loaded { + group = &TailscaleUserGroup{UserID: peer.UserID} + userGroupMap[peer.UserID] = group + } + group.Peers = append(group.Peers, protoPeer) + } + userGroups := make([]*TailscaleUserGroup, 0, len(userGroupMap)) + for _, group := range userGroupMap { + userGroups = append(userGroups, group) + } + result := &TailscaleEndpointStatus{ + EndpointTag: tag, + BackendState: s.BackendState, + AuthURL: s.AuthURL, + NetworkName: s.NetworkName, + MagicDNSSuffix: s.MagicDNSSuffix, + UserGroups: userGroups, + } + if s.Self != nil { + result.Self = tailscalePeerToProto(s.Self) + } + return result +} + +func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer { + return &TailscalePeer{ + HostName: peer.HostName, + DnsName: peer.DNSName, + Os: peer.OS, + TailscaleIPs: peer.TailscaleIPs, + Online: peer.Online, + ExitNode: peer.ExitNode, + ExitNodeOption: peer.ExitNodeOption, + Active: peer.Active, + RxBytes: peer.RxBytes, + TxBytes: peer.TxBytes, + KeyExpiry: peer.KeyExpiry, + } +} + func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() { } diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index c48ea4fe7..402b01013 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -2248,6 +2248,342 @@ func (x *STUNTestProgress) GetNatTypeSupported() bool { return false } +type TailscaleStatusUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoints []*TailscaleEndpointStatus `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscaleStatusUpdate) Reset() { + *x = TailscaleStatusUpdate{} + mi := &file_daemon_started_service_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscaleStatusUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscaleStatusUpdate) ProtoMessage() {} + +func (x *TailscaleStatusUpdate) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[31] + 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 TailscaleStatusUpdate.ProtoReflect.Descriptor instead. +func (*TailscaleStatusUpdate) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{31} +} + +func (x *TailscaleStatusUpdate) GetEndpoints() []*TailscaleEndpointStatus { + if x != nil { + return x.Endpoints + } + return nil +} + +type TailscaleEndpointStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + BackendState string `protobuf:"bytes,2,opt,name=backendState,proto3" json:"backendState,omitempty"` + AuthURL string `protobuf:"bytes,3,opt,name=authURL,proto3" json:"authURL,omitempty"` + NetworkName string `protobuf:"bytes,4,opt,name=networkName,proto3" json:"networkName,omitempty"` + MagicDNSSuffix string `protobuf:"bytes,5,opt,name=magicDNSSuffix,proto3" json:"magicDNSSuffix,omitempty"` + Self *TailscalePeer `protobuf:"bytes,6,opt,name=self,proto3" json:"self,omitempty"` + UserGroups []*TailscaleUserGroup `protobuf:"bytes,7,rep,name=userGroups,proto3" json:"userGroups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscaleEndpointStatus) Reset() { + *x = TailscaleEndpointStatus{} + mi := &file_daemon_started_service_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscaleEndpointStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscaleEndpointStatus) ProtoMessage() {} + +func (x *TailscaleEndpointStatus) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[32] + 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 TailscaleEndpointStatus.ProtoReflect.Descriptor instead. +func (*TailscaleEndpointStatus) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{32} +} + +func (x *TailscaleEndpointStatus) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *TailscaleEndpointStatus) GetBackendState() string { + if x != nil { + return x.BackendState + } + return "" +} + +func (x *TailscaleEndpointStatus) GetAuthURL() string { + if x != nil { + return x.AuthURL + } + return "" +} + +func (x *TailscaleEndpointStatus) GetNetworkName() string { + if x != nil { + return x.NetworkName + } + return "" +} + +func (x *TailscaleEndpointStatus) GetMagicDNSSuffix() string { + if x != nil { + return x.MagicDNSSuffix + } + return "" +} + +func (x *TailscaleEndpointStatus) GetSelf() *TailscalePeer { + if x != nil { + return x.Self + } + return nil +} + +func (x *TailscaleEndpointStatus) GetUserGroups() []*TailscaleUserGroup { + if x != nil { + return x.UserGroups + } + return nil +} + +type TailscaleUserGroup struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserID int64 `protobuf:"varint,1,opt,name=userID,proto3" json:"userID,omitempty"` + LoginName string `protobuf:"bytes,2,opt,name=loginName,proto3" json:"loginName,omitempty"` + DisplayName string `protobuf:"bytes,3,opt,name=displayName,proto3" json:"displayName,omitempty"` + ProfilePicURL string `protobuf:"bytes,4,opt,name=profilePicURL,proto3" json:"profilePicURL,omitempty"` + Peers []*TailscalePeer `protobuf:"bytes,5,rep,name=peers,proto3" json:"peers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscaleUserGroup) Reset() { + *x = TailscaleUserGroup{} + mi := &file_daemon_started_service_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscaleUserGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscaleUserGroup) ProtoMessage() {} + +func (x *TailscaleUserGroup) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[33] + 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 TailscaleUserGroup.ProtoReflect.Descriptor instead. +func (*TailscaleUserGroup) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{33} +} + +func (x *TailscaleUserGroup) GetUserID() int64 { + if x != nil { + return x.UserID + } + return 0 +} + +func (x *TailscaleUserGroup) GetLoginName() string { + if x != nil { + return x.LoginName + } + return "" +} + +func (x *TailscaleUserGroup) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *TailscaleUserGroup) GetProfilePicURL() string { + if x != nil { + return x.ProfilePicURL + } + return "" +} + +func (x *TailscaleUserGroup) GetPeers() []*TailscalePeer { + if x != nil { + return x.Peers + } + return nil +} + +type TailscalePeer struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostName string `protobuf:"bytes,1,opt,name=hostName,proto3" json:"hostName,omitempty"` + DnsName string `protobuf:"bytes,2,opt,name=dnsName,proto3" json:"dnsName,omitempty"` + Os string `protobuf:"bytes,3,opt,name=os,proto3" json:"os,omitempty"` + TailscaleIPs []string `protobuf:"bytes,4,rep,name=tailscaleIPs,proto3" json:"tailscaleIPs,omitempty"` + Online bool `protobuf:"varint,5,opt,name=online,proto3" json:"online,omitempty"` + ExitNode bool `protobuf:"varint,6,opt,name=exitNode,proto3" json:"exitNode,omitempty"` + ExitNodeOption bool `protobuf:"varint,7,opt,name=exitNodeOption,proto3" json:"exitNodeOption,omitempty"` + Active bool `protobuf:"varint,8,opt,name=active,proto3" json:"active,omitempty"` + RxBytes int64 `protobuf:"varint,9,opt,name=rxBytes,proto3" json:"rxBytes,omitempty"` + TxBytes int64 `protobuf:"varint,10,opt,name=txBytes,proto3" json:"txBytes,omitempty"` + KeyExpiry int64 `protobuf:"varint,11,opt,name=keyExpiry,proto3" json:"keyExpiry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscalePeer) Reset() { + *x = TailscalePeer{} + mi := &file_daemon_started_service_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscalePeer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscalePeer) ProtoMessage() {} + +func (x *TailscalePeer) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[34] + 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 TailscalePeer.ProtoReflect.Descriptor instead. +func (*TailscalePeer) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{34} +} + +func (x *TailscalePeer) GetHostName() string { + if x != nil { + return x.HostName + } + return "" +} + +func (x *TailscalePeer) GetDnsName() string { + if x != nil { + return x.DnsName + } + return "" +} + +func (x *TailscalePeer) GetOs() string { + if x != nil { + return x.Os + } + return "" +} + +func (x *TailscalePeer) GetTailscaleIPs() []string { + if x != nil { + return x.TailscaleIPs + } + return nil +} + +func (x *TailscalePeer) GetOnline() bool { + if x != nil { + return x.Online + } + return false +} + +func (x *TailscalePeer) GetExitNode() bool { + if x != nil { + return x.ExitNode + } + return false +} + +func (x *TailscalePeer) GetExitNodeOption() bool { + if x != nil { + return x.ExitNodeOption + } + return false +} + +func (x *TailscalePeer) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + +func (x *TailscalePeer) GetRxBytes() int64 { + if x != nil { + return x.RxBytes + } + return 0 +} + +func (x *TailscalePeer) GetTxBytes() int64 { + if x != nil { + return x.TxBytes + } + return 0 +} + +func (x *TailscalePeer) GetKeyExpiry() int64 { + if x != nil { + return x.KeyExpiry + } + return 0 +} + type Log_Message struct { state protoimpl.MessageState `protogen:"open.v1"` Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` @@ -2258,7 +2594,7 @@ type Log_Message struct { func (x *Log_Message) Reset() { *x = Log_Message{} - mi := &file_daemon_started_service_proto_msgTypes[31] + mi := &file_daemon_started_service_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2270,7 +2606,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[31] + mi := &file_daemon_started_service_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2472,7 +2808,38 @@ const file_daemon_started_service_proto_rawDesc = "" + "\fnatFiltering\x18\x05 \x01(\x05R\fnatFiltering\x12\x18\n" + "\aisFinal\x18\x06 \x01(\bR\aisFinal\x12\x14\n" + "\x05error\x18\a \x01(\tR\x05error\x12*\n" + - "\x10natTypeSupported\x18\b \x01(\bR\x10natTypeSupported*U\n" + + "\x10natTypeSupported\x18\b \x01(\bR\x10natTypeSupported\"V\n" + + "\x15TailscaleStatusUpdate\x12=\n" + + "\tendpoints\x18\x01 \x03(\v2\x1f.daemon.TailscaleEndpointStatusR\tendpoints\"\xaa\x02\n" + + "\x17TailscaleEndpointStatus\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\"\n" + + "\fbackendState\x18\x02 \x01(\tR\fbackendState\x12\x18\n" + + "\aauthURL\x18\x03 \x01(\tR\aauthURL\x12 \n" + + "\vnetworkName\x18\x04 \x01(\tR\vnetworkName\x12&\n" + + "\x0emagicDNSSuffix\x18\x05 \x01(\tR\x0emagicDNSSuffix\x12)\n" + + "\x04self\x18\x06 \x01(\v2\x15.daemon.TailscalePeerR\x04self\x12:\n" + + "\n" + + "userGroups\x18\a \x03(\v2\x1a.daemon.TailscaleUserGroupR\n" + + "userGroups\"\xbf\x01\n" + + "\x12TailscaleUserGroup\x12\x16\n" + + "\x06userID\x18\x01 \x01(\x03R\x06userID\x12\x1c\n" + + "\tloginName\x18\x02 \x01(\tR\tloginName\x12 \n" + + "\vdisplayName\x18\x03 \x01(\tR\vdisplayName\x12$\n" + + "\rprofilePicURL\x18\x04 \x01(\tR\rprofilePicURL\x12+\n" + + "\x05peers\x18\x05 \x03(\v2\x15.daemon.TailscalePeerR\x05peers\"\xbf\x02\n" + + "\rTailscalePeer\x12\x1a\n" + + "\bhostName\x18\x01 \x01(\tR\bhostName\x12\x18\n" + + "\adnsName\x18\x02 \x01(\tR\adnsName\x12\x0e\n" + + "\x02os\x18\x03 \x01(\tR\x02os\x12\"\n" + + "\ftailscaleIPs\x18\x04 \x03(\tR\ftailscaleIPs\x12\x16\n" + + "\x06online\x18\x05 \x01(\bR\x06online\x12\x1a\n" + + "\bexitNode\x18\x06 \x01(\bR\bexitNode\x12&\n" + + "\x0eexitNodeOption\x18\a \x01(\bR\x0eexitNodeOption\x12\x16\n" + + "\x06active\x18\b \x01(\bR\x06active\x12\x18\n" + + "\arxBytes\x18\t \x01(\x03R\arxBytes\x12\x18\n" + + "\atxBytes\x18\n" + + " \x01(\x03R\atxBytes\x12\x1c\n" + + "\tkeyExpiry\x18\v \x01(\x03R\tkeyExpiry*U\n" + "\bLogLevel\x12\t\n" + "\x05PANIC\x10\x00\x12\t\n" + "\x05FATAL\x10\x01\x12\t\n" + @@ -2484,7 +2851,7 @@ 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\xac\x0f\n" + + "\x17CONNECTION_EVENT_CLOSED\x10\x022\x83\x10\n" + "\x0eStartedService\x12=\n" + "\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" + "\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" + @@ -2512,7 +2879,8 @@ const file_daemon_started_service_proto_rawDesc = "" + "\rListOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x00\x12F\n" + "\x12SubscribeOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x000\x01\x12d\n" + "\x17StartNetworkQualityTest\x12!.daemon.NetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01\x12F\n" + - "\rStartSTUNTest\x12\x17.daemon.STUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" + "\rStartSTUNTest\x12\x17.daemon.STUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01\x12U\n" + + "\x18SubscribeTailscaleStatus\x12\x16.google.protobuf.Empty\x1a\x1d.daemon.TailscaleStatusUpdate\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" var ( file_daemon_started_service_proto_rawDescOnce sync.Once @@ -2528,7 +2896,7 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte { var ( file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) - file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 32) + file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 36) file_daemon_started_service_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ConnectionEventType)(0), // 1: daemon.ConnectionEventType @@ -2565,14 +2933,18 @@ var ( (*NetworkQualityTestProgress)(nil), // 32: daemon.NetworkQualityTestProgress (*STUNTestRequest)(nil), // 33: daemon.STUNTestRequest (*STUNTestProgress)(nil), // 34: daemon.STUNTestProgress - (*Log_Message)(nil), // 35: daemon.Log.Message - (*emptypb.Empty)(nil), // 36: google.protobuf.Empty + (*TailscaleStatusUpdate)(nil), // 35: daemon.TailscaleStatusUpdate + (*TailscaleEndpointStatus)(nil), // 36: daemon.TailscaleEndpointStatus + (*TailscaleUserGroup)(nil), // 37: daemon.TailscaleUserGroup + (*TailscalePeer)(nil), // 38: daemon.TailscalePeer + (*Log_Message)(nil), // 39: daemon.Log.Message + (*emptypb.Empty)(nil), // 40: google.protobuf.Empty } ) var file_daemon_started_service_proto_depIdxs = []int32{ 2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type - 35, // 1: daemon.Log.messages:type_name -> daemon.Log.Message + 39, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel 11, // 3: daemon.Groups.group:type_name -> daemon.Group 12, // 4: daemon.Group.items:type_name -> daemon.GroupItem @@ -2583,66 +2955,72 @@ var file_daemon_started_service_proto_depIdxs = []int32{ 25, // 9: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo 28, // 10: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning 12, // 11: daemon.OutboundList.outbounds:type_name -> daemon.GroupItem - 0, // 12: daemon.Log.Message.level:type_name -> daemon.LogLevel - 36, // 13: daemon.StartedService.StopService:input_type -> google.protobuf.Empty - 36, // 14: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty - 36, // 15: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty - 36, // 16: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty - 36, // 17: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty - 36, // 18: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty - 6, // 19: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest - 36, // 20: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty - 36, // 21: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty - 36, // 22: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty - 16, // 23: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode - 13, // 24: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest - 14, // 25: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest - 15, // 26: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest - 36, // 27: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty - 19, // 28: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest - 20, // 29: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest - 36, // 30: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty - 21, // 31: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest - 26, // 32: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest - 36, // 33: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty - 36, // 34: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty - 36, // 35: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty - 36, // 36: daemon.StartedService.ListOutbounds:input_type -> google.protobuf.Empty - 36, // 37: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty - 31, // 38: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest - 33, // 39: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest - 36, // 40: daemon.StartedService.StopService:output_type -> google.protobuf.Empty - 36, // 41: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty - 4, // 42: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus - 7, // 43: daemon.StartedService.SubscribeLog:output_type -> daemon.Log - 8, // 44: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel - 36, // 45: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty - 9, // 46: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status - 10, // 47: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups - 17, // 48: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus - 16, // 49: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode - 36, // 50: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty - 36, // 51: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty - 36, // 52: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty - 36, // 53: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty - 18, // 54: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus - 36, // 55: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty - 36, // 56: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty - 36, // 57: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty - 23, // 58: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents - 36, // 59: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty - 36, // 60: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty - 27, // 61: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings - 29, // 62: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt - 30, // 63: daemon.StartedService.ListOutbounds:output_type -> daemon.OutboundList - 30, // 64: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList - 32, // 65: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress - 34, // 66: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress - 40, // [40:67] is the sub-list for method output_type - 13, // [13:40] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 36, // 12: daemon.TailscaleStatusUpdate.endpoints:type_name -> daemon.TailscaleEndpointStatus + 38, // 13: daemon.TailscaleEndpointStatus.self:type_name -> daemon.TailscalePeer + 37, // 14: daemon.TailscaleEndpointStatus.userGroups:type_name -> daemon.TailscaleUserGroup + 38, // 15: daemon.TailscaleUserGroup.peers:type_name -> daemon.TailscalePeer + 0, // 16: daemon.Log.Message.level:type_name -> daemon.LogLevel + 40, // 17: daemon.StartedService.StopService:input_type -> google.protobuf.Empty + 40, // 18: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty + 40, // 19: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty + 40, // 20: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty + 40, // 21: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty + 40, // 22: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty + 6, // 23: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest + 40, // 24: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty + 40, // 25: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty + 40, // 26: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty + 16, // 27: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode + 13, // 28: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest + 14, // 29: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest + 15, // 30: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest + 40, // 31: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty + 19, // 32: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest + 20, // 33: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest + 40, // 34: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty + 21, // 35: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest + 26, // 36: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest + 40, // 37: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty + 40, // 38: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty + 40, // 39: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty + 40, // 40: daemon.StartedService.ListOutbounds:input_type -> google.protobuf.Empty + 40, // 41: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty + 31, // 42: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest + 33, // 43: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest + 40, // 44: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty + 40, // 45: daemon.StartedService.StopService:output_type -> google.protobuf.Empty + 40, // 46: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty + 4, // 47: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus + 7, // 48: daemon.StartedService.SubscribeLog:output_type -> daemon.Log + 8, // 49: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel + 40, // 50: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty + 9, // 51: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status + 10, // 52: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups + 17, // 53: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus + 16, // 54: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode + 40, // 55: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty + 40, // 56: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty + 40, // 57: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty + 40, // 58: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty + 18, // 59: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus + 40, // 60: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty + 40, // 61: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty + 40, // 62: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty + 23, // 63: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents + 40, // 64: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty + 40, // 65: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty + 27, // 66: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings + 29, // 67: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt + 30, // 68: daemon.StartedService.ListOutbounds:output_type -> daemon.OutboundList + 30, // 69: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList + 32, // 70: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 34, // 71: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress + 35, // 72: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate + 45, // [45:73] is the sub-list for method output_type + 17, // [17:45] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_daemon_started_service_proto_init() } @@ -2656,7 +3034,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: 4, - NumMessages: 32, + NumMessages: 36, NumExtensions: 0, NumServices: 1, }, diff --git a/daemon/started_service.proto b/daemon/started_service.proto index 6d30da2f9..f2531f08d 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -39,6 +39,7 @@ service StartedService { rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {} rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {} rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {} + rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {} } message ServiceStatus { @@ -278,3 +279,39 @@ message STUNTestProgress { string error = 7; bool natTypeSupported = 8; } + +message TailscaleStatusUpdate { + repeated TailscaleEndpointStatus endpoints = 1; +} + +message TailscaleEndpointStatus { + string endpointTag = 1; + string backendState = 2; + string authURL = 3; + string networkName = 4; + string magicDNSSuffix = 5; + TailscalePeer self = 6; + repeated TailscaleUserGroup userGroups = 7; +} + +message TailscaleUserGroup { + int64 userID = 1; + string loginName = 2; + string displayName = 3; + string profilePicURL = 4; + repeated TailscalePeer peers = 5; +} + +message TailscalePeer { + string hostName = 1; + string dnsName = 2; + string os = 3; + repeated string tailscaleIPs = 4; + bool online = 5; + bool exitNode = 6; + bool exitNodeOption = 7; + bool active = 8; + int64 rxBytes = 9; + int64 txBytes = 10; + int64 keyExpiry = 11; +} diff --git a/daemon/started_service_grpc.pb.go b/daemon/started_service_grpc.pb.go index fec7afa0b..af8024035 100644 --- a/daemon/started_service_grpc.pb.go +++ b/daemon/started_service_grpc.pb.go @@ -15,33 +15,34 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService" - StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService" - StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" - StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" - StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" - StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs" - StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" - StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" - StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" - StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode" - StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode" - StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest" - StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound" - StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" - StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus" - StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled" - StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash" - StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport" - StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" - StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" - StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" - StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" - StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" - StartedService_ListOutbounds_FullMethodName = "/daemon.StartedService/ListOutbounds" - StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds" - StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest" - StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" + StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService" + StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService" + StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" + StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" + StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" + StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs" + StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" + StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" + StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" + StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode" + StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode" + StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest" + StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound" + StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" + StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus" + StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled" + StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash" + StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport" + StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" + StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" + StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" + StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" + StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" + StartedService_ListOutbounds_FullMethodName = "/daemon.StartedService/ListOutbounds" + StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds" + StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest" + StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" + StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus" ) // StartedServiceClient is the client API for StartedService service. @@ -75,6 +76,7 @@ type StartedServiceClient interface { SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) + SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) } type startedServiceClient struct { @@ -436,6 +438,25 @@ func (c *startedServiceClient) StartSTUNTest(ctx context.Context, in *STUNTestRe // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_StartSTUNTestClient = grpc.ServerStreamingClient[STUNTestProgress] +func (c *startedServiceClient) SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[9], StartedService_SubscribeTailscaleStatus_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, TailscaleStatusUpdate]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeTailscaleStatusClient = grpc.ServerStreamingClient[TailscaleStatusUpdate] + // StartedServiceServer is the server API for StartedService service. // All implementations must embed UnimplementedStartedServiceServer // for forward compatibility. @@ -467,6 +488,7 @@ type StartedServiceServer interface { SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error + SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error mustEmbedUnimplementedStartedServiceServer() } @@ -584,6 +606,10 @@ func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQuality func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error { return status.Error(codes.Unimplemented, "method StartSTUNTest not implemented") } + +func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error { + return status.Error(codes.Unimplemented, "method SubscribeTailscaleStatus not implemented") +} func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {} func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} @@ -1028,6 +1054,17 @@ func _StartedService_StartSTUNTest_Handler(srv interface{}, stream grpc.ServerSt // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_StartSTUNTestServer = grpc.ServerStreamingServer[STUNTestProgress] +func _StartedService_SubscribeTailscaleStatus_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeTailscaleStatus(m, &grpc.GenericServerStream[emptypb.Empty, TailscaleStatusUpdate]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeTailscaleStatusServer = grpc.ServerStreamingServer[TailscaleStatusUpdate] + // StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1154,6 +1191,11 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ Handler: _StartedService_StartSTUNTest_Handler, ServerStreams: true, }, + { + StreamName: "SubscribeTailscaleStatus", + Handler: _StartedService_SubscribeTailscaleStatus_Handler, + ServerStreams: true, + }, }, Metadata: "daemon/started_service.proto", } diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index d72c95d04..f7871c620 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -720,3 +720,25 @@ func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler handler.OnProgress(stunTestProgressFromGRPC(event)) } } + +func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) error { + client, err := c.getClientForCall() + if err != nil { + return err + } + if c.standalone { + defer c.closeConnection() + } + stream, err := client.SubscribeTailscaleStatus(context.Background(), &emptypb.Empty{}) + if err != nil { + return err + } + for { + event, recvErr := stream.Recv() + if recvErr != nil { + handler.OnError(recvErr.Error()) + return recvErr + } + handler.OnStatusUpdate(tailscaleStatusUpdateFromGRPC(event)) + } +} diff --git a/experimental/libbox/command_types_tailscale.go b/experimental/libbox/command_types_tailscale.go new file mode 100644 index 000000000..dc17639df --- /dev/null +++ b/experimental/libbox/command_types_tailscale.go @@ -0,0 +1,132 @@ +package libbox + +import "github.com/sagernet/sing-box/daemon" + +type TailscaleStatusUpdate struct { + endpoints []*TailscaleEndpointStatus +} + +func (u *TailscaleStatusUpdate) Endpoints() TailscaleEndpointStatusIterator { + return newIterator(u.endpoints) +} + +type TailscaleEndpointStatusIterator interface { + Next() *TailscaleEndpointStatus + HasNext() bool +} + +type TailscaleEndpointStatus struct { + EndpointTag string + BackendState string + AuthURL string + NetworkName string + MagicDNSSuffix string + Self *TailscalePeer + userGroups []*TailscaleUserGroup +} + +func (s *TailscaleEndpointStatus) UserGroups() TailscaleUserGroupIterator { + return newIterator(s.userGroups) +} + +type TailscaleUserGroupIterator interface { + Next() *TailscaleUserGroup + HasNext() bool +} + +type TailscaleUserGroup struct { + UserID int64 + LoginName string + DisplayName string + ProfilePicURL string + peers []*TailscalePeer +} + +func (g *TailscaleUserGroup) Peers() TailscalePeerIterator { + return newIterator(g.peers) +} + +type TailscalePeerIterator interface { + Next() *TailscalePeer + HasNext() bool +} + +type TailscalePeer struct { + HostName string + DNSName string + OS string + tailscaleIPs []string + Online bool + ExitNode bool + ExitNodeOption bool + Active bool + RxBytes int64 + TxBytes int64 + KeyExpiry int64 +} + +func (p *TailscalePeer) TailscaleIPs() StringIterator { + return newIterator(p.tailscaleIPs) +} + +type TailscaleStatusHandler interface { + OnStatusUpdate(status *TailscaleStatusUpdate) + OnError(message string) +} + +func tailscaleStatusUpdateFromGRPC(update *daemon.TailscaleStatusUpdate) *TailscaleStatusUpdate { + endpoints := make([]*TailscaleEndpointStatus, len(update.Endpoints)) + for i, endpoint := range update.Endpoints { + endpoints[i] = tailscaleEndpointStatusFromGRPC(endpoint) + } + return &TailscaleStatusUpdate{endpoints: endpoints} +} + +func tailscaleEndpointStatusFromGRPC(status *daemon.TailscaleEndpointStatus) *TailscaleEndpointStatus { + userGroups := make([]*TailscaleUserGroup, len(status.UserGroups)) + for i, group := range status.UserGroups { + userGroups[i] = tailscaleUserGroupFromGRPC(group) + } + result := &TailscaleEndpointStatus{ + EndpointTag: status.EndpointTag, + BackendState: status.BackendState, + AuthURL: status.AuthURL, + NetworkName: status.NetworkName, + MagicDNSSuffix: status.MagicDNSSuffix, + userGroups: userGroups, + } + if status.Self != nil { + result.Self = tailscalePeerFromGRPC(status.Self) + } + return result +} + +func tailscaleUserGroupFromGRPC(group *daemon.TailscaleUserGroup) *TailscaleUserGroup { + peers := make([]*TailscalePeer, len(group.Peers)) + for i, peer := range group.Peers { + peers[i] = tailscalePeerFromGRPC(peer) + } + return &TailscaleUserGroup{ + UserID: group.UserID, + LoginName: group.LoginName, + DisplayName: group.DisplayName, + ProfilePicURL: group.ProfilePicURL, + peers: peers, + } +} + +func tailscalePeerFromGRPC(peer *daemon.TailscalePeer) *TailscalePeer { + return &TailscalePeer{ + HostName: peer.HostName, + DNSName: peer.DnsName, + OS: peer.Os, + tailscaleIPs: peer.TailscaleIPs, + Online: peer.Online, + ExitNode: peer.ExitNode, + ExitNodeOption: peer.ExitNodeOption, + Active: peer.Active, + RxBytes: peer.RxBytes, + TxBytes: peer.TxBytes, + KeyExpiry: peer.KeyExpiry, + } +} diff --git a/experimental/libbox/debug.go b/experimental/libbox/debug.go index 63f2b49e9..75942976f 100644 --- a/experimental/libbox/debug.go +++ b/experimental/libbox/debug.go @@ -1,9 +1,12 @@ package libbox -import "time" +import ( + "time" + "unsafe" +) func TriggerGoPanic() { time.AfterFunc(200*time.Millisecond, func() { - panic("debug go crash") + *(*int)(unsafe.Pointer(uintptr(0))) = 0 }) } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index ac706e9db..9f8aa03cf 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing-box/common/networkquality" "github.com/sagernet/sing-box/common/stun" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/experimental/locale" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/service/oomkiller" @@ -181,6 +182,10 @@ func FormatNATFiltering(value int32) string { return stun.NATFiltering(value).String() } +func FormatFQDN(fqdn string) string { + return dns.FqdnToDomain(fqdn) +} + func ProxyDisplayType(proxyType string) string { return C.ProxyDisplayName(proxyType) } diff --git a/protocol/tailscale/status.go b/protocol/tailscale/status.go new file mode 100644 index 000000000..af6ce1039 --- /dev/null +++ b/protocol/tailscale/status.go @@ -0,0 +1,92 @@ +//go:build with_gvisor + +package tailscale + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/tailscale/ipn" + "github.com/sagernet/tailscale/ipn/ipnstate" + "github.com/sagernet/tailscale/tailcfg" +) + +var _ adapter.TailscaleStatusProvider = (*Endpoint)(nil) + +func (t *Endpoint) SubscribeTailscaleStatus(ctx context.Context, fn func(*adapter.TailscaleEndpointStatus)) error { + localBackend := t.server.ExportLocalBackend() + sendStatus := func() { + status := localBackend.Status() + fn(convertTailscaleStatus(status)) + } + sendStatus() + localBackend.WatchNotifications(ctx, ipn.NotifyInitialState|ipn.NotifyInitialNetMap|ipn.NotifyRateLimit, nil, func(roNotify *ipn.Notify) (keepGoing bool) { + select { + case <-ctx.Done(): + return false + default: + } + if roNotify.State != nil || roNotify.NetMap != nil || roNotify.BrowseToURL != nil { + sendStatus() + } + return true + }) + return ctx.Err() +} + +func convertTailscaleStatus(status *ipnstate.Status) *adapter.TailscaleEndpointStatus { + result := &adapter.TailscaleEndpointStatus{ + BackendState: status.BackendState, + AuthURL: status.AuthURL, + } + if status.CurrentTailnet != nil { + result.NetworkName = status.CurrentTailnet.Name + result.MagicDNSSuffix = status.CurrentTailnet.MagicDNSSuffix + } + if status.Self != nil { + result.Self = convertTailscalePeer(status.Self) + } + result.Users = make(map[int64]*adapter.TailscaleUser, len(status.User)) + for userID, profile := range status.User { + result.Users[int64(userID)] = convertTailscaleUser(userID, profile) + } + result.Peers = make([]*adapter.TailscalePeer, 0, len(status.Peer)) + for _, peer := range status.Peer { + result.Peers = append(result.Peers, convertTailscalePeer(peer)) + } + return result +} + +func convertTailscalePeer(peer *ipnstate.PeerStatus) *adapter.TailscalePeer { + ips := make([]string, len(peer.TailscaleIPs)) + for i, ip := range peer.TailscaleIPs { + ips[i] = ip.String() + } + var keyExpiry int64 + if peer.KeyExpiry != nil { + keyExpiry = peer.KeyExpiry.Unix() + } + return &adapter.TailscalePeer{ + HostName: peer.HostName, + DNSName: peer.DNSName, + OS: peer.OS, + TailscaleIPs: ips, + Online: peer.Online, + ExitNode: peer.ExitNode, + ExitNodeOption: peer.ExitNodeOption, + Active: peer.Active, + RxBytes: peer.RxBytes, + TxBytes: peer.TxBytes, + UserID: int64(peer.UserID), + KeyExpiry: keyExpiry, + } +} + +func convertTailscaleUser(id tailcfg.UserID, profile tailcfg.UserProfile) *adapter.TailscaleUser { + return &adapter.TailscaleUser{ + ID: int64(id), + LoginName: profile.LoginName, + DisplayName: profile.DisplayName, + ProfilePicURL: profile.ProfilePicURL, + } +}