-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Backport #51279 to branch/v17
- Loading branch information
Showing
5 changed files
with
542 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.