diff --git a/gen/proto/go/teleport/lib/vnet/v1/client_application_service.pb.go b/gen/proto/go/teleport/lib/vnet/v1/client_application_service.pb.go index a30d0cece9171..6e007a0134f22 100644 --- a/gen/proto/go/teleport/lib/vnet/v1/client_application_service.pb.go +++ b/gen/proto/go/teleport/lib/vnet/v1/client_application_service.pb.go @@ -1247,8 +1247,8 @@ func (x *SignForAppResponse) GetSignature() []byte { return nil } -// OnNewConnectionRequest is a request for OnNewConnection. -type OnNewConnectionRequest struct { +// OnNewAppConnectionRequest is a request for OnNewAppConnection. +type OnNewAppConnectionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // AppKey identifies the app the connection is being made for. AppKey *AppKey `protobuf:"bytes,1,opt,name=app_key,json=appKey,proto3" json:"app_key,omitempty"` @@ -1256,20 +1256,20 @@ type OnNewConnectionRequest struct { sizeCache protoimpl.SizeCache } -func (x *OnNewConnectionRequest) Reset() { - *x = OnNewConnectionRequest{} +func (x *OnNewAppConnectionRequest) Reset() { + *x = OnNewAppConnectionRequest{} mi := &file_teleport_lib_vnet_v1_client_application_service_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *OnNewConnectionRequest) String() string { +func (x *OnNewAppConnectionRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*OnNewConnectionRequest) ProtoMessage() {} +func (*OnNewAppConnectionRequest) ProtoMessage() {} -func (x *OnNewConnectionRequest) ProtoReflect() protoreflect.Message { +func (x *OnNewAppConnectionRequest) ProtoReflect() protoreflect.Message { mi := &file_teleport_lib_vnet_v1_client_application_service_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1281,39 +1281,39 @@ func (x *OnNewConnectionRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use OnNewConnectionRequest.ProtoReflect.Descriptor instead. -func (*OnNewConnectionRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use OnNewAppConnectionRequest.ProtoReflect.Descriptor instead. +func (*OnNewAppConnectionRequest) Descriptor() ([]byte, []int) { return file_teleport_lib_vnet_v1_client_application_service_proto_rawDescGZIP(), []int{20} } -func (x *OnNewConnectionRequest) GetAppKey() *AppKey { +func (x *OnNewAppConnectionRequest) GetAppKey() *AppKey { if x != nil { return x.AppKey } return nil } -// OnNewConnectionRequest is a response for OnNewConnection. -type OnNewConnectionResponse struct { +// OnNewAppConnectionResponse is a response for OnNewAppConnection. +type OnNewAppConnectionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *OnNewConnectionResponse) Reset() { - *x = OnNewConnectionResponse{} +func (x *OnNewAppConnectionResponse) Reset() { + *x = OnNewAppConnectionResponse{} mi := &file_teleport_lib_vnet_v1_client_application_service_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *OnNewConnectionResponse) String() string { +func (x *OnNewAppConnectionResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*OnNewConnectionResponse) ProtoMessage() {} +func (*OnNewAppConnectionResponse) ProtoMessage() {} -func (x *OnNewConnectionResponse) ProtoReflect() protoreflect.Message { +func (x *OnNewAppConnectionResponse) ProtoReflect() protoreflect.Message { mi := &file_teleport_lib_vnet_v1_client_application_service_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1325,8 +1325,8 @@ func (x *OnNewConnectionResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use OnNewConnectionResponse.ProtoReflect.Descriptor instead. -func (*OnNewConnectionResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use OnNewAppConnectionResponse.ProtoReflect.Descriptor instead. +func (*OnNewAppConnectionResponse) Descriptor() ([]byte, []int) { return file_teleport_lib_vnet_v1_client_application_service_proto_rawDescGZIP(), []int{21} } @@ -2187,10 +2187,10 @@ const file_teleport_lib_vnet_v1_client_application_service_proto_rawDesc = "" + "\x0fpss_salt_length\x18\x03 \x01(\x05H\x00R\rpssSaltLength\x88\x01\x01B\x12\n" + "\x10_pss_salt_length\"2\n" + "\x12SignForAppResponse\x12\x1c\n" + - "\tsignature\x18\x01 \x01(\fR\tsignature\"O\n" + - "\x16OnNewConnectionRequest\x125\n" + - "\aapp_key\x18\x01 \x01(\v2\x1c.teleport.lib.vnet.v1.AppKeyR\x06appKey\"\x19\n" + - "\x17OnNewConnectionResponse\"v\n" + + "\tsignature\x18\x01 \x01(\fR\tsignature\"R\n" + + "\x19OnNewAppConnectionRequest\x125\n" + + "\aapp_key\x18\x01 \x01(\v2\x1c.teleport.lib.vnet.v1.AppKeyR\x06appKey\"\x1c\n" + + "\x1aOnNewAppConnectionResponse\"v\n" + "\x19OnInvalidLocalPortRequest\x128\n" + "\bapp_info\x18\x01 \x01(\v2\x1d.teleport.lib.vnet.v1.AppInfoR\aappInfo\x12\x1f\n" + "\vtarget_port\x18\x02 \x01(\rR\n" + @@ -2237,7 +2237,7 @@ const file_teleport_lib_vnet_v1_client_application_service_proto_rawDesc = "" + "\x04Hash\x12\x14\n" + "\x10HASH_UNSPECIFIED\x10\x00\x12\r\n" + "\tHASH_NONE\x10\x01\x12\x0f\n" + - "\vHASH_SHA256\x10\x022\xbc\f\n" + + "\vHASH_SHA256\x10\x022\xc5\f\n" + "\x18ClientApplicationService\x12z\n" + "\x13AuthenticateProcess\x120.teleport.lib.vnet.v1.AuthenticateProcessRequest\x1a1.teleport.lib.vnet.v1.AuthenticateProcessResponse\x12\x83\x01\n" + "\x16ReportNetworkStackInfo\x123.teleport.lib.vnet.v1.ReportNetworkStackInfoRequest\x1a4.teleport.lib.vnet.v1.ReportNetworkStackInfoResponse\x12M\n" + @@ -2245,8 +2245,8 @@ const file_teleport_lib_vnet_v1_client_application_service_proto_rawDesc = "" + "\vResolveFQDN\x12(.teleport.lib.vnet.v1.ResolveFQDNRequest\x1a).teleport.lib.vnet.v1.ResolveFQDNResponse\x12k\n" + "\x0eReissueAppCert\x12+.teleport.lib.vnet.v1.ReissueAppCertRequest\x1a,.teleport.lib.vnet.v1.ReissueAppCertResponse\x12_\n" + "\n" + - "SignForApp\x12'.teleport.lib.vnet.v1.SignForAppRequest\x1a(.teleport.lib.vnet.v1.SignForAppResponse\x12n\n" + - "\x0fOnNewConnection\x12,.teleport.lib.vnet.v1.OnNewConnectionRequest\x1a-.teleport.lib.vnet.v1.OnNewConnectionResponse\x12w\n" + + "SignForApp\x12'.teleport.lib.vnet.v1.SignForAppRequest\x1a(.teleport.lib.vnet.v1.SignForAppResponse\x12w\n" + + "\x12OnNewAppConnection\x12/.teleport.lib.vnet.v1.OnNewAppConnectionRequest\x1a0.teleport.lib.vnet.v1.OnNewAppConnectionResponse\x12w\n" + "\x12OnInvalidLocalPort\x12/.teleport.lib.vnet.v1.OnInvalidLocalPortRequest\x1a0.teleport.lib.vnet.v1.OnInvalidLocalPortResponse\x12\x89\x01\n" + "\x18GetTargetOSConfiguration\x125.teleport.lib.vnet.v1.GetTargetOSConfigurationRequest\x1a6.teleport.lib.vnet.v1.GetTargetOSConfigurationResponse\x12b\n" + "\vUserTLSCert\x12(.teleport.lib.vnet.v1.UserTLSCertRequest\x1a).teleport.lib.vnet.v1.UserTLSCertResponse\x12k\n" + @@ -2291,8 +2291,8 @@ var file_teleport_lib_vnet_v1_client_application_service_proto_goTypes = []any{ (*SignForAppRequest)(nil), // 18: teleport.lib.vnet.v1.SignForAppRequest (*SignRequest)(nil), // 19: teleport.lib.vnet.v1.SignRequest (*SignForAppResponse)(nil), // 20: teleport.lib.vnet.v1.SignForAppResponse - (*OnNewConnectionRequest)(nil), // 21: teleport.lib.vnet.v1.OnNewConnectionRequest - (*OnNewConnectionResponse)(nil), // 22: teleport.lib.vnet.v1.OnNewConnectionResponse + (*OnNewAppConnectionRequest)(nil), // 21: teleport.lib.vnet.v1.OnNewAppConnectionRequest + (*OnNewAppConnectionResponse)(nil), // 22: teleport.lib.vnet.v1.OnNewAppConnectionResponse (*OnInvalidLocalPortRequest)(nil), // 23: teleport.lib.vnet.v1.OnInvalidLocalPortRequest (*OnInvalidLocalPortResponse)(nil), // 24: teleport.lib.vnet.v1.OnInvalidLocalPortResponse (*GetTargetOSConfigurationRequest)(nil), // 25: teleport.lib.vnet.v1.GetTargetOSConfigurationRequest @@ -2323,7 +2323,7 @@ var file_teleport_lib_vnet_v1_client_application_service_proto_depIdxs = []int32 14, // 9: teleport.lib.vnet.v1.SignForAppRequest.app_key:type_name -> teleport.lib.vnet.v1.AppKey 19, // 10: teleport.lib.vnet.v1.SignForAppRequest.sign:type_name -> teleport.lib.vnet.v1.SignRequest 0, // 11: teleport.lib.vnet.v1.SignRequest.hash:type_name -> teleport.lib.vnet.v1.Hash - 14, // 12: teleport.lib.vnet.v1.OnNewConnectionRequest.app_key:type_name -> teleport.lib.vnet.v1.AppKey + 14, // 12: teleport.lib.vnet.v1.OnNewAppConnectionRequest.app_key:type_name -> teleport.lib.vnet.v1.AppKey 13, // 13: teleport.lib.vnet.v1.OnInvalidLocalPortRequest.app_info:type_name -> teleport.lib.vnet.v1.AppInfo 27, // 14: teleport.lib.vnet.v1.GetTargetOSConfigurationResponse.target_os_configuration:type_name -> teleport.lib.vnet.v1.TargetOSConfiguration 15, // 15: teleport.lib.vnet.v1.UserTLSCertResponse.dial_options:type_name -> teleport.lib.vnet.v1.DialOptions @@ -2335,7 +2335,7 @@ var file_teleport_lib_vnet_v1_client_application_service_proto_depIdxs = []int32 8, // 21: teleport.lib.vnet.v1.ClientApplicationService.ResolveFQDN:input_type -> teleport.lib.vnet.v1.ResolveFQDNRequest 16, // 22: teleport.lib.vnet.v1.ClientApplicationService.ReissueAppCert:input_type -> teleport.lib.vnet.v1.ReissueAppCertRequest 18, // 23: teleport.lib.vnet.v1.ClientApplicationService.SignForApp:input_type -> teleport.lib.vnet.v1.SignForAppRequest - 21, // 24: teleport.lib.vnet.v1.ClientApplicationService.OnNewConnection:input_type -> teleport.lib.vnet.v1.OnNewConnectionRequest + 21, // 24: teleport.lib.vnet.v1.ClientApplicationService.OnNewAppConnection:input_type -> teleport.lib.vnet.v1.OnNewAppConnectionRequest 23, // 25: teleport.lib.vnet.v1.ClientApplicationService.OnInvalidLocalPort:input_type -> teleport.lib.vnet.v1.OnInvalidLocalPortRequest 25, // 26: teleport.lib.vnet.v1.ClientApplicationService.GetTargetOSConfiguration:input_type -> teleport.lib.vnet.v1.GetTargetOSConfigurationRequest 28, // 27: teleport.lib.vnet.v1.ClientApplicationService.UserTLSCert:input_type -> teleport.lib.vnet.v1.UserTLSCertRequest @@ -2349,7 +2349,7 @@ var file_teleport_lib_vnet_v1_client_application_service_proto_depIdxs = []int32 9, // 35: teleport.lib.vnet.v1.ClientApplicationService.ResolveFQDN:output_type -> teleport.lib.vnet.v1.ResolveFQDNResponse 17, // 36: teleport.lib.vnet.v1.ClientApplicationService.ReissueAppCert:output_type -> teleport.lib.vnet.v1.ReissueAppCertResponse 20, // 37: teleport.lib.vnet.v1.ClientApplicationService.SignForApp:output_type -> teleport.lib.vnet.v1.SignForAppResponse - 22, // 38: teleport.lib.vnet.v1.ClientApplicationService.OnNewConnection:output_type -> teleport.lib.vnet.v1.OnNewConnectionResponse + 22, // 38: teleport.lib.vnet.v1.ClientApplicationService.OnNewAppConnection:output_type -> teleport.lib.vnet.v1.OnNewAppConnectionResponse 24, // 39: teleport.lib.vnet.v1.ClientApplicationService.OnInvalidLocalPort:output_type -> teleport.lib.vnet.v1.OnInvalidLocalPortResponse 26, // 40: teleport.lib.vnet.v1.ClientApplicationService.GetTargetOSConfiguration:output_type -> teleport.lib.vnet.v1.GetTargetOSConfigurationResponse 29, // 41: teleport.lib.vnet.v1.ClientApplicationService.UserTLSCert:output_type -> teleport.lib.vnet.v1.UserTLSCertResponse diff --git a/gen/proto/go/teleport/lib/vnet/v1/client_application_service_grpc.pb.go b/gen/proto/go/teleport/lib/vnet/v1/client_application_service_grpc.pb.go index e153a98dd6646..1be07457d7424 100644 --- a/gen/proto/go/teleport/lib/vnet/v1/client_application_service_grpc.pb.go +++ b/gen/proto/go/teleport/lib/vnet/v1/client_application_service_grpc.pb.go @@ -41,7 +41,7 @@ const ( ClientApplicationService_ResolveFQDN_FullMethodName = "/teleport.lib.vnet.v1.ClientApplicationService/ResolveFQDN" ClientApplicationService_ReissueAppCert_FullMethodName = "/teleport.lib.vnet.v1.ClientApplicationService/ReissueAppCert" ClientApplicationService_SignForApp_FullMethodName = "/teleport.lib.vnet.v1.ClientApplicationService/SignForApp" - ClientApplicationService_OnNewConnection_FullMethodName = "/teleport.lib.vnet.v1.ClientApplicationService/OnNewConnection" + ClientApplicationService_OnNewAppConnection_FullMethodName = "/teleport.lib.vnet.v1.ClientApplicationService/OnNewAppConnection" ClientApplicationService_OnInvalidLocalPort_FullMethodName = "/teleport.lib.vnet.v1.ClientApplicationService/OnInvalidLocalPort" ClientApplicationService_GetTargetOSConfiguration_FullMethodName = "/teleport.lib.vnet.v1.ClientApplicationService/GetTargetOSConfiguration" ClientApplicationService_UserTLSCert_FullMethodName = "/teleport.lib.vnet.v1.ClientApplicationService/UserTLSCert" @@ -77,9 +77,9 @@ type ClientApplicationServiceClient interface { // SignForApp issues a signature with the private key associated with an x509 // certificate previously issued for a requested app. SignForApp(ctx context.Context, in *SignForAppRequest, opts ...grpc.CallOption) (*SignForAppResponse, error) - // OnNewConnection gets called whenever a new connection is about to be + // OnNewAppConnection gets called whenever a new app connection is about to be // established through VNet for observability. - OnNewConnection(ctx context.Context, in *OnNewConnectionRequest, opts ...grpc.CallOption) (*OnNewConnectionResponse, error) + OnNewAppConnection(ctx context.Context, in *OnNewAppConnectionRequest, opts ...grpc.CallOption) (*OnNewAppConnectionResponse, error) // OnInvalidLocalPort gets called before VNet refuses to handle a connection // to a multi-port TCP app because the provided port does not match any of the // TCP ports in the app spec. @@ -168,10 +168,10 @@ func (c *clientApplicationServiceClient) SignForApp(ctx context.Context, in *Sig return out, nil } -func (c *clientApplicationServiceClient) OnNewConnection(ctx context.Context, in *OnNewConnectionRequest, opts ...grpc.CallOption) (*OnNewConnectionResponse, error) { +func (c *clientApplicationServiceClient) OnNewAppConnection(ctx context.Context, in *OnNewAppConnectionRequest, opts ...grpc.CallOption) (*OnNewAppConnectionResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(OnNewConnectionResponse) - err := c.cc.Invoke(ctx, ClientApplicationService_OnNewConnection_FullMethodName, in, out, cOpts...) + out := new(OnNewAppConnectionResponse) + err := c.cc.Invoke(ctx, ClientApplicationService_OnNewAppConnection_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -274,9 +274,9 @@ type ClientApplicationServiceServer interface { // SignForApp issues a signature with the private key associated with an x509 // certificate previously issued for a requested app. SignForApp(context.Context, *SignForAppRequest) (*SignForAppResponse, error) - // OnNewConnection gets called whenever a new connection is about to be + // OnNewAppConnection gets called whenever a new app connection is about to be // established through VNet for observability. - OnNewConnection(context.Context, *OnNewConnectionRequest) (*OnNewConnectionResponse, error) + OnNewAppConnection(context.Context, *OnNewAppConnectionRequest) (*OnNewAppConnectionResponse, error) // OnInvalidLocalPort gets called before VNet refuses to handle a connection // to a multi-port TCP app because the provided port does not match any of the // TCP ports in the app spec. @@ -323,8 +323,8 @@ func (UnimplementedClientApplicationServiceServer) ReissueAppCert(context.Contex func (UnimplementedClientApplicationServiceServer) SignForApp(context.Context, *SignForAppRequest) (*SignForAppResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SignForApp not implemented") } -func (UnimplementedClientApplicationServiceServer) OnNewConnection(context.Context, *OnNewConnectionRequest) (*OnNewConnectionResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method OnNewConnection not implemented") +func (UnimplementedClientApplicationServiceServer) OnNewAppConnection(context.Context, *OnNewAppConnectionRequest) (*OnNewAppConnectionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method OnNewAppConnection not implemented") } func (UnimplementedClientApplicationServiceServer) OnInvalidLocalPort(context.Context, *OnInvalidLocalPortRequest) (*OnInvalidLocalPortResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method OnInvalidLocalPort not implemented") @@ -477,20 +477,20 @@ func _ClientApplicationService_SignForApp_Handler(srv interface{}, ctx context.C return interceptor(ctx, in, info, handler) } -func _ClientApplicationService_OnNewConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(OnNewConnectionRequest) +func _ClientApplicationService_OnNewAppConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OnNewAppConnectionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(ClientApplicationServiceServer).OnNewConnection(ctx, in) + return srv.(ClientApplicationServiceServer).OnNewAppConnection(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: ClientApplicationService_OnNewConnection_FullMethodName, + FullMethod: ClientApplicationService_OnNewAppConnection_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ClientApplicationServiceServer).OnNewConnection(ctx, req.(*OnNewConnectionRequest)) + return srv.(ClientApplicationServiceServer).OnNewAppConnection(ctx, req.(*OnNewAppConnectionRequest)) } return interceptor(ctx, in, info, handler) } @@ -653,8 +653,8 @@ var ClientApplicationService_ServiceDesc = grpc.ServiceDesc{ Handler: _ClientApplicationService_SignForApp_Handler, }, { - MethodName: "OnNewConnection", - Handler: _ClientApplicationService_OnNewConnection_Handler, + MethodName: "OnNewAppConnection", + Handler: _ClientApplicationService_OnNewAppConnection_Handler, }, { MethodName: "OnInvalidLocalPort", diff --git a/lib/teleterm/vnet/service.go b/lib/teleterm/vnet/service.go index 748ef53224511..12f18e80a6ad8 100644 --- a/lib/teleterm/vnet/service.go +++ b/lib/teleterm/vnet/service.go @@ -530,11 +530,25 @@ func (p *clientApplication) GetDialOptions(ctx context.Context, profileName stri return dialOpts, nil } -// OnNewConnection submits a usage event once per clientApplication lifetime. -// That is, if a user makes multiple connections to a single app, OnNewConnection submits a single +// OnNewSSHSession submits a usage event for a new SSH session. +func (p *clientApplication) OnNewSSHSession(ctx context.Context, profileName, targetClusterName string) { + // Enqueue the event from a separate goroutine since we don't care about errors anyway and we also + // don't want to slow down VNet connections. + go func() { + // Not passing ctx to ReportSSHSession since ctx is tied to the + // lifetime of a short-lived API call, inheriting the context could + // interrupt reporting. + if err := p.usageReporter.ReportSSHSession(profileName, targetClusterName); err != nil { + log.ErrorContext(ctx, "Failed to submit SSH usage event") + } + }() +} + +// OnNewAppConnection submits an app usage event once per clientApplication lifetime. +// That is, if a user makes multiple connections to a single app, OnNewAppConnection submits a single // event. This is to mimic how Connect submits events for its app gateways. This lets us compare // popularity of VNet and app gateways. -func (p *clientApplication) OnNewConnection(ctx context.Context, appKey *vnetv1.AppKey) error { +func (p *clientApplication) OnNewAppConnection(ctx context.Context, appKey *vnetv1.AppKey) error { // Enqueue the event from a separate goroutine since we don't care about errors anyway and we also // don't want to slow down VNet connections. go func() { @@ -542,9 +556,8 @@ func (p *clientApplication) OnNewConnection(ctx context.Context, appKey *vnetv1. // Not passing ctx to ReportApp since ctx is tied to the lifetime of the connection. // If it's a short-lived connection, inheriting its context would interrupt reporting. - err := p.usageReporter.ReportApp(uri) - if err != nil { - log.ErrorContext(ctx, "Failed to submit usage event", "app", uri, "error", err) + if err := p.usageReporter.ReportApp(uri); err != nil { + log.ErrorContext(ctx, "Failed to submit app usage event", "app", uri, "error", err) } }() @@ -606,6 +619,7 @@ func (p *clientApplication) OnInvalidLocalPort(ctx context.Context, appInfo *vne type usageReporter interface { ReportApp(uri.ResourceURI) error + ReportSSHSession(profileName, rootClusterName string) error Stop() } @@ -675,6 +689,51 @@ func newDaemonUsageReporter(cfg daemonUsageReporterConfig) (*daemonUsageReporter }, nil } +// ReportSSHSession adds an event for a new SSH session to the events queue. +// It reports a new event for each new SSH session, in contrast to ReportApp +// which only reports each unique app once, to align with how Connect reports +// usage events for SSH sessions. +func (r *daemonUsageReporter) ReportSSHSession(profileName, rootClusterName string) error { + r.mu.Lock() + defer r.mu.Unlock() + + if r.closed.Load() { + return trace.CompareFailed("usage reporter has been stopped") + } + + rootClusterURI := uri.NewClusterURI(profileName) + _, tc, err := r.cfg.ClientCache.ResolveClusterURI(rootClusterURI) + if err != nil { + return trace.Wrap(err) + } + + clusterID, ok := r.cfg.ClusterIDCache.Load(rootClusterURI) + if !ok { + return trace.NotFound("cluster ID for %q not found", rootClusterURI) + } + + log.DebugContext(context.Background(), "Reporting SSH usage event", "profile", profileName, "root_cluster", rootClusterName) + if err := r.cfg.EventConsumer.ReportUsageEvent(&apiteleterm.ReportUsageEventRequest{ + AuthClusterId: clusterID, + PrehogReq: &prehogv1alpha.SubmitConnectEventRequest{ + DistinctId: r.cfg.InstallationID, + Timestamp: timestamppb.Now(), + Event: &prehogv1alpha.SubmitConnectEventRequest_ProtocolUse{ + ProtocolUse: &prehogv1alpha.ConnectProtocolUseEvent{ + ClusterName: rootClusterName, + UserName: tc.Username, + Protocol: "ssh", + Origin: "vnet", + AccessThrough: "vnet", + }, + }, + }, + }); err != nil { + return trace.Wrap(err, "adding SSH usage event to queue") + } + return nil +} + // ReportApp adds an event related to the given app to the events queue, if the app wasn't reported // already. Only one invocation of ReportApp can be in flight at a time. func (r *daemonUsageReporter) ReportApp(appURI uri.ResourceURI) error { @@ -716,9 +775,9 @@ func (r *daemonUsageReporter) ReportApp(appURI uri.ResourceURI) error { return trace.NotFound("cluster ID for %q not found", rootClusterURI) } - log.DebugContext(ctx, "Reporting usage event", "app", appURI.String()) + log.DebugContext(ctx, "Reporting app usage event", "app", appURI.String()) - err = r.cfg.EventConsumer.ReportUsageEvent(&apiteleterm.ReportUsageEventRequest{ + if err := r.cfg.EventConsumer.ReportUsageEvent(&apiteleterm.ReportUsageEventRequest{ AuthClusterId: clusterID, PrehogReq: &prehogv1alpha.SubmitConnectEventRequest{ DistinctId: r.cfg.InstallationID, @@ -733,9 +792,8 @@ func (r *daemonUsageReporter) ReportApp(appURI uri.ResourceURI) error { }, }, }, - }) - if err != nil { - return trace.Wrap(err, "adding usage event to queue") + }); err != nil { + return trace.Wrap(err, "adding app usage event to queue") } r.reportedApps[appURI.String()] = struct{}{} @@ -762,7 +820,12 @@ func (r *daemonUsageReporter) Stop() { type disabledTelemetryUsageReporter struct{} func (r *disabledTelemetryUsageReporter) ReportApp(appURI uri.ResourceURI) error { - log.DebugContext(context.Background(), "Skipping usage event, usage reporting is turned off", "app", appURI.String()) + log.DebugContext(context.Background(), "Skipping app usage event, usage reporting is turned off", "app", appURI.String()) + return nil +} + +func (r *disabledTelemetryUsageReporter) ReportSSHSession(profileName, rootClusterName string) error { + log.DebugContext(context.Background(), "Skipping SSH usage event, usage reporting is turned off", "profile", profileName, "root_cluster", rootClusterName) return nil } diff --git a/lib/teleterm/vnet/service_test.go b/lib/teleterm/vnet/service_test.go index 58fba75e2afe9..b97c73acdcfdf 100644 --- a/lib/teleterm/vnet/service_test.go +++ b/lib/teleterm/vnet/service_test.go @@ -77,6 +77,11 @@ func TestDaemonUsageReporter(t *testing.T) { err = usageReporter.ReportApp(clusterWithoutClusterID.AppendApp("bar")) require.ErrorIs(t, err, trace.NotFound("cluster ID for \"/clusters/no-cluster-id\" not found")) require.Equal(t, 1, eventConsumer.EventCount()) + + // Verify that reporting an SSH session works. + err = usageReporter.ReportSSHSession(validCluster.GetProfileName(), "foo") + require.NoError(t, err) + require.Equal(t, 2, eventConsumer.EventCount()) } func TestDaemonUsageReporter_Stop(t *testing.T) { diff --git a/lib/vnet/app_handler.go b/lib/vnet/app_handler.go index aa0e2d7fee8b7..aefa77fed33f1 100644 --- a/lib/vnet/app_handler.go +++ b/lib/vnet/app_handler.go @@ -165,7 +165,7 @@ func (i *appCertIssuer) IssueCert(ctx context.Context) (tls.Certificate, error) } // localProxyMiddleware wraps around [client.CertChecker] and additionally makes it so that its -// OnNewConnection method calls the same method of [appProvider]. +// OnNewConnection method calls OnNewAppConnection on [appProvider]. type localProxyMiddleware struct { appKey *vnetv1.AppKey certChecker *client.CertChecker @@ -177,7 +177,7 @@ func (m *localProxyMiddleware) OnNewConnection(ctx context.Context, lp *alpnprox if err != nil { return trace.Wrap(err) } - return trace.Wrap(m.appProvider.OnNewConnection(ctx, m.appKey)) + return trace.Wrap(m.appProvider.OnNewAppConnection(ctx, m.appKey)) } func (m *localProxyMiddleware) OnStart(ctx context.Context, lp *alpnproxy.LocalProxy) error { diff --git a/lib/vnet/app_provider.go b/lib/vnet/app_provider.go index 113b0cbafc38d..c8ec1e8d3e269 100644 --- a/lib/vnet/app_provider.go +++ b/lib/vnet/app_provider.go @@ -74,9 +74,9 @@ func (p *appProvider) newAppCertSigner(cert []byte, appKey *vnetv1.AppKey, targe }, nil } -// OnNewConnection reports a new TCP connection to the target app. -func (p *appProvider) OnNewConnection(ctx context.Context, appKey *vnetv1.AppKey) error { - if err := p.clt.OnNewConnection(ctx, appKey); err != nil { +// OnNewAppConnection reports a new TCP connection to the target app. +func (p *appProvider) OnNewAppConnection(ctx context.Context, appKey *vnetv1.AppKey) error { + if err := p.clt.OnNewAppConnection(ctx, appKey); err != nil { return trace.Wrap(err) } return nil diff --git a/lib/vnet/client_application_service.go b/lib/vnet/client_application_service.go index 4aeeac71bbd5e..94f58a48c3baa 100644 --- a/lib/vnet/client_application_service.go +++ b/lib/vnet/client_application_service.go @@ -217,13 +217,11 @@ func (s *clientApplicationService) getSignerForApp(appKey *vnetv1.AppKey, target return signer, ok } -// OnNewConnection gets called whenever a new connection is about to be +// OnNewAppConnection gets called whenever a new app connection is about to be // established through VNet for observability. -func (s *clientApplicationService) OnNewConnection(ctx context.Context, req *vnetv1.OnNewConnectionRequest) (*vnetv1.OnNewConnectionResponse, error) { - if err := s.cfg.clientApplication.OnNewConnection(ctx, req.GetAppKey()); err != nil { - return nil, trace.Wrap(err) - } - return &vnetv1.OnNewConnectionResponse{}, nil +func (s *clientApplicationService) OnNewAppConnection(ctx context.Context, req *vnetv1.OnNewAppConnectionRequest) (*vnetv1.OnNewAppConnectionResponse, error) { + s.cfg.clientApplication.OnNewAppConnection(ctx, req.GetAppKey()) + return &vnetv1.OnNewAppConnectionResponse{}, nil } // OnInvalidLocalPort gets called before VNet refuses to handle a connection @@ -373,6 +371,10 @@ func (s *clientApplicationService) SessionSSHConfig(ctx context.Context, req *vn return nil, trace.Errorf("user KeyRing host no trusted SSH CAs for cluster %s", targetCluster) } sessionID := s.setSignerForSSHSession(keyRing.SSHPrivateKey) + + // Submit usage event. + s.cfg.clientApplication.OnNewSSHSession(ctx, req.GetProfile(), req.GetRootCluster()) + return &vnetv1.SessionSSHConfigResponse{ SessionId: sessionID, Cert: sshCert.Marshal(), diff --git a/lib/vnet/client_application_service_client.go b/lib/vnet/client_application_service_client.go index 72d6c33b68f53..ffee0471ba349 100644 --- a/lib/vnet/client_application_service_client.go +++ b/lib/vnet/client_application_service_client.go @@ -134,13 +134,13 @@ func (c *clientApplicationServiceClient) SignForApp(ctx context.Context, req *vn return resp.GetSignature(), nil } -// OnNewConnection reports a new TCP connection to the target app. -func (c *clientApplicationServiceClient) OnNewConnection(ctx context.Context, appKey *vnetv1.AppKey) error { - _, err := c.clt.OnNewConnection(ctx, &vnetv1.OnNewConnectionRequest{ +// OnNewAppConnection reports a new TCP connection to the target app. +func (c *clientApplicationServiceClient) OnNewAppConnection(ctx context.Context, appKey *vnetv1.AppKey) error { + _, err := c.clt.OnNewAppConnection(ctx, &vnetv1.OnNewAppConnectionRequest{ AppKey: appKey, }) if err != nil { - return trace.Wrap(err, "calling OnNewConnection rpc") + return trace.Wrap(err, "calling OnNewAppConnection rpc") } return nil } diff --git a/lib/vnet/user_process.go b/lib/vnet/user_process.go index f0c502f18e822..b74a662d60098 100644 --- a/lib/vnet/user_process.go +++ b/lib/vnet/user_process.go @@ -52,13 +52,17 @@ type ClientApplication interface { // GetDialOptions returns ALPN dial options for the profile. GetDialOptions(ctx context.Context, profileName string) (*vnetv1.DialOptions, error) - // OnNewConnection gets called whenever a new connection is about to be established through VNet. - // By the time OnNewConnection, VNet has already verified that the user holds a valid cert for the + // OnNewSSHSession should be called whenever a new SSH session is about to be + // started, after getting the user SSH certificate for the session. + OnNewSSHSession(ctx context.Context, profileName, rootClusterName string) + + // OnNewAppConnection gets called whenever a new app connection is about to be established through VNet. + // By the time OnNewAppConnection, VNet has already verified that the user holds a valid cert for the // app. // - // The connection won't be established until OnNewConnection returns. Returning an error prevents + // The connection won't be established until OnNewAppConnection returns. Returning an error prevents // the connection from being made. - OnNewConnection(ctx context.Context, appKey *vnetv1.AppKey) error + OnNewAppConnection(ctx context.Context, appKey *vnetv1.AppKey) error // OnInvalidLocalPort gets called before VNet refuses to handle a connection to a multi-port TCP app // because the provided port does not match any of the TCP ports in the app spec. diff --git a/lib/vnet/vnet_test.go b/lib/vnet/vnet_test.go index 6d4e584ec0bf2..d60ea0e007d7f 100644 --- a/lib/vnet/vnet_test.go +++ b/lib/vnet/vnet_test.go @@ -365,7 +365,8 @@ type fakeClientApp struct { teleportHostCA ssh.Signer teleportUserCA ssh.Signer - onNewConnectionCallCount atomic.Uint32 + onNewSSHSessionCallCount atomic.Uint32 + onNewAppConnectionCallCount atomic.Uint32 onInvalidLocalPortCallCount atomic.Uint32 // requestedRouteToApps indexed by public address. requestedRouteToApps map[string][]*proto.RouteToApp @@ -565,8 +566,12 @@ func (p *fakeClientApp) GetVnetConfig(ctx context.Context, profileName, leafClus return cfg, nil } -func (p *fakeClientApp) OnNewConnection(_ context.Context, _ *vnetv1.AppKey) error { - p.onNewConnectionCallCount.Add(1) +func (p *fakeClientApp) OnNewSSHSession(ctx context.Context, profileName, rootClusterName string) { + p.onNewSSHSessionCallCount.Add(1) +} + +func (p *fakeClientApp) OnNewAppConnection(_ context.Context, _ *vnetv1.AppKey) error { + p.onNewAppConnectionCallCount.Add(1) return nil } @@ -1037,9 +1042,9 @@ func testEchoConnection(t *testing.T, conn net.Conn) { } } -// TestOnNewConnection tests that the client applications OnNewConnection method +// TestOnNewAppConnection tests that the client applications OnNewAppConnection method // is called when a user connects to a valid TCP app. -func TestOnNewConnection(t *testing.T) { +func TestOnNewAppConnection(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -1067,19 +1072,19 @@ func TestOnNewConnection(t *testing.T) { fakeClientApp: clientApp, }) - // Attempt to establish a connection to an invalid app and verify that OnNewConnection was not + // Attempt to establish a connection to an invalid app and verify that OnNewAppConnection was not // called. lookupCtx, lookupCtxCancel := context.WithTimeout(ctx, 200*time.Millisecond) defer lookupCtxCancel() _, err := p.lookupHost(lookupCtx, invalidAppName) require.Error(t, err, "Expected lookup of an invalid app to fail") - require.Equal(t, uint32(0), clientApp.onNewConnectionCallCount.Load()) + require.Equal(t, uint32(0), clientApp.onNewAppConnectionCallCount.Load()) - // Establish a connection to a valid app and verify that OnNewConnection was called. + // Establish a connection to a valid app and verify that OnNewAppConnection was called. conn, err := p.dialHost(ctx, validAppName, 80 /* bogus port */) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, conn.Close()) }) - require.Equal(t, uint32(1), clientApp.onNewConnectionCallCount.Load()) + require.Equal(t, uint32(1), clientApp.onNewAppConnectionCallCount.Load()) } // TestWithAlgorithmSuites tests basic VNet functionality with each signature @@ -1230,6 +1235,15 @@ func TestSSH(t *testing.T) { badUserSigner, err := ssh.NewSignerFromSigner(badUserKey) require.NoError(t, err) + // Check that each successful SSH session is reported to the client + // application, do this in a t.Cleanup func so that the check runs after + // all parallel subtests have completed. + var expectReportedSSHSessions atomic.Uint32 + t.Cleanup(func() { + assert.Equal(t, expectReportedSSHSessions.Load(), clientApp.onNewSSHSessionCallCount.Load(), + "OnNewSSHSession call count does not match the expected number of reported SSH sessions") + }) + for _, tc := range []struct { dialAddr string dialPort int @@ -1240,22 +1254,25 @@ func TestSSH(t *testing.T) { sshUserSigner ssh.Signer expectSSHHandshakeToFail bool expectBannerMessages []string + expectSSHSessionReported bool }{ { // Connection to node in root cluster should work. - dialAddr: "node.root1.example.com", - dialPort: 22, - expectCIDR: root1CIDR, - sshUser: "testuser", - sshUserSigner: sshUserSigner, + dialAddr: "node.root1.example.com", + dialPort: 22, + expectCIDR: root1CIDR, + sshUser: "testuser", + sshUserSigner: sshUserSigner, + expectSSHSessionReported: true, }, { // Fully-qualified hostname should also work. - dialAddr: "node.root1.example.com.", - dialPort: 22, - expectCIDR: root1CIDR, - sshUser: "testuser", - sshUserSigner: sshUserSigner, + dialAddr: "node.root1.example.com.", + dialPort: 22, + expectCIDR: root1CIDR, + sshUser: "testuser", + sshUserSigner: sshUserSigner, + expectSSHSessionReported: true, }, { // Dial should fail on non-standard SSH port. @@ -1297,32 +1314,40 @@ func TestSSH(t *testing.T) { "VNet: access denied to denyuser connecting to node\n", }, expectSSHHandshakeToFail: true, + // The session should be reported because VNet successfully got a + // Teleport user SSH cert for this session and made the SSH dial to + // the target, only then the target SSH server rejected the + // connection. + expectSSHSessionReported: true, }, { // Connection to node in leaf cluster should work. - dialAddr: "node.leaf1.example.com", - dialPort: 22, - expectCIDR: leaf1CIDR, - sshUser: "testuser", - sshUserSigner: sshUserSigner, + dialAddr: "node.leaf1.example.com", + dialPort: 22, + expectCIDR: leaf1CIDR, + sshUser: "testuser", + sshUserSigner: sshUserSigner, + expectSSHSessionReported: true, }, { // Connection to node in root cluster in alternate profile should // work. - dialAddr: "node.root2.example.com", - dialPort: 22, - expectCIDR: root2CIDR, - sshUser: "testuser", - sshUserSigner: sshUserSigner, + dialAddr: "node.root2.example.com", + dialPort: 22, + expectCIDR: root2CIDR, + sshUser: "testuser", + sshUserSigner: sshUserSigner, + expectSSHSessionReported: true, }, { // Connection to node in leaf cluster in alternate profile should // work. - dialAddr: "node.leaf2.example.com", - dialPort: 22, - expectCIDR: leaf2CIDR, - sshUser: "testuser", - sshUserSigner: sshUserSigner, + dialAddr: "node.leaf2.example.com", + dialPort: 22, + expectCIDR: leaf2CIDR, + sshUser: "testuser", + sshUserSigner: sshUserSigner, + expectSSHSessionReported: true, }, { // DNS lookup should fail if the FQDN doesn't match any cluster. @@ -1342,6 +1367,10 @@ func TestSSH(t *testing.T) { t.Run(fmt.Sprintf("%s@%s:%d", tc.sshUser, tc.dialAddr, tc.dialPort), func(t *testing.T) { t.Parallel() + if tc.expectSSHSessionReported { + expectReportedSSHSessions.Add(1) + } + if tc.expectLookupToFail { // In these cases the DNS lookup is expected to fail, just run the DNS lookup. ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) @@ -1400,6 +1429,7 @@ func TestSSH(t *testing.T) { return nil }, } + sshConn, chans, reqs, err := ssh.NewClientConn(conn, fmt.Sprintf("%s:%d", tc.dialAddr, tc.dialPort), clientConfig) assert.Equal(t, tc.expectBannerMessages, bannerMessages, "actual banner messages did not match the expected") if tc.expectSSHHandshakeToFail { @@ -1433,6 +1463,7 @@ func TestSSH(t *testing.T) { sshConn, _, _, err := ssh.NewClientConn(conn, "node.root1.example.com:22", clientConfig) require.NoError(t, err) sshConn.Close() + expectReportedSSHSessions.Add(1) } require.Len(t, checkedHostCerts, connections) for i := range connections - 1 { diff --git a/proto/teleport/lib/vnet/v1/client_application_service.proto b/proto/teleport/lib/vnet/v1/client_application_service.proto index 85625f7b693cc..b8193a53ff4fa 100644 --- a/proto/teleport/lib/vnet/v1/client_application_service.proto +++ b/proto/teleport/lib/vnet/v1/client_application_service.proto @@ -44,9 +44,9 @@ service ClientApplicationService { // SignForApp issues a signature with the private key associated with an x509 // certificate previously issued for a requested app. rpc SignForApp(SignForAppRequest) returns (SignForAppResponse); - // OnNewConnection gets called whenever a new connection is about to be + // OnNewAppConnection gets called whenever a new app connection is about to be // established through VNet for observability. - rpc OnNewConnection(OnNewConnectionRequest) returns (OnNewConnectionResponse); + rpc OnNewAppConnection(OnNewAppConnectionRequest) returns (OnNewAppConnectionResponse); // OnInvalidLocalPort gets called before VNet refuses to handle a connection // to a multi-port TCP app because the provided port does not match any of the // TCP ports in the app spec. @@ -262,14 +262,14 @@ message SignForAppResponse { bytes signature = 1; } -// OnNewConnectionRequest is a request for OnNewConnection. -message OnNewConnectionRequest { +// OnNewAppConnectionRequest is a request for OnNewAppConnection. +message OnNewAppConnectionRequest { // AppKey identifies the app the connection is being made for. AppKey app_key = 1; } -// OnNewConnectionRequest is a response for OnNewConnection. -message OnNewConnectionResponse {} +// OnNewAppConnectionResponse is a response for OnNewAppConnection. +message OnNewAppConnectionResponse {} // OnInvalidLocalPortRequest is a request for OnInvalidLocalPort. message OnInvalidLocalPortRequest { diff --git a/tool/tsh/common/vnet_client_application.go b/tool/tsh/common/vnet_client_application.go index cbffa21f90fd1..a4e8c9a87241f 100644 --- a/tool/tsh/common/vnet_client_application.go +++ b/tool/tsh/common/vnet_client_application.go @@ -134,9 +134,14 @@ func (p *vnetClientApplication) GetDialOptions(ctx context.Context, profileName return dialOpts, nil } -// OnNewConnection gets called before each VNet connection. It's a noop as tsh doesn't need to do +// OnNewSSHSession gets called before each VNet SSH connection. It's a noop as +// tsh doesn't need to do anything extra here. +func (p *vnetClientApplication) OnNewSSHSession(ctx context.Context, profileName, rootClusterName string) { +} + +// OnNewAppConnection gets called before each VNet app connection. It's a noop as tsh doesn't need to do // anything extra here. -func (p *vnetClientApplication) OnNewConnection(_ context.Context, _ *vnetv1.AppKey) error { +func (p *vnetClientApplication) OnNewAppConnection(_ context.Context, _ *vnetv1.AppKey) error { return nil }