Skip to content

Commit

Permalink
[v17][vnet] remote app provider
Browse files Browse the repository at this point in the history
Backport #51279 to branch/v17
  • Loading branch information
nklaassen committed Jan 29, 2025
1 parent d3252b9 commit bfa3a98
Show file tree
Hide file tree
Showing 5 changed files with 542 additions and 3 deletions.
184 changes: 184 additions & 0 deletions lib/vnet/client_application_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// 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 <http://www.gnu.org/licenses/>.

package vnet

import (
"context"
"crypto"
"crypto/rand"
"sync"

"github.com/gravitational/trace"

"github.com/gravitational/teleport/api"
vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1"
)

// clientApplicationService wraps a local app provider to implement the gRPC
// [vnetv1.ClientApplicationServiceServer] to expose Teleport apps to a VNet
// service running in another process.
type clientApplicationService struct {
// opt-in to compilation errors if this doesn't implement
// [vnetv1.ClientApplicationServiceServer]
vnetv1.UnsafeClientApplicationServiceServer

appProvider appProvider

// mu protects appSignerCache
mu sync.Mutex
// appSignerCache caches the crypto.Signer for each certificate issued by
// ReissueAppCert so that SignForApp can later use that signer.
//
// Signers are never deleted from the map. When the cert expires, the local
// proxy in the admin process will detect the cert expiry and call
// ReissueAppCert, which will overwrite the signer for the app with a new
// one.
appSignerCache map[appKey]crypto.Signer
}

func newClientApplicationService(appProvider appProvider) *clientApplicationService {
return &clientApplicationService{
appProvider: appProvider,
appSignerCache: make(map[appKey]crypto.Signer),
}
}

// Ping implements [vnetv1.ClientApplicationServiceServer.Ping].
func (s *clientApplicationService) Ping(ctx context.Context, req *vnetv1.PingRequest) (*vnetv1.PingResponse, error) {
return &vnetv1.PingResponse{}, nil
}

// AuthenticateProcess implements [vnetv1.ClientApplicationServiceServer.AuthenticateProcess].
func (s *clientApplicationService) AuthenticateProcess(ctx context.Context, req *vnetv1.AuthenticateProcessRequest) (*vnetv1.AuthenticateProcessResponse, error) {
log.DebugContext(ctx, "Received AuthenticateProcess request from admin process")
if req.Version != api.Version {
return nil, trace.BadParameter("version mismatch, user process version is %s, admin process version is %s",
api.Version, req.Version)
}
// TODO(nklaassen): implement process authentication.
return &vnetv1.AuthenticateProcessResponse{
Version: api.Version,
}, nil
}

// ResolveAppInfo implements [vnetv1.ClientApplicationServiceServer.ResolveAppInfo].
func (s *clientApplicationService) ResolveAppInfo(ctx context.Context, req *vnetv1.ResolveAppInfoRequest) (*vnetv1.ResolveAppInfoResponse, error) {
appInfo, err := s.appProvider.ResolveAppInfo(ctx, req.GetFqdn())
if err != nil {
return nil, trace.Wrap(err, "resolving app info")
}
return &vnetv1.ResolveAppInfoResponse{
AppInfo: appInfo,
}, nil
}

// ReissueAppCert implements [vnetv1.ClientApplicationServiceServer.ReissueAppCert].
// It caches the signer issued for each app so that it can later be used to
// issue signatures in [clientApplicationService.SignForApp].
func (s *clientApplicationService) ReissueAppCert(ctx context.Context, req *vnetv1.ReissueAppCertRequest) (*vnetv1.ReissueAppCertResponse, error) {
if req.AppInfo == nil {
return nil, trace.BadParameter("missing AppInfo")
}
cert, err := s.appProvider.ReissueAppCert(ctx, req.GetAppInfo(), uint16(req.GetTargetPort()))
if err != nil {
return nil, trace.Wrap(err, "reissuing app certificate")
}
s.setSignerForApp(req.GetAppInfo().GetAppKey(), uint16(req.GetTargetPort()), cert.PrivateKey.(crypto.Signer))
return &vnetv1.ReissueAppCertResponse{
Cert: cert.Certificate[0],
}, nil
}

// SignForApp implements [vnetv1.ClientApplicationServiceServer.SignForApp].
// It uses a cached signer for the requested app, which must have previously
// been issued a certificate via [clientApplicationService.ReissueAppCert].
func (s *clientApplicationService) SignForApp(ctx context.Context, req *vnetv1.SignForAppRequest) (*vnetv1.SignForAppResponse, error) {
log.DebugContext(ctx, "Got SignForApp request",
"app", req.GetAppKey(),
"hash", req.GetHash(),
"digest_len", len(req.GetDigest()),
)
var hash crypto.Hash
switch req.GetHash() {
case vnetv1.Hash_HASH_NONE:
hash = crypto.Hash(0)
case vnetv1.Hash_HASH_SHA256:
hash = crypto.SHA256
default:
return nil, trace.BadParameter("unsupported hash %v", req.GetHash())
}
appKey := req.GetAppKey()

signer, ok := s.getSignerForApp(req.GetAppKey(), uint16(req.GetTargetPort()))
if !ok {
return nil, trace.BadParameter("no signer for app %v", appKey)
}

signature, err := signer.Sign(rand.Reader, req.GetDigest(), hash)
if err != nil {
return nil, trace.Wrap(err, "signing for app %v", appKey)
}
return &vnetv1.SignForAppResponse{
Signature: signature,
}, nil
}

func (s *clientApplicationService) setSignerForApp(appKey *vnetv1.AppKey, targetPort uint16, signer crypto.Signer) {
s.mu.Lock()
defer s.mu.Unlock()
s.appSignerCache[newAppKey(appKey, targetPort)] = signer
}

func (s *clientApplicationService) getSignerForApp(appKey *vnetv1.AppKey, targetPort uint16) (crypto.Signer, bool) {
s.mu.Lock()
defer s.mu.Unlock()
signer, ok := s.appSignerCache[newAppKey(appKey, targetPort)]
return signer, ok
}

// OnNewConnection gets called whenever a new 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.appProvider.OnNewConnection(ctx, req.GetAppKey()); err != nil {
return nil, trace.Wrap(err)
}
return &vnetv1.OnNewConnectionResponse{}, nil
}

// 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.
func (s *clientApplicationService) OnInvalidLocalPort(ctx context.Context, req *vnetv1.OnInvalidLocalPortRequest) (*vnetv1.OnInvalidLocalPortResponse, error) {
s.appProvider.OnInvalidLocalPort(ctx, req.GetAppInfo(), uint16(req.GetTargetPort()))
return &vnetv1.OnInvalidLocalPortResponse{}, nil
}

// appKey is a clone of [vnetv1.AppKey] that is not a protobuf type so it can be
// used as a key in maps.
type appKey struct {
profile, leafCluster, app string
port uint16
}

func newAppKey(protoAppKey *vnetv1.AppKey, port uint16) appKey {
return appKey{
profile: protoAppKey.GetProfile(),
leafCluster: protoAppKey.GetLeafCluster(),
app: protoAppKey.GetName(),
port: port,
}
}
138 changes: 138 additions & 0 deletions lib/vnet/client_application_service_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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 <http://www.gnu.org/licenses/>.

package vnet

import (
"context"

"github.com/gravitational/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

"github.com/gravitational/teleport/api"
"github.com/gravitational/teleport/api/utils/grpc/interceptors"
vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1"
)

// clientApplicationServiceClient is a gRPC client for the client application
// service. This client is used in the Windows admin service to make requests to
// the VNet client application.
type clientApplicationServiceClient struct {
clt vnetv1.ClientApplicationServiceClient
conn *grpc.ClientConn
}

func newClientApplicationServiceClient(ctx context.Context, addr string) (*clientApplicationServiceClient, error) {
conn, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(interceptors.GRPCClientUnaryErrorInterceptor),
grpc.WithStreamInterceptor(interceptors.GRPCClientStreamErrorInterceptor),
)
if err != nil {
return nil, trace.Wrap(err, "creating user process gRPC client")
}
return &clientApplicationServiceClient{
clt: vnetv1.NewClientApplicationServiceClient(conn),
conn: conn,
}, nil
}

func (c *clientApplicationServiceClient) close() error {
return trace.Wrap(c.conn.Close())
}

// Ping pings the client application.
func (c *clientApplicationServiceClient) Ping(ctx context.Context) error {
if _, err := c.clt.Ping(ctx, &vnetv1.PingRequest{}); err != nil {
return trace.Wrap(err, "pinging client application")
}
return nil
}

// Authenticate process authenticates the client application process.
func (c *clientApplicationServiceClient) AuthenticateProcess(ctx context.Context, pipePath string) error {
resp, err := c.clt.AuthenticateProcess(ctx, &vnetv1.AuthenticateProcessRequest{
Version: api.Version,
PipePath: pipePath,
})
if err != nil {
return trace.Wrap(err, "authenticating process")
}
if resp.Version != api.Version {
return trace.BadParameter("version mismatch, user process version is %s, admin process version is %s",
resp.Version, api.Version)
}
return nil
}

// ResolveAppInfo resolves fqdn to a [*vnetv1.AppInfo], or returns an error if
// no matching app is found.
func (c *clientApplicationServiceClient) ResolveAppInfo(ctx context.Context, fqdn string) (*vnetv1.AppInfo, error) {
resp, err := c.clt.ResolveAppInfo(ctx, &vnetv1.ResolveAppInfoRequest{
Fqdn: fqdn,
})
if err != nil {
return nil, trace.Wrap(err, "resolving app info")
}
return resp.GetAppInfo(), nil
}

// ReissueAppCert issues a new certificate for the requested app.
func (c *clientApplicationServiceClient) ReissueAppCert(ctx context.Context, appInfo *vnetv1.AppInfo, targetPort uint16) ([]byte, error) {
resp, err := c.clt.ReissueAppCert(ctx, &vnetv1.ReissueAppCertRequest{
AppInfo: appInfo,
TargetPort: uint32(targetPort),
})
if err != nil {
return nil, trace.Wrap(err, "reissuing app cert")
}
return resp.GetCert(), nil
}

// SignForApp returns a cryptographic signature with the key associated with the
// requested app. The key resides in the client application process.
func (c *clientApplicationServiceClient) SignForApp(ctx context.Context, req *vnetv1.SignForAppRequest) ([]byte, error) {
resp, err := c.clt.SignForApp(ctx, req)
if err != nil {
return nil, trace.Wrap(err, "signing for app")
}
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{
AppKey: appKey,
})
if err != nil {
return trace.Wrap(err)
}
return nil
}

// OnInvalidLocalPort reports a failed connection to an invalid local port for
// the target app.
func (c *clientApplicationServiceClient) OnInvalidLocalPort(ctx context.Context, appInfo *vnetv1.AppInfo, targetPort uint16) error {
_, err := c.clt.OnInvalidLocalPort(ctx, &vnetv1.OnInvalidLocalPortRequest{
AppInfo: appInfo,
TargetPort: uint32(targetPort),
})
if err != nil {
return trace.Wrap(err)
}
return nil
}
2 changes: 1 addition & 1 deletion lib/vnet/network_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ type tcpHandlerResolver interface {
//
// Avoid using [trace.Wrap] on errNoTCPHandler where possible, this isn't an
// unexpected error that should require the overhead of collecting a stack trace.
var errNoTCPHandler = errors.New("no handler for address")
var errNoTCPHandler = &trace.NotFoundError{Message: "no handler for address"}

// tcpHandlerSpec specifies a VNet TCP handler.
type tcpHandlerSpec struct {
Expand Down
Loading

0 comments on commit bfa3a98

Please sign in to comment.