diff --git a/adapter/tailscale.go b/adapter/tailscale.go index 22f48e62b..3da71d915 100644 --- a/adapter/tailscale.go +++ b/adapter/tailscale.go @@ -5,6 +5,7 @@ import "context" type TailscaleEndpoint interface { SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error + SetTailscaleExitNode(ctx context.Context, stableID string) error } type TailscalePingResult struct { @@ -22,6 +23,7 @@ type TailscaleEndpointStatus struct { NetworkName string MagicDNSSuffix string Self *TailscalePeer + ExitNode *TailscalePeer UserGroups []*TailscaleUserGroup } @@ -34,6 +36,7 @@ type TailscaleUserGroup struct { } type TailscalePeer struct { + StableID string HostName string DNSName string OS string diff --git a/daemon/started_service.go b/daemon/started_service.go index 5310d85cc..17837b576 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -1384,11 +1384,15 @@ func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStat if s.Self != nil { result.Self = tailscalePeerToProto(s.Self) } + if s.ExitNode != nil { + result.ExitNode = tailscalePeerToProto(s.ExitNode) + } return result } func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer { return &TailscalePeer{ + StableID: peer.StableID, HostName: peer.HostName, DnsName: peer.DNSName, Os: peer.OS, @@ -1462,6 +1466,40 @@ func (s *StartedService) StartTailscalePing( }) } +func (s *StartedService) SetTailscaleExitNode(ctx context.Context, request *SetTailscaleExitNodeRequest) (*emptypb.Empty, error) { + err := s.waitForStarted(ctx) + if err != nil { + return nil, err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + if endpointManager == nil { + return nil, status.Error(codes.FailedPrecondition, "endpoint manager not available") + } + if request.EndpointTag == "" { + return nil, status.Error(codes.InvalidArgument, "endpoint tag is required") + } + endpoint, loaded := endpointManager.Get(request.EndpointTag) + if !loaded { + return nil, status.Error(codes.NotFound, "endpoint not found: "+request.EndpointTag) + } + if endpoint.Type() != C.TypeTailscale { + return nil, status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+request.EndpointTag) + } + tsEndpoint, loaded := endpoint.(adapter.TailscaleEndpoint) + if !loaded { + return nil, status.Error(codes.FailedPrecondition, "endpoint does not support tailscale") + } + err = tsEndpoint.SetTailscaleExitNode(ctx, request.StableID) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() { } diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index 289069608..cd32d12b3 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -2301,6 +2301,7 @@ type TailscaleEndpointStatus struct { 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"` + ExitNode *TailscalePeer `protobuf:"bytes,8,opt,name=exitNode,proto3" json:"exitNode,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2384,6 +2385,13 @@ func (x *TailscaleEndpointStatus) GetUserGroups() []*TailscaleUserGroup { return nil } +func (x *TailscaleEndpointStatus) GetExitNode() *TailscalePeer { + if x != nil { + return x.ExitNode + } + return nil +} + type TailscaleUserGroup struct { state protoimpl.MessageState `protogen:"open.v1"` UserID int64 `protobuf:"varint,1,opt,name=userID,proto3" json:"userID,omitempty"` @@ -2473,6 +2481,7 @@ type TailscalePeer struct { 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"` + StableID string `protobuf:"bytes,12,opt,name=stableID,proto3" json:"stableID,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2584,6 +2593,13 @@ func (x *TailscalePeer) GetKeyExpiry() int64 { return 0 } +func (x *TailscalePeer) GetStableID() string { + if x != nil { + return x.StableID + } + return "" +} + type TailscalePingRequest struct { state protoimpl.MessageState `protogen:"open.v1"` EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` @@ -2720,6 +2736,58 @@ func (x *TailscalePingResponse) GetError() string { return "" } +type SetTailscaleExitNodeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + StableID string `protobuf:"bytes,2,opt,name=stableID,proto3" json:"stableID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetTailscaleExitNodeRequest) Reset() { + *x = SetTailscaleExitNodeRequest{} + mi := &file_daemon_started_service_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetTailscaleExitNodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetTailscaleExitNodeRequest) ProtoMessage() {} + +func (x *SetTailscaleExitNodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[37] + 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 SetTailscaleExitNodeRequest.ProtoReflect.Descriptor instead. +func (*SetTailscaleExitNodeRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{37} +} + +func (x *SetTailscaleExitNodeRequest) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *SetTailscaleExitNodeRequest) GetStableID() string { + if x != nil { + return x.StableID + } + return "" +} + 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"` @@ -2730,7 +2798,7 @@ type Log_Message struct { func (x *Log_Message) Reset() { *x = Log_Message{} - 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) } @@ -2742,7 +2810,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[37] + mi := &file_daemon_started_service_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2946,7 +3014,7 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x05error\x18\a \x01(\tR\x05error\x12*\n" + "\x10natTypeSupported\x18\b \x01(\bR\x10natTypeSupported\"V\n" + "\x15TailscaleStatusUpdate\x12=\n" + - "\tendpoints\x18\x01 \x03(\v2\x1f.daemon.TailscaleEndpointStatusR\tendpoints\"\xaa\x02\n" + + "\tendpoints\x18\x01 \x03(\v2\x1f.daemon.TailscaleEndpointStatusR\tendpoints\"\xdd\x02\n" + "\x17TailscaleEndpointStatus\x12 \n" + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\"\n" + "\fbackendState\x18\x02 \x01(\tR\fbackendState\x12\x18\n" + @@ -2956,13 +3024,14 @@ const file_daemon_started_service_proto_rawDesc = "" + "\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" + + "userGroups\x121\n" + + "\bexitNode\x18\b \x01(\v2\x15.daemon.TailscalePeerR\bexitNode\"\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" + + "\x05peers\x18\x05 \x03(\v2\x15.daemon.TailscalePeerR\x05peers\"\xdb\x02\n" + "\rTailscalePeer\x12\x1a\n" + "\bhostName\x18\x01 \x01(\tR\bhostName\x12\x18\n" + "\adnsName\x18\x02 \x01(\tR\adnsName\x12\x0e\n" + @@ -2975,7 +3044,8 @@ const file_daemon_started_service_proto_rawDesc = "" + "\arxBytes\x18\t \x01(\x03R\arxBytes\x12\x18\n" + "\atxBytes\x18\n" + " \x01(\x03R\atxBytes\x12\x1c\n" + - "\tkeyExpiry\x18\v \x01(\x03R\tkeyExpiry\"P\n" + + "\tkeyExpiry\x18\v \x01(\x03R\tkeyExpiry\x12\x1a\n" + + "\bstableID\x18\f \x01(\tR\bstableID\"P\n" + "\x14TailscalePingRequest\x12 \n" + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x16\n" + "\x06peerIP\x18\x02 \x01(\tR\x06peerIP\"\xcf\x01\n" + @@ -2985,7 +3055,10 @@ const file_daemon_started_service_proto_rawDesc = "" + "\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\"\n" + "\fderpRegionID\x18\x04 \x01(\x05R\fderpRegionID\x12&\n" + "\x0ederpRegionCode\x18\x05 \x01(\tR\x0ederpRegionCode\x12\x14\n" + - "\x05error\x18\x06 \x01(\tR\x05error*U\n" + + "\x05error\x18\x06 \x01(\tR\x05error\"[\n" + + "\x1bSetTailscaleExitNodeRequest\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x1a\n" + + "\bstableID\x18\x02 \x01(\tR\bstableID*U\n" + "\bLogLevel\x12\t\n" + "\x05PANIC\x10\x00\x12\t\n" + "\x05FATAL\x10\x01\x12\t\n" + @@ -2997,7 +3070,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\x99\x10\n" + + "\x17CONNECTION_EVENT_CLOSED\x10\x022\xf0\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" + @@ -3026,7 +3099,8 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x17StartNetworkQualityTest\x12!.daemon.NetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01\x12F\n" + "\rStartSTUNTest\x12\x17.daemon.STUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01\x12U\n" + "\x18SubscribeTailscaleStatus\x12\x16.google.protobuf.Empty\x1a\x1d.daemon.TailscaleStatusUpdate\"\x000\x01\x12U\n" + - "\x12StartTailscalePing\x12\x1c.daemon.TailscalePingRequest\x1a\x1d.daemon.TailscalePingResponse\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" + "\x12StartTailscalePing\x12\x1c.daemon.TailscalePingRequest\x1a\x1d.daemon.TailscalePingResponse\"\x000\x01\x12U\n" + + "\x14SetTailscaleExitNode\x12#.daemon.SetTailscaleExitNodeRequest\x1a\x16.google.protobuf.Empty\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" var ( file_daemon_started_service_proto_rawDescOnce sync.Once @@ -3042,7 +3116,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, 38) + file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 39) file_daemon_started_service_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ConnectionEventType)(0), // 1: daemon.ConnectionEventType @@ -3085,14 +3159,15 @@ var ( (*TailscalePeer)(nil), // 38: daemon.TailscalePeer (*TailscalePingRequest)(nil), // 39: daemon.TailscalePingRequest (*TailscalePingResponse)(nil), // 40: daemon.TailscalePingResponse - (*Log_Message)(nil), // 41: daemon.Log.Message - (*emptypb.Empty)(nil), // 42: google.protobuf.Empty + (*SetTailscaleExitNodeRequest)(nil), // 41: daemon.SetTailscaleExitNodeRequest + (*Log_Message)(nil), // 42: daemon.Log.Message + (*emptypb.Empty)(nil), // 43: google.protobuf.Empty } ) var file_daemon_started_service_proto_depIdxs = []int32{ 2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type - 41, // 1: daemon.Log.messages:type_name -> daemon.Log.Message + 42, // 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 @@ -3106,69 +3181,72 @@ var file_daemon_started_service_proto_depIdxs = []int32{ 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 - 42, // 17: daemon.StartedService.StopService:input_type -> google.protobuf.Empty - 42, // 18: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty - 42, // 19: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty - 42, // 20: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty - 42, // 21: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty - 42, // 22: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty - 6, // 23: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest - 42, // 24: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty - 42, // 25: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty - 42, // 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 - 42, // 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 - 42, // 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 - 42, // 37: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty - 42, // 38: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty - 42, // 39: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty - 42, // 40: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty - 31, // 41: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest - 33, // 42: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest - 42, // 43: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty - 39, // 44: daemon.StartedService.StartTailscalePing:input_type -> daemon.TailscalePingRequest - 42, // 45: daemon.StartedService.StopService:output_type -> google.protobuf.Empty - 42, // 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 - 42, // 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 - 42, // 55: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty - 42, // 56: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty - 42, // 57: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty - 42, // 58: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty - 18, // 59: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus - 42, // 60: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty - 42, // 61: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty - 42, // 62: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty - 23, // 63: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents - 42, // 64: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty - 42, // 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.SubscribeOutbounds:output_type -> daemon.OutboundList - 32, // 69: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress - 34, // 70: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress - 35, // 71: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate - 40, // 72: daemon.StartedService.StartTailscalePing:output_type -> daemon.TailscalePingResponse - 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 + 38, // 15: daemon.TailscaleEndpointStatus.exitNode:type_name -> daemon.TailscalePeer + 38, // 16: daemon.TailscaleUserGroup.peers:type_name -> daemon.TailscalePeer + 0, // 17: daemon.Log.Message.level:type_name -> daemon.LogLevel + 43, // 18: daemon.StartedService.StopService:input_type -> google.protobuf.Empty + 43, // 19: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty + 43, // 20: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty + 43, // 21: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty + 43, // 22: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty + 43, // 23: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty + 6, // 24: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest + 43, // 25: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty + 43, // 26: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty + 43, // 27: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty + 16, // 28: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode + 13, // 29: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest + 14, // 30: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest + 15, // 31: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest + 43, // 32: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty + 19, // 33: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest + 20, // 34: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest + 43, // 35: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty + 21, // 36: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest + 26, // 37: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest + 43, // 38: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty + 43, // 39: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty + 43, // 40: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty + 43, // 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 + 43, // 44: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty + 39, // 45: daemon.StartedService.StartTailscalePing:input_type -> daemon.TailscalePingRequest + 41, // 46: daemon.StartedService.SetTailscaleExitNode:input_type -> daemon.SetTailscaleExitNodeRequest + 43, // 47: daemon.StartedService.StopService:output_type -> google.protobuf.Empty + 43, // 48: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty + 4, // 49: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus + 7, // 50: daemon.StartedService.SubscribeLog:output_type -> daemon.Log + 8, // 51: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel + 43, // 52: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty + 9, // 53: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status + 10, // 54: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups + 17, // 55: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus + 16, // 56: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode + 43, // 57: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty + 43, // 58: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty + 43, // 59: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty + 43, // 60: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty + 18, // 61: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus + 43, // 62: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty + 43, // 63: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty + 43, // 64: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty + 23, // 65: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents + 43, // 66: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty + 43, // 67: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty + 27, // 68: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings + 29, // 69: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt + 30, // 70: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList + 32, // 71: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 34, // 72: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress + 35, // 73: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate + 40, // 74: daemon.StartedService.StartTailscalePing:output_type -> daemon.TailscalePingResponse + 43, // 75: daemon.StartedService.SetTailscaleExitNode:output_type -> google.protobuf.Empty + 47, // [47:76] is the sub-list for method output_type + 18, // [18:47] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_daemon_started_service_proto_init() } @@ -3182,7 +3260,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: 38, + NumMessages: 39, NumExtensions: 0, NumServices: 1, }, diff --git a/daemon/started_service.proto b/daemon/started_service.proto index 2c3140a91..7f5f3a10a 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -40,6 +40,7 @@ service StartedService { rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {} rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {} rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {} + rpc SetTailscaleExitNode(SetTailscaleExitNodeRequest) returns (google.protobuf.Empty) {} } message ServiceStatus { @@ -292,6 +293,7 @@ message TailscaleEndpointStatus { string magicDNSSuffix = 5; TailscalePeer self = 6; repeated TailscaleUserGroup userGroups = 7; + TailscalePeer exitNode = 8; } message TailscaleUserGroup { @@ -314,6 +316,7 @@ message TailscalePeer { int64 rxBytes = 9; int64 txBytes = 10; int64 keyExpiry = 11; + string stableID = 12; } message TailscalePingRequest { @@ -329,3 +332,8 @@ message TailscalePingResponse { string derpRegionCode = 5; string error = 6; } + +message SetTailscaleExitNodeRequest { + string endpointTag = 1; + string stableID = 2; +} diff --git a/daemon/started_service_grpc.pb.go b/daemon/started_service_grpc.pb.go index 967757f1a..b4b220c3c 100644 --- a/daemon/started_service_grpc.pb.go +++ b/daemon/started_service_grpc.pb.go @@ -43,6 +43,7 @@ const ( StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus" StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing" + StartedService_SetTailscaleExitNode_FullMethodName = "/daemon.StartedService/SetTailscaleExitNode" ) // StartedServiceClient is the client API for StartedService service. @@ -77,6 +78,7 @@ type StartedServiceClient interface { 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) StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) + SetTailscaleExitNode(ctx context.Context, in *SetTailscaleExitNodeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } type startedServiceClient struct { @@ -466,6 +468,16 @@ func (c *startedServiceClient) StartTailscalePing(ctx context.Context, in *Tails // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_StartTailscalePingClient = grpc.ServerStreamingClient[TailscalePingResponse] +func (c *startedServiceClient) SetTailscaleExitNode(ctx context.Context, in *SetTailscaleExitNodeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_SetTailscaleExitNode_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // StartedServiceServer is the server API for StartedService service. // All implementations must embed UnimplementedStartedServiceServer // for forward compatibility. @@ -498,6 +510,7 @@ type StartedServiceServer interface { StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error + SetTailscaleExitNode(context.Context, *SetTailscaleExitNodeRequest) (*emptypb.Empty, error) mustEmbedUnimplementedStartedServiceServer() } @@ -619,6 +632,10 @@ func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty func (UnimplementedStartedServiceServer) StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error { return status.Error(codes.Unimplemented, "method StartTailscalePing not implemented") } + +func (UnimplementedStartedServiceServer) SetTailscaleExitNode(context.Context, *SetTailscaleExitNodeRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method SetTailscaleExitNode not implemented") +} func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {} func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} @@ -1067,6 +1084,24 @@ func _StartedService_StartTailscalePing_Handler(srv interface{}, stream grpc.Ser // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_StartTailscalePingServer = grpc.ServerStreamingServer[TailscalePingResponse] +func _StartedService_SetTailscaleExitNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetTailscaleExitNodeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).SetTailscaleExitNode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_SetTailscaleExitNode_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).SetTailscaleExitNode(ctx, req.(*SetTailscaleExitNodeRequest)) + } + return interceptor(ctx, in, info, handler) +} + // 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) @@ -1142,6 +1177,10 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetStartedAt", Handler: _StartedService_GetStartedAt_Handler, }, + { + MethodName: "SetTailscaleExitNode", + Handler: _StartedService_SetTailscaleExitNode_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index 6f3c2e430..0319b8d65 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -780,6 +780,19 @@ func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) } } +func (c *CommandClient) SetTailscaleExitNode(endpointTag string, stableID string) error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.SetTailscaleExitNode(context.Background(), &daemon.SetTailscaleExitNodeRequest{ + EndpointTag: endpointTag, + StableID: stableID, + }) + }) + if err != nil { + return E.Cause(err, "set tailscale exit node") + } + return nil +} + func (c *CommandClient) StartTailscalePing(endpointTag string, peerIP string, handler TailscalePingHandler) error { client, err := c.getClientForCall() if err != nil { diff --git a/experimental/libbox/command_types_tailscale.go b/experimental/libbox/command_types_tailscale.go index dc17639df..ab2691634 100644 --- a/experimental/libbox/command_types_tailscale.go +++ b/experimental/libbox/command_types_tailscale.go @@ -22,6 +22,7 @@ type TailscaleEndpointStatus struct { NetworkName string MagicDNSSuffix string Self *TailscalePeer + ExitNode *TailscalePeer userGroups []*TailscaleUserGroup } @@ -52,6 +53,7 @@ type TailscalePeerIterator interface { } type TailscalePeer struct { + StableID string HostName string DNSName string OS string @@ -98,6 +100,9 @@ func tailscaleEndpointStatusFromGRPC(status *daemon.TailscaleEndpointStatus) *Ta if status.Self != nil { result.Self = tailscalePeerFromGRPC(status.Self) } + if status.ExitNode != nil { + result.ExitNode = tailscalePeerFromGRPC(status.ExitNode) + } return result } @@ -117,6 +122,7 @@ func tailscaleUserGroupFromGRPC(group *daemon.TailscaleUserGroup) *TailscaleUser func tailscalePeerFromGRPC(peer *daemon.TailscalePeer) *TailscalePeer { return &TailscalePeer{ + StableID: peer.StableID, HostName: peer.HostName, DNSName: peer.DnsName, OS: peer.Os, diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 41e6ad50b..de7780f95 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -53,6 +53,7 @@ import ( "github.com/sagernet/tailscale/net/netns" "github.com/sagernet/tailscale/net/tsaddr" tsTUN "github.com/sagernet/tailscale/net/tstun" + "github.com/sagernet/tailscale/tailcfg" "github.com/sagernet/tailscale/tsnet" "github.com/sagernet/tailscale/types/ipproto" "github.com/sagernet/tailscale/types/nettype" @@ -487,6 +488,49 @@ func (t *Endpoint) watchState() { } } +func (t *Endpoint) SetTailscaleExitNode(ctx context.Context, stableID string) error { + if !t.started.Load() { + return E.New("Tailscale is not ready yet") + } + if t.advertiseExitNode && stableID != "" { + return E.New("cannot advertise an exit node and use an exit node at the same time") + } + perfs := &ipn.MaskedPrefs{ + Prefs: ipn.Prefs{ + ExitNodeID: tailcfg.StableNodeID(stableID), + ExitNodeAllowLANAccess: t.exitNodeAllowLANAccess, + }, + ExitNodeIDSet: true, + ExitNodeIPSet: true, + ExitNodeAllowLANAccessSet: true, + } + if stableID != "" { + status, err := common.Must1(t.server.LocalClient()).Status(ctx) + if err != nil { + return E.Cause(err, "get tailscale status") + } + found := false + for _, peer := range status.Peer { + if peer.ID != tailcfg.StableNodeID(stableID) { + continue + } + if !peer.ExitNodeOption { + return E.New("peer does not offer exit node: ", stableID) + } + found = true + break + } + if !found { + return E.New("peer not found: ", stableID) + } + } + _, err := t.server.ExportLocalBackend().EditPrefs(perfs) + if err != nil { + return E.Cause(err, "update prefs") + } + return nil +} + func (t *Endpoint) Close() error { var err error t.started.Store(false) diff --git a/protocol/tailscale/status.go b/protocol/tailscale/status.go index a4d14ee14..7ccbeedd2 100644 --- a/protocol/tailscale/status.go +++ b/protocol/tailscale/status.go @@ -26,7 +26,7 @@ func (t *Endpoint) SubscribeTailscaleStatus(ctx context.Context, fn func(*adapte return false default: } - if roNotify.State != nil || roNotify.NetMap != nil || roNotify.BrowseToURL != nil { + if roNotify.State != nil || roNotify.NetMap != nil || roNotify.BrowseToURL != nil || roNotify.Prefs != nil { sendStatus() } return true @@ -76,6 +76,27 @@ func convertTailscaleStatus(status *ipnstate.Status) *adapter.TailscaleEndpointS return 0 }) } + if status.ExitNodeStatus != nil { + for _, peerKey := range status.Peers() { + peer := status.Peer[peerKey] + if peer.ID == status.ExitNodeStatus.ID { + result.ExitNode = convertTailscalePeer(peer) + break + } + } + if result.ExitNode == nil { + ips := make([]string, 0, len(status.ExitNodeStatus.TailscaleIPs)) + for _, prefix := range status.ExitNodeStatus.TailscaleIPs { + ips = append(ips, prefix.Addr().String()) + } + result.ExitNode = &adapter.TailscalePeer{ + StableID: string(status.ExitNodeStatus.ID), + TailscaleIPs: ips, + Online: status.ExitNodeStatus.Online, + ExitNode: true, + } + } + } return result } @@ -89,6 +110,7 @@ func convertTailscalePeer(peer *ipnstate.PeerStatus) *adapter.TailscalePeer { keyExpiry = peer.KeyExpiry.Unix() } return &adapter.TailscalePeer{ + StableID: string(peer.ID), HostName: peer.HostName, DNSName: peer.DNSName, OS: peer.OS,