Files
threshold/internal/daemon/worker/session/session.go
T
Johan Brandhorst-SatzkornandDamian Debkowski ec0a3f964b Worker status refactor (#5417)
* oss: feat(proto): statistics rpc added to server coordination service

* oss: feat(session): add update statistics status service

* oss: feat(handlers): add Statistics method to worker service server

* oss: feat(worker): add statistics ticking

* oss: internal/proto: define the new RoutingInfo RPC

* oss: internal/server: repository changes for RoutingInfo RPC

* oss: internal/daemon: add RoutingInfo handler to worker service

The new RoutingInfo RPC is used to get information from the worker
that is used to make routing decisions on the controller. It also
handles the initial request from the worker.

* oss: feat(db): server_worker_session_info table

* oss: feat(proto): session info rpc for server coordination service

* oss: feat(server): worker session info request data type

* oss: feat(worker): session info ticker

* oss: feat(handler): server coordination handler for session info

* oss: feat(session): utilize session info request time for session cleanup

* oss: proto: fix some gotags comments

Some of these fields were missing comments, while others had
incorrectly formatted comments.

* oss: worker: send connection status to server

This is required for the correct tracking of orphaned connections

* oss: internal/daemon/worker: add function to wait for Statistics RPC

* oss: internal/daemon/worker: add routing info RPC

The new RoutingInfo RPC replaces part of the Status RPC,
namely the part which is used by the controller and worker
to perform decisions around the establishment of new sessions
and new worker connections.

* oss: internal/daemon/worker: remove unnecessary error check

* oss: internal/daemon/worker: lock access to config and upstreams

The new confLock protects access to the conf value and to the
addressReceivers slice.

* oss: worker: replace Status RPC with RoutingInfo

* oss: worker: deprecate the Status RPC and related types

The Status RPC and related messages have been replaced
by the RoutingInfo/SessionInfo/Statistics RPCs. The controller
will continue to support the Status RPC until two new minor
versions have been released.

* oss: internal/event: add default deny for new worker RPCs

* oss: internal/daemon/worker: allow RPC interval to be set

The new SessionInfo/RoutingInfo/Statistics RPCs run at
long intervals, which make some of our cluster tests take
a long time to complete. This allows their interval to be
configured in tests.

* oss: all: use TestWorkerRPCInterval in slow tests

This should speed up these tests significantly.

* oss: internal/tests/cluster: reset sys eventer in test

---------

Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com>
2025-01-09 10:26:37 -08:00

643 lines
21 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package session
import (
"context"
"crypto"
"crypto/x509"
"errors"
"fmt"
"sync"
"time"
"github.com/hashicorp/boundary/internal/event"
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
"github.com/hashicorp/boundary/internal/session"
)
// ValidateSessionTimeout is the duration of the timeout when the worker queries the
// controller for the sessionId for which the connection is being requested.
const ValidateSessionTimeout = 90 * time.Second
var errMakeSessionCloseInfoNilCloseInfo = errors.New("nil closeInfo supplied to makeSessionCloseInfo, this is a bug, please report it")
// ConnInfo defines the information about a connection attached to a session
type ConnInfo struct {
Id string
Status pbs.CONNECTIONSTATUS
// The context.CancelFunc for the proxy connection. Calling this function
// closes the proxy connection.
connCtxCancelFunc context.CancelFunc
// The number of bytes uploaded from the client.
BytesUp func() int64
// The number of bytes downloaded to the client.
BytesDown func() int64
// The time the controller has successfully reported that this connection is
// closed.
CloseTime time.Time
}
// ConnectionCloseData encapsulates the data we need to send via CloseConnection
// RPC to the controller.
type ConnectionCloseData struct {
SessionId string
BytesUp int64
BytesDown int64
}
// Session is the local representation of a session. After initial loading
// the only values that will change will be the status (readable from
// GetStatus()) and the Connections (GetLocalConnections()).
type Session interface {
// ApplyLocalConnectionStatus set's a connection's status to the one provided.
// If there is no connection with the provided id, an error is returned.
ApplyLocalConnectionStatus(connId string, status pbs.CONNECTIONSTATUS) error
// ApplyLocalStatus updates the given session with the status provided by
// the SessionJobInfo. It returns an error if any of the connections
// in the SessionJobInfo are not present, however, it still applies the
// status change to the session and the connections which are present.
ApplyLocalStatus(st pbs.SESSIONSTATUS)
GetStatus() pbs.SESSIONSTATUS
// GetLocalConnections returns the connections this session is handling.
GetLocalConnections() map[string]ConnInfo
GetTofuToken() string
GetConnectionLimit() int32
GetEndpoint() string
GetHostKeys() ([]crypto.Signer, error)
GetCredentials() []*pbs.Credential
GetExpiration() time.Time
GetCertificate() *x509.Certificate
GetPrivateKey() []byte
GetId() string
// CancelOpenLocalConnections closes the local connections in this session
//based on the connection's state by calling the connections context cancel
// function.
//
// The returned slice are connection ids that were closed.
CancelOpenLocalConnections() []string
// CancelAllLocalConnections close connections regardless of connection's state
// by calling the connection context's CancelFunc.
//
// The returned slice is the connection ids which were closed.
CancelAllLocalConnections() []string
// RequestCancel sends session cancellation request to the controller. If there is no
// error the local session's status is updated with the result of the cancel
// request
RequestCancel(ctx context.Context) error
// RequestActivate sends session activation request to the controller. The Session's
// status is then updated with the result of the call. After a successful
// call to RequestActivate, subsequent calls will fail.
RequestActivate(ctx context.Context, tofu string) error
// ApplyConnectionCounterCallbacks sets a connection's bytes up and bytes
// down callbacks to the provided functions. Both functions must be safe for
// concurrent use. If there is no connection with the provided id, an error
// is returned.
ApplyConnectionCounterCallbacks(connId string, bytesUp func() int64, bytesDown func() int64) error
// RequestAuthorizeConnection sends an AuthorizeConnection request to
// the controller.
// It is called by the worker handler after a connection has been received by
// the worker, and the session has been validated.
// The passed in context.CancelFunc is used to terminate any ongoing local proxy
// connections.
// The connection status is then viewable in this session's GetLocalConnections() call.
RequestAuthorizeConnection(ctx context.Context, workerId string, connCancel context.CancelFunc) (*pbs.AuthorizeConnectionResponse, int32, error)
// RequestConnectConnection sends a RequestConnectConnection request to the controller. It
// should only be called by the worker handler after a connection has been
// authorized. The local connection's status is updated with the result of the
// call.
RequestConnectConnection(ctx context.Context, info *pbs.ConnectConnectionRequest) error
}
type sess struct {
lock sync.RWMutex
client pbs.SessionServiceClient
connInfoMap map[string]*ConnInfo
resp *pbs.LookupSessionResponse
status pbs.SESSIONSTATUS
cert *x509.Certificate
sessionId string
tofuToken string
}
func newSess(client pbs.SessionServiceClient, resp *pbs.LookupSessionResponse) (*sess, error) {
switch {
case isNil(client):
return nil, errors.New("SessionServiceClient is nil")
case resp.GetExpiration().AsTime().Before(time.Now()):
return nil, fmt.Errorf("session is expired")
}
parsedCert, err := x509.ParseCertificate(resp.GetAuthorization().GetCertificate())
if err != nil {
return nil, fmt.Errorf("error parsing session certificate: %w", err)
}
s := &sess{
client: client,
connInfoMap: make(map[string]*ConnInfo),
resp: resp,
status: resp.GetStatus(),
cert: parsedCert,
sessionId: resp.GetAuthorization().GetSessionId(),
}
return s, nil
}
// ApplyLocalConnectionStatus Satisfies the Session interface
func (s *sess) ApplyLocalConnectionStatus(connId string, status pbs.CONNECTIONSTATUS) error {
s.lock.Lock()
defer s.lock.Unlock()
// Update connection status if there are any connections in
// the request.
connInfo, ok := s.connInfoMap[connId]
if !ok {
return fmt.Errorf("could not find connection ID %q for session ID %q in local state",
connId,
s.GetId())
}
connInfo.Status = status
if connInfo.Status == pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
connInfo.CloseTime = time.Now()
}
return nil
}
func (s *sess) ApplySessionUpdate(r *pbs.LookupSessionResponse) {
s.lock.Lock()
defer s.lock.Unlock()
s.resp = r
s.status = r.Status
}
func (s *sess) ApplyLocalStatus(st pbs.SESSIONSTATUS) {
s.lock.Lock()
defer s.lock.Unlock()
s.status = st
}
func (s *sess) ApplyLocalTofuToken(tt string) {
s.lock.Lock()
defer s.lock.Unlock()
s.tofuToken = tt
}
func (s *sess) GetLocalConnections() map[string]ConnInfo {
s.lock.RLock()
defer s.lock.RUnlock()
res := make(map[string]ConnInfo, len(s.connInfoMap))
// Returning the s.connInfoMap directly wouldn't be thread safe.
for k, v := range s.connInfoMap {
res[k] = ConnInfo{
Id: v.Id,
Status: v.Status,
CloseTime: v.CloseTime,
BytesUp: v.BytesUp,
BytesDown: v.BytesDown,
}
}
return res
}
// Return local tofu token, if available; otherwise return the tofu token from the resp
func (s *sess) GetTofuToken() string {
s.lock.RLock()
defer s.lock.RUnlock()
if len(s.tofuToken) > 0 {
return s.tofuToken
}
if s.resp != nil {
return s.resp.GetTofuToken()
}
return ""
}
func (s *sess) GetConnectionLimit() int32 {
s.lock.RLock()
defer s.lock.RUnlock()
return s.resp.GetConnectionLimit()
}
func (s *sess) GetEndpoint() string {
s.lock.RLock()
defer s.lock.RUnlock()
return s.resp.GetEndpoint()
}
func (s *sess) GetHostKeys() ([]crypto.Signer, error) {
s.lock.RLock()
defer s.lock.RUnlock()
pkcs8Keys := s.resp.GetPkcs8HostKeys()
var hostKeys []crypto.Signer
for _, hostKey := range pkcs8Keys {
p, err := x509.ParsePKCS8PrivateKey(hostKey)
if err != nil {
return nil, errors.New("error parsing host keys")
}
hostKey, ok := p.(crypto.Signer)
if !ok {
return nil, fmt.Errorf("unsupported host key %T", p)
}
hostKeys = append(hostKeys, hostKey)
}
return hostKeys, nil
}
func (s *sess) GetCredentials() []*pbs.Credential {
s.lock.RLock()
defer s.lock.RUnlock()
return s.resp.GetCredentials()
}
func (s *sess) GetStatus() pbs.SESSIONSTATUS {
s.lock.RLock()
defer s.lock.RUnlock()
return s.status
}
func (s *sess) GetExpiration() time.Time {
s.lock.RLock()
defer s.lock.RUnlock()
return s.resp.GetExpiration().AsTime()
}
func (s *sess) GetCertificate() *x509.Certificate {
s.lock.RLock()
defer s.lock.RUnlock()
return s.cert
}
func (s *sess) GetPrivateKey() []byte {
s.lock.RLock()
defer s.lock.RUnlock()
return s.resp.GetAuthorization().GetPrivateKey()
}
func (s *sess) GetId() string {
return s.sessionId
}
func (s *sess) RequestCancel(ctx context.Context) error {
st, err := cancel(ctx, s.client, s.GetId())
if err != nil {
return err
}
s.ApplyLocalStatus(st)
return nil
}
func (s *sess) RequestActivate(ctx context.Context, tofu string) error {
st, err := activate(ctx, s.client, s.GetId(), tofu, s.resp.GetVersion())
if err != nil {
return err
}
s.ApplyLocalTofuToken(tofu)
s.ApplyLocalStatus(st)
return nil
}
func (s *sess) RequestAuthorizeConnection(ctx context.Context, workerId string, connCancel context.CancelFunc) (*pbs.AuthorizeConnectionResponse, int32, error) {
switch {
case connCancel == nil:
return nil, 0, errors.New("the provided context.CancelFunc was nil")
case workerId == "":
return nil, 0, errors.New("worker id is empty")
}
resp, err := s.client.AuthorizeConnection(ctx, &pbs.AuthorizeConnectionRequest{
SessionId: s.GetId(),
WorkerId: workerId,
})
if err != nil {
return nil, 0, fmt.Errorf("error authorizing connection: %w", err)
}
ci := &ConnInfo{
Id: resp.GetConnectionId(),
Status: resp.GetStatus(),
connCtxCancelFunc: connCancel,
}
// Install safe callbacks before connection has been established. These
// should be replaced when `ApplyConnectionCounterCallbacks` gets called on
// the `sess` object.
ci.BytesUp = func() int64 { return 0 }
ci.BytesDown = func() int64 { return 0 }
s.lock.Lock()
defer s.lock.Unlock()
s.connInfoMap[ci.Id] = ci
return resp, resp.GetConnectionsLeft(), err
}
func (s *sess) RequestConnectConnection(ctx context.Context, info *pbs.ConnectConnectionRequest) error {
st, err := connectConnection(ctx, s.client, info)
if err != nil {
return err
}
if err := s.ApplyLocalConnectionStatus(info.GetConnectionId(), st); err != nil {
return fmt.Errorf("error applying local connection status: %w", err)
}
return nil
}
// CancelOpenLocalConnections closes the local connections in this session
// based on the connection's state by calling the connections context cancel
// function.
//
// The returned slice are connection ids that were closed.
func (s *sess) CancelOpenLocalConnections() []string {
s.lock.RLock()
defer s.lock.RUnlock()
var closedIds []string
for k, v := range s.connInfoMap {
if v.Status != pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
continue
}
v.connCtxCancelFunc()
// CloseTime is set when the controller has reported that the connection
// is closed. The worker should only request a connection be marked
// closed after it has already been cancelled.
if v.CloseTime.IsZero() {
closedIds = append(closedIds, k)
}
}
return closedIds
}
// CancelAllLocalConnections close connections regardless of connection's state
// by calling the connection context's CancelFunc.
//
// The returned slice is the connection ids which were closed.
func (s *sess) CancelAllLocalConnections() []string {
s.lock.RLock()
defer s.lock.RUnlock()
var closedIds []string
for k, v := range s.connInfoMap {
v.connCtxCancelFunc()
// CloseTime is set when the controller has reported that the connection
// is closed. The worker should only request a connection be marked
// closed after it has already been cancelled.
if v.CloseTime.IsZero() {
closedIds = append(closedIds, k)
}
}
return closedIds
}
// ApplyConnectionCounterCallbacks sets a connection's bytes up and bytes
// down callbacks to the provided functions. Both functions must be safe for
// concurrent use. If there is no connection with the provided id, an error
// is returned.
func (s *sess) ApplyConnectionCounterCallbacks(connId string, bytesUp func() int64, bytesDown func() int64) error {
s.lock.Lock()
defer s.lock.Unlock()
ci, ok := s.connInfoMap[connId]
if !ok {
return fmt.Errorf("failed to find connection info for connection id %q", connId)
}
ci.BytesUp = bytesUp
ci.BytesDown = bytesDown
return nil
}
// activate is a helper worker function that sends session activation request to the
// controller.
func activate(ctx context.Context, sessClient pbs.SessionServiceClient, sessionId, tofuToken string, version uint32) (pbs.SESSIONSTATUS, error) {
resp, err := sessClient.ActivateSession(ctx, &pbs.ActivateSessionRequest{
SessionId: sessionId,
TofuToken: tofuToken,
Version: version,
})
if err != nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, fmt.Errorf("error activating session: %w", err)
}
return resp.GetStatus(), nil
}
// cancel is a helper worker function that sends session cancellation request to the
// controller.
func cancel(ctx context.Context, sessClient pbs.SessionServiceClient, sessionId string) (pbs.SESSIONSTATUS, error) {
resp, err := sessClient.CancelSession(ctx, &pbs.CancelSessionRequest{
SessionId: sessionId,
})
if err != nil {
return pbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED, fmt.Errorf("error canceling session: %w", err)
}
return resp.GetStatus(), nil
}
// connectConnection is a helper worker function that sends connection
// connect request to the controller. It is called by the worker handler after a
// connection has been authorized.
func connectConnection(ctx context.Context, sessClient pbs.SessionServiceClient, req *pbs.ConnectConnectionRequest) (pbs.CONNECTIONSTATUS, error) {
resp, err := sessClient.ConnectConnection(ctx, req)
if err != nil {
return pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_UNSPECIFIED, err
}
if resp.GetStatus() != pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CONNECTED {
return pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_UNSPECIFIED, fmt.Errorf("unexpected state returned: %v", resp.GetStatus().String())
}
return resp.GetStatus(), nil
}
// TODO: Move these to manager.go. This is kept here for now simply to make it easier to see the diff.
func closeConnection(ctx context.Context, sessClient pbs.SessionServiceClient, req *pbs.CloseConnectionRequest) (*pbs.CloseConnectionResponse, error) {
const op = "session.closeConnection"
resp, err := sessClient.CloseConnection(ctx, req)
if err != nil {
return nil, err
}
if len(resp.GetCloseResponseData()) != len(req.GetCloseRequestData()) {
event.WriteError(ctx, op, errors.New("mismatched number of states returned on connection closed"), event.WithInfo("expected", len(req.GetCloseRequestData()), "got", len(resp.GetCloseResponseData())))
}
return resp, nil
}
// closeConnections is a helper worker function that sends connection close
// requests to the controller, and sets close times within the worker. It is
// called during the worker session info loop and on connection exit on the proxy.
//
// The boolean indicates whether the function was successful, e.g. had any
// errors. Individual events will be sent for the errors if there are any.
//
// closeInfo is a map of connection id to connection metadata.
func closeConnections(ctx context.Context, sessClient pbs.SessionServiceClient, sManager Manager, closeInfo map[string]*ConnectionCloseData) bool {
const op = "session.closeConnections"
if len(closeInfo) == 0 {
// This should not happen, but it's a no-op if it does. Just
// return.
return false
}
// How we handle close info depends on whether or not we succeeded with
// marking them closed on the controller.
var sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData
var err error
// TODO: This, along with the session info call to the controller, probably needs a
// bit of formalization in terms of how we handle timeouts. For now, this
// just ensures consistency with the same session info call in that it times out
// within an adequate period of time.
closeConnCtx, closeConnCancel := context.WithTimeout(ctx, time.Duration(CloseCallTimeout.Load()))
defer closeConnCancel()
response, err := closeConnection(closeConnCtx, sessClient, makeCloseConnectionRequest(closeInfo))
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error marking connections closed",
"warning", "error contacting controller, connections will be closed only on worker",
"session_and_connection_ids", fmt.Sprintf("%#v", closeInfo),
"timeout", time.Duration(CloseCallTimeout.Load()).String(),
))
// Since we could not reach the controller, we have to make a "fake" response set.
sessionCloseInfo, err = makeFakeSessionCloseInfo(closeInfo)
} else {
// Connection succeeded, so we can proceed with making the sessionCloseInfo
// off of the response data.
sessionCloseInfo, err = makeSessionCloseInfo(closeInfo, response)
}
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("serious error in processing return data from controller, aborting additional session/connection state modification"))
return false
}
// Mark connections as closed
_, errs := setCloseTimeForResponse(sManager, sessionCloseInfo)
if len(errs) > 0 {
for _, err := range errs {
event.WriteError(ctx, op, err, event.WithInfoMsg("error marking connection closed in state"))
}
return false
}
return true
}
// makeCloseConnectionRequest creates a CloseConnectionRequest for
// use with closing connections.
//
// closeInfo is a map, indexed by connection ID, to the individual
// sessions IDs that those connections belong to. The values are
// ignored; the parameter is expected as such just for convenience of
// its caller.
func makeCloseConnectionRequest(closeInfo map[string]*ConnectionCloseData) *pbs.CloseConnectionRequest {
closeData := make([]*pbs.CloseConnectionRequestData, 0, len(closeInfo))
for connId, data := range closeInfo {
closeData = append(closeData, &pbs.CloseConnectionRequestData{
ConnectionId: connId,
Reason: session.UnknownReason.String(),
BytesUp: data.BytesUp,
BytesDown: data.BytesDown,
})
}
return &pbs.CloseConnectionRequest{
CloseRequestData: closeData,
}
}
// makeSessionCloseInfo takes the response from closeConnections and
// our original closeInfo map and makes a map of slices, indexed by
// session ID, of all of the connection responses. This allows us to
// easily lock on session once for all connections in
// setCloseTimeForResponse.
func makeSessionCloseInfo(
closeInfo map[string]*ConnectionCloseData,
response *pbs.CloseConnectionResponse,
) (map[string][]*pbs.CloseConnectionResponseData, error) {
if closeInfo == nil {
return nil, errMakeSessionCloseInfoNilCloseInfo
}
result := make(map[string][]*pbs.CloseConnectionResponseData)
for _, v := range response.GetCloseResponseData() {
sessionId := closeInfo[v.GetConnectionId()].SessionId
result[sessionId] = append(result[sessionId], v)
}
return result, nil
}
// makeFakeSessionCloseInfo makes a "fake" makeFakeSessionCloseInfo, intended
// for use when we can't contact the controller.
func makeFakeSessionCloseInfo(
closeInfo map[string]*ConnectionCloseData,
) (map[string][]*pbs.CloseConnectionResponseData, error) {
if closeInfo == nil {
return nil, errMakeSessionCloseInfoNilCloseInfo
}
result := make(map[string][]*pbs.CloseConnectionResponseData)
for connectionId, closeData := range closeInfo {
sessionId := closeData.SessionId
result[sessionId] = append(result[sessionId], &pbs.CloseConnectionResponseData{
ConnectionId: connectionId,
Status: pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED,
})
}
return result, nil
}
// setCloseTimeForResponse iterates a CloseConnectionResponse and
// sets the close time for any connection found to be closed to the
// current time.
//
// sessionCloseInfo can be derived from the closeInfo supplied to
// makeCloseConnectionRequest through reverseCloseInfo, which creates
// a session ID to connection ID mapping.
//
// A non-zero error count does not necessarily mean the operation
// failed, as some connections may have been marked as closed. The
// actual list of connection IDs closed is returned as the first
// return value.
func setCloseTimeForResponse(sManager Manager, sessionCloseInfo map[string][]*pbs.CloseConnectionResponseData) ([]string, []error) {
closedIds := make([]string, 0)
var result []error
for sessionId, responses := range sessionCloseInfo {
si := sManager.Get(sessionId)
if si == nil {
result = append(result, fmt.Errorf("could not find session ID %q in local state after closing connections", sessionId))
continue
}
connStatus := make(map[string]pbs.CONNECTIONSTATUS, len(responses))
for _, response := range responses {
if err := si.ApplyLocalConnectionStatus(response.GetConnectionId(), response.GetStatus()); err != nil {
result = append(result, err)
continue
}
connStatus[response.GetConnectionId()] = response.GetStatus()
if response.GetStatus() == pbs.CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
closedIds = append(closedIds, response.GetConnectionId())
}
}
}
return closedIds, result
}