-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
router.go
513 lines (441 loc) · 15.6 KB
/
router.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
// Copyright 2022 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proxy
import (
"context"
"errors"
"fmt"
"net"
"os"
"strconv"
"sync"
"github.com/gravitational/trace"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/attribute"
oteltrace "go.opentelemetry.io/otel/trace"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/observability/tracing"
"github.com/gravitational/teleport/api/types"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/aws"
"github.com/gravitational/teleport/lib/agentless"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/observability/metrics"
"github.com/gravitational/teleport/lib/reversetunnelclient"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/teleagent"
"github.com/gravitational/teleport/lib/utils"
)
const errDirectDialing = `Direct dialing to nodes not found in the inventory is not supported.
If you want to connect to a node without installing Teleport on it, consider registering it with
your cluster with 'teleport join openssh'.
See https://goteleport.com/docs/ver/14.x/server-access/guides/openssh/ for more details.`
var (
// proxiedSessions counts successful connections to nodes
proxiedSessions = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: teleport.MetricProxySSHSessions,
Help: "Number of active sessions through this proxy",
},
)
// failedConnectingToNode counts failed attempts to connect to nodes
failedConnectingToNode = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricFailedConnectToNodeAttempts,
Help: "Number of failed SSH connection attempts to a node. Use with `teleport_connect_to_node_attempts_total` to get the failure rate.",
},
)
// connectingToNode counts connection attempts to nodes
connectingToNode = prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: teleport.MetricNamespace,
Name: teleport.MetricConnectToNodeAttempts,
Help: "Number of SSH connection attempts to a node. Use with `failed_connect_to_node_attempts_total` to get the failure rate.",
},
)
)
func init() {
metrics.RegisterPrometheusCollectors(proxiedSessions, failedConnectingToNode, connectingToNode)
}
// ProxiedMetricConn wraps [net.Conn] opened by
// the [Router] so that the proxiedSessions counter
// can be decremented when it is closed.
type ProxiedMetricConn struct {
// once ensures that proxiedSessions is only decremented
// a single time per [net.Conn]
once sync.Once
net.Conn
}
// NewProxiedMetricConn increments proxiedSessions and creates
// a ProxiedMetricConn that defers to the provided [net.Conn].
func NewProxiedMetricConn(conn net.Conn) *ProxiedMetricConn {
proxiedSessions.Inc()
return &ProxiedMetricConn{Conn: conn}
}
func (c *ProxiedMetricConn) Close() error {
c.once.Do(proxiedSessions.Dec)
return trace.Wrap(c.Conn.Close())
}
type serverResolverFn = func(ctx context.Context, host, port string, site site) (types.Server, error)
// SiteGetter provides access to connected local or remote sites
type SiteGetter interface {
// GetSite returns the site matching the provided clusterName
GetSite(clusterName string) (reversetunnelclient.RemoteSite, error)
}
// RemoteClusterGetter provides access to remote cluster resources
type RemoteClusterGetter interface {
// GetRemoteCluster returns a remote cluster by name
GetRemoteCluster(clusterName string) (types.RemoteCluster, error)
}
// RouterConfig contains all the dependencies required
// by the Router
type RouterConfig struct {
// ClusterName indicates which cluster the router is for
ClusterName string
// Log is the logger to use
Log *logrus.Entry
// AccessPoint is the proxy cache
RemoteClusterGetter RemoteClusterGetter
// SiteGetter allows looking up sites
SiteGetter SiteGetter
// TracerProvider allows tracers to be created
TracerProvider oteltrace.TracerProvider
// serverResolver is used to resolve hosts, used by tests
serverResolver serverResolverFn
}
// CheckAndSetDefaults ensures the required items were populated
func (c *RouterConfig) CheckAndSetDefaults() error {
if c.Log == nil {
c.Log = logrus.WithField(trace.Component, "Router")
}
if c.ClusterName == "" {
return trace.BadParameter("ClusterName must be provided")
}
if c.RemoteClusterGetter == nil {
return trace.BadParameter("RemoteClusterGetter must be provided")
}
if c.SiteGetter == nil {
return trace.BadParameter("SiteGetter must be provided")
}
if c.TracerProvider == nil {
c.TracerProvider = tracing.DefaultProvider()
}
if c.serverResolver == nil {
c.serverResolver = getServer
}
return nil
}
// Router is used by the proxy to establish connections to both
// nodes and other clusters.
type Router struct {
clusterName string
log *logrus.Entry
clusterGetter RemoteClusterGetter
localSite reversetunnelclient.RemoteSite
siteGetter SiteGetter
tracer oteltrace.Tracer
serverResolver serverResolverFn
// DELETE IN 15.0.0: necessary for smoothing over v13 to v14 transition only.
permitUnlistedDialing bool
}
// NewRouter creates and returns a Router that is populated
// from the provided RouterConfig.
func NewRouter(cfg RouterConfig) (*Router, error) {
if err := cfg.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
localSite, err := cfg.SiteGetter.GetSite(cfg.ClusterName)
if err != nil {
return nil, trace.Wrap(err)
}
return &Router{
clusterName: cfg.ClusterName,
log: cfg.Log,
clusterGetter: cfg.RemoteClusterGetter,
localSite: localSite,
siteGetter: cfg.SiteGetter,
tracer: cfg.TracerProvider.Tracer("Router"),
serverResolver: cfg.serverResolver,
permitUnlistedDialing: os.Getenv("TELEPORT_UNSTABLE_UNLISTED_AGENT_DIALING") == "yes",
}, nil
}
// DialHost dials the node that matches the provided host, port and cluster. If no matching node
// is found an error is returned. If more than one matching node is found and the cluster networking
// configuration is not set to route to the most recent an error is returned.
func (r *Router) DialHost(ctx context.Context, clientSrcAddr, clientDstAddr net.Addr, host, port, clusterName string, accessChecker services.AccessChecker, agentGetter teleagent.Getter, signer agentless.SignerCreator) (_ net.Conn, err error) {
ctx, span := r.tracer.Start(
ctx,
"router/DialHost",
oteltrace.WithAttributes(
attribute.String("host", host),
attribute.String("port", port),
attribute.String("cluster", clusterName),
),
)
defer func() {
if err != nil {
failedConnectingToNode.Inc()
}
span.End()
}()
site := r.localSite
if clusterName != r.clusterName {
remoteSite, err := r.getRemoteCluster(ctx, clusterName, accessChecker)
if err != nil {
return nil, trace.Wrap(err)
}
site = remoteSite
}
span.AddEvent("looking up server")
target, err := r.serverResolver(ctx, host, port, remoteSite{site})
if err != nil {
return nil, trace.Wrap(err)
}
span.AddEvent("retrieved target server")
principals := []string{host}
var (
isAgentlessNode bool
isNotInventoryNode bool
serverID string
serverAddr string
proxyIDs []string
sshSigner ssh.Signer
)
if target != nil {
proxyIDs = target.GetProxyIDs()
serverID = fmt.Sprintf("%v.%v", target.GetName(), clusterName)
// add hostUUID.cluster to the principals
principals = append(principals, serverID)
// add ip if it exists to the principals
serverAddr = target.GetAddr()
switch {
case serverAddr != "":
h, _, err := net.SplitHostPort(serverAddr)
if err != nil {
return nil, trace.Wrap(err)
}
principals = append(principals, h)
case serverAddr == "" && target.GetUseTunnel():
serverAddr = reversetunnelclient.LocalNode
}
// If the node is a registered openssh node don't set agentGetter
// so a SSH user agent will not be created when connecting to the remote node.
if target.IsOpenSSHNode() {
agentGetter = nil
isAgentlessNode = true
if target.GetSubKind() == types.SubKindOpenSSHNode {
// If the node is of SubKindOpenSSHNode, create the signer.
client, err := r.GetSiteClient(ctx, clusterName)
if err != nil {
return nil, trace.Wrap(err)
}
sshSigner, err = signer(ctx, client)
if err != nil {
return nil, trace.Wrap(err)
}
}
}
} else {
if !r.permitUnlistedDialing {
return nil, trace.ConnectionProblem(errors.New("connection problem"), errDirectDialing)
}
// Prepare a dummy server resource so this connection will not be
// treated like a connection to a Teleport node
isNotInventoryNode = true
isAgentlessNode = true
if port == "" || port == "0" {
port = strconv.Itoa(defaults.SSHServerListenPort)
}
serverAddr = net.JoinHostPort(host, port)
name := "unknown server " + serverAddr
target, err = types.NewServer(name, types.KindNode, types.ServerSpecV2{
Addr: serverAddr,
Hostname: host,
})
if err != nil {
return nil, trace.Wrap(err)
}
target.SetSubKind(types.SubKindOpenSSHNode)
r.log.Warnf("server lookup failed: using default=%v", serverAddr)
}
conn, err := site.Dial(reversetunnelclient.DialParams{
From: clientSrcAddr,
To: &utils.NetAddr{AddrNetwork: "tcp", Addr: serverAddr},
OriginalClientDstAddr: clientDstAddr,
GetUserAgent: agentGetter,
IsNotInventoryNode: isNotInventoryNode,
IsAgentlessNode: isAgentlessNode,
AgentlessSigner: sshSigner,
Address: host,
Principals: principals,
ServerID: serverID,
ProxyIDs: proxyIDs,
ConnType: types.NodeTunnel,
TargetServer: target,
})
if err != nil {
return nil, trace.Wrap(err)
}
return NewProxiedMetricConn(conn), trace.Wrap(err)
}
// getRemoteCluster looks up the provided clusterName to determine if a remote site exists with
// that name and determines if the user has access to it.
func (r *Router) getRemoteCluster(ctx context.Context, clusterName string, checker services.AccessChecker) (reversetunnelclient.RemoteSite, error) {
_, span := r.tracer.Start(
ctx,
"router/getRemoteCluster",
oteltrace.WithAttributes(
attribute.String("cluster", clusterName),
),
)
defer span.End()
site, err := r.siteGetter.GetSite(clusterName)
if err != nil {
return nil, trace.Wrap(err)
}
rc, err := r.clusterGetter.GetRemoteCluster(clusterName)
if err != nil {
return nil, trace.Wrap(err)
}
if err := checker.CheckAccessToRemoteCluster(rc); err != nil {
return nil, utils.OpaqueAccessDenied(err)
}
return site, nil
}
// site is the minimum interface needed to match servers
// for a reversetunnelclient.RemoteSite. It makes testing easier.
type site interface {
GetNodes(ctx context.Context, fn func(n services.Node) bool) ([]types.Server, error)
GetClusterNetworkingConfig(ctx context.Context, opts ...services.MarshalOption) (types.ClusterNetworkingConfig, error)
}
// remoteSite is a site implementation that wraps
// a reversetunnelclient.RemoteSite
type remoteSite struct {
site reversetunnelclient.RemoteSite
}
// GetNodes uses the wrapped sites NodeWatcher to filter nodes
func (r remoteSite) GetNodes(ctx context.Context, fn func(n services.Node) bool) ([]types.Server, error) {
watcher, err := r.site.NodeWatcher()
if err != nil {
return nil, trace.Wrap(err)
}
return watcher.GetNodes(ctx, fn), nil
}
// GetClusterNetworkingConfig uses the wrapped sites cache to retrieve the ClusterNetworkingConfig
func (r remoteSite) GetClusterNetworkingConfig(ctx context.Context, opts ...services.MarshalOption) (types.ClusterNetworkingConfig, error) {
ap, err := r.site.CachingAccessPoint()
if err != nil {
return nil, trace.Wrap(err)
}
cfg, err := ap.GetClusterNetworkingConfig(ctx, opts...)
return cfg, trace.Wrap(err)
}
// getServer attempts to locate a node matching the provided host and port in
// the provided site.
func getServer(ctx context.Context, host, port string, site site) (types.Server, error) {
if site == nil {
return nil, trace.BadParameter("invalid remote site provided")
}
strategy := types.RoutingStrategy_UNAMBIGUOUS_MATCH
var caseInsensitiveRouting bool
if cfg, err := site.GetClusterNetworkingConfig(ctx); err == nil {
strategy = cfg.GetRoutingStrategy()
caseInsensitiveRouting = cfg.GetCaseInsensitiveRouting()
}
routeMatcher := apiutils.NewSSHRouteMatcher(host, port, caseInsensitiveRouting)
matches, err := site.GetNodes(ctx, func(server services.Node) bool {
return routeMatcher.RouteToServer(server)
})
if err != nil {
return nil, trace.Wrap(err)
}
if routeMatcher.MatchesServerIDs() && len(matches) > 1 {
// if a dial request for an id-like target creates multiple matches,
// give precedence to the exact match if one exists. If not, handle
// multiple matchers per-usual below.
for _, m := range matches {
if m.GetName() == host {
matches = []types.Server{m}
break
}
}
}
var server types.Server
switch {
case strategy == types.RoutingStrategy_MOST_RECENT:
for _, m := range matches {
if server == nil || m.Expiry().After(server.Expiry()) {
server = m
}
}
case len(matches) > 1:
return nil, trace.NotFound(teleport.NodeIsAmbiguous)
case len(matches) == 1:
server = matches[0]
}
if routeMatcher.MatchesServerIDs() && server == nil {
idType := "UUID"
if aws.IsEC2NodeID(host) {
idType = "EC2"
}
return nil, trace.NotFound("unable to locate node matching %s-like target %s", idType, host)
}
return server, nil
}
// DialSite establishes a connection to the auth server in the provided
// cluster. If the clusterName is an empty string then a connection to
// the local auth server will be established.
func (r *Router) DialSite(ctx context.Context, clusterName string, clientSrcAddr, clientDstAddr net.Addr) (net.Conn, error) {
_, span := r.tracer.Start(
ctx,
"router/DialSite",
oteltrace.WithAttributes(
attribute.String("cluster", clusterName),
),
)
defer span.End()
// default to local cluster if one wasn't provided
if clusterName == "" {
clusterName = r.clusterName
}
// dial the local auth server
if clusterName == r.clusterName {
conn, err := r.localSite.DialAuthServer(reversetunnelclient.DialParams{From: clientSrcAddr, OriginalClientDstAddr: clientDstAddr})
return conn, trace.Wrap(err)
}
// lookup the site and dial its auth server
site, err := r.siteGetter.GetSite(clusterName)
if err != nil {
return nil, trace.Wrap(err)
}
conn, err := site.DialAuthServer(reversetunnelclient.DialParams{From: clientSrcAddr, OriginalClientDstAddr: clientDstAddr})
if err != nil {
return nil, trace.Wrap(err)
}
return NewProxiedMetricConn(conn), trace.Wrap(err)
}
// GetSiteClient returns an auth client for the provided cluster.
func (r *Router) GetSiteClient(ctx context.Context, clusterName string) (auth.ClientI, error) {
if clusterName == r.clusterName {
return r.localSite.GetClient()
}
site, err := r.siteGetter.GetSite(clusterName)
if err != nil {
return nil, trace.Wrap(err)
}
return site.GetClient()
}