forked from gravitational/teleport
/
monitor.go
484 lines (437 loc) · 14.6 KB
/
monitor.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
/*
Copyright 2019 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 srv
import (
"context"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"github.com/zmb3/teleport/api/constants"
"github.com/zmb3/teleport/api/types"
apievents "github.com/zmb3/teleport/api/types/events"
"github.com/zmb3/teleport/lib/events"
"github.com/zmb3/teleport/lib/services"
"github.com/zmb3/teleport/lib/tlsca"
)
// ActivityTracker is a connection activity tracker,
// it allows to update the activity on the connection
// and retrieve the time when the connection was last active
type ActivityTracker interface {
// GetClientLastActive returns the time of the last recorded activity
GetClientLastActive() time.Time
// UpdateClientActivity updates the last active timestamp
UpdateClientActivity()
}
// TrackingConn is an interface representing tracking connection
type TrackingConn interface {
// LocalAddr returns local address
LocalAddr() net.Addr
// RemoteAddr returns remote address
RemoteAddr() net.Addr
// Close closes the connection
Close() error
}
// MonitorConfig is a wiretap configuration
type MonitorConfig struct {
// LockWatcher is a lock watcher.
LockWatcher *services.LockWatcher
// LockTargets is used to detect a lock applicable to the connection.
LockTargets []types.LockTarget
// LockingMode determines how to handle possibly stale lock views.
LockingMode constants.LockingMode
// DisconnectExpiredCert is a point in time when
// the certificate should be disconnected
DisconnectExpiredCert time.Time
// ClientIdleTimeout is a timeout of inactivity
// on the wire
ClientIdleTimeout time.Duration
// Clock is a clock, realtime or fixed in tests
Clock clockwork.Clock
// Tracker is activity tracker
Tracker ActivityTracker
// Conn is a connection to close
Conn TrackingConn
// Context is an external context to cancel the operation
Context context.Context
// Login is linux box login
Login string
// TeleportUser is a teleport user name
TeleportUser string
// ServerID is a session server ID
ServerID string
// Emitter is events emitter
Emitter apievents.Emitter
// Entry is a logging entry
Entry log.FieldLogger
// IdleTimeoutMessage is sent to the client when the idle timeout expires.
IdleTimeoutMessage string
// MessageWriter wraps a channel to send text messages to the client. Use
// for disconnection messages, etc.
MessageWriter io.StringWriter
// MonitorCloseChannel will be signaled when the monitor closes a connection.
// Used only for testing. Optional.
MonitorCloseChannel chan struct{}
}
// CheckAndSetDefaults checks values and sets defaults
func (m *MonitorConfig) CheckAndSetDefaults() error {
if m.Context == nil {
return trace.BadParameter("missing parameter Context")
}
if m.LockWatcher == nil {
return trace.BadParameter("missing parameter LockWatcher")
}
if len(m.LockTargets) == 0 {
return trace.BadParameter("missing parameter LockTargets")
}
if m.Conn == nil {
return trace.BadParameter("missing parameter Conn")
}
if m.Entry == nil {
return trace.BadParameter("missing parameter Entry")
}
if m.Tracker == nil {
return trace.BadParameter("missing parameter Tracker")
}
if m.Emitter == nil {
return trace.BadParameter("missing parameter Emitter")
}
if m.Clock == nil {
m.Clock = clockwork.NewRealClock()
}
return nil
}
// StartMonitor starts a new monitor.
func StartMonitor(cfg MonitorConfig) error {
if err := cfg.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
w := &Monitor{
MonitorConfig: cfg,
}
// If an applicable lock is already in force, close the connection immediately.
if lockErr := w.LockWatcher.CheckLockInForce(w.LockingMode, w.LockTargets...); lockErr != nil {
w.handleLockInForce(lockErr)
return nil
}
lockWatch, err := w.LockWatcher.Subscribe(w.Context, w.LockTargets...)
if err != nil {
return trace.Wrap(err)
}
go func() {
w.start(lockWatch)
if w.MonitorCloseChannel != nil {
// Non blocking send to the close channel.
select {
case w.MonitorCloseChannel <- struct{}{}:
default:
}
}
}()
return nil
}
// Monitor monitors the activity on a single connection and disconnects
// that connection if the certificate expires, if a new lock is placed
// that applies to the connection, or after periods of inactivity
type Monitor struct {
// MonitorConfig is a connection monitor configuration
MonitorConfig
}
// start starts monitoring connection.
func (w *Monitor) start(lockWatch types.Watcher) {
lockWatchDoneC := lockWatch.Done()
defer func() {
if err := lockWatch.Close(); err != nil {
w.Entry.WithError(err).Warn("Failed to close lock watcher subscription.")
}
}()
var certTime <-chan time.Time
if !w.DisconnectExpiredCert.IsZero() {
discTime := w.DisconnectExpiredCert.Sub(w.Clock.Now().UTC())
if discTime <= 0 {
// Client cert is already expired.
// Disconnect the client immediately.
w.disconnectClientOnExpiredCert()
return
}
t := w.Clock.NewTicker(discTime)
defer t.Stop()
certTime = t.Chan()
}
var idleTime <-chan time.Time
if w.ClientIdleTimeout != 0 {
idleTime = w.Clock.After(w.ClientIdleTimeout)
}
for {
select {
// Expired certificate.
case <-certTime:
w.disconnectClientOnExpiredCert()
return
// Idle timeout.
case <-idleTime:
clientLastActive := w.Tracker.GetClientLastActive()
since := w.Clock.Since(clientLastActive)
if since >= w.ClientIdleTimeout {
reason := "client reported no activity"
if !clientLastActive.IsZero() {
reason = fmt.Sprintf("client is idle for %v, exceeded idle timeout of %v",
since, w.ClientIdleTimeout)
}
if w.MessageWriter != nil && w.IdleTimeoutMessage != "" {
if _, err := w.MessageWriter.WriteString(w.IdleTimeoutMessage); err != nil {
w.Entry.WithError(err).Warn("Failed to send idle timeout message.")
}
}
w.Entry.Debugf("Disconnecting client: %v", reason)
if err := w.Conn.Close(); err != nil {
w.Entry.WithError(err).Error("Failed to close connection.")
}
if err := w.emitDisconnectEvent(reason); err != nil {
w.Entry.WithError(err).Warn("Failed to emit audit event.")
}
return
}
next := w.ClientIdleTimeout - since
w.Entry.Debugf("Client activity detected %v ago; next check in %v", since, next)
idleTime = w.Clock.After(next)
// Lock in force.
case lockEvent := <-lockWatch.Events():
var lockErr error
switch lockEvent.Type {
case types.OpPut:
lock, ok := lockEvent.Resource.(types.Lock)
if !ok {
w.Entry.Warnf("Skipping unexpected lock event resource type %T.", lockEvent.Resource)
} else {
lockErr = services.LockInForceAccessDenied(lock)
}
case types.OpDelete:
// Lock deletion can be ignored.
case types.OpUnreliable:
if w.LockingMode == constants.LockingModeStrict {
lockErr = services.StrictLockingModeAccessDenied
}
default:
w.Entry.Warnf("Skipping unexpected lock event type %q.", lockEvent.Type)
}
if lockErr != nil {
w.handleLockInForce(lockErr)
return
}
case <-lockWatchDoneC:
w.Entry.WithError(lockWatch.Error()).Warn("Lock watcher subscription was closed.")
if w.DisconnectExpiredCert.IsZero() && w.ClientIdleTimeout == 0 {
return
}
// Prevent spinning on the zero value received from closed lockWatchDoneC.
lockWatchDoneC = nil
case <-w.Context.Done():
w.Entry.Debugf("Releasing associated resources - context has been closed.")
return
}
}
}
func (w *Monitor) disconnectClientOnExpiredCert() {
reason := fmt.Sprintf("client certificate expired at %v", w.Clock.Now().UTC())
w.Entry.Debugf("Disconnecting client: %v", reason)
if err := w.Conn.Close(); err != nil {
w.Entry.WithError(err).Error("Failed to close connection.")
}
if err := w.emitDisconnectEvent(reason); err != nil {
w.Entry.WithError(err).Warn("Failed to emit audit event.")
}
}
func (w *Monitor) emitDisconnectEvent(reason string) error {
event := &apievents.ClientDisconnect{
Metadata: apievents.Metadata{
Type: events.ClientDisconnectEvent,
Code: events.ClientDisconnectCode,
},
UserMetadata: apievents.UserMetadata{
Login: w.Login,
User: w.TeleportUser,
},
ConnectionMetadata: apievents.ConnectionMetadata{
LocalAddr: w.Conn.LocalAddr().String(),
RemoteAddr: w.Conn.RemoteAddr().String(),
},
ServerMetadata: apievents.ServerMetadata{
ServerID: w.ServerID,
},
Reason: reason,
}
return trace.Wrap(w.Emitter.EmitAuditEvent(w.Context, event))
}
func (w *Monitor) handleLockInForce(lockErr error) {
reason := lockErr.Error()
if w.MessageWriter != nil {
if _, err := w.MessageWriter.WriteString(reason); err != nil {
w.Entry.WithError(err).Warn("Failed to send lock-in-force message.")
}
}
w.Entry.Debugf("Disconnecting client: %v.", reason)
if err := w.Conn.Close(); err != nil {
w.Entry.WithError(err).Error("Failed to close connection.")
}
if err := w.emitDisconnectEvent(reason); err != nil {
w.Entry.WithError(err).Warn("Failed to emit audit event.")
}
}
type trackingChannel struct {
ssh.Channel
t ActivityTracker
}
func newTrackingChannel(ch ssh.Channel, t ActivityTracker) ssh.Channel {
return trackingChannel{
Channel: ch,
t: t,
}
}
func (ch trackingChannel) Read(buf []byte) (int, error) {
n, err := ch.Channel.Read(buf)
ch.t.UpdateClientActivity()
return n, err
}
func (ch trackingChannel) Write(buf []byte) (int, error) {
n, err := ch.Channel.Write(buf)
ch.t.UpdateClientActivity()
return n, err
}
// TrackingReadConnConfig is a TrackingReadConn configuration.
type TrackingReadConnConfig struct {
// Conn is a client connection.
Conn net.Conn
// Clock is a clock, realtime or fixed in tests.
Clock clockwork.Clock
// Context is an external context to cancel the operation.
Context context.Context
// Cancel is called whenever client context is closed.
Cancel context.CancelFunc
}
// CheckAndSetDefaults checks and sets defaults.
func (c *TrackingReadConnConfig) CheckAndSetDefaults() error {
if c.Conn == nil {
return trace.BadParameter("missing parameter Conn")
}
if c.Clock == nil {
c.Clock = clockwork.NewRealClock()
}
if c.Context == nil {
return trace.BadParameter("missing parameter Context")
}
if c.Cancel == nil {
return trace.BadParameter("missing parameter Cancel")
}
return nil
}
// NewTrackingReadConn returns a new tracking read connection.
func NewTrackingReadConn(cfg TrackingReadConnConfig) (*TrackingReadConn, error) {
if err := cfg.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return &TrackingReadConn{
cfg: cfg,
mtx: sync.RWMutex{},
Conn: cfg.Conn,
lastActive: time.Time{},
}, nil
}
// TrackingReadConn allows to wrap net.Conn and keeps track of the latest conn read activity.
type TrackingReadConn struct {
cfg TrackingReadConnConfig
mtx sync.RWMutex
net.Conn
lastActive time.Time
}
// Read reads data from the connection.
// Read can be made to time out and return an Error with Timeout() == true
// after a fixed time limit; see SetDeadline and SetReadDeadline.
func (t *TrackingReadConn) Read(b []byte) (int, error) {
n, err := t.Conn.Read(b)
t.UpdateClientActivity()
// This has to use the original error type or else utilities using the connection
// (like io.Copy, which is used by the oxy forwarder) may incorrectly categorize
// the error produced by this and terminate the connection unnecessarily.
return n, err
}
func (t *TrackingReadConn) Close() error {
t.cfg.Cancel()
return t.Conn.Close()
}
// GetClientLastActive returns time when client was last active
func (t *TrackingReadConn) GetClientLastActive() time.Time {
t.mtx.RLock()
defer t.mtx.RUnlock()
return t.lastActive
}
// UpdateClientActivity sets last recorded client activity
func (t *TrackingReadConn) UpdateClientActivity() {
t.mtx.Lock()
defer t.mtx.Unlock()
t.lastActive = t.cfg.Clock.Now().UTC()
}
// GetDisconnectExpiredCertFromIdentity calculates the proper value for DisconnectExpiredCert
// based on whether a connection is set to disconnect on cert expiry, and whether
// the cert is a short lived (<1m) one issued for an MFA verified session. If the session
// doesn't need to be disconnected on cert expiry it will return the default value for time.Time.
func GetDisconnectExpiredCertFromIdentity(
checker services.AccessChecker,
authPref types.AuthPreference,
identity *tlsca.Identity,
) time.Time {
// In the case where both disconnect_expired_cert and require_session_mfa are enabled,
// the PreviousIdentityExpires value of the certificate will be used, which is the
// expiry of the certificate used to issue the short lived MFA verified certificate.
//
// See https://github.com/gravitational/teleport/issues/18544
// If the session doesn't need to be disconnected on cert expiry just return the default value.
if !checker.AdjustDisconnectExpiredCert(authPref.GetDisconnectExpiredCert()) {
return time.Time{}
}
if !identity.PreviousIdentityExpires.IsZero() {
// If this is a short-lived mfa verified cert, return the certificate extension
// that holds its' issuing cert's expiry value.
return identity.PreviousIdentityExpires
}
// Otherwise just return the current cert's expiration
return identity.Expires
}
// See GetDisconnectExpiredCertFromIdentity
func getDisconnectExpiredCertFromIdentityContext(
checker services.AccessChecker,
authPref types.AuthPreference,
identity *IdentityContext,
) time.Time {
// In the case where both disconnect_expired_cert and require_session_mfa are enabled,
// the PreviousIdentityExpires value of the certificate will be used, which is the
// expiry of the certificate used to issue the short lived MFA verified certificate.
//
// See https://github.com/gravitational/teleport/issues/18544
// If the session doesn't need to be disconnected on cert expiry just return the default value.
if !checker.AdjustDisconnectExpiredCert(authPref.GetDisconnectExpiredCert()) {
return time.Time{}
}
if !identity.PreviousIdentityExpires.IsZero() {
// If this is a short-lived mfa verified cert, return the certificate extension
// that holds its' issuing cert's expiry value.
return identity.PreviousIdentityExpires
}
// Otherwise just return the current cert's expiration
return identity.CertValidBefore
}