Skip to content

Commit beb4617

Browse files
authored
[v16] Add basic VNet integration to Connect (#42225)
* Expose clusters.Cluster.ReissueAppCert It will be needed in VNet to issue app certs. * Make teleterm VNet actually start VNet * Rename Storage.ReadAll to ListRootClusters * Replace client.Store in VNet service with calls on daemon.Service * Adjust copy of search bar app results * Fix copying app name to clipboard
1 parent 628f890 commit beb4617

File tree

13 files changed

+297
-37
lines changed

13 files changed

+297
-37
lines changed

lib/teleterm/apiserver/apiserver.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030
vnetapi "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/vnet/v1"
3131
"github.com/gravitational/teleport/lib/defaults"
3232
"github.com/gravitational/teleport/lib/teleterm/apiserver/handler"
33-
vnet "github.com/gravitational/teleport/lib/teleterm/vnet"
33+
"github.com/gravitational/teleport/lib/teleterm/vnet"
3434
"github.com/gravitational/teleport/lib/utils"
3535
)
3636

@@ -63,7 +63,13 @@ func New(cfg Config) (*APIServer, error) {
6363
return nil, trace.Wrap(err)
6464
}
6565

66-
vnetService := &vnet.Service{}
66+
vnetService, err := vnet.New(vnet.Config{
67+
DaemonService: cfg.Daemon,
68+
InsecureSkipVerify: cfg.InsecureSkipVerify,
69+
})
70+
if err != nil {
71+
return nil, trace.Wrap(err)
72+
}
6773

6874
api.RegisterTerminalServiceServer(grpcServer, serviceHandler)
6975
vnetapi.RegisterVnetServiceServer(grpcServer, vnetService)
@@ -84,7 +90,9 @@ func (s *APIServer) Serve() error {
8490
// Stop stops the server and closes all listeners
8591
func (s *APIServer) Stop() {
8692
s.grpcServer.GracefulStop()
87-
s.vnetService.Close()
93+
if err := s.vnetService.Close(); err != nil {
94+
log.WithError(err).Error("Error while closing VNet service")
95+
}
8896
}
8997

9098
func newListener(hostAddr string, listeningC chan<- utils.NetAddr) (net.Listener, error) {

lib/teleterm/apiserver/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ import (
3131
// Config is the APIServer configuration
3232
type Config struct {
3333
// HostAddr is the APIServer host address
34-
HostAddr string
34+
HostAddr string
35+
InsecureSkipVerify bool
3536
// Daemon is the terminal daemon service
3637
Daemon *daemon.Service
3738
// Log is a component logger

lib/teleterm/clusters/cluster_apps.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ func (c *Cluster) getApp(ctx context.Context, authClient authclient.ClientI, app
148148
return app, trace.Wrap(err)
149149
}
150150

151-
// reissueAppCert issue new certificates for the app and saves them to disk.
152-
func (c *Cluster) reissueAppCert(ctx context.Context, clusterClient *client.ClusterClient, app types.Application) (tls.Certificate, error) {
151+
// ReissueAppCert issue new certificates for the app and saves them to disk.
152+
func (c *Cluster) ReissueAppCert(ctx context.Context, clusterClient *client.ClusterClient, app types.Application) (tls.Certificate, error) {
153153
if app.IsAWSConsole() || app.IsGCP() || app.IsAzureCloud() {
154154
return tls.Certificate{}, trace.BadParameter("cloud applications are not supported")
155155
}

lib/teleterm/clusters/cluster_gateways.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayPara
165165
var cert tls.Certificate
166166

167167
if err := AddMetadataToRetryableError(ctx, func() error {
168-
cert, err = c.reissueAppCert(ctx, params.ClusterClient, app)
168+
cert, err = c.ReissueAppCert(ctx, params.ClusterClient, app)
169169
return trace.Wrap(err)
170170
}); err != nil {
171171
return nil, trace.Wrap(err)
@@ -230,7 +230,7 @@ func (c *Cluster) ReissueGatewayCerts(ctx context.Context, clusterClient *client
230230
}
231231

232232
// The cert is returned from this function and finally set on LocalProxy by the middleware.
233-
cert, err := c.reissueAppCert(ctx, clusterClient, app)
233+
cert, err := c.ReissueAppCert(ctx, clusterClient, app)
234234
return cert, trace.Wrap(err)
235235
default:
236236
return tls.Certificate{}, trace.NotImplemented("ReissueGatewayCerts does not support this gateway kind %v", g.TargetURI().String())

lib/teleterm/clusters/storage.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,15 @@ func NewStorage(cfg Config) (*Storage, error) {
3939
return &Storage{Config: cfg}, nil
4040
}
4141

42-
// ReadAll reads clusters from profiles
43-
func (s *Storage) ReadAll() ([]*Cluster, error) {
42+
// ListProfileNames returns just the names of profiles in s.Dir.
43+
func (s *Storage) ListProfileNames() ([]string, error) {
4444
pfNames, err := profile.ListProfileNames(s.Dir)
45+
return pfNames, trace.Wrap(err)
46+
}
47+
48+
// ListRootClusters reads root clusters from profiles.
49+
func (s *Storage) ListRootClusters() ([]*Cluster, error) {
50+
pfNames, err := s.ListProfileNames()
4551
if err != nil {
4652
return nil, trace.Wrap(err)
4753
}

lib/teleterm/daemon/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ import (
3838
type Storage interface {
3939
clusters.Resolver
4040

41-
ReadAll() ([]*clusters.Cluster, error)
41+
ListProfileNames() ([]string, error)
42+
ListRootClusters() ([]*clusters.Cluster, error)
4243
Add(ctx context.Context, webProxyAddress string) (*clusters.Cluster, *client.TeleportClient, error)
4344
Remove(ctx context.Context, profileName string) error
4445
GetByResourceURI(resourceURI uri.ResourceURI) (*clusters.Cluster, *client.TeleportClient, error)

lib/teleterm/daemon/daemon.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,17 @@ func (s *Service) retryWithRelogin(ctx context.Context, reloginReq *api.ReloginR
160160
return trace.Wrap(err)
161161
}
162162

163+
// ListProfileNames lists profile names from storage. It's a lightweight alternative to
164+
// ListRootClusters, which also reads all profiles beyond their names and initializes clients.
165+
func (s *Service) ListProfileNames() ([]string, error) {
166+
pfNames, err := s.cfg.Storage.ListProfileNames()
167+
return pfNames, trace.Wrap(err)
168+
}
169+
163170
// ListRootClusters returns a list of root clusters
164171
func (s *Service) ListRootClusters(ctx context.Context) ([]*clusters.Cluster, error) {
165-
clusters, err := s.cfg.Storage.ReadAll()
166-
if err != nil {
167-
return nil, trace.Wrap(err)
168-
}
169-
170-
return clusters, nil
172+
clusters, err := s.cfg.Storage.ListRootClusters()
173+
return clusters, trace.Wrap(err)
171174
}
172175

173176
// ListLeafClusters returns a list of leaf clusters

lib/teleterm/daemon/daemon_headless.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func (s *Service) StartHeadlessWatchers() error {
7575
s.headlessWatcherClosersMu.Lock()
7676
defer s.headlessWatcherClosersMu.Unlock()
7777

78-
clusters, err := s.cfg.Storage.ReadAll()
78+
clusters, err := s.cfg.Storage.ListRootClusters()
7979
if err != nil {
8080
return trace.Wrap(err)
8181
}

lib/teleterm/teleterm.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@ func Serve(ctx context.Context, cfg Config) error {
6767
}
6868

6969
apiServer, err := apiserver.New(apiserver.Config{
70-
HostAddr: cfg.Addr,
71-
Daemon: daemonService,
72-
TshdServerCreds: grpcCredentials.tshd,
73-
ListeningC: cfg.ListeningC,
70+
HostAddr: cfg.Addr,
71+
InsecureSkipVerify: cfg.InsecureSkipVerify,
72+
Daemon: daemonService,
73+
TshdServerCreds: grpcCredentials.tshd,
74+
ListeningC: cfg.ListeningC,
7475
})
7576
if err != nil {
7677
return trace.Wrap(err)

lib/teleterm/vnet/service.go

Lines changed: 199 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,226 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
package service
17+
package vnet
1818

1919
import (
2020
"context"
21-
"math/rand"
22-
"time"
21+
"crypto/tls"
22+
"errors"
23+
"sync"
2324

25+
"github.com/gravitational/trace"
26+
27+
"github.com/gravitational/teleport"
28+
vnetproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
29+
"github.com/gravitational/teleport/api/types"
2430
api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/vnet/v1"
31+
"github.com/gravitational/teleport/lib/client"
32+
"github.com/gravitational/teleport/lib/teleterm/api/uri"
33+
"github.com/gravitational/teleport/lib/teleterm/daemon"
34+
logutils "github.com/gravitational/teleport/lib/utils/log"
35+
"github.com/gravitational/teleport/lib/vnet"
36+
)
37+
38+
var log = logutils.NewPackageLogger(teleport.ComponentKey, "term:vnet")
39+
40+
type status int
41+
42+
const (
43+
statusNotRunning status = iota
44+
statusRunning
45+
statusClosed
2546
)
2647

2748
// Service implements gRPC service for VNet.
2849
type Service struct {
2950
api.UnimplementedVnetServiceServer
51+
52+
cfg Config
53+
mu sync.Mutex
54+
status status
55+
processManager *vnet.ProcessManager
56+
}
57+
58+
// New creates an instance of Service.
59+
func New(cfg Config) (*Service, error) {
60+
if err := cfg.CheckAndSetDefaults(); err != nil {
61+
return nil, trace.Wrap(err)
62+
}
63+
64+
return &Service{
65+
cfg: cfg,
66+
}, nil
67+
}
68+
69+
type Config struct {
70+
DaemonService *daemon.Service
71+
InsecureSkipVerify bool
72+
}
73+
74+
// CheckAndSetDefaults checks and sets the defaults
75+
func (c *Config) CheckAndSetDefaults() error {
76+
if c.DaemonService == nil {
77+
return trace.BadParameter("missing DaemonService")
78+
}
79+
80+
return nil
3081
}
3182

3283
func (s *Service) Start(ctx context.Context, req *api.StartRequest) (*api.StartResponse, error) {
33-
n := rand.Intn(10)
34-
randomDelay := time.Duration(n) * 100 * time.Millisecond
35-
time.Sleep(randomDelay + 400*time.Millisecond)
84+
s.mu.Lock()
85+
defer s.mu.Unlock()
86+
87+
if s.status == statusClosed {
88+
return nil, trace.CompareFailed("VNet service has been closed")
89+
}
90+
91+
if s.status == statusRunning {
92+
return &api.StartResponse{}, nil
93+
}
94+
95+
appProvider := &appProvider{
96+
daemonService: s.cfg.DaemonService,
97+
insecureSkipVerify: s.cfg.InsecureSkipVerify,
98+
}
99+
100+
processManager, err := vnet.SetupAndRun(ctx, appProvider)
101+
if err != nil {
102+
return nil, trace.Wrap(err)
103+
}
104+
105+
go func() {
106+
err := processManager.Wait()
107+
if err != nil && !errors.Is(err, context.Canceled) {
108+
log.ErrorContext(ctx, "VNet closed with an error", "error", err)
109+
} else {
110+
log.DebugContext(ctx, "VNet closed")
111+
}
112+
113+
// TODO(ravicious): Notify the Electron app about change of VNet state, but only if it's
114+
// running. If it's not running, then the Start RPC has already failed and forwarded the error
115+
// to the user.
116+
117+
s.mu.Lock()
118+
defer s.mu.Unlock()
119+
120+
if s.status == statusRunning {
121+
s.status = statusNotRunning
122+
}
123+
}()
124+
125+
s.processManager = processManager
126+
s.status = statusRunning
36127
return &api.StartResponse{}, nil
37128
}
38129

130+
// Stop stops VNet and cleans up used resources. Blocks until VNet stops or ctx is canceled.
39131
func (s *Service) Stop(ctx context.Context, req *api.StopRequest) (*api.StopResponse, error) {
132+
s.mu.Lock()
133+
defer s.mu.Unlock()
134+
135+
err := s.stopLocked()
136+
if err != nil {
137+
return nil, trace.Wrap(err)
138+
}
139+
40140
return &api.StopResponse{}, nil
41141
}
42142

43-
// Close stops the current VNet instance and prevents new instances from being started.
44-
//
143+
func (s *Service) stopLocked() error {
144+
if s.status == statusClosed {
145+
return trace.CompareFailed("VNet service has been closed")
146+
}
147+
148+
if s.status == statusNotRunning {
149+
return nil
150+
}
151+
152+
s.processManager.Close()
153+
err := s.processManager.Wait()
154+
if err != nil && !errors.Is(err, context.Canceled) {
155+
return trace.Wrap(err)
156+
}
157+
158+
s.status = statusNotRunning
159+
return nil
160+
}
161+
162+
// Close stops VNet service and prevents it from being started again. Blocks until VNet stops.
45163
// Intended for cleanup code when tsh daemon gets terminated.
46164
func (s *Service) Close() error {
165+
s.mu.Lock()
166+
defer s.mu.Unlock()
167+
168+
err := s.stopLocked()
169+
if err != nil {
170+
return trace.Wrap(err)
171+
}
172+
173+
s.status = statusClosed
47174
return nil
48175
}
176+
177+
type appProvider struct {
178+
daemonService *daemon.Service
179+
insecureSkipVerify bool
180+
}
181+
182+
func (p *appProvider) ListProfiles() ([]string, error) {
183+
profiles, err := p.daemonService.ListProfileNames()
184+
return profiles, trace.Wrap(err)
185+
}
186+
187+
func (p *appProvider) GetCachedClient(ctx context.Context, profileName, leafClusterName string) (*client.ClusterClient, error) {
188+
uri := uri.NewClusterURI(profileName).AppendLeafCluster(leafClusterName)
189+
client, err := p.daemonService.GetCachedClient(ctx, uri)
190+
return client, trace.Wrap(err)
191+
}
192+
193+
func (p *appProvider) ReissueAppCert(ctx context.Context, profileName, leafClusterName string, app types.Application) (tls.Certificate, error) {
194+
clusterURI := uri.NewClusterURI(profileName).AppendLeafCluster(leafClusterName)
195+
cluster, _, err := p.daemonService.ResolveClusterURI(clusterURI)
196+
if err != nil {
197+
return tls.Certificate{}, trace.Wrap(err)
198+
}
199+
200+
client, err := p.daemonService.GetCachedClient(ctx, clusterURI)
201+
if err != nil {
202+
return tls.Certificate{}, trace.Wrap(err)
203+
}
204+
205+
// TODO(ravicious): Copy stuff from DaemonService.reissueGatewayCerts in order to handle expired certs.
206+
cert, err := cluster.ReissueAppCert(ctx, client, app)
207+
return cert, trace.Wrap(err)
208+
}
209+
210+
// GetDialOptions returns ALPN dial options for the profile.
211+
func (p *appProvider) GetDialOptions(ctx context.Context, profileName string) (*vnet.DialOptions, error) {
212+
cluster, tc, err := p.daemonService.ResolveClusterURI(uri.NewClusterURI(profileName))
213+
if err != nil {
214+
return nil, trace.Wrap(err, "resolving cluster by URI")
215+
}
216+
217+
dialOpts := &vnet.DialOptions{
218+
WebProxyAddr: cluster.GetProxyHost(),
219+
ALPNConnUpgradeRequired: tc.TLSRoutingConnUpgradeRequired,
220+
InsecureSkipVerify: p.insecureSkipVerify,
221+
}
222+
if dialOpts.ALPNConnUpgradeRequired {
223+
dialOpts.RootClusterCACertPool, err = tc.RootClusterCACertPool(ctx)
224+
if err != nil {
225+
return nil, trace.Wrap(err, "loading root cluster CA cert pool")
226+
}
227+
}
228+
return dialOpts, nil
229+
}
230+
231+
func (p *appProvider) GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnetproto.VnetConfig, error) {
232+
clusterClient, err := p.GetCachedClient(ctx, profileName, leafClusterName)
233+
if err != nil {
234+
return nil, trace.Wrap(err)
235+
}
236+
vnetConfigClient := clusterClient.AuthClient.VnetConfigServiceClient()
237+
vnetConfig, err := vnetConfigClient.GetVnetConfig(ctx, &vnetproto.GetVnetConfigRequest{})
238+
return vnetConfig, trace.Wrap(err)
239+
}

0 commit comments

Comments
 (0)