mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-31 20:39:37 +08:00
Session control logic existed within `HandleNewConn` of `srv/regular.Server`. This prevented any of it from being used by other components that also needed to enforce session control. All the logic from within `HandleNewConn` was refactored to a new `srv.SessionController` object which the `regular.Server` now uses to perform session control. There were a few additional changes needed to accomadate that session control now exists outside the server and to make tests easier to write. Namely, altering `srv.ComputLockTargets` to not take a `Server` as a parameter and leveraging a clock within `services.SemaphoreLock`. This is step 2 in addressing #15167. Before the web apiserver can leverage the newly introduced proxy.Router and bypass making ssh connections to the proxy ssh server it needs to be able to perfrom session control.
335 lines
9.4 KiB
Go
335 lines
9.4 KiB
Go
// Copyright 2021 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 services
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/utils/retryutils"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
type SemaphoreLockConfig struct {
|
|
// Service is the service against which all semaphore
|
|
// operations are performed.
|
|
Service types.Semaphores
|
|
// Expiry is an optional lease expiry parameter.
|
|
Expiry time.Duration
|
|
// TickRate is the rate at which lease renewals are attempted
|
|
// and defaults to 1/2 expiry. Used to accelerate tests.
|
|
TickRate time.Duration
|
|
// Params holds the semaphore lease acquisition parameters.
|
|
Params types.AcquireSemaphoreRequest
|
|
// Clock used to alter time in tests
|
|
Clock clockwork.Clock
|
|
}
|
|
|
|
// CheckAndSetDefaults checks and sets default parameters
|
|
func (l *SemaphoreLockConfig) CheckAndSetDefaults() error {
|
|
if l.Clock == nil {
|
|
l.Clock = clockwork.NewRealClock()
|
|
}
|
|
|
|
if l.Service == nil {
|
|
return trace.BadParameter("missing semaphore service")
|
|
}
|
|
if l.Expiry == 0 {
|
|
l.Expiry = defaults.SessionControlTimeout
|
|
}
|
|
if l.Expiry < time.Millisecond {
|
|
return trace.BadParameter("sub-millisecond lease expiry is not supported: %v", l.Expiry)
|
|
}
|
|
if l.TickRate == 0 {
|
|
l.TickRate = l.Expiry / 2
|
|
}
|
|
if l.TickRate >= l.Expiry {
|
|
return trace.BadParameter("tick-rate must be less than expiry")
|
|
}
|
|
if l.Params.Expires.IsZero() {
|
|
l.Params.Expires = l.Clock.Now().UTC().Add(l.Expiry)
|
|
}
|
|
if err := l.Params.Check(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SemaphoreLock provides a convenient interface for managing
|
|
// semaphore lease keepalive operations.
|
|
type SemaphoreLock struct {
|
|
cfg SemaphoreLockConfig
|
|
lease0 types.SemaphoreLease
|
|
retry retryutils.Retry
|
|
ticker clockwork.Ticker
|
|
doneC chan struct{}
|
|
closeOnce sync.Once
|
|
renewalC chan struct{}
|
|
cond *sync.Cond
|
|
err error
|
|
fin bool
|
|
}
|
|
|
|
// finish registers the final result of the background
|
|
// goroutine. must be called even if err is nil in
|
|
// order to wake any goroutines waiting on the error
|
|
// and mark the lock as finished.
|
|
func (l *SemaphoreLock) finish(err error) {
|
|
l.cond.L.Lock()
|
|
defer l.cond.L.Unlock()
|
|
l.err = err
|
|
l.fin = true
|
|
l.cond.Broadcast()
|
|
}
|
|
|
|
// Done signals that lease keepalive operations
|
|
// have stopped.
|
|
func (l *SemaphoreLock) Done() <-chan struct{} {
|
|
return l.doneC
|
|
}
|
|
|
|
// Wait blocks until the final result is available. Note that
|
|
// this method may block longer than desired since cancellation of
|
|
// the parent context triggers the *start* of the release operation.
|
|
func (l *SemaphoreLock) Wait() error {
|
|
l.cond.L.Lock()
|
|
defer l.cond.L.Unlock()
|
|
for !l.fin {
|
|
l.cond.Wait()
|
|
}
|
|
return l.err
|
|
}
|
|
|
|
// Stop stops associated lease keepalive.
|
|
func (l *SemaphoreLock) Stop() {
|
|
l.closeOnce.Do(func() {
|
|
l.ticker.Stop()
|
|
close(l.doneC)
|
|
})
|
|
}
|
|
|
|
// Renewed notifies on next successful lease keepalive.
|
|
// Used in tests to block until next renewal.
|
|
func (l *SemaphoreLock) Renewed() <-chan struct{} {
|
|
return l.renewalC
|
|
}
|
|
|
|
func (l *SemaphoreLock) keepAlive(ctx context.Context) {
|
|
var nodrop bool
|
|
var err error
|
|
lease := l.lease0
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer func() {
|
|
cancel()
|
|
l.Stop()
|
|
defer l.finish(err)
|
|
if nodrop {
|
|
// non-standard exit conditions; don't bother handling
|
|
// cancellation/expiry.
|
|
return
|
|
}
|
|
if lease.Expires.After(l.cfg.Clock.Now().UTC()) {
|
|
// parent context is closed. create orphan context with generous
|
|
// timeout for lease cancellation scope. this will not block any
|
|
// caller that is not explicitly waiting on the final error value.
|
|
cancelContext, cancel := context.WithTimeout(context.Background(), l.cfg.Expiry/4)
|
|
defer cancel()
|
|
err = l.cfg.Service.CancelSemaphoreLease(cancelContext, lease)
|
|
if err != nil {
|
|
log.Warnf("Failed to cancel semaphore lease %s/%s: %v", lease.SemaphoreKind, lease.SemaphoreName, err)
|
|
}
|
|
} else {
|
|
log.Errorf("Semaphore lease expired: %s/%s", lease.SemaphoreKind, lease.SemaphoreName)
|
|
}
|
|
}()
|
|
Outer:
|
|
for {
|
|
select {
|
|
case tick := <-l.ticker.Chan():
|
|
leaseContext, leaseCancel := context.WithDeadline(ctx, lease.Expires)
|
|
nextLease := lease
|
|
nextLease.Expires = tick.Add(l.cfg.Expiry)
|
|
for {
|
|
err = l.cfg.Service.KeepAliveSemaphoreLease(leaseContext, nextLease)
|
|
if trace.IsNotFound(err) {
|
|
leaseCancel()
|
|
// semaphore and/or lease no longer exist; best to log the error
|
|
// and exit immediately.
|
|
log.Warnf("Halting keepalive on semaphore %s/%s early: %v", lease.SemaphoreKind, lease.SemaphoreName, err)
|
|
nodrop = true
|
|
return
|
|
}
|
|
if err == nil {
|
|
leaseCancel()
|
|
lease = nextLease
|
|
l.retry.Reset()
|
|
select {
|
|
case l.renewalC <- struct{}{}:
|
|
default:
|
|
}
|
|
continue Outer
|
|
}
|
|
log.Debugf("Failed to renew semaphore lease %s/%s: %v", lease.SemaphoreKind, lease.SemaphoreName, err)
|
|
l.retry.Inc()
|
|
select {
|
|
case <-l.retry.After():
|
|
case tick = <-l.ticker.Chan():
|
|
// check to make sure that we still have some time on the lease. the default tick rate would have
|
|
// us waking _as_ the lease expires here, but if we're working with a higher tick rate, its worth
|
|
// retrying again.
|
|
if !lease.Expires.After(tick) {
|
|
leaseCancel()
|
|
return
|
|
}
|
|
case <-leaseContext.Done():
|
|
leaseCancel() // demanded by linter
|
|
return
|
|
case <-l.Done():
|
|
leaseCancel()
|
|
return
|
|
}
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
case <-l.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// AcquireSemaphoreWithRetryConfig contains parameters for trying to acquire a
|
|
// semaphore with a retry.
|
|
type AcquireSemaphoreWithRetryConfig struct {
|
|
Service types.Semaphores
|
|
Request types.AcquireSemaphoreRequest
|
|
Retry retryutils.LinearConfig
|
|
}
|
|
|
|
// AcquireSemaphoreWithRetry tries to acquire the semaphore according to the
|
|
// retry schedule until it succeeds or context expires.
|
|
func AcquireSemaphoreWithRetry(ctx context.Context, req AcquireSemaphoreWithRetryConfig) (*types.SemaphoreLease, error) {
|
|
retry, err := retryutils.NewLinear(req.Retry)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
var lease *types.SemaphoreLease
|
|
err = retry.For(ctx, func() (err error) {
|
|
lease, err = req.Service.AcquireSemaphore(ctx, req.Request)
|
|
return trace.Wrap(err)
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return lease, nil
|
|
}
|
|
|
|
// AcquireSemaphoreLock attempts to acquire and hold a semaphore lease. If successfully acquired,
|
|
// background keepalive processes are started and an associated lock handle is returned. Canceling
|
|
// the supplied context releases the semaphore.
|
|
func AcquireSemaphoreLock(ctx context.Context, cfg SemaphoreLockConfig) (*SemaphoreLock, error) {
|
|
if err := cfg.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// set up retry with a ratio which will result in 3-4 retries before the lease expires
|
|
retry, err := retryutils.NewLinear(retryutils.LinearConfig{
|
|
Max: cfg.Expiry / 4,
|
|
Step: cfg.Expiry / 16,
|
|
Jitter: retryutils.NewJitter(),
|
|
Clock: cfg.Clock,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
lease, err := cfg.Service.AcquireSemaphore(ctx, cfg.Params)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
lock := &SemaphoreLock{
|
|
cfg: cfg,
|
|
lease0: *lease,
|
|
retry: retry,
|
|
ticker: cfg.Clock.NewTicker(cfg.TickRate),
|
|
doneC: make(chan struct{}),
|
|
renewalC: make(chan struct{}),
|
|
cond: sync.NewCond(&sync.Mutex{}),
|
|
}
|
|
go lock.keepAlive(ctx)
|
|
return lock, nil
|
|
}
|
|
|
|
// UnmarshalSemaphore unmarshals the Semaphore resource from JSON.
|
|
func UnmarshalSemaphore(bytes []byte, opts ...MarshalOption) (types.Semaphore, error) {
|
|
var semaphore types.SemaphoreV3
|
|
|
|
if len(bytes) == 0 {
|
|
return nil, trace.BadParameter("missing resource data")
|
|
}
|
|
|
|
cfg, err := CollectOptions(opts)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if err := utils.FastUnmarshal(bytes, &semaphore); err != nil {
|
|
return nil, trace.BadParameter(err.Error())
|
|
}
|
|
|
|
err = semaphore.CheckAndSetDefaults()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if cfg.ID != 0 {
|
|
semaphore.SetResourceID(cfg.ID)
|
|
}
|
|
if !cfg.Expires.IsZero() {
|
|
semaphore.SetExpiry(cfg.Expires)
|
|
}
|
|
return &semaphore, nil
|
|
}
|
|
|
|
// MarshalSemaphore marshals the Semaphore resource to JSON.
|
|
func MarshalSemaphore(semaphore types.Semaphore, opts ...MarshalOption) ([]byte, error) {
|
|
if err := semaphore.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
cfg, err := CollectOptions(opts)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
switch semaphore := semaphore.(type) {
|
|
case *types.SemaphoreV3:
|
|
if !cfg.PreserveResourceID {
|
|
// avoid modifying the original object
|
|
// to prevent unexpected data races
|
|
copy := *semaphore
|
|
copy.SetResourceID(0)
|
|
semaphore = ©
|
|
}
|
|
return utils.FastMarshal(semaphore)
|
|
default:
|
|
return nil, trace.BadParameter("unrecognized resource version %T", semaphore)
|
|
}
|
|
}
|