diff --git a/gen/proto/go/teleport/relaytunnel/v1alpha/discovery_service.pb.go b/gen/proto/go/teleport/relaytunnel/v1alpha/discovery_service.pb.go new file mode 100644 index 00000000000..d35a5a3cc12 --- /dev/null +++ b/gen/proto/go/teleport/relaytunnel/v1alpha/discovery_service.pb.go @@ -0,0 +1,192 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc (unknown) +// source: teleport/relaytunnel/v1alpha/discovery_service.proto + +package relaytunnelv1alpha + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// request message for DiscoveryService.Discover +type DiscoverRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiscoverRequest) Reset() { + *x = DiscoverRequest{} + mi := &file_teleport_relaytunnel_v1alpha_discovery_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiscoverRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiscoverRequest) ProtoMessage() {} + +func (x *DiscoverRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_relaytunnel_v1alpha_discovery_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiscoverRequest.ProtoReflect.Descriptor instead. +func (*DiscoverRequest) Descriptor() ([]byte, []int) { + return file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDescGZIP(), []int{0} +} + +// response message for DiscoveryService.Discover +type DiscoverResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + RelayGroup string `protobuf:"bytes,1,opt,name=relay_group,json=relayGroup,proto3" json:"relay_group,omitempty"` + TargetConnectionCount int32 `protobuf:"varint,2,opt,name=target_connection_count,json=targetConnectionCount,proto3" json:"target_connection_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiscoverResponse) Reset() { + *x = DiscoverResponse{} + mi := &file_teleport_relaytunnel_v1alpha_discovery_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiscoverResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiscoverResponse) ProtoMessage() {} + +func (x *DiscoverResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_relaytunnel_v1alpha_discovery_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiscoverResponse.ProtoReflect.Descriptor instead. +func (*DiscoverResponse) Descriptor() ([]byte, []int) { + return file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDescGZIP(), []int{1} +} + +func (x *DiscoverResponse) GetRelayGroup() string { + if x != nil { + return x.RelayGroup + } + return "" +} + +func (x *DiscoverResponse) GetTargetConnectionCount() int32 { + if x != nil { + return x.TargetConnectionCount + } + return 0 +} + +var File_teleport_relaytunnel_v1alpha_discovery_service_proto protoreflect.FileDescriptor + +const file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDesc = "" + + "\n" + + "4teleport/relaytunnel/v1alpha/discovery_service.proto\x12\x1cteleport.relaytunnel.v1alpha\"\x11\n" + + "\x0fDiscoverRequest\"k\n" + + "\x10DiscoverResponse\x12\x1f\n" + + "\vrelay_group\x18\x01 \x01(\tR\n" + + "relayGroup\x126\n" + + "\x17target_connection_count\x18\x02 \x01(\x05R\x15targetConnectionCount2}\n" + + "\x10DiscoveryService\x12i\n" + + "\bDiscover\x12-.teleport.relaytunnel.v1alpha.DiscoverRequest\x1a..teleport.relaytunnel.v1alpha.DiscoverResponseB`Z^github.com/gravitational/teleport/gen/proto/go/teleport/relaytunnel/v1alpha;relaytunnelv1alphab\x06proto3" + +var ( + file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDescOnce sync.Once + file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDescData []byte +) + +func file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDescGZIP() []byte { + file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDescOnce.Do(func() { + file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDesc), len(file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDesc))) + }) + return file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDescData +} + +var file_teleport_relaytunnel_v1alpha_discovery_service_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_teleport_relaytunnel_v1alpha_discovery_service_proto_goTypes = []any{ + (*DiscoverRequest)(nil), // 0: teleport.relaytunnel.v1alpha.DiscoverRequest + (*DiscoverResponse)(nil), // 1: teleport.relaytunnel.v1alpha.DiscoverResponse +} +var file_teleport_relaytunnel_v1alpha_discovery_service_proto_depIdxs = []int32{ + 0, // 0: teleport.relaytunnel.v1alpha.DiscoveryService.Discover:input_type -> teleport.relaytunnel.v1alpha.DiscoverRequest + 1, // 1: teleport.relaytunnel.v1alpha.DiscoveryService.Discover:output_type -> teleport.relaytunnel.v1alpha.DiscoverResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_teleport_relaytunnel_v1alpha_discovery_service_proto_init() } +func file_teleport_relaytunnel_v1alpha_discovery_service_proto_init() { + if File_teleport_relaytunnel_v1alpha_discovery_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDesc), len(file_teleport_relaytunnel_v1alpha_discovery_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_teleport_relaytunnel_v1alpha_discovery_service_proto_goTypes, + DependencyIndexes: file_teleport_relaytunnel_v1alpha_discovery_service_proto_depIdxs, + MessageInfos: file_teleport_relaytunnel_v1alpha_discovery_service_proto_msgTypes, + }.Build() + File_teleport_relaytunnel_v1alpha_discovery_service_proto = out.File + file_teleport_relaytunnel_v1alpha_discovery_service_proto_goTypes = nil + file_teleport_relaytunnel_v1alpha_discovery_service_proto_depIdxs = nil +} diff --git a/gen/proto/go/teleport/relaytunnel/v1alpha/discovery_service_grpc.pb.go b/gen/proto/go/teleport/relaytunnel/v1alpha/discovery_service_grpc.pb.go new file mode 100644 index 00000000000..4cea59d3b45 --- /dev/null +++ b/gen/proto/go/teleport/relaytunnel/v1alpha/discovery_service_grpc.pb.go @@ -0,0 +1,145 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: teleport/relaytunnel/v1alpha/discovery_service.proto + +package relaytunnelv1alpha + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DiscoveryService_Discover_FullMethodName = "/teleport.relaytunnel.v1alpha.DiscoveryService/Discover" +) + +// DiscoveryServiceClient is the client API for DiscoveryService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// rpcs related to the distribution of information about the relay group to +// agents +type DiscoveryServiceClient interface { + // returns info about the relay group and how agents should behave + Discover(ctx context.Context, in *DiscoverRequest, opts ...grpc.CallOption) (*DiscoverResponse, error) +} + +type discoveryServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewDiscoveryServiceClient(cc grpc.ClientConnInterface) DiscoveryServiceClient { + return &discoveryServiceClient{cc} +} + +func (c *discoveryServiceClient) Discover(ctx context.Context, in *DiscoverRequest, opts ...grpc.CallOption) (*DiscoverResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DiscoverResponse) + err := c.cc.Invoke(ctx, DiscoveryService_Discover_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DiscoveryServiceServer is the server API for DiscoveryService service. +// All implementations must embed UnimplementedDiscoveryServiceServer +// for forward compatibility. +// +// rpcs related to the distribution of information about the relay group to +// agents +type DiscoveryServiceServer interface { + // returns info about the relay group and how agents should behave + Discover(context.Context, *DiscoverRequest) (*DiscoverResponse, error) + mustEmbedUnimplementedDiscoveryServiceServer() +} + +// UnimplementedDiscoveryServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDiscoveryServiceServer struct{} + +func (UnimplementedDiscoveryServiceServer) Discover(context.Context, *DiscoverRequest) (*DiscoverResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Discover not implemented") +} +func (UnimplementedDiscoveryServiceServer) mustEmbedUnimplementedDiscoveryServiceServer() {} +func (UnimplementedDiscoveryServiceServer) testEmbeddedByValue() {} + +// UnsafeDiscoveryServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DiscoveryServiceServer will +// result in compilation errors. +type UnsafeDiscoveryServiceServer interface { + mustEmbedUnimplementedDiscoveryServiceServer() +} + +func RegisterDiscoveryServiceServer(s grpc.ServiceRegistrar, srv DiscoveryServiceServer) { + // If the following call pancis, it indicates UnimplementedDiscoveryServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DiscoveryService_ServiceDesc, srv) +} + +func _DiscoveryService_Discover_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DiscoverRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DiscoveryServiceServer).Discover(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DiscoveryService_Discover_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DiscoveryServiceServer).Discover(ctx, req.(*DiscoverRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// DiscoveryService_ServiceDesc is the grpc.ServiceDesc for DiscoveryService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DiscoveryService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "teleport.relaytunnel.v1alpha.DiscoveryService", + HandlerType: (*DiscoveryServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Discover", + Handler: _DiscoveryService_Discover_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "teleport/relaytunnel/v1alpha/discovery_service.proto", +} diff --git a/lib/multiplexer/grpc/proxyline.go b/lib/multiplexer/grpc/proxyline.go new file mode 100644 index 00000000000..5c87f8b6f4b --- /dev/null +++ b/lib/multiplexer/grpc/proxyline.go @@ -0,0 +1,59 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package grpc + +import ( + "net" + + "github.com/gravitational/trace" + "google.golang.org/grpc/credentials" + + "github.com/gravitational/teleport/lib/multiplexer" + "github.com/gravitational/teleport/lib/utils" +) + +// PPV2ServerCredentials is a wrapper around a gRPC +// [credentials.TransportCredentials] that reads a PROXY protocol v2 header +// before passing the incoming connection to the wrapped credentials. When used +// as client credentials, the wrapped credentials are used as-is. +type PPV2ServerCredentials struct { + _ struct{} + + credentials.TransportCredentials +} + +var _ credentials.TransportCredentials = PPV2ServerCredentials{} + +// Clone implements [credentials.TransportCredentials]. +func (p PPV2ServerCredentials) Clone() credentials.TransportCredentials { + return PPV2ServerCredentials{ + TransportCredentials: p.TransportCredentials.Clone(), + } +} + +// ServerHandshake implements [credentials.TransportCredentials]. +func (p PPV2ServerCredentials) ServerHandshake(nc net.Conn) (net.Conn, credentials.AuthInfo, error) { + proxyLine, err := multiplexer.ReadProxyLineV2(nc) + if err != nil { + _ = nc.Close() + return nil, nil, trace.Wrap(err) + } + if proxyLine != nil { + nc = utils.NewConnWithAddr(nc, &proxyLine.Destination, &proxyLine.Source) + } + return p.TransportCredentials.ServerHandshake(nc) +} diff --git a/lib/multiplexer/grpc/proxyline_test.go b/lib/multiplexer/grpc/proxyline_test.go new file mode 100644 index 00000000000..1bcb996dfa3 --- /dev/null +++ b/lib/multiplexer/grpc/proxyline_test.go @@ -0,0 +1,74 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package grpc + +import ( + "bytes" + "net" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + "github.com/gravitational/teleport/lib/multiplexer" +) + +func TestPPV2ServerCredentials(t *testing.T) { + dispatchC := make(chan net.Conn, 1) + + creds := PPV2ServerCredentials{TransportCredentials: dispatchCredentials{dispatchC: dispatchC}} + + srv := grpc.NewServer(grpc.Creds(creds)) + defer srv.Stop() + + lis, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer lis.Close() + go srv.Serve(lis) + + rawConn, err := net.Dial(lis.Addr().Network(), lis.Addr().String()) + require.NoError(t, err) + defer rawConn.Close() + + // source=127.0.0.3:12345 destination=127.0.0.2:42 + sampleIPv4Addresses := []byte{0x7F, 0x00, 0x00, 0x03, 0x7F, 0x00, 0x00, 0x02, 0x30, 0x39, 0x00, 0x2A} + // {0x21, 0x11, 0x00, 0x0C} - 4 bits version, 4 bits command, 4 bits address family, 4 bits protocol, 16 bits length + sampleProxyV2Line := bytes.Join([][]byte{multiplexer.ProxyV2Prefix, {0x21, 0x11, 0x00, 0x0C}, sampleIPv4Addresses}, nil) + + require.NotEqual(t, "127.0.0.3:12345", rawConn.LocalAddr().String()) + require.NotEqual(t, "127.0.0.2:42", rawConn.RemoteAddr().String()) + + _, err = rawConn.Write(sampleProxyV2Line) + require.NoError(t, err) + + wrappedConn := <-dispatchC + defer wrappedConn.Close() + require.Equal(t, "127.0.0.2:42", wrappedConn.LocalAddr().String()) + require.Equal(t, "127.0.0.3:12345", wrappedConn.RemoteAddr().String()) +} + +type dispatchCredentials struct { + credentials.TransportCredentials + dispatchC chan net.Conn +} + +// ServerHandshake implements [credentials.TransportCredentials]. +func (d dispatchCredentials) ServerHandshake(c net.Conn) (net.Conn, credentials.AuthInfo, error) { + d.dispatchC <- c + return nil, nil, credentials.ErrConnDispatched +} diff --git a/lib/relaytunnel/discover.go b/lib/relaytunnel/discover.go new file mode 100644 index 00000000000..72f1a003ee3 --- /dev/null +++ b/lib/relaytunnel/discover.go @@ -0,0 +1,131 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package relaytunnel + +import ( + "context" + "crypto/tls" + "crypto/x509" + + "github.com/gravitational/trace" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + "github.com/gravitational/teleport/api/metadata" + "github.com/gravitational/teleport/api/utils/grpc/interceptors" + relaytunnelv1alpha "github.com/gravitational/teleport/gen/proto/go/teleport/relaytunnel/v1alpha" +) + +//nolint:unused // used by StaticDiscoverServiceServer +type unimplementedDiscoveryServiceServer = relaytunnelv1alpha.UnimplementedDiscoveryServiceServer + +// StaticDiscoverServiceServer is a [relaytunnelv1alpha.DiscoveryServiceServer] +// implementation that responds with fixed data to the Discover rpc. +type StaticDiscoverServiceServer struct { + _ struct{} // prevent unkeyed literals + + unimplementedDiscoveryServiceServer //nolint:unused // required and used by grpc-go + + RelayGroup string + TargetConnectionCount int32 +} + +var _ relaytunnelv1alpha.DiscoveryServiceServer = (*StaticDiscoverServiceServer)(nil) + +// Discover implements [relaytunnelv1alpha.DiscoveryServiceServer]. +func (d *StaticDiscoverServiceServer) Discover(ctx context.Context, req *relaytunnelv1alpha.DiscoverRequest) (*relaytunnelv1alpha.DiscoverResponse, error) { + return &relaytunnelv1alpha.DiscoverResponse{ + RelayGroup: d.RelayGroup, + TargetConnectionCount: d.TargetConnectionCount, + }, nil +} + +type DiscoverParams struct { + GetCertificate func() (*tls.Certificate, error) + GetPool func() (*x509.CertPool, error) + Ciphersuites []uint16 + + Target string +} + +// discover returns configuration and connectivity information from a Relay and +// its group, given an API endpoint and some cluster authentication data. As the +// discover API is intended to only be used sporadically and with what should be +// a fresh server behind a load balancer every time, this function establishes a +// brand new connection and disposes of it before returning. +func discover(ctx context.Context, params DiscoverParams) (*relaytunnelv1alpha.DiscoverResponse, error) { + if params.GetCertificate == nil { + return nil, trace.BadParameter("missing GetCertificate") + } + if params.GetPool == nil { + return nil, trace.BadParameter("missing GetPool") + } + + cert, err := params.GetCertificate() + if err != nil { + return nil, trace.Wrap(err) + } + pool, err := params.GetPool() + if err != nil { + return nil, trace.Wrap(err) + } + + tlsConfig := &tls.Config{ + GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return cert, nil + }, + RootCAs: pool, + + // the [credentials.NewTLS] transport credentials will take care of SNI + // and ALPN + NextProtos: nil, + ServerName: "", + + CipherSuites: params.Ciphersuites, + MinVersion: tls.VersionTLS12, + } + + cc, err := grpc.NewClient(params.Target, + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + grpc.WithChainUnaryInterceptor( + metadata.UnaryClientInterceptor, + interceptors.GRPCClientUnaryErrorInterceptor, + ), + grpc.WithChainStreamInterceptor( + metadata.StreamClientInterceptor, + interceptors.GRPCClientStreamErrorInterceptor, + ), + ) + if err != nil { + return nil, trace.Wrap(err) + } + defer cc.Close() + + clt := relaytunnelv1alpha.NewDiscoveryServiceClient(cc) + + resp, err := clt.Discover(ctx, &relaytunnelv1alpha.DiscoverRequest{}) + if err != nil { + return nil, trace.Wrap(err) + } + + return resp, nil +} + +// TODO(espadolini): remove once the function is actually used +var _ = discover diff --git a/lib/relaytunnel/tunnel_common.go b/lib/relaytunnel/tunnel_common.go new file mode 100644 index 00000000000..67cd9f9e8ef --- /dev/null +++ b/lib/relaytunnel/tunnel_common.go @@ -0,0 +1,22 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package relaytunnel + +// yamuxTunnelALPN is the ALPN protocol name for the bespoke protocol used by +// tunnel connections. Future variants of the protocol should use a different +// ALPN protocol name. +const yamuxTunnelALPN = "teleport-relaytunnel" diff --git a/lib/relaytunnel/tunnel_server.go b/lib/relaytunnel/tunnel_server.go new file mode 100644 index 00000000000..5fc312e24ef --- /dev/null +++ b/lib/relaytunnel/tunnel_server.go @@ -0,0 +1,179 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package relaytunnel + +import ( + "context" + "crypto/tls" + "crypto/x509" + "net" + "time" + + "github.com/gravitational/trace" + "google.golang.org/grpc/credentials" + + "github.com/gravitational/teleport/lib/tlsca" +) + +type ServerConfig struct { + GetCertificate func(ctx context.Context) (*tls.Certificate, error) + GetPool func(ctx context.Context) (*x509.CertPool, error) + Ciphersuites []uint16 +} + +func NewServer(cfg ServerConfig) (*Server, error) { + if cfg.GetCertificate == nil { + return nil, trace.BadParameter("missing GetCertificate") + } + if cfg.GetPool == nil { + return nil, trace.BadParameter("missing GetPool") + } + return &Server{ + getCertificate: cfg.GetCertificate, + getPool: cfg.GetPool, + ciphersuites: cfg.Ciphersuites, + }, nil +} + +type Server struct { + getCertificate func(ctx context.Context) (*tls.Certificate, error) + getPool func(ctx context.Context) (*x509.CertPool, error) + ciphersuites []uint16 +} + +func (s *Server) GRPCServerCredentials() credentials.TransportCredentials { + return &grpcServerCredentials{ + tunnelServer: s, + + getCertificate: s.getCertificate, + getPool: s.getPool, + ciphersuites: s.ciphersuites, + } +} + +type grpcServerCredentials struct { + tunnelServer *Server + + getCertificate func(ctx context.Context) (*tls.Certificate, error) + getPool func(ctx context.Context) (*x509.CertPool, error) + ciphersuites []uint16 +} + +// ClientHandshake implements [credentials.TransportCredentials]. +func (*grpcServerCredentials) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { + _ = rawConn.Close() + return nil, nil, trace.NotImplemented("these transport credentials can only be used as a server") +} + +// OverrideServerName implements implements [credentials.TransportCredentials]. +func (*grpcServerCredentials) OverrideServerName(string) error { + return nil +} + +// Clone implements implements [credentials.TransportCredentials]. +func (s *grpcServerCredentials) Clone() credentials.TransportCredentials { + // s is immutable so there's no need to copy anything + return s +} + +// Info implements implements [credentials.TransportCredentials]. +func (s *grpcServerCredentials) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{ + SecurityProtocol: "tls", + SecurityVersion: "1.2", + } +} + +// ServerHandshake implements implements [credentials.TransportCredentials]. +func (s *grpcServerCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + cert, err := s.getCertificate(ctx) + if err != nil { + _ = rawConn.Close() + return nil, nil, trace.Wrap(err) + } + pool, err := s.getPool(ctx) + if err != nil { + _ = rawConn.Close() + return nil, nil, trace.Wrap(err) + } + + var clientID *tlsca.Identity + tlsConfig := &tls.Config{ + GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return cert, nil + }, + VerifyConnection: func(cs tls.ConnectionState) error { + if cs.NegotiatedProtocol == "" { + // client tried to connect with no ALPN (or with http/1.1 in its + // protocol list because of an undocumented behavior of the + // crypto/tls server handshake) + return trace.NotImplemented("missing ALPN in TLS ClientHello") + } + if len(cs.VerifiedChains) < 1 { + return trace.AccessDenied("missing or invalid client certificate") + } + + if cs.NegotiatedProtocol == "h2" { + return nil + } + + id, err := tlsca.FromSubject(cs.VerifiedChains[0][0].Subject, cs.VerifiedChains[0][0].NotAfter) + if err != nil { + return trace.Wrap(err) + } + clientID = id + + return nil + }, + NextProtos: []string{yamuxTunnelALPN, "h2"}, + + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: pool, + + InsecureSkipVerify: false, + + MinVersion: tls.VersionTLS12, + CipherSuites: s.ciphersuites, + SessionTicketsDisabled: true, + } + + tlsConn := tls.Server(rawConn, tlsConfig) + if err := tlsConn.Handshake(); err != nil { + _ = tlsConn.Close() + return nil, nil, trace.Wrap(err) + } + + cs := tlsConn.ConnectionState() + if cs.NegotiatedProtocol == yamuxTunnelALPN { + // TODO(espadolini): handle the actual tunnel connection, using clientID + // for authz + _ = tlsConn.Close() + _ = clientID + return nil, nil, credentials.ErrConnDispatched + } + tlsInfo := credentials.TLSInfo{ + State: cs, + CommonAuthInfo: credentials.CommonAuthInfo{ + SecurityLevel: credentials.PrivacyAndIntegrity, + }, + } + + return tlsConn, tlsInfo, nil +} diff --git a/lib/service/listeners.go b/lib/service/listeners.go index 1a2e7094654..24b0a677819 100644 --- a/lib/service/listeners.go +++ b/lib/service/listeners.go @@ -48,6 +48,7 @@ var ( ListenerProxyPeer = ListenerType(teleport.Component(teleport.ComponentProxy, "peer")) ListenerMetrics = ListenerType(teleport.ComponentMetrics) ListenerWindowsDesktop = ListenerType(teleport.ComponentWindowsDesktop) + ListenerRelayTunnel = ListenerType(teleport.Component(teleport.ComponentRelay, "tunnel")) ) // Network returns the network used by the listener. diff --git a/lib/service/relay.go b/lib/service/relay.go index b4dc61adfbb..56579079037 100644 --- a/lib/service/relay.go +++ b/lib/service/relay.go @@ -18,17 +18,27 @@ package service import ( "context" + "crypto/tls" + "crypto/x509" "log/slog" "sync/atomic" "github.com/google/uuid" "github.com/gravitational/trace" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" "google.golang.org/protobuf/proto" "github.com/gravitational/teleport" headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" presencev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/presence/v1" apitypes "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils/grpc/interceptors" + relaytunnelv1alpha "github.com/gravitational/teleport/gen/proto/go/teleport/relaytunnel/v1alpha" + "github.com/gravitational/teleport/lib/auth/authclient" + multiplexergrpc "github.com/gravitational/teleport/lib/multiplexer/grpc" + "github.com/gravitational/teleport/lib/relaytunnel" "github.com/gravitational/teleport/lib/srv" ) @@ -60,8 +70,50 @@ func (process *TeleportProcess) runRelayService() error { return err } - // TODO(espadolini): use the access point - _ = accessPoint + tunnelServer, err := relaytunnel.NewServer(relaytunnel.ServerConfig{ + GetCertificate: func(ctx context.Context) (*tls.Certificate, error) { + return conn.serverGetCertificate() + }, + GetPool: func(ctx context.Context) (*x509.CertPool, error) { + pool, _, err := authclient.ClientCertPool(ctx, accessPoint, conn.clusterName, apitypes.HostCA) + if err != nil { + return nil, trace.Wrap(err) + } + return pool, nil + }, + Ciphersuites: process.Config.CipherSuites, + }) + if err != nil { + return trace.Wrap(err) + } + + tunnelCreds := tunnelServer.GRPCServerCredentials() + if process.Config.Relay.TunnelPROXYProtocol { + tunnelCreds = multiplexergrpc.PPV2ServerCredentials{TransportCredentials: tunnelCreds} + } + tunnelGRPCServer := grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler()), + grpc.ChainUnaryInterceptor( + interceptors.GRPCServerUnaryErrorInterceptor, + ), + grpc.ChainStreamInterceptor( + interceptors.GRPCServerStreamErrorInterceptor, + ), + grpc.Creds(tunnelCreds), + ) + defer tunnelGRPCServer.Stop() + + relaytunnelv1alpha.RegisterDiscoveryServiceServer(tunnelGRPCServer, &relaytunnel.StaticDiscoverServiceServer{ + RelayGroup: process.Config.Relay.RelayGroup, + TargetConnectionCount: process.Config.Relay.TargetConnectionCount, + }) + + tunnelListener, err := process.importOrCreateListener(ListenerRelayTunnel, process.Config.Relay.TunnelListenAddr.String()) + if err != nil { + return trace.Wrap(err) + } + defer tunnelListener.Close() + go tunnelGRPCServer.Serve(tunnelListener) nonce := uuid.NewString() var relayServer atomic.Pointer[presencev1.RelayServer] @@ -127,6 +179,15 @@ func (process *TeleportProcess) runRelayService() error { log.InfoContext(ctx, "Stopping the relay service") } + log.DebugContext(ctx, "Stopping servers") + eg, egCtx := errgroup.WithContext(ctx) + eg.Go(func() error { + defer context.AfterFunc(egCtx, tunnelGRPCServer.Stop)() + tunnelGRPCServer.GracefulStop() + return nil + }) + warnOnErr(egCtx, eg.Wait(), log) + warnOnErr(ctx, hb.Close(), log) warnOnErr(ctx, conn.Close(), log) diff --git a/proto/teleport/relaytunnel/v1alpha/discovery_service.proto b/proto/teleport/relaytunnel/v1alpha/discovery_service.proto new file mode 100644 index 00000000000..a16fcd67787 --- /dev/null +++ b/proto/teleport/relaytunnel/v1alpha/discovery_service.proto @@ -0,0 +1,38 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +syntax = "proto3"; + +package teleport.relaytunnel.v1alpha; + +option go_package = "github.com/gravitational/teleport/gen/proto/go/teleport/relaytunnel/v1alpha;relaytunnelv1alpha"; + +// rpcs related to the distribution of information about the relay group to +// agents +service DiscoveryService { + // returns info about the relay group and how agents should behave + rpc Discover(DiscoverRequest) returns (DiscoverResponse); +} + +// request message for DiscoveryService.Discover +message DiscoverRequest {} + +// response message for DiscoveryService.Discover +message DiscoverResponse { + string relay_group = 1; + + int32 target_connection_count = 2; +}